cleaaning

This commit is contained in:
2026-04-28 10:42:57 +02:00
parent a259c3d2e2
commit af35150452
11 changed files with 104 additions and 260 deletions
-3
View File
@@ -19,8 +19,5 @@ export default defineConfig([
ecmaVersion: 2020, ecmaVersion: 2020,
globals: globals.browser, globals: globals.browser,
}, },
rules: {
"react-hooks/set-state-in-effect": "off",
},
}, },
]); ]);
-2
View File
@@ -2,8 +2,6 @@ export const INTERACTION_DEBUG_SPHERE_SEGMENTS = 16;
export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15"; export const INTERACTION_DEBUG_SPHERE_COLOR = "#facc15";
export const INTERACTION_DEBUG_SPHERE_OPACITY = 0.25; 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_DAMPING_FACTOR = 0.05;
export const DEBUG_CAMERA_MIN_DISTANCE = 100; export const DEBUG_CAMERA_MIN_DISTANCE = 100;
export const DEBUG_CAMERA_MAX_DISTANCE = 1000; export const DEBUG_CAMERA_MAX_DISTANCE = 1000;
@@ -4,12 +4,14 @@ import {
useCallback, useCallback,
forwardRef, forwardRef,
useImperativeHandle, useImperativeHandle,
type ElementRef,
} from "react"; } from "react";
import { useFrame, useThree } from "@react-three/fiber"; import { useFrame, useThree } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei"; import { OrbitControls } from "@react-three/drei";
import type { OrbitControls as OrbitControlsType } from "three-stdlib";
import * as THREE from "three"; import * as THREE from "three";
type OrbitControlsRef = ElementRef<typeof OrbitControls>;
interface FlyControllerProps { interface FlyControllerProps {
speed?: number; speed?: number;
verticalSpeed?: number; verticalSpeed?: number;
@@ -17,8 +19,8 @@ interface FlyControllerProps {
disabled?: boolean; disabled?: boolean;
} }
export interface FlyControllerRef { interface FlyControllerRef {
controls: OrbitControlsType | null; controls: OrbitControlsRef | null;
} }
export const FlyController = forwardRef<FlyControllerRef, FlyControllerProps>( export const FlyController = forwardRef<FlyControllerRef, FlyControllerProps>(
@@ -29,7 +31,7 @@ export const FlyController = forwardRef<FlyControllerRef, FlyControllerProps>(
const { camera: rawCamera } = useThree(); const { camera: rawCamera } = useThree();
const cameraRef = useRef(rawCamera); const cameraRef = useRef(rawCamera);
const keys = useRef<{ [key: string]: boolean }>({}); const keys = useRef<{ [key: string]: boolean }>({});
const controlsRef = useRef<OrbitControlsType | null>(null); const controlsRef = useRef<OrbitControlsRef | null>(null);
const lastPosition = useRef(new THREE.Vector3()); const lastPosition = useRef(new THREE.Vector3());
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
@@ -54,13 +56,12 @@ export const FlyController = forwardRef<FlyControllerRef, FlyControllerProps>(
}, [handleKeyDown, handleKeyUp]); }, [handleKeyDown, handleKeyUp]);
useFrame((_, delta) => { useFrame((_, delta) => {
// En mode disabled: ZQSD désactivé, on garde que OrbitControls // Disabled mode keeps OrbitControls active without keyboard movement.
if (disabled) { if (disabled) {
return; return;
} }
// ZQSD (AZERTY): Z=forward, S=backward, Q=left, D=right // Supports AZERTY, QWERTY, and arrow-key movement.
// Support aussi QWERTY et flèches
const isForward = const isForward =
keys.current["KeyW"] || keys.current["KeyZ"] || keys.current["ArrowUp"]; keys.current["KeyW"] || keys.current["KeyZ"] || keys.current["ArrowUp"];
const isBackward = keys.current["KeyS"] || keys.current["ArrowDown"]; const isBackward = keys.current["KeyS"] || keys.current["ArrowDown"];
@@ -89,7 +90,7 @@ export const FlyController = forwardRef<FlyControllerRef, FlyControllerProps>(
cameraRef.current.position.add(direction); cameraRef.current.position.add(direction);
} }
// Space = monter, Shift = descendre // Space moves up; Shift moves down.
if (keys.current["Space"]) { if (keys.current["Space"]) {
cameraRef.current.position.y += verticalSpeed * delta; cameraRef.current.position.y += verticalSpeed * delta;
} }
@@ -11,8 +11,11 @@ interface ObjectTransform {
class HistoryManager { class HistoryManager {
private history: ObjectTransform[][] = []; private history: ObjectTransform[][] = [];
private currentIndex = -1; private currentIndex = -1;
private maxSize: number;
constructor(private maxSize = 50) {} constructor(maxSize = 50) {
this.maxSize = maxSize;
}
saveSnapshot(objects: ObjectTransform[]): void { saveSnapshot(objects: ObjectTransform[]): void {
if (this.currentIndex < this.history.length - 1) { if (this.currentIndex < this.history.length - 1) {
+76 -103
View File
@@ -1,5 +1,6 @@
import { useMemo, useRef, useEffect, useState } from "react"; import { useMemo, useRef, useEffect, useState } from "react";
import { Grid, TransformControls, useGLTF } from "@react-three/drei"; import { Grid, TransformControls, useGLTF } from "@react-three/drei";
import type { ThreeEvent } from "@react-three/fiber";
import * as THREE from "three"; import * as THREE from "three";
import type { SceneData, MapNode, TransformMode } from "@/types/editor"; import type { SceneData, MapNode, TransformMode } from "@/types/editor";
@@ -16,6 +17,53 @@ interface EditorMapProps {
onNodeTransform: (nodeIndex: number, transform: MapNode) => void; onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
} }
type EditorNodeObjectRef = React.RefObject<Map<number, THREE.Object3D>>;
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<THREE.Object3D | null>,
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({ export function EditorMap({
sceneData, sceneData,
selectedNodeIndex, selectedNodeIndex,
@@ -82,8 +130,8 @@ export function EditorMap({
<axesHelper args={[10]} /> <axesHelper args={[10]} />
<group <group
onClick={(e: unknown) => { onClick={(e: ThreeEvent<MouseEvent>) => {
(e as { stopPropagation?: () => void }).stopPropagation?.(); e.stopPropagation();
onSelectNode(null); onSelectNode(null);
}} }}
> >
@@ -142,68 +190,30 @@ function EditorModelNode({
objectsMapRef, objectsMapRef,
onSelectNode, onSelectNode,
onHoverNode, onHoverNode,
}: { }: EditorNodeCommonProps & {
index: number;
node: MapNode;
modelUrl: string; modelUrl: string;
isSelected: boolean;
isHovered: boolean;
objectsMapRef: React.RefObject<Map<number, THREE.Object3D>>;
onSelectNode: (index: number | null) => void;
onHoverNode: (index: number | null) => void;
}) { }) {
const groupRef = useRef<THREE.Group>(null); const groupRef = useRef<THREE.Group>(null);
const { scene } = useGLTF(modelUrl); const { scene } = useGLTF(modelUrl);
const sceneInstance = useMemo(() => scene.clone(true), [scene]); const sceneInstance = useMemo(() => scene.clone(true), [scene]);
useRegisteredEditorNode(groupRef, index, node, objectsMapRef);
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]);
useEffect(() => { useEffect(() => {
if (!groupRef.current) return; if (!groupRef.current) return;
groupRef.current.traverse((child) => { groupRef.current.traverse((child) => {
if ((child as THREE.Mesh).isMesh) { if (!(child instanceof THREE.Mesh)) {
const mesh = child as THREE.Mesh; return;
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.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} position={node.position}
rotation={node.rotation} rotation={node.rotation}
scale={node.scale} scale={node.scale}
onClick={(e: unknown) => { onClick={(e: ThreeEvent<MouseEvent>) => {
(e as { stopPropagation?: () => void }).stopPropagation?.(); e.stopPropagation();
onSelectNode(index); onSelectNode(index);
}} }}
onPointerEnter={(e: unknown) => { onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
(e as { stopPropagation?: () => void }).stopPropagation?.(); e.stopPropagation();
onHoverNode(index); onHoverNode(index);
}} }}
onPointerLeave={(e: unknown) => { onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
(e as { stopPropagation?: () => void }).stopPropagation?.(); e.stopPropagation();
onHoverNode(null); onHoverNode(null);
}} }}
/> />
@@ -240,46 +250,9 @@ function EditorFallbackNode({
objectsMapRef, objectsMapRef,
onSelectNode, onSelectNode,
onHoverNode, onHoverNode,
}: { }: EditorNodeCommonProps) {
index: number;
node: MapNode;
isSelected: boolean;
isHovered: boolean;
objectsMapRef: React.RefObject<Map<number, THREE.Object3D>>;
onSelectNode: (index: number | null) => void;
onHoverNode: (index: number | null) => void;
}) {
const meshRef = useRef<THREE.Mesh>(null); const meshRef = useRef<THREE.Mesh>(null);
useRegisteredEditorNode(meshRef, index, node, objectsMapRef);
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]);
const color = isSelected ? "#ffffff" : isHovered ? "#b8b8b8" : "#6f6f6f"; const color = isSelected ? "#ffffff" : isHovered ? "#b8b8b8" : "#6f6f6f";
@@ -289,16 +262,16 @@ function EditorFallbackNode({
position={node.position} position={node.position}
rotation={node.rotation} rotation={node.rotation}
scale={node.scale} scale={node.scale}
onClick={(e: unknown) => { onClick={(e: ThreeEvent<MouseEvent>) => {
(e as { stopPropagation?: () => void }).stopPropagation?.(); e.stopPropagation();
onSelectNode(index); onSelectNode(index);
}} }}
onPointerEnter={(e: unknown) => { onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
(e as { stopPropagation?: () => void }).stopPropagation?.(); e.stopPropagation();
onHoverNode(index); onHoverNode(index);
}} }}
onPointerLeave={(e: unknown) => { onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
(e as { stopPropagation?: () => void }).stopPropagation?.(); e.stopPropagation();
onHoverNode(null); onHoverNode(null);
}} }}
> >
+5 -3
View File
@@ -1,9 +1,11 @@
import type { Vector3Tuple } from "@/types/3d";
export interface MapNode { export interface MapNode {
name: string; name: string;
type: string; type: string;
position: [number, number, number]; position: Vector3Tuple;
rotation: [number, number, number]; rotation: Vector3Tuple;
scale: [number, number, number]; scale: Vector3Tuple;
} }
export interface SceneData { export interface SceneData {
+1 -1
View File
@@ -1,6 +1,6 @@
export type InteractableKind = "grab" | "trigger"; export type InteractableKind = "grab" | "trigger";
export interface TriggerInteractableHandle { interface TriggerInteractableHandle {
kind: "trigger"; kind: "trigger";
label: string; label: string;
onPress: () => void; onPress: () => void;
+1 -1
View File
@@ -1,6 +1,6 @@
export type LogLevel = "debug" | "info" | "warn" | "error"; export type LogLevel = "debug" | "info" | "warn" | "error";
export type LogValue = type LogValue =
| string | string
| number | number
| boolean | boolean
+1 -3
View File
@@ -30,9 +30,7 @@ export async function createSceneDataFromFiles(
} }
function getProjectRelativePath(file: File): string { function getProjectRelativePath(file: File): string {
const relativePath = const relativePath = file.webkitRelativePath || file.name;
(file as File & { webkitRelativePath?: string }).webkitRelativePath ||
file.name;
if (!relativePath.includes("/")) { if (!relativePath.includes("/")) {
return `/${relativePath}`; return `/${relativePath}`;
+1 -5
View File
@@ -14,7 +14,7 @@ export async function loadMapSceneData(): Promise<SceneData | null> {
return createSceneData(mapNodes); return createSceneData(mapNodes);
} }
export async function createSceneData(mapNodes: MapNode[]): Promise<SceneData> { async function createSceneData(mapNodes: MapNode[]): Promise<SceneData> {
const models = await loadMapModelUrls(mapNodes); const models = await loadMapModelUrls(mapNodes);
return { mapNodes, models }; return { mapNodes, models };
} }
@@ -28,7 +28,6 @@ async function loadMapModelUrls(
for (const modelName of uniqueModelNames) { for (const modelName of uniqueModelNames) {
const modelUrl = `/models/${modelName}/${MODEL_FILE_NAME}`; const modelUrl = `/models/${modelName}/${MODEL_FILE_NAME}`;
try {
const modelResponse = await fetch(modelUrl); const modelResponse = await fetch(modelUrl);
if (!modelResponse.ok) continue; if (!modelResponse.ok) continue;
@@ -36,9 +35,6 @@ async function loadMapModelUrls(
if (isGltfContent(text)) { if (isGltfContent(text)) {
models.set(modelName, modelUrl); models.set(modelName, modelUrl);
} }
} catch {
/* Missing models are expected while editing incomplete maps. */
}
} }
return models; return models;
-124
View File
@@ -1,124 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>La-Fabrik Editor - Test Page</title>
<style>
body {
font-family: system-ui, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 20px;
line-height: 1.6;
}
.links {
display: flex;
gap: 20px;
margin: 30px 0;
}
.link {
padding: 15px 30px;
background: #ff6600;
color: white;
text-decoration: none;
border-radius: 8px;
font-weight: bold;
transition: background 0.2s;
}
.link:hover {
background: #ff8533;
}
.info {
background: #f0f0f0;
padding: 20px;
border-radius: 8px;
margin: 20px 0;
}
code {
background: #e0e0e0;
padding: 2px 6px;
border-radius: 4px;
font-family: "Courier New", monospace;
}
</style>
</head>
<body>
<h1>La-Fabrik Editor Integration</h1>
<div class="info">
<h3>✅ Integration Status: COMPLETED</h3>
<p>
L'éditeur est maintenant intégré à la route <code>/editor</code> du
projet La-Fabrik.
</p>
</div>
<div class="links">
<a href="http://localhost:5176/" class="link">🎮 Jouer au jeu</a>
<a href="http://localhost:5176/editor" class="link"
>✏️ Ouvrir l'éditeur</a
>
</div>
<h2>Fonctionnalités de l'éditeur</h2>
<ul>
<li>
<strong>Routing React</strong> : React Router pour naviguer entre jeu et
éditeur
</li>
<li>
<strong>Chargement automatique</strong> : Recherche de
<code>map.json</code> dans <code>public/</code>
</li>
<li>
<strong>Upload de dossier</strong> : Si pas de map.json, possibilité
d'upload
</li>
<li>
<strong>Visualisation 3D</strong> : Canvas Three.js avec SceneData
</li>
<li>
<strong>Control de caméra</strong> :
<ul>
<li>Mode debug : OrbitControls (rotation/pan/zoom)</li>
<li>Mode player : FPS controller custom (WASD/ZQSD + souris)</li>
</ul>
</li>
<li><strong>Sélection d'objets</strong> : Click sur cubes/modèles</li>
<li><strong>Transformations</strong> : Panneau avec boutons T/R/S</li>
<li><strong>Undo/Redo</strong> : Ctrl+Z / Ctrl+Y (compte affiché)</li>
<li>
<strong>Export JSON</strong> : Bouton pour exporter map.json modifié
</li>
</ul>
<h2>Structure de fichiers</h2>
<pre>
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
</pre>
<h2>À tester</h2>
<ol>
<li>
Accéder à <code>/editor</code> - devrait montrer erreur "map.json
introuvable"
</li>
<li>Uploader un dossier test avec map.json + models/</li>
<li>Tester la visualisation 3D (cubes de test existent dans map.json)</li>
<li>Tester le mode player (WASD + souris)</li>
<li>Tester les transformations T/R/S</li>
<li>Tester Undo/Redo (Ctrl+Z / Ctrl+Y)</li>
<li>Tester export JSON (bouton "Export JSON")</li>
<li>Naviguer vers <code>/</code> - retour au jeu original</li>
</ol>
</body>
</html>