diff --git a/src/pages/editor/page.tsx b/src/pages/editor/page.tsx index 5da7919..be4d228 100644 --- a/src/pages/editor/page.tsx +++ b/src/pages/editor/page.tsx @@ -9,7 +9,12 @@ import { Subtitles } from "@/components/ui/Subtitles"; import { useEditorHistory } from "@/hooks/editor/useEditorHistory"; import type { CinematicDefinition } from "@/types/cinematics/cinematics"; import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData"; -import type { MapNode, SceneData, TransformMode } from "@/types/editor/editor"; +import type { + HierarchicalMapNode, + MapNode, + SceneData, + TransformMode, +} from "@/types/editor/editor"; import { INITIAL_SCENE_LOADING_STATE, type SceneLoadingChangeHandler, @@ -24,7 +29,74 @@ interface EditorSceneLoadingTrackerProps { } function serializeMapNodes(sceneData: SceneData): string { - return JSON.stringify(sceneData.mapNodes, null, 2); + const mapPayload = sceneData.mapTree + ? mergeFlatNodeTransformsIntoTree(sceneData) + : sceneData.mapNodes.map(removeEditorMetadata); + + return JSON.stringify(mapPayload, null, 2); +} + +function createSourcePathKey(sourcePath: readonly number[]): string { + return sourcePath.join("."); +} + +function removeEditorMetadata(node: MapNode): MapNode { + return { + name: node.name, + type: node.type, + position: node.position, + rotation: node.rotation, + scale: node.scale, + }; +} + +function mergeFlatNodeTransformsIntoTree( + sceneData: SceneData, +): HierarchicalMapNode | HierarchicalMapNode[] { + const nodesBySourcePath = new Map(); + + for (const node of sceneData.mapNodes) { + if (!node.sourcePath) continue; + nodesBySourcePath.set(createSourcePathKey(node.sourcePath), node); + } + + const cloneNode = ( + node: HierarchicalMapNode, + path: number[], + ): HierarchicalMapNode => { + const updatedNode = nodesBySourcePath.get(createSourcePathKey(path)); + const nextNode: HierarchicalMapNode = { + name: node.name, + type: node.type, + position: updatedNode?.position ?? node.position, + rotation: updatedNode?.rotation ?? node.rotation, + scale: updatedNode?.scale ?? node.scale, + }; + + if (node.role) { + nextNode.role = node.role; + } + + if (node.children) { + nextNode.children = node.children.map((child, index) => + cloneNode(child, [...path, index]), + ); + } + + return nextNode; + }; + + const mapTree = sceneData.mapTree; + + if (!mapTree) { + return sceneData.mapNodes.map(removeEditorMetadata); + } + + if (Array.isArray(mapTree)) { + return mapTree.map((node, index) => cloneNode(node, [index])); + } + + return cloneNode(mapTree, []); } function EditorSceneLoadingTracker({ diff --git a/src/types/editor/editor.ts b/src/types/editor/editor.ts index ceda325..b000680 100644 --- a/src/types/editor/editor.ts +++ b/src/types/editor/editor.ts @@ -6,6 +6,7 @@ export interface MapNode { position: Vector3Tuple; rotation: Vector3Tuple; scale: Vector3Tuple; + sourcePath?: number[]; } export interface HierarchicalMapNode extends MapNode { @@ -16,6 +17,7 @@ export interface HierarchicalMapNode extends MapNode { export interface SceneData { mapNodes: MapNode[]; models: Map; + mapTree?: HierarchicalMapNode | HierarchicalMapNode[]; } export type TransformMode = "translate" | "rotate" | "scale"; diff --git a/src/utils/editor/loadEditorScene.ts b/src/utils/editor/loadEditorScene.ts index 3da9c53..17ec5bf 100644 --- a/src/utils/editor/loadEditorScene.ts +++ b/src/utils/editor/loadEditorScene.ts @@ -1,5 +1,5 @@ import type { SceneData } from "@/types/editor/editor"; -import { parseMapNodes } from "@/utils/map/mapNodeValidation"; +import { parseMapData } from "@/utils/map/mapNodeValidation"; 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 { mapNodes, mapTree } = parseMapData(mapPayload); const models = new Map(); for (const [path, file] of fileMap.entries()) { @@ -31,7 +31,7 @@ export async function createSceneDataFromFiles( } } - return { mapNodes, models }; + return { mapNodes, models, mapTree }; } function getProjectRelativePath(file: File): string { diff --git a/src/utils/map/loadMapSceneData.ts b/src/utils/map/loadMapSceneData.ts index 2efef22..494de62 100644 --- a/src/utils/map/loadMapSceneData.ts +++ b/src/utils/map/loadMapSceneData.ts @@ -1,5 +1,9 @@ -import type { MapNode, SceneData } from "@/types/editor/editor"; -import { parseMapNodes } from "@/utils/map/mapNodeValidation"; +import type { + HierarchicalMapNode, + MapNode, + SceneData, +} from "@/types/editor/editor"; +import { parseMapData } from "@/utils/map/mapNodeValidation"; const MAP_JSON_PATH = "/map.json"; const MODEL_FILE_NAMES = ["model.glb", "model.gltf"]; @@ -21,8 +25,12 @@ export async function loadMapSceneData(): Promise { } loadingPromise = loadMapSceneDataInternal(); - cachedSceneData = await loadingPromise; - loadingPromise = null; + + try { + cachedSceneData = await loadingPromise; + } finally { + loadingPromise = null; + } return cachedSceneData; } @@ -45,9 +53,9 @@ async function loadMapSceneDataInternal(): Promise { } const mapPayload: unknown = await response.json(); - const mapNodes = parseMapNodes(mapPayload); + const { mapNodes, mapTree } = parseMapData(mapPayload); const deduplicatedNodes = deduplicateMapNodes(mapNodes); - return createSceneData(deduplicatedNodes); + return createSceneData(deduplicatedNodes, mapTree); } function createPositionKey(node: MapNode): string { @@ -84,9 +92,12 @@ function deduplicateMapNodes(nodes: MapNode[]): MapNode[] { return result; } -async function createSceneData(mapNodes: MapNode[]): Promise { +async function createSceneData( + mapNodes: MapNode[], + mapTree: HierarchicalMapNode | HierarchicalMapNode[], +): Promise { const models = await loadMapModelUrls(mapNodes); - return { mapNodes, models }; + return { mapNodes, models, mapTree }; } async function loadMapModelUrls( diff --git a/src/utils/map/mapNodeValidation.ts b/src/utils/map/mapNodeValidation.ts index 7610bd8..55af091 100644 --- a/src/utils/map/mapNodeValidation.ts +++ b/src/utils/map/mapNodeValidation.ts @@ -1,5 +1,10 @@ import type { HierarchicalMapNode, MapNode } from "../../types/editor/editor"; +export interface ParsedMapNodes { + mapNodes: MapNode[]; + mapTree: HierarchicalMapNode | HierarchicalMapNode[]; +} + function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null; } @@ -46,15 +51,19 @@ function isHierarchicalMapNode(value: unknown): value is HierarchicalMapNode { ); } -function flattenMapNode(node: HierarchicalMapNode): MapNode[] { +function flattenMapNode(node: HierarchicalMapNode, path: number[]): MapNode[] { const mapNode: MapNode = { name: node.name, type: node.type, position: node.position, rotation: node.rotation, scale: node.scale, + sourcePath: path, }; - const childNodes = node.children?.flatMap(flattenMapNode) ?? []; + const childNodes = + node.children?.flatMap((child, index) => + flattenMapNode(child, [...path, index]), + ) ?? []; if (node.role === "group") { return childNodes; @@ -64,12 +73,22 @@ function flattenMapNode(node: HierarchicalMapNode): MapNode[] { } export function parseMapNodes(value: unknown): MapNode[] { + return parseMapData(value).mapNodes; +} + +export function parseMapData(value: unknown): ParsedMapNodes { if (Array.isArray(value) && value.every(isHierarchicalMapNode)) { - return value.flatMap(flattenMapNode); + return { + mapNodes: value.flatMap((node, index) => flattenMapNode(node, [index])), + mapTree: value, + }; } if (isHierarchicalMapNode(value)) { - return flattenMapNode(value); + return { + mapNodes: flattenMapNode(value, []), + mapTree: value, + }; } throw new Error("Invalid map node data");