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
-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_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) {
+77 -104
View File
@@ -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
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
export type InteractableKind = "grab" | "trigger";
export interface TriggerInteractableHandle {
interface TriggerInteractableHandle {
kind: "trigger";
label: string;
onPress: () => void;
+1 -1
View File
@@ -1,6 +1,6 @@
export type LogLevel = "debug" | "info" | "warn" | "error";
export type LogValue =
type LogValue =
| string
| number
| boolean
+1 -3
View File
@@ -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}`;
+6 -10
View File
@@ -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,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);
}
}