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
🔍 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:
@@ -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
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user