Feat/map-environment #6
@@ -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({
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
Reference in New Issue
Block a user