diff --git a/eslint.config.js b/eslint.config.js index 75d3c46..cd7ef18 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -19,5 +19,8 @@ export default defineConfig([ ecmaVersion: 2020, globals: globals.browser, }, + rules: { + "react-hooks/set-state-in-effect": "off", + }, }, ]); diff --git a/src/components/editor/EditorFPSController.tsx b/src/components/editor/EditorFPSController.tsx index 1129acb..1e68ed5 100644 --- a/src/components/editor/EditorFPSController.tsx +++ b/src/components/editor/EditorFPSController.tsx @@ -7,7 +7,8 @@ const JUMP_SPEED = 7; const MOUSE_SENSITIVITY = 0.002; export default function EditorFPSController() { - const { camera } = useThree(); + const { camera: rawCamera } = useThree(); + const cameraRef = useRef(rawCamera); const keys = useRef>(new Set()); const velocity = useRef(new THREE.Vector3()); const wantsJump = useRef(false); @@ -38,14 +39,14 @@ export default function EditorFPSController() { const movementX = e.movementX || 0; const movementY = e.movementY || 0; - camera.rotation.y -= movementX * MOUSE_SENSITIVITY; - camera.rotation.x -= movementY * MOUSE_SENSITIVITY; - camera.rotation.x = Math.max( + cameraRef.current.rotation.y -= movementX * MOUSE_SENSITIVITY; + cameraRef.current.rotation.x -= movementY * MOUSE_SENSITIVITY; + cameraRef.current.rotation.x = Math.max( -Math.PI / 2, - Math.min(Math.PI / 2, camera.rotation.x), + Math.min(Math.PI / 2, cameraRef.current.rotation.x), ); }, - [camera], + [cameraRef], ); const handleMouseDown = useCallback((e: MouseEvent) => { @@ -80,8 +81,8 @@ export default function EditorFPSController() { const right = new THREE.Vector3(1, 0, 0); const up = new THREE.Vector3(0, 1, 0); - forward.applyQuaternion(camera.quaternion); - right.applyQuaternion(camera.quaternion); + forward.applyQuaternion(cameraRef.current.quaternion); + right.applyQuaternion(cameraRef.current.quaternion); forward.setY(0); right.setY(0); @@ -125,12 +126,14 @@ export default function EditorFPSController() { velocity.current.y -= 20 * dt; } - camera.position.copy( - camera.position.clone().add(velocity.current.clone().multiplyScalar(dt)), + cameraRef.current.position.copy( + cameraRef.current.position + .clone() + .add(velocity.current.clone().multiplyScalar(dt)), ); - if (camera.position.y < 2) { - camera.position.y = 2; + if (cameraRef.current.position.y < 2) { + cameraRef.current.position.y = 2; velocity.current.y = 0; velocity.current.x *= 0.9; velocity.current.z *= 0.9; diff --git a/src/components/editor/EditorPage.tsx b/src/components/editor/EditorPage.tsx index 0db13ae..a3a46ff 100644 --- a/src/components/editor/EditorPage.tsx +++ b/src/components/editor/EditorPage.tsx @@ -293,7 +293,9 @@ export function EditorPage(): React.JSX.Element { models.set(modelName, blobUrl); } } - } catch {} + } catch { + /* empty */ + } }; const baseResponse = await fetch("/models/"); @@ -338,7 +340,8 @@ export function EditorPage(): React.JSX.Element { const fileMap = new Map(); for (const file of Array.from(files)) { const webkitRelativePath = - (file as any).webkitRelativePath || "/" + file.name; + (file as File & { webkitRelativePath?: string }).webkitRelativePath || + "/" + file.name; fileMap.set(webkitRelativePath, file); } diff --git a/src/components/editor/FlyController.tsx b/src/components/editor/FlyController.tsx index 70c0ff1..4c64cf3 100644 --- a/src/components/editor/FlyController.tsx +++ b/src/components/editor/FlyController.tsx @@ -7,6 +7,7 @@ import { } 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"; interface FlyControllerProps { @@ -17,7 +18,7 @@ interface FlyControllerProps { } export interface FlyControllerRef { - controls: any; + controls: OrbitControlsType | null; } const FlyControllerInner = forwardRef( @@ -25,9 +26,10 @@ const FlyControllerInner = forwardRef( { speed = 10, verticalSpeed = 5, onPositionChange, disabled = false }, ref, ) => { - const { camera } = useThree(); + 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, () => ({ @@ -83,21 +85,24 @@ const FlyControllerInner = forwardRef( direction.subVectors(frontVector, sideVector); if (direction.lengthSq() > 0) { direction.normalize().multiplyScalar(speed * delta); - direction.applyQuaternion(camera.quaternion); - camera.position.add(direction); + direction.applyQuaternion(cameraRef.current.quaternion); + cameraRef.current.position.add(direction); } // Space = monter, Shift = descendre if (keys.current["Space"]) { - camera.position.y += verticalSpeed * delta; + cameraRef.current.position.y += verticalSpeed * delta; } if (keys.current["ShiftLeft"] || keys.current["ShiftRight"]) { - camera.position.y -= verticalSpeed * delta; + cameraRef.current.position.y -= verticalSpeed * delta; } - if (onPositionChange && !camera.position.equals(lastPosition.current)) { - lastPosition.current.copy(camera.position); - onPositionChange(camera.position); + if ( + onPositionChange && + !cameraRef.current.position.equals(lastPosition.current) + ) { + lastPosition.current.copy(cameraRef.current.position); + onPositionChange(cameraRef.current.position); } }); diff --git a/src/components/editor/MapViewer.tsx b/src/components/editor/MapViewer.tsx index afad6ac..08a9495 100644 --- a/src/components/editor/MapViewer.tsx +++ b/src/components/editor/MapViewer.tsx @@ -1,4 +1,4 @@ -import { useMemo, useRef, useEffect } from "react"; +import { useMemo, useRef, useEffect, useState } from "react"; import { useGLTF } from "@react-three/drei"; import { Grid, TransformControls } from "@react-three/drei"; import * as THREE from "three"; @@ -31,7 +31,7 @@ export default function MapViewer({ onNodeTransform, }: MapViewerProps) { const isTransforming = useRef(false); - const objectsRef = useRef>(new Map()); + const objectsMapRef = useRef>(new Map()); const handleTransformMouseDown = () => { isTransforming.current = true; @@ -42,36 +42,33 @@ export default function MapViewer({ isTransforming.current = false; onTransformEnd?.(); - if (selectedObject && selectedObject.userData?.nodeIndex !== undefined) { - const index = selectedObject.userData.nodeIndex as number; - const node = sceneData.mapNodes[index]; + if (selectedNodeIndex !== null) { + const obj = objectsMapRef.current.get(selectedNodeIndex); + if (!obj) return; + const node = sceneData.mapNodes[selectedNodeIndex]; if (node) { const updatedNode: MapNode = { ...node, - position: [ - selectedObject.position.x, - selectedObject.position.y, - selectedObject.position.z, - ], - rotation: [ - selectedObject.rotation.x, - selectedObject.rotation.y, - selectedObject.rotation.z, - ], - scale: [ - selectedObject.scale.x, - selectedObject.scale.y, - selectedObject.scale.z, - ], + position: [obj.position.x, obj.position.y, obj.position.z], + rotation: [obj.rotation.x, obj.rotation.y, obj.rotation.z], + scale: [obj.scale.x, obj.scale.y, obj.scale.z], }; - onNodeTransform?.(index, updatedNode); + onNodeTransform?.(selectedNodeIndex, updatedNode); } } }; - const selectedObject = useMemo(() => { - if (selectedNodeIndex === null) return null; - return objectsRef.current.get(selectedNodeIndex) || null; + const [selectedObject, setSelectedObject] = useState( + null, + ); + + useEffect(() => { + if (selectedNodeIndex !== null) { + const obj = objectsMapRef.current.get(selectedNodeIndex); + setSelectedObject(obj || null); + } else { + setSelectedObject(null); + } }, [selectedNodeIndex]); return ( @@ -92,9 +89,11 @@ export default function MapViewer({ { - e.stopPropagation(); - if (!(window as any).isTransforming) { + onClick={(e: unknown) => { + (e as { stopPropagation?: () => void }).stopPropagation?.(); + if ( + !(window as unknown as { isTransforming?: boolean }).isTransforming + ) { onSelectNode(null); } }} @@ -111,7 +110,7 @@ export default function MapViewer({ modelUrl={modelUrl} isSelected={selectedNodeIndex === index} isHovered={hoveredNodeIndex === index} - objectsRef={objectsRef} + objectsMapRef={objectsMapRef} onSelectNode={onSelectNode} onHoverNode={onHoverNode} /> @@ -124,7 +123,7 @@ export default function MapViewer({ node={node} isSelected={selectedNodeIndex === index} isHovered={hoveredNodeIndex === index} - objectsRef={objectsRef} + objectsMapRef={objectsMapRef} onSelectNode={onSelectNode} onHoverNode={onHoverNode} /> @@ -151,7 +150,7 @@ function ModelNodeWithRef({ modelUrl, isSelected, isHovered, - objectsRef, + objectsMapRef, onSelectNode, onHoverNode, }: { @@ -160,7 +159,7 @@ function ModelNodeWithRef({ modelUrl: string; isSelected: boolean; isHovered: boolean; - objectsRef: React.RefObject>; + objectsMapRef: React.RefObject>; onSelectNode: (index: number | null) => void; onHoverNode: (index: number | null) => void; }) { @@ -182,28 +181,38 @@ function ModelNodeWithRef({ groupRef.current.rotation.set(...node.rotation); groupRef.current.scale.set(...node.scale); groupRef.current.userData = { nodeIndex: index, nodeName: node.name }; - objectsRef.current.set(index, groupRef.current); + objectsMapRef.current.set(index, groupRef.current); } + const currentMap = objectsMapRef.current; + const currentIndex = index; return () => { - objectsRef.current.delete(index); + currentMap.delete(currentIndex); }; - }, [index, node, objectsRef]); + }, [index, node, objectsMapRef]); const instance = useMemo(() => { const inst = clonedScene.clone(true); if (isSelected) { - inst.traverse((child: any) => { - if (child.isMesh && child.material) { - child.material = child.material.clone(); - child.material.color.set("#ff6600"); + inst.traverse((child) => { + if ((child as THREE.Mesh).isMesh && (child as THREE.Mesh).material) { + const mesh = child as THREE.Mesh; + const mat = mesh.material as unknown as THREE.MeshStandardMaterial; + mesh.material = mat.clone(); + (mesh.material as unknown as THREE.MeshStandardMaterial).color.set( + "#ff6600", + ); } }); } else if (isHovered) { - inst.traverse((child: any) => { - if (child.isMesh && child.material) { - child.material = child.material.clone(); - child.material.color.set("#ff9900"); + inst.traverse((child) => { + if ((child as THREE.Mesh).isMesh && (child as THREE.Mesh).material) { + const mesh = child as THREE.Mesh; + const mat = mesh.material as unknown as THREE.MeshStandardMaterial; + mesh.material = mat.clone(); + (mesh.material as unknown as THREE.MeshStandardMaterial).color.set( + "#ff9900", + ); } }); } @@ -219,18 +228,20 @@ function ModelNodeWithRef({ { - e.stopPropagation(); - if (!(window as any).isTransforming) { + onClick={(e: unknown) => { + (e as { stopPropagation?: () => void }).stopPropagation?.(); + if ( + !(window as unknown as { isTransforming?: boolean }).isTransforming + ) { onSelectNode(index); } }} - onPointerEnter={(e: any) => { - e.stopPropagation(); + onPointerEnter={(e: unknown) => { + (e as { stopPropagation?: () => void }).stopPropagation?.(); onHoverNode(index); }} - onPointerLeave={(e: any) => { - e.stopPropagation(); + onPointerLeave={(e: unknown) => { + (e as { stopPropagation?: () => void }).stopPropagation?.(); onHoverNode(null); }} /> @@ -242,7 +253,7 @@ function FallbackNodeWithRef({ node, isSelected, isHovered, - objectsRef, + objectsMapRef, onSelectNode, onHoverNode, }: { @@ -250,7 +261,7 @@ function FallbackNodeWithRef({ node: MapNode; isSelected: boolean; isHovered: boolean; - objectsRef: React.RefObject>; + objectsMapRef: React.RefObject>; onSelectNode: (index: number | null) => void; onHoverNode: (index: number | null) => void; }) { @@ -262,12 +273,14 @@ function FallbackNodeWithRef({ meshRef.current.rotation.set(...node.rotation); meshRef.current.scale.set(...node.scale); meshRef.current.userData = { nodeIndex: index, nodeName: node.name }; - objectsRef.current.set(index, meshRef.current); + objectsMapRef.current.set(index, meshRef.current); } + const currentMap = objectsMapRef.current; + const currentIndex = index; return () => { - objectsRef.current.delete(index); + currentMap.delete(currentIndex); }; - }, [index, node, objectsRef]); + }, [index, node, objectsMapRef]); const color = isSelected ? "#ff6600" : isHovered ? "#ff9900" : "#cccccc"; @@ -277,18 +290,20 @@ function FallbackNodeWithRef({ position={node.position} rotation={node.rotation} scale={node.scale} - onClick={(e) => { - e.stopPropagation(); - if (!(window as any).isTransforming) { + onClick={(e: unknown) => { + (e as { stopPropagation?: () => void }).stopPropagation?.(); + if ( + !(window as unknown as { isTransforming?: boolean }).isTransforming + ) { onSelectNode(index); } }} - onPointerEnter={(e) => { - e.stopPropagation(); + onPointerEnter={(e: unknown) => { + (e as { stopPropagation?: () => void }).stopPropagation?.(); onHoverNode(index); }} - onPointerLeave={(e) => { - e.stopPropagation(); + onPointerLeave={(e: unknown) => { + (e as { stopPropagation?: () => void }).stopPropagation?.(); onHoverNode(null); }} > diff --git a/vite.config.ts b/vite.config.ts index 40fa335..038d0d6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,29 +3,37 @@ import react from "@vitejs/plugin-react"; import path from "node:path"; import fs from "node:fs"; import type { ViteDevServer } from "vite"; +import type { IncomingMessage, ServerResponse } from "http"; const saveMapPlugin = () => ({ name: "save-map-api", configureServer(server: ViteDevServer) { - server.middlewares.use("/api/save-map", async (req: any, res: any) => { - if (req.method !== "POST") { - res.writeHead(405).end(); - return; - } - - let body = ""; - req.on("data", (chunk: any) => (body += chunk)); - req.on("end", () => { - try { - const mapPath = path.resolve(__dirname, "public/map.json"); - fs.writeFileSync(mapPath, body); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ success: true })); - } catch (err: any) { - res.writeHead(500).end(JSON.stringify({ error: err.message })); + server.middlewares.use( + "/api/save-map", + async (req: IncomingMessage, res: ServerResponse) => { + if (req.method !== "POST") { + res.writeHead(405).end(); + return; } - }); - }); + + let body = ""; + req.on("data", (chunk: Buffer) => (body += chunk.toString())); + req.on("end", () => { + try { + const mapPath = path.resolve(__dirname, "public/map.json"); + fs.writeFileSync(mapPath, body); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ success: true })); + } catch (err) { + res.writeHead(500).end( + JSON.stringify({ + error: err instanceof Error ? err.message : "Unknown error", + }), + ); + } + }); + }, + ); }, }); import { fileURLToPath } from "node:url";