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
+3 -3
View File
@@ -1,5 +1,5 @@
import type { SceneData } from "@/types/editor/editor";
import { parseMapNodes } from "@/utils/map/mapNodeValidation";
import { createSceneDataFromMapPayload } from "@/utils/map/loadMapSceneData";
const MAP_JSON_PATH = "/map.json";
@@ -18,7 +18,7 @@ export async function createSceneDataFromFiles(
}
const mapPayload: unknown = JSON.parse(await mapFile.text());
const mapNodes = parseMapNodes(mapPayload);
const sceneData = await createSceneDataFromMapPayload(mapPayload);
const models = new Map<string, string>();
for (const [path, file] of fileMap.entries()) {
@@ -31,7 +31,7 @@ export async function createSceneDataFromFiles(
}
}
return { mapNodes, models };
return { ...sceneData, models };
}
function getProjectRelativePath(file: File): string {
+92 -7
View File
@@ -1,5 +1,13 @@
import type { MapNode, SceneData } from "@/types/editor/editor";
import { parseMapNodes } from "@/utils/map/mapNodeValidation";
import type {
EditableMapNode,
HierarchicalMapNode,
MapNode,
SceneData,
} from "@/types/editor/editor";
import {
parseHierarchicalMapPayload,
parseMapNodes,
} from "@/utils/map/mapNodeValidation";
const MAP_JSON_PATH = "/map.json";
const MODEL_FILE_NAMES = ["model.glb", "model.gltf"];
@@ -45,9 +53,59 @@ async function loadMapSceneDataInternal(): Promise<SceneData | null> {
}
const mapPayload: unknown = await response.json();
const mapNodes = parseMapNodes(mapPayload);
return createSceneDataFromMapPayload(mapPayload);
}
export async function createSceneDataFromMapPayload(
mapPayload: unknown,
): Promise<SceneData> {
const mapTree = parseHierarchicalMapPayload(mapPayload);
const mapNodes = parseMapNodes(mapTree);
const editableNodes = createEditableMapNodes(mapTree);
const deduplicatedNodes = deduplicateMapNodes(mapNodes);
return createSceneData(deduplicatedNodes);
const deduplicatedEditableNodes = deduplicateEditableMapNodes(editableNodes);
return createSceneData(mapTree, deduplicatedEditableNodes, deduplicatedNodes);
}
function toMapNode(node: HierarchicalMapNode): MapNode {
return {
name: node.name,
position: node.position,
rotation: node.rotation,
scale: node.scale,
type: node.type,
};
}
function flattenEditableMapNode(
node: HierarchicalMapNode,
path: number[],
): EditableMapNode[] {
if (node.name === "terrain") {
return [];
}
if (node.role === "group") {
return (
node.children?.flatMap((child, index) =>
flattenEditableMapNode(child, [...path, index]),
) ?? []
);
}
return [{ ...toMapNode(node), path }];
}
function createEditableMapNodes(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): EditableMapNode[] {
if (Array.isArray(mapTree)) {
return mapTree.flatMap((node, index) =>
flattenEditableMapNode(node, [index]),
);
}
return flattenEditableMapNode(mapTree, []);
}
function createPositionKey(node: MapNode): string {
@@ -84,9 +142,36 @@ function deduplicateMapNodes(nodes: MapNode[]): MapNode[] {
return result;
}
async function createSceneData(mapNodes: MapNode[]): Promise<SceneData> {
const models = await loadMapModelUrls(mapNodes);
return { mapNodes, models };
function deduplicateEditableMapNodes(
nodes: EditableMapNode[],
): EditableMapNode[] {
const seen = new Set<string>();
const result: EditableMapNode[] = [];
const sortedNodes = [...nodes].sort((a, b) => {
if (a.type === "Object3D" && b.type !== "Object3D") return -1;
if (a.type !== "Object3D" && b.type === "Object3D") return 1;
return 0;
});
for (const node of sortedNodes) {
const key = createPositionKey(node);
if (!seen.has(key)) {
seen.add(key);
result.push(node);
}
}
return result;
}
async function createSceneData(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
mapNodes: EditableMapNode[],
modelLookupNodes: MapNode[],
): Promise<SceneData> {
const models = await loadMapModelUrls(modelLookupNodes);
return { mapNodes, mapTree, models };
}
async function loadMapModelUrls(
+19 -5
View File
@@ -26,7 +26,9 @@ function isMapNode(value: unknown): value is MapNode {
);
}
function isHierarchicalMapNode(value: unknown): value is HierarchicalMapNode {
export function isHierarchicalMapNode(
value: unknown,
): value is HierarchicalMapNode {
if (!isMapNode(value)) {
return false;
}
@@ -54,13 +56,25 @@ function flattenMapNode(node: HierarchicalMapNode): MapNode[] {
rotation: node.rotation,
scale: node.scale,
};
const childNodes = node.children?.flatMap(flattenMapNode) ?? [];
if (node.role === "group") {
return childNodes;
return node.children?.flatMap(flattenMapNode) ?? [];
}
return [mapNode, ...childNodes];
return [mapNode];
}
export function parseHierarchicalMapPayload(
value: unknown,
): HierarchicalMapNode | HierarchicalMapNode[] {
if (Array.isArray(value) && value.every(isHierarchicalMapNode)) {
return value;
}
if (isHierarchicalMapNode(value)) {
return value;
}
throw new Error("Invalid map node data");
}
export function parseMapNodes(value: unknown): MapNode[] {