Feat/map-environment #6

Merged
math-pixel merged 116 commits from feat/map-environment into develop 2026-05-29 00:00:51 +00:00
5 changed files with 121 additions and 17 deletions
Showing only changes of commit d38ad242d6 - Show all commits
+74 -2
View File
@@ -9,7 +9,12 @@ import { Subtitles } from "@/components/ui/Subtitles";
import { useEditorHistory } from "@/hooks/editor/useEditorHistory"; import { useEditorHistory } from "@/hooks/editor/useEditorHistory";
import type { CinematicDefinition } from "@/types/cinematics/cinematics"; import type { CinematicDefinition } from "@/types/cinematics/cinematics";
import { useEditorSceneData } from "@/hooks/editor/useEditorSceneData"; 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 { import {
INITIAL_SCENE_LOADING_STATE, INITIAL_SCENE_LOADING_STATE,
type SceneLoadingChangeHandler, type SceneLoadingChangeHandler,
@@ -24,7 +29,74 @@ interface EditorSceneLoadingTrackerProps {
} }
function serializeMapNodes(sceneData: SceneData): string { 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<string, MapNode>();
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({ function EditorSceneLoadingTracker({
+2
View File
@@ -6,6 +6,7 @@ export interface MapNode {
position: Vector3Tuple; position: Vector3Tuple;
rotation: Vector3Tuple; rotation: Vector3Tuple;
scale: Vector3Tuple; scale: Vector3Tuple;
sourcePath?: number[];
} }
export interface HierarchicalMapNode extends MapNode { export interface HierarchicalMapNode extends MapNode {
@@ -16,6 +17,7 @@ export interface HierarchicalMapNode extends MapNode {
export interface SceneData { export interface SceneData {
mapNodes: MapNode[]; mapNodes: MapNode[];
models: Map<string, string>; models: Map<string, string>;
mapTree?: HierarchicalMapNode | HierarchicalMapNode[];
} }
export type TransformMode = "translate" | "rotate" | "scale"; export type TransformMode = "translate" | "rotate" | "scale";
+3 -3
View File
@@ -1,5 +1,5 @@
import type { SceneData } from "@/types/editor/editor"; 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"; const MAP_JSON_PATH = "/map.json";
@@ -18,7 +18,7 @@ export async function createSceneDataFromFiles(
} }
const mapPayload: unknown = JSON.parse(await mapFile.text()); const mapPayload: unknown = JSON.parse(await mapFile.text());
const mapNodes = parseMapNodes(mapPayload); const { mapNodes, mapTree } = parseMapData(mapPayload);
const models = new Map<string, string>(); const models = new Map<string, string>();
for (const [path, file] of fileMap.entries()) { 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 { function getProjectRelativePath(file: File): string {
+19 -8
View File
@@ -1,5 +1,9 @@
import type { MapNode, SceneData } from "@/types/editor/editor"; import type {
import { parseMapNodes } from "@/utils/map/mapNodeValidation"; HierarchicalMapNode,
MapNode,
SceneData,
} from "@/types/editor/editor";
import { parseMapData } from "@/utils/map/mapNodeValidation";
const MAP_JSON_PATH = "/map.json"; const MAP_JSON_PATH = "/map.json";
const MODEL_FILE_NAMES = ["model.glb", "model.gltf"]; const MODEL_FILE_NAMES = ["model.glb", "model.gltf"];
@@ -21,8 +25,12 @@ export async function loadMapSceneData(): Promise<SceneData | null> {
} }
loadingPromise = loadMapSceneDataInternal(); loadingPromise = loadMapSceneDataInternal();
cachedSceneData = await loadingPromise;
loadingPromise = null; try {
cachedSceneData = await loadingPromise;
} finally {
loadingPromise = null;
}
return cachedSceneData; return cachedSceneData;
} }
@@ -45,9 +53,9 @@ async function loadMapSceneDataInternal(): Promise<SceneData | null> {
} }
const mapPayload: unknown = await response.json(); const mapPayload: unknown = await response.json();
const mapNodes = parseMapNodes(mapPayload); const { mapNodes, mapTree } = parseMapData(mapPayload);
const deduplicatedNodes = deduplicateMapNodes(mapNodes); const deduplicatedNodes = deduplicateMapNodes(mapNodes);
return createSceneData(deduplicatedNodes); return createSceneData(deduplicatedNodes, mapTree);
} }
function createPositionKey(node: MapNode): string { function createPositionKey(node: MapNode): string {
@@ -84,9 +92,12 @@ function deduplicateMapNodes(nodes: MapNode[]): MapNode[] {
return result; return result;
} }
async function createSceneData(mapNodes: MapNode[]): Promise<SceneData> { async function createSceneData(
mapNodes: MapNode[],
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): Promise<SceneData> {
const models = await loadMapModelUrls(mapNodes); const models = await loadMapModelUrls(mapNodes);
return { mapNodes, models }; return { mapNodes, models, mapTree };
} }
async function loadMapModelUrls( async function loadMapModelUrls(
+23 -4
View File
@@ -1,5 +1,10 @@
import type { HierarchicalMapNode, MapNode } from "../../types/editor/editor"; import type { HierarchicalMapNode, MapNode } from "../../types/editor/editor";
export interface ParsedMapNodes {
mapNodes: MapNode[];
mapTree: HierarchicalMapNode | HierarchicalMapNode[];
}
function isRecord(value: unknown): value is Record<string, unknown> { function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null; 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 = { const mapNode: MapNode = {
name: node.name, name: node.name,
type: node.type, type: node.type,
position: node.position, position: node.position,
rotation: node.rotation, rotation: node.rotation,
scale: node.scale, 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") { if (node.role === "group") {
return childNodes; return childNodes;
@@ -64,12 +73,22 @@ function flattenMapNode(node: HierarchicalMapNode): MapNode[] {
} }
export function parseMapNodes(value: unknown): MapNode[] { export function parseMapNodes(value: unknown): MapNode[] {
return parseMapData(value).mapNodes;
}
export function parseMapData(value: unknown): ParsedMapNodes {
if (Array.isArray(value) && value.every(isHierarchicalMapNode)) { 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)) { if (isHierarchicalMapNode(value)) {
return flattenMapNode(value); return {
mapNodes: flattenMapNode(value, []),
mapTree: value,
};
} }
throw new Error("Invalid map node data"); throw new Error("Invalid map node data");