cleaaning
This commit is contained in:
@@ -19,8 +19,5 @@ export default defineConfig([
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
rules: {
|
||||
"react-hooks/set-state-in-effect": "off",
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<typeof OrbitControls>;
|
||||
|
||||
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<FlyControllerRef, FlyControllerProps>(
|
||||
@@ -29,7 +31,7 @@ export const FlyController = forwardRef<FlyControllerRef, FlyControllerProps>(
|
||||
const { camera: rawCamera } = useThree();
|
||||
const cameraRef = useRef(rawCamera);
|
||||
const keys = useRef<{ [key: string]: boolean }>({});
|
||||
const controlsRef = useRef<OrbitControlsType | null>(null);
|
||||
const controlsRef = useRef<OrbitControlsRef | null>(null);
|
||||
const lastPosition = useRef(new THREE.Vector3());
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
@@ -54,13 +56,12 @@ export const FlyController = forwardRef<FlyControllerRef, FlyControllerProps>(
|
||||
}, [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<FlyControllerRef, FlyControllerProps>(
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<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({
|
||||
sceneData,
|
||||
selectedNodeIndex,
|
||||
@@ -82,8 +130,8 @@ export function EditorMap({
|
||||
<axesHelper args={[10]} />
|
||||
|
||||
<group
|
||||
onClick={(e: unknown) => {
|
||||
(e as { stopPropagation?: () => void }).stopPropagation?.();
|
||||
onClick={(e: ThreeEvent<MouseEvent>) => {
|
||||
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<Map<number, THREE.Object3D>>;
|
||||
onSelectNode: (index: number | null) => void;
|
||||
onHoverNode: (index: number | null) => void;
|
||||
}) {
|
||||
const groupRef = useRef<THREE.Group>(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<MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
onSelectNode(index);
|
||||
}}
|
||||
onPointerEnter={(e: unknown) => {
|
||||
(e as { stopPropagation?: () => void }).stopPropagation?.();
|
||||
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
onHoverNode(index);
|
||||
}}
|
||||
onPointerLeave={(e: unknown) => {
|
||||
(e as { stopPropagation?: () => void }).stopPropagation?.();
|
||||
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
onHoverNode(null);
|
||||
}}
|
||||
/>
|
||||
@@ -240,46 +250,9 @@ function EditorFallbackNode({
|
||||
objectsMapRef,
|
||||
onSelectNode,
|
||||
onHoverNode,
|
||||
}: {
|
||||
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;
|
||||
}) {
|
||||
}: EditorNodeCommonProps) {
|
||||
const meshRef = useRef<THREE.Mesh>(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<MouseEvent>) => {
|
||||
e.stopPropagation();
|
||||
onSelectNode(index);
|
||||
}}
|
||||
onPointerEnter={(e: unknown) => {
|
||||
(e as { stopPropagation?: () => void }).stopPropagation?.();
|
||||
onPointerEnter={(e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
onHoverNode(index);
|
||||
}}
|
||||
onPointerLeave={(e: unknown) => {
|
||||
(e as { stopPropagation?: () => void }).stopPropagation?.();
|
||||
onPointerLeave={(e: ThreeEvent<PointerEvent>) => {
|
||||
e.stopPropagation();
|
||||
onHoverNode(null);
|
||||
}}
|
||||
>
|
||||
|
||||
+5
-3
@@ -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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export type InteractableKind = "grab" | "trigger";
|
||||
|
||||
export interface TriggerInteractableHandle {
|
||||
interface TriggerInteractableHandle {
|
||||
kind: "trigger";
|
||||
label: string;
|
||||
onPress: () => void;
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
export type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
export type LogValue =
|
||||
type LogValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -14,7 +14,7 @@ export async function loadMapSceneData(): Promise<SceneData | null> {
|
||||
return createSceneData(mapNodes);
|
||||
}
|
||||
|
||||
export async function createSceneData(mapNodes: MapNode[]): Promise<SceneData> {
|
||||
async function createSceneData(mapNodes: MapNode[]): Promise<SceneData> {
|
||||
const models = await loadMapModelUrls(mapNodes);
|
||||
return { mapNodes, models };
|
||||
}
|
||||
@@ -28,7 +28,6 @@ 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;
|
||||
|
||||
@@ -36,9 +35,6 @@ async function loadMapModelUrls(
|
||||
if (isGltfContent(text)) {
|
||||
models.set(modelName, modelUrl);
|
||||
}
|
||||
} catch {
|
||||
/* Missing models are expected while editing incomplete maps. */
|
||||
}
|
||||
}
|
||||
|
||||
return models;
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user