feat(editor): edit hierarchical map nodes

This commit is contained in:
Tom Boullay
2026-05-27 08:30:54 +02:00
parent ab100c683f
commit c2b16434fb
16 changed files with 740 additions and 64 deletions
+72 -3
View File
@@ -1,13 +1,76 @@
import { useCallback, useRef, useState } from "react";
import type { MapNode, SceneData } from "@/types/editor/editor";
import type {
HierarchicalMapNode,
MapNode,
SceneData,
} from "@/types/editor/editor";
interface ObjectTransform {
uuid: string;
path: number[];
position: { x: number; y: number; z: number };
rotation: { x: number; y: number; z: number };
scale: { x: number; y: number; z: number };
}
function cloneMapTree(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): HierarchicalMapNode | HierarchicalMapNode[] {
return JSON.parse(JSON.stringify(mapTree)) as
| HierarchicalMapNode
| HierarchicalMapNode[];
}
function updateTreeNodeAtPath(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
path: number[],
transform: ObjectTransform,
): HierarchicalMapNode | HierarchicalMapNode[] {
const nextTree = cloneMapTree(mapTree);
const rootNodes = Array.isArray(nextTree) ? nextTree : [nextTree];
const targetIndex = path[path.length - 1] ?? 0;
const isRootTarget = Array.isArray(nextTree)
? path.length === 1
: path.length === 0;
const updateNode = (node: HierarchicalMapNode): HierarchicalMapNode => ({
...node,
position: [
transform.position.x,
transform.position.y,
transform.position.z,
],
rotation: [
transform.rotation.x,
transform.rotation.y,
transform.rotation.z,
],
scale: [transform.scale.x, transform.scale.y, transform.scale.z],
});
if (isRootTarget) {
rootNodes[targetIndex] = updateNode(
rootNodes[targetIndex] as HierarchicalMapNode,
);
return nextTree;
}
const parentPath = path.slice(0, -1);
let parent = Array.isArray(nextTree)
? rootNodes[parentPath[0] ?? 0]
: rootNodes[0];
const childPath = Array.isArray(nextTree) ? parentPath.slice(1) : parentPath;
for (const index of childPath) {
parent = parent?.children?.[index];
}
if (parent?.children?.[targetIndex]) {
parent.children[targetIndex] = updateNode(parent.children[targetIndex]);
}
return nextTree;
}
class HistoryManager {
private history: ObjectTransform[][] = [];
private currentIndex = -1;
@@ -81,13 +144,14 @@ export function useEditorHistory(
setSceneData((prev) => {
if (!prev) return null;
let mapTree = prev.mapTree;
const mapNodes = prev.mapNodes.map((node, index) => {
const transform = snapshot.find(
(item) => item.uuid === `node-${index}`,
);
if (!transform) return node;
return {
const nextNode = {
...node,
position: [
transform.position.x,
@@ -101,9 +165,13 @@ export function useEditorHistory(
],
scale: [transform.scale.x, transform.scale.y, transform.scale.z],
} satisfies MapNode;
mapTree = updateTreeNodeAtPath(mapTree, node.path, transform);
return nextNode;
});
return { ...prev, mapNodes };
return { ...prev, mapNodes, mapTree };
});
},
[setSceneData],
@@ -149,6 +217,7 @@ export function useEditorHistory(
function createSnapshot(sceneData: SceneData): ObjectTransform[] {
return sceneData.mapNodes.map((node, index) => ({
uuid: `node-${index}`,
path: node.path,
position: {
x: node.position[0],
y: node.position[1],
+10
View File
@@ -57,6 +57,16 @@ export function useTerrainSnappedPosition(
}, [position, terrainHeight]);
}
export function getObjectBottomOffset(
object: THREE.Object3D,
scale: Vector3Tuple = [1, 1, 1],
): number {
const bounds = new THREE.Box3().setFromObject(object);
if (bounds.isEmpty()) return 0;
return -bounds.min.y * scale[1];
}
export function normalizeMapScale(scale: Vector3Tuple): Vector3Tuple {
const [x, y, z] = scale;
const isUniform = Math.abs(x - y) < 0.001 && Math.abs(x - z) < 0.001;