diff --git a/eslint.config.js b/eslint.config.js index cd7ef18..75d3c46 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -19,8 +19,5 @@ export default defineConfig([ ecmaVersion: 2020, globals: globals.browser, }, - rules: { - "react-hooks/set-state-in-effect": "off", - }, }, ]); diff --git a/src/data/debugConfig.ts b/src/data/debugConfig.ts index 6f5e4fc..618ddc6 100644 --- a/src/data/debugConfig.ts +++ b/src/data/debugConfig.ts @@ -2,8 +2,6 @@ export const INTERACTION_DEBUG_SPHERE_SEGMENTS = 16; export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15"; export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25; -export const MAP_DEBUG_BOX_HELPER_COLOR = 0x00ff88; - export const DEBUG_CAMERA_DAMPING_FACTOR = 0.05; export const DEBUG_CAMERA_MIN_DISTANCE = 100; export const DEBUG_CAMERA_MAX_DISTANCE = 1000; diff --git a/src/features/editor/controls/FlyController.tsx b/src/features/editor/controls/FlyController.tsx index 41da391..329b415 100644 --- a/src/features/editor/controls/FlyController.tsx +++ b/src/features/editor/controls/FlyController.tsx @@ -4,12 +4,14 @@ import { useCallback, forwardRef, useImperativeHandle, + type ElementRef, } from "react"; import { useFrame, useThree } from "@react-three/fiber"; import { OrbitControls } from "@react-three/drei"; -import type { OrbitControls as OrbitControlsType } from "three-stdlib"; import * as THREE from "three"; +type OrbitControlsRef = ElementRef; + interface FlyControllerProps { speed?: number; verticalSpeed?: number; @@ -17,8 +19,8 @@ interface FlyControllerProps { disabled?: boolean; } -export interface FlyControllerRef { - controls: OrbitControlsType | null; +interface FlyControllerRef { + controls: OrbitControlsRef | null; } export const FlyController = forwardRef( @@ -29,7 +31,7 @@ export const FlyController = forwardRef( const { camera: rawCamera } = useThree(); const cameraRef = useRef(rawCamera); const keys = useRef<{ [key: string]: boolean }>({}); - const controlsRef = useRef(null); + const controlsRef = useRef(null); const lastPosition = useRef(new THREE.Vector3()); useImperativeHandle(ref, () => ({ @@ -54,13 +56,12 @@ export const FlyController = forwardRef( }, [handleKeyDown, handleKeyUp]); useFrame((_, delta) => { - // En mode disabled: ZQSD désactivé, on garde que OrbitControls + // Disabled mode keeps OrbitControls active without keyboard movement. if (disabled) { return; } - // ZQSD (AZERTY): Z=forward, S=backward, Q=left, D=right - // Support aussi QWERTY et flèches + // Supports AZERTY, QWERTY, and arrow-key movement. const isForward = keys.current["KeyW"] || keys.current["KeyZ"] || keys.current["ArrowUp"]; const isBackward = keys.current["KeyS"] || keys.current["ArrowDown"]; @@ -89,7 +90,7 @@ export const FlyController = forwardRef( cameraRef.current.position.add(direction); } - // Space = monter, Shift = descendre + // Space moves up; Shift moves down. if (keys.current["Space"]) { cameraRef.current.position.y += verticalSpeed * delta; } diff --git a/src/features/editor/hooks/useEditorHistory.ts b/src/features/editor/hooks/useEditorHistory.ts index 4ad3ea4..275bcad 100644 --- a/src/features/editor/hooks/useEditorHistory.ts +++ b/src/features/editor/hooks/useEditorHistory.ts @@ -11,8 +11,11 @@ interface ObjectTransform { class HistoryManager { private history: ObjectTransform[][] = []; private currentIndex = -1; + private maxSize: number; - constructor(private maxSize = 50) {} + constructor(maxSize = 50) { + this.maxSize = maxSize; + } saveSnapshot(objects: ObjectTransform[]): void { if (this.currentIndex < this.history.length - 1) { diff --git a/src/features/editor/scene/EditorMap.tsx b/src/features/editor/scene/EditorMap.tsx index c60fbb5..16596a0 100644 --- a/src/features/editor/scene/EditorMap.tsx +++ b/src/features/editor/scene/EditorMap.tsx @@ -1,5 +1,6 @@ import { useMemo, useRef, useEffect, useState } from "react"; import { Grid, TransformControls, useGLTF } from "@react-three/drei"; +import type { ThreeEvent } from "@react-three/fiber"; import * as THREE from "three"; import type { SceneData, MapNode, TransformMode } from "@/types/editor"; @@ -16,6 +17,53 @@ interface EditorMapProps { onNodeTransform: (nodeIndex: number, transform: MapNode) => void; } +type EditorNodeObjectRef = React.RefObject>; + +interface EditorNodeCommonProps { + index: number; + node: MapNode; + isSelected: boolean; + isHovered: boolean; + objectsMapRef: EditorNodeObjectRef; + onSelectNode: (index: number | null) => void; + onHoverNode: (index: number | null) => void; +} + +function applyNodeTransform(object: THREE.Object3D, node: MapNode): void { + object.position.set(...node.position); + object.rotation.set(...node.rotation); + object.scale.set(...node.scale); +} + +function useRegisteredEditorNode( + objectRef: React.RefObject, + index: number, + node: MapNode, + objectsMapRef: EditorNodeObjectRef, +): void { + useEffect(() => { + const object = objectRef.current; + if (object) { + applyNodeTransform(object, node); + object.userData = { nodeIndex: index, nodeName: node.name }; + objectsMapRef.current.set(index, object); + } + + const currentMap = objectsMapRef.current; + const currentIndex = index; + return () => { + currentMap.delete(currentIndex); + }; + }, [index, node, objectRef, objectsMapRef]); + + useEffect(() => { + const object = objectRef.current; + if (object) { + applyNodeTransform(object, node); + } + }, [node, objectRef]); +} + export function EditorMap({ sceneData, selectedNodeIndex, @@ -82,8 +130,8 @@ export function EditorMap({ { - (e as { stopPropagation?: () => void }).stopPropagation?.(); + onClick={(e: ThreeEvent) => { + e.stopPropagation(); onSelectNode(null); }} > @@ -142,68 +190,30 @@ function EditorModelNode({ objectsMapRef, onSelectNode, onHoverNode, -}: { - index: number; - node: MapNode; +}: EditorNodeCommonProps & { modelUrl: string; - isSelected: boolean; - isHovered: boolean; - objectsMapRef: React.RefObject>; - onSelectNode: (index: number | null) => void; - onHoverNode: (index: number | null) => void; }) { const groupRef = useRef(null); const { scene } = useGLTF(modelUrl); const sceneInstance = useMemo(() => scene.clone(true), [scene]); - - useEffect(() => { - if (groupRef.current) { - groupRef.current.position.set(...node.position); - groupRef.current.rotation.set(...node.rotation); - groupRef.current.scale.set(...node.scale); - groupRef.current.userData = { nodeIndex: index, nodeName: node.name }; - objectsMapRef.current.set(index, groupRef.current); - } - const currentMap = objectsMapRef.current; - const currentIndex = index; - return () => { - currentMap.delete(currentIndex); - }; - }, [ - index, - node.name, - node.position, - node.rotation, - node.scale, - objectsMapRef, - ]); - - useEffect(() => { - if (groupRef.current) { - groupRef.current.position.set(...node.position); - groupRef.current.rotation.set(...node.rotation); - groupRef.current.scale.set(...node.scale); - } - }, [node.position, node.rotation, node.scale]); + useRegisteredEditorNode(groupRef, index, node, objectsMapRef); useEffect(() => { if (!groupRef.current) return; groupRef.current.traverse((child) => { - if ((child as THREE.Mesh).isMesh) { - const mesh = child as THREE.Mesh; - if ( - mesh.material && - mesh.material instanceof THREE.MeshStandardMaterial - ) { - if (isSelected) { - mesh.material = mesh.material.clone(); - (mesh.material as THREE.MeshStandardMaterial).color.set("#ffffff"); - } else if (isHovered) { - mesh.material = mesh.material.clone(); - (mesh.material as THREE.MeshStandardMaterial).color.set("#b8b8b8"); - } + if (!(child instanceof THREE.Mesh)) { + return; + } + + if (child.material instanceof THREE.MeshStandardMaterial) { + if (isSelected) { + child.material = child.material.clone(); + child.material.color.set("#ffffff"); + } else if (isHovered) { + child.material = child.material.clone(); + child.material.color.set("#b8b8b8"); } } }); @@ -216,16 +226,16 @@ function EditorModelNode({ position={node.position} rotation={node.rotation} scale={node.scale} - onClick={(e: unknown) => { - (e as { stopPropagation?: () => void }).stopPropagation?.(); + onClick={(e: ThreeEvent) => { + e.stopPropagation(); onSelectNode(index); }} - onPointerEnter={(e: unknown) => { - (e as { stopPropagation?: () => void }).stopPropagation?.(); + onPointerEnter={(e: ThreeEvent) => { + e.stopPropagation(); onHoverNode(index); }} - onPointerLeave={(e: unknown) => { - (e as { stopPropagation?: () => void }).stopPropagation?.(); + onPointerLeave={(e: ThreeEvent) => { + e.stopPropagation(); onHoverNode(null); }} /> @@ -240,46 +250,9 @@ function EditorFallbackNode({ objectsMapRef, onSelectNode, onHoverNode, -}: { - index: number; - node: MapNode; - isSelected: boolean; - isHovered: boolean; - objectsMapRef: React.RefObject>; - onSelectNode: (index: number | null) => void; - onHoverNode: (index: number | null) => void; -}) { +}: EditorNodeCommonProps) { const meshRef = useRef(null); - - useEffect(() => { - if (meshRef.current) { - meshRef.current.position.set(...node.position); - meshRef.current.rotation.set(...node.rotation); - meshRef.current.scale.set(...node.scale); - meshRef.current.userData = { nodeIndex: index, nodeName: node.name }; - objectsMapRef.current.set(index, meshRef.current); - } - const currentMap = objectsMapRef.current; - const currentIndex = index; - return () => { - currentMap.delete(currentIndex); - }; - }, [ - index, - node.name, - node.position, - node.rotation, - node.scale, - objectsMapRef, - ]); - - useEffect(() => { - if (meshRef.current) { - meshRef.current.position.set(...node.position); - meshRef.current.rotation.set(...node.rotation); - meshRef.current.scale.set(...node.scale); - } - }, [node.position, node.rotation, node.scale]); + useRegisteredEditorNode(meshRef, index, node, objectsMapRef); const color = isSelected ? "#ffffff" : isHovered ? "#b8b8b8" : "#6f6f6f"; @@ -289,16 +262,16 @@ function EditorFallbackNode({ position={node.position} rotation={node.rotation} scale={node.scale} - onClick={(e: unknown) => { - (e as { stopPropagation?: () => void }).stopPropagation?.(); + onClick={(e: ThreeEvent) => { + e.stopPropagation(); onSelectNode(index); }} - onPointerEnter={(e: unknown) => { - (e as { stopPropagation?: () => void }).stopPropagation?.(); + onPointerEnter={(e: ThreeEvent) => { + e.stopPropagation(); onHoverNode(index); }} - onPointerLeave={(e: unknown) => { - (e as { stopPropagation?: () => void }).stopPropagation?.(); + onPointerLeave={(e: ThreeEvent) => { + e.stopPropagation(); onHoverNode(null); }} > diff --git a/src/types/editor.ts b/src/types/editor.ts index 712ea05..89b601f 100644 --- a/src/types/editor.ts +++ b/src/types/editor.ts @@ -1,9 +1,11 @@ +import type { Vector3Tuple } from "@/types/3d"; + export interface MapNode { name: string; type: string; - position: [number, number, number]; - rotation: [number, number, number]; - scale: [number, number, number]; + position: Vector3Tuple; + rotation: Vector3Tuple; + scale: Vector3Tuple; } export interface SceneData { diff --git a/src/types/interaction.ts b/src/types/interaction.ts index 986bed9..5e65bf6 100644 --- a/src/types/interaction.ts +++ b/src/types/interaction.ts @@ -1,6 +1,6 @@ export type InteractableKind = "grab" | "trigger"; -export interface TriggerInteractableHandle { +interface TriggerInteractableHandle { kind: "trigger"; label: string; onPress: () => void; diff --git a/src/types/logger.ts b/src/types/logger.ts index 3eb022a..7384c4c 100644 --- a/src/types/logger.ts +++ b/src/types/logger.ts @@ -1,6 +1,6 @@ export type LogLevel = "debug" | "info" | "warn" | "error"; -export type LogValue = +type LogValue = | string | number | boolean diff --git a/src/utils/editor/loadEditorScene.ts b/src/utils/editor/loadEditorScene.ts index c5f6e8f..1ca8896 100644 --- a/src/utils/editor/loadEditorScene.ts +++ b/src/utils/editor/loadEditorScene.ts @@ -30,9 +30,7 @@ export async function createSceneDataFromFiles( } function getProjectRelativePath(file: File): string { - const relativePath = - (file as File & { webkitRelativePath?: string }).webkitRelativePath || - file.name; + const relativePath = file.webkitRelativePath || file.name; if (!relativePath.includes("/")) { return `/${relativePath}`; diff --git a/src/utils/loadMapSceneData.ts b/src/utils/loadMapSceneData.ts index 1aff429..479e3f5 100644 --- a/src/utils/loadMapSceneData.ts +++ b/src/utils/loadMapSceneData.ts @@ -14,7 +14,7 @@ export async function loadMapSceneData(): Promise { return createSceneData(mapNodes); } -export async function createSceneData(mapNodes: MapNode[]): Promise { +async function createSceneData(mapNodes: MapNode[]): Promise { const models = await loadMapModelUrls(mapNodes); return { mapNodes, models }; } @@ -28,16 +28,12 @@ async function loadMapModelUrls( for (const modelName of uniqueModelNames) { const modelUrl = `/models/${modelName}/${MODEL_FILE_NAME}`; - try { - const modelResponse = await fetch(modelUrl); - if (!modelResponse.ok) continue; + const modelResponse = await fetch(modelUrl); + if (!modelResponse.ok) continue; - const text = await modelResponse.text(); - if (isGltfContent(text)) { - models.set(modelName, modelUrl); - } - } catch { - /* Missing models are expected while editing incomplete maps. */ + const text = await modelResponse.text(); + if (isGltfContent(text)) { + models.set(modelName, modelUrl); } } diff --git a/test-editor.html b/test-editor.html deleted file mode 100644 index 83e86f8..0000000 --- a/test-editor.html +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - La-Fabrik Editor - Test Page - - - -

La-Fabrik Editor Integration

- -
-

✅ Integration Status: COMPLETED

-

- L'éditeur est maintenant intégré à la route /editor du - projet La-Fabrik. -

-
- - - -

Fonctionnalités de l'éditeur

-
    -
  • - Routing React : React Router pour naviguer entre jeu et - éditeur -
  • -
  • - Chargement automatique : Recherche de - map.json dans public/ -
  • -
  • - Upload de dossier : Si pas de map.json, possibilité - d'upload -
  • -
  • - Visualisation 3D : Canvas Three.js avec SceneData -
  • -
  • - Control de caméra : -
      -
    • Mode debug : OrbitControls (rotation/pan/zoom)
    • -
    • Mode player : FPS controller custom (WASD/ZQSD + souris)
    • -
    -
  • -
  • Sélection d'objets : Click sur cubes/modèles
  • -
  • Transformations : Panneau avec boutons T/R/S
  • -
  • Undo/Redo : Ctrl+Z / Ctrl+Y (compte affiché)
  • -
  • - Export JSON : Bouton pour exporter map.json modifié -
  • -
- -

Structure de fichiers

-
-src/components/editor/
-├── EditorPage.tsx         # Page route /editor
-├── EditorViewer.tsx      # Composant principal 3D
-├── EditorCamera.tsx      # Caméra (OrbitControls + useCameraMode)
-├── EditorFPSController.tsx # Controller FPS custom
-├── MapViewer.tsx         # Visualisation map.json + modèles
-├── EditorControls.tsx    # Panneau latéral UI
-├── types.ts              # Types MapNode, SceneData, etc.
-└── EditorPage.css        # Styles scoped
-    
- -

À tester

-
    -
  1. - Accéder à /editor - devrait montrer erreur "map.json - introuvable" -
  2. -
  3. Uploader un dossier test avec map.json + models/
  4. -
  5. Tester la visualisation 3D (cubes de test existent dans map.json)
  6. -
  7. Tester le mode player (WASD + souris)
  8. -
  9. Tester les transformations T/R/S
  10. -
  11. Tester Undo/Redo (Ctrl+Z / Ctrl+Y)
  12. -
  13. Tester export JSON (bouton "Export JSON")
  14. -
  15. Naviguer vers / - retour au jeu original
  16. -
- -