Merge remote map editor updates

This commit is contained in:
Tom Boullay
2026-05-27 22:24:59 +02:00
20 changed files with 718 additions and 209 deletions
+12 -80
View File
@@ -1,13 +1,9 @@
import type {
EditableMapNode,
HierarchicalMapNode,
MapNode,
SceneData,
} from "@/types/editor/editor";
import {
parseHierarchicalMapPayload,
parseMapNodes,
} from "@/utils/map/mapNodeValidation";
import { parseMapData } from "@/utils/map/mapNodeValidation";
const MAP_JSON_PATH = "/map.json";
const MODEL_FILE_NAMES = ["model.glb", "model.gltf"];
@@ -29,8 +25,12 @@ export async function loadMapSceneData(): Promise<SceneData | null> {
}
loadingPromise = loadMapSceneDataInternal();
cachedSceneData = await loadingPromise;
loadingPromise = null;
try {
cachedSceneData = await loadingPromise;
} finally {
loadingPromise = null;
}
return cachedSceneData;
}
@@ -59,53 +59,9 @@ async function loadMapSceneDataInternal(): Promise<SceneData | null> {
export async function createSceneDataFromMapPayload(
mapPayload: unknown,
): Promise<SceneData> {
const mapTree = parseHierarchicalMapPayload(mapPayload);
const mapNodes = parseMapNodes(mapTree);
const editableNodes = createEditableMapNodes(mapTree);
const { mapNodes, mapTree } = parseMapData(mapPayload);
const deduplicatedNodes = deduplicateMapNodes(mapNodes);
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, []);
return createSceneData(deduplicatedNodes, mapTree);
}
function createPositionKey(node: MapNode): string {
@@ -142,36 +98,12 @@ function deduplicateMapNodes(nodes: MapNode[]): MapNode[] {
return result;
}
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(
mapNodes: MapNode[],
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
mapNodes: EditableMapNode[],
modelLookupNodes: MapNode[],
): Promise<SceneData> {
const models = await loadMapModelUrls(modelLookupNodes);
return { mapNodes, mapTree, models };
const models = await loadMapModelUrls(mapNodes);
return { mapNodes, models, mapTree };
}
async function loadMapModelUrls(
+27 -6
View File
@@ -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<string, unknown> {
return typeof value === "object" && value !== null;
}
@@ -48,19 +53,25 @@ export function isHierarchicalMapNode(
);
}
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,
};
if (node.role === "group") {
return node.children?.flatMap(flattenMapNode) ?? [];
const childNodes =
node.children?.flatMap((child, index) =>
flattenMapNode(child, [...path, index]),
) ?? [];
if (node.role === "group" || node.type === "Mesh") {
return childNodes;
}
return [mapNode];
return [mapNode, ...childNodes];
}
export function parseHierarchicalMapPayload(
@@ -78,12 +89,22 @@ export function parseHierarchicalMapPayload(
}
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");
+39
View File
@@ -0,0 +1,39 @@
import type { MapNode } from "@/types/editor/editor";
import { isInstancedMapNodeName } from "@/world/map-instancing/mapInstancingConfig";
const MAP_STRUCTURE_NODE_NAMES = new Set(["Scene", "blocking", "terrain"]);
const RUNTIME_VEGETATION_NODE_NAMES = new Set([
"arbre",
"buisson",
"champdeble",
"champdesoja",
"champsdetournesol",
"sapin",
]);
export function isRuntimeStructureMapNode(name: string): boolean {
return MAP_STRUCTURE_NODE_NAMES.has(name);
}
export function isRuntimeSingleMapNode(node: MapNode): boolean {
if (isRuntimeStructureMapNode(node.name)) {
return false;
}
if (node.type === "Mesh") {
return false;
}
return (
!RUNTIME_VEGETATION_NODE_NAMES.has(node.name) &&
!isInstancedMapNodeName(node.name)
);
}
export function isEditorVisibleMapNode(node: MapNode): boolean {
return !isRuntimeStructureMapNode(node.name) && node.type !== "Mesh";
}
export function getTerrainMapNode(nodes: readonly MapNode[]): MapNode | null {
return nodes.find((node) => node.name === "terrain") ?? null;
}