feat: restaure l'éditeur map et ajoute les personnages
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled

This commit is contained in:
tom-boullay
2026-05-28 15:49:57 +02:00
parent fcdbf7270c
commit d5675fe82c
21 changed files with 454 additions and 57 deletions
+12 -1
View File
@@ -41,6 +41,7 @@ interface EditorControlsProps {
onClearSelection: () => void;
snapToTerrain: boolean;
onSnapToTerrainToggle: () => void;
onSnapAllToTerrain: () => void;
newNodeName: string;
onNewNodeNameChange: (value: string) => void;
onAddNode: () => void;
@@ -70,7 +71,7 @@ const EDITOR_SHORTCUTS = [
["Shift + Right click", "Toggle multi-selection"],
["T / R / S", "Transform mode"],
["Ctrl Z / Y", "Undo / redo"],
["Esc", "Deselect"],
["Esc / X button", "Clear selection"],
["WASD", "Move when locked"],
] as const;
@@ -117,6 +118,7 @@ export function EditorControls({
onClearSelection,
snapToTerrain,
onSnapToTerrainToggle,
onSnapAllToTerrain,
newNodeName,
onNewNodeNameChange,
onAddNode,
@@ -228,6 +230,15 @@ export function EditorControls({
/>
<span>Snap terrain on move</span>
</label>
<button
type="button"
className="editor-history-button"
onClick={onSnapAllToTerrain}
>
<ScanSearch size={15} aria-hidden="true" />
Snap all to terrain
</button>
</section>
<section
+74 -4
View File
@@ -12,6 +12,8 @@ import {
isEditorVisibleMapNode,
getTerrainMapNode,
} from "@/utils/map/mapRuntimeClassification";
import { getMapModelScaleMultiplier } from "@/data/world/mapInstancingConfig";
import { getVegetationModelScaleMultiplier } from "@/data/world/vegetationConfig";
interface EditorMapProps {
sceneData: SceneData;
@@ -28,6 +30,8 @@ interface EditorMapProps {
onTransformStart: () => void;
onTransformEnd: () => void;
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
snapAllToTerrainRequest: number;
onSnapAllToTerrain: (mapNodes: MapNode[]) => void;
}
type EditorNodeObjectRef = React.RefObject<Map<number, THREE.Object3D>>;
@@ -64,6 +68,32 @@ const TEMP_POSITION = new THREE.Vector3();
const TEMP_QUATERNION = new THREE.Quaternion();
const TEMP_SCALE = new THREE.Vector3();
function isOriginPosition(position: MapNode["position"]): boolean {
return position.every((value) => Math.abs(value) < 0.0001);
}
function isSnapAllCandidate(node: MapNode): boolean {
return (
isEditorVisibleMapNode(node) &&
node.name !== "terrain" &&
!isOriginPosition(node.position)
);
}
function shouldRenderEditorNode(
node: MapNode,
selectedNodeName: string | null,
): boolean {
if (!isEditorVisibleMapNode(node)) return false;
return selectedNodeName === null || node.name === selectedNodeName;
}
function getEditorModelVisualScaleMultiplier(name: string): number {
return (
getMapModelScaleMultiplier(name) * getVegetationModelScaleMultiplier(name)
);
}
function applyNodeTransform(object: THREE.Object3D, node: MapNode): void {
object.position.set(...node.position);
object.rotation.set(...node.rotation);
@@ -177,14 +207,21 @@ export function EditorMap({
onTransformStart,
onTransformEnd,
onNodeTransform,
snapAllToTerrainRequest,
onSnapAllToTerrain,
}: EditorMapProps): React.JSX.Element {
const objectsMapRef = useRef<Map<number, THREE.Object3D>>(new Map());
const transformGroupRef = useRef<THREE.Group>(null);
const transformSnapshotRef = useRef<TransformSnapshot | null>(null);
const terrainHeight = useTerrainHeightSampler();
const lastSnapAllToTerrainRequestRef = useRef(0);
const selectedIndexSet = new Set(selectedNodeIndexes);
const isMultiSelection = selectedNodeIndexes.length > 1;
const selectedNodeName =
selectedNodeIndex !== null
? (sceneData.mapNodes[selectedNodeIndex]?.name ?? null)
: null;
const getTransformObject = useCallback(() => {
if (isMultiSelection) {
@@ -333,6 +370,37 @@ export function EditorMap({
prepareTransformGroup();
}, [prepareTransformGroup]);
useEffect(() => {
if (
snapAllToTerrainRequest === 0 ||
snapAllToTerrainRequest === lastSnapAllToTerrainRequestRef.current
) {
return;
}
lastSnapAllToTerrainRequestRef.current = snapAllToTerrainRequest;
const snappedNodes = sceneData.mapNodes.map((node) => {
if (!isSnapAllCandidate(node)) return node;
const [x, y, z] = node.position;
const terrainY = terrainHeight.getHeight(x, z);
if (terrainY === null || Math.abs(terrainY - y) < 0.0001) return node;
return {
...node,
position: [x, terrainY, z] satisfies [number, number, number],
};
});
onSnapAllToTerrain(snappedNodes);
}, [
onSnapAllToTerrain,
sceneData.mapNodes,
snapAllToTerrainRequest,
terrainHeight,
]);
// TransformControls needs the current Three object; editor refs are managed outside React rendering.
// eslint-disable-next-line react-hooks/refs
const selectedObject = getTransformObject();
@@ -370,7 +438,7 @@ export function EditorMap({
/>
) : null}
{sceneData.mapNodes.map((node, index) => {
if (!isEditorVisibleMapNode(node)) {
if (!shouldRenderEditorNode(node, selectedNodeName)) {
return null;
}
@@ -451,6 +519,7 @@ function EditorModelNode({
scale: node.scale,
});
const sceneInstance = useClonedObject(scene);
const visualScaleMultiplier = getEditorModelVisualScaleMultiplier(node.name);
const pointerHandlers = createEditorNodePointerHandlers(
index,
onSelectNode,
@@ -512,14 +581,15 @@ function EditorModelNode({
}, []);
return (
<primitive
<group
ref={groupRef}
object={sceneInstance}
position={node.position}
rotation={node.rotation}
scale={node.scale}
{...pointerHandlers}
/>
>
<primitive object={sceneInstance} scale={visualScaleMultiplier} />
</group>
);
}
@@ -6,6 +6,7 @@ import * as THREE from "three";
import type { OrbitControls as OrbitControlsImpl } from "three-stdlib";
import { EditorMap } from "@/components/editor/scene/EditorMap";
import { FlyController } from "@/controls/editor/FlyController";
import { PersonnageSystem } from "@/world/personnages/PersonnageSystem";
import type { CinematicDefinition } from "@/types/cinematics/cinematics";
import type { MapNode, TransformMode, SceneData } from "@/types/editor/editor";
@@ -33,6 +34,8 @@ interface EditorSceneProps {
onTransformStart: () => void;
onTransformEnd: () => void;
onNodeTransform: (nodeIndex: number, transform: MapNode) => void;
snapAllToTerrainRequest: number;
onSnapAllToTerrain: (mapNodes: MapNode[]) => void;
onUndo: () => void;
onRedo: () => void;
resetCameraRequest: number;
@@ -58,6 +61,8 @@ export function EditorScene({
onTransformStart,
onTransformEnd,
onNodeTransform,
snapAllToTerrainRequest,
onSnapAllToTerrain,
onUndo,
onRedo,
resetCameraRequest,
@@ -224,8 +229,12 @@ export function EditorScene({
onTransformStart={onTransformStart}
onTransformEnd={onTransformEnd}
onNodeTransform={onNodeTransform}
snapAllToTerrainRequest={snapAllToTerrainRequest}
onSnapAllToTerrain={onSnapAllToTerrain}
/>
<PersonnageSystem />
<ambientLight intensity={0.6} />
<directionalLight position={[10, 20, 10]} intensity={1.5} castShadow />
<directionalLight position={[-10, 10, -10]} intensity={0.5} />
+26 -28
View File
@@ -68,32 +68,6 @@ export function AnimatedModel({
}
}, [mixer, onAnimationEnd]);
const play = useCallback(
(name: string, fade = fadeDuration) => {
const action = actions[name];
if (action) {
Object.values(actions).forEach((a) => {
if (a && a !== action) a.fadeOut(fade);
});
action.reset().fadeIn(fade).play();
setCurrentAnim(name);
}
},
[actions, fadeDuration],
);
const stop = useCallback(
(fade = fadeDuration) => {
Object.values(actions).forEach((a) => a?.fadeOut(fade));
const defaultAction = actions[defaultAnimation];
if (defaultAction) {
defaultAction.reset().fadeIn(fade).play();
setCurrentAnim(defaultAnimation);
}
},
[actions, defaultAnimation, fadeDuration],
);
const fadeTo = useCallback(
(name: string, fade = fadeDuration) => {
const action = actions[name];
@@ -107,6 +81,19 @@ export function AnimatedModel({
},
[actions, fadeDuration],
);
const play = fadeTo;
const stop = useCallback(
(fade = fadeDuration) => {
Object.values(actions).forEach((a) => a?.fadeOut(fade));
const defaultAction = actions[defaultAnimation];
if (defaultAction) {
defaultAction.reset().fadeIn(fade).play();
setCurrentAnim(defaultAnimation);
}
},
[actions, defaultAnimation, fadeDuration],
);
const setSpeed = useCallback(
(newSpeed: number) => {
@@ -140,10 +127,21 @@ export function AnimatedModel({
}
if (defaultAction) {
defaultAction.play();
Object.values(actions).forEach((action) => {
if (action && action !== defaultAction) action.fadeOut(fadeDuration);
});
defaultAction.reset().fadeIn(fadeDuration).play();
onLoaded?.();
}
}, [actions, defaultAnimation, modelPath, names, autoPlay, onLoaded]);
}, [
actions,
defaultAnimation,
fadeDuration,
modelPath,
names,
autoPlay,
onLoaded,
]);
const contextValue: AnimatedModelContextValue = {
play,