import { useEffect, useState, useCallback, useRef } from "react"; import { Canvas } from "@react-three/fiber"; import EditorViewer from "./EditorViewer"; import EditorControls from "./EditorControls"; import type { TransformMode, MapNode } from "./types"; import type { SceneData } from "./types"; import "./EditorPage.css"; interface ObjectTransform { uuid: string; position: { x: number; y: number; z: number }; rotation: { x: number; y: number; z: number }; scale: { x: number; y: number; z: number }; } class HistoryManager { private history: ObjectTransform[][] = []; private currentIndex = -1; private maxSize: number; constructor(maxSize = 50) { this.maxSize = maxSize; } saveSnapshot(objects: ObjectTransform[]) { if (this.currentIndex < this.history.length - 1) { this.history = this.history.slice(0, this.currentIndex + 1); } this.history.push(objects.map((obj) => ({ ...obj }))); this.currentIndex = this.history.length - 1; if (this.history.length > this.maxSize) { this.history.shift(); this.currentIndex--; } } undo(): ObjectTransform[] | undefined { if (this.currentIndex > 0) { this.currentIndex--; return this.history[this.currentIndex]; } return undefined; } redo(): ObjectTransform[] | undefined { if (this.currentIndex < this.history.length - 1) { this.currentIndex++; return this.history[this.currentIndex]; } return undefined; } getUndoCount(): number { return this.currentIndex; } getRedoCount(): number { return this.history.length - 1 - this.currentIndex; } clear() { this.history = []; this.currentIndex = -1; } canUndo(): boolean { return this.currentIndex > 0; } canRedo(): boolean { return this.currentIndex < this.history.length - 1; } } export function EditorPage(): React.JSX.Element { const [hasMapJson, setHasMapJson] = useState(false); const [isMapLoading, setIsMapLoading] = useState(true); const [sceneData, setSceneData] = useState(null); // État partagé entre Canvas (3D) et EditorControls (HTML) const [selectedNodeIndex, setSelectedNodeIndex] = useState( null, ); const [hoveredNodeIndex, setHoveredNodeIndex] = useState(null); const [transformMode, setTransformMode] = useState("translate"); const [undoCount, setUndoCount] = useState(0); const [redoCount, setRedoCount] = useState(0); const [isPlayerMode, setIsPlayerMode] = useState(false); const historyManagerRef = useCallback(() => new HistoryManager(50), []); const historyManager = useRef(historyManagerRef()); // Callbacks partagés const handleSelectNode = useCallback((index: number | null) => { setSelectedNodeIndex(index); }, []); const handleHoverNode = useCallback((index: number | null) => { setHoveredNodeIndex(index); }, []); const handleTransformModeChange = useCallback((mode: TransformMode) => { setTransformMode(mode); }, []); const applySnapshot = useCallback( (snapshot: ObjectTransform[]) => { if (!sceneData) return; setSceneData((prev) => { if (!prev) return null; const newMapNodes = prev.mapNodes.map((node, index) => { const transform = snapshot.find((s) => s.uuid === `node-${index}`); if (transform) { return { ...node, position: [ transform.position.x, transform.position.y, transform.position.z, ] as [number, number, number], rotation: [ transform.rotation.x, transform.rotation.y, transform.rotation.z, ] as [number, number, number], scale: [ transform.scale.x, transform.scale.y, transform.scale.z, ] as [number, number, number], }; } return node; }); return { ...prev, mapNodes: newMapNodes }; }); }, [sceneData], ); const handleUndo = useCallback(() => { const snapshot = historyManager.current.undo(); if (snapshot) { applySnapshot(snapshot); setUndoCount(historyManager.current.getUndoCount()); setRedoCount(historyManager.current.getRedoCount()); } }, [applySnapshot]); const handleRedo = useCallback(() => { const snapshot = historyManager.current.redo(); if (snapshot) { applySnapshot(snapshot); setUndoCount(historyManager.current.getUndoCount()); setRedoCount(historyManager.current.getRedoCount()); } }, [applySnapshot]); const handleSaveToServer = useCallback(async () => { if (!sceneData) return; const json = JSON.stringify(sceneData.mapNodes, null, 2); try { const response = await fetch("/api/save-map", { method: "POST", headers: { "Content-Type": "application/json" }, body: json, }); if (response.ok) { alert("Map enregistrée avec succès!"); } else { alert("Erreur lors de l'enregistrement"); } } catch (err) { console.error("Error saving map:", err); alert("Erreur lors de l'enregistrement"); } }, [sceneData]); const handleExportJson = useCallback(() => { if (!sceneData) return; const json = JSON.stringify(sceneData.mapNodes, null, 2); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "map.json"; a.click(); URL.revokeObjectURL(url); }, [sceneData]); const handleResetCamera = useCallback(() => { // Logique pour reset camera console.log("Reset camera"); }, []); const handlePlayerMode = useCallback(() => { setIsPlayerMode((prev) => !prev); }, []); const handleTransformStart = useCallback(() => { if (!sceneData) return; const snapshot = sceneData.mapNodes.map((node, index) => ({ uuid: `node-${index}`, position: { x: node.position[0], y: node.position[1], z: node.position[2], }, rotation: { x: node.rotation[0], y: node.rotation[1], z: node.rotation[2], }, scale: { x: node.scale[0], y: node.scale[1], z: node.scale[2] }, })); historyManager.current.saveSnapshot(snapshot); }, [sceneData]); const handleTransformEnd = useCallback(() => { if (!sceneData) return; const snapshot = sceneData.mapNodes.map((node, index) => ({ uuid: `node-${index}`, position: { x: node.position[0], y: node.position[1], z: node.position[2], }, rotation: { x: node.rotation[0], y: node.rotation[1], z: node.rotation[2], }, scale: { x: node.scale[0], y: node.scale[1], z: node.scale[2] }, })); historyManager.current.saveSnapshot(snapshot); setUndoCount(historyManager.current.getUndoCount()); setRedoCount(historyManager.current.getRedoCount()); }, [sceneData]); const handleNodeTransform = useCallback( (nodeIndex: number, updatedNode: MapNode) => { if (!sceneData) return; setSceneData((prev) => { if (!prev) return null; const newMapNodes = [...prev.mapNodes]; newMapNodes[nodeIndex] = updatedNode; return { ...prev, mapNodes: newMapNodes }; }); }, [sceneData], ); useEffect(() => { const loadMapData = async (): Promise => { setIsMapLoading(true); try { const response = await fetch("/map.json"); if (!response.ok) { setHasMapJson(false); setIsMapLoading(false); return; } const mapNodes: MapNode[] = await response.json(); const models = new Map(); const uniqueModelNames = [ ...new Set(mapNodes.map((n: MapNode) => n.name)), ]; console.log("Unique model names in map:", uniqueModelNames); for (const modelName of uniqueModelNames) { try { const modelUrl = `/models/${modelName}/model.gltf`; const modelResponse = await fetch(modelUrl); if (modelResponse.ok) { const contentType = modelResponse.headers.get("content-type") || ""; if ( contentType.includes("gltf") || contentType.includes("json") || contentType.includes("model") ) { const text = await modelResponse.text(); if ( text.includes('"glTF"') || text.includes('"scene"') || text.includes('"nodes"') ) { models.set(modelName, modelUrl); } else { console.warn( `Invalid GLTF content for ${modelName}:`, text.substring(0, 100), ); } } else { console.warn( `Invalid Content-Type for ${modelName}:`, contentType, ); } } } catch { /* empty */ } } console.log("Loaded models:", Array.from(models.keys())); setSceneData({ mapNodes, models }); setHasMapJson(true); } catch (error) { console.error("Error loading map data:", error); setHasMapJson(false); } finally { setIsMapLoading(false); } }; loadMapData(); }, []); const handleFolderUpload = async ( event: React.ChangeEvent, ): Promise => { const files = event.target.files; if (!files) return; const fileMap = new Map(); for (const file of Array.from(files)) { const webkitRelativePath = (file as File & { webkitRelativePath?: string }).webkitRelativePath || "/" + file.name; fileMap.set(webkitRelativePath, file); } const mapFile = fileMap.get("/map.json"); if (!mapFile) { alert("Fichier map.json manquant à la racine du dossier"); return; } try { const mapText = await mapFile.text(); const mapNodes = JSON.parse(mapText); const models = new Map(); for (const [path, file] of fileMap.entries()) { const modelMatch = path && path.match(/^\/models\/(.+)\/model\.gltf$/); if (modelMatch && modelMatch[1]) { const modelName = modelMatch[1]; const blobUrl = URL.createObjectURL(file); models.set(modelName, blobUrl); } } setSceneData({ mapNodes, models }); setHasMapJson(true); } catch (error) { console.error("Error processing upload:", error); alert("Erreur lors du traitement du dossier"); } }; if (isMapLoading) { return (

Chargement de l'éditeur...

Vérification de map.json dans public/

); } if (!hasMapJson) { return (

Erreur : map.json introuvable

Le fichier map.json est requis dans le dossier public/.

Télécharger un dossier contenant map.json

Structure requise :

                public/ ├── map.json (à la racine) └── models/
                ├── arbre/ │ └── model.glb ├── building/ │ └── model.glb └── ...
              
); } return (
{ gl.setClearColor("#1e293b"); }} > {/* EditorControls rendu en dehors du Canvas (HTML overlay) */} {sceneData && ( )}
); }