feat: ajoute les potagers à la map

This commit is contained in:
tom-boullay
2026-05-28 15:48:33 +02:00
parent 9bbed06ddc
commit 0b3d49e8d1
12 changed files with 283 additions and 58 deletions
+107 -4
View File
@@ -3,7 +3,13 @@ import type {
MapNode,
SceneData,
} from "@/types/map/mapScene";
import { logger } from "@/utils/core/Logger";
import { parseMapData } from "@/utils/map/mapNodeValidation";
import {
createPotagerMapNode,
isPotagerSourceMapNode,
POTAGER_MAP_NAME,
} from "@/utils/map/potagerMapNodes";
const MAP_JSON_PATH = "/map.json";
const MODEL_FILE_NAMES = ["model.glb", "model.gltf"];
@@ -59,9 +65,101 @@ async function loadMapSceneDataInternal(): Promise<SceneData | null> {
export async function createSceneDataFromMapPayload(
mapPayload: unknown,
): Promise<SceneData> {
const { mapNodes, mapTree } = parseMapData(mapPayload);
const { mapTree } = parseMapData(mapPayload);
const mapTreeWithPotagers = ensurePotagerMapTree(mapTree);
const mapNodes = flattenMapTree(mapTreeWithPotagers);
const deduplicatedNodes = deduplicateMapNodes(mapNodes);
return createSceneData(deduplicatedNodes, mapTree);
return createSceneData(deduplicatedNodes, mapTreeWithPotagers);
}
function isSamePosition(a: MapNode, b: MapNode): boolean {
return a.position.every((value, index) => {
const otherValue = b.position[index];
return otherValue !== undefined && Math.abs(value - otherValue) < 0.0001;
});
}
function cloneMapTree(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): HierarchicalMapNode | HierarchicalMapNode[] {
return JSON.parse(JSON.stringify(mapTree)) as
| HierarchicalMapNode
| HierarchicalMapNode[];
}
function flattenMapNode(node: HierarchicalMapNode, path: number[]): MapNode[] {
const childNodes =
node.children?.flatMap((child, index) =>
flattenMapNode(child, [...path, index]),
) ?? [];
if (node.role === "group" || node.type === "Mesh") {
return childNodes;
}
return [
{
name: node.name,
type: node.type,
position: node.position,
rotation: node.rotation,
scale: node.scale,
sourcePath: path,
},
...childNodes,
];
}
function flattenMapTree(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): MapNode[] {
return Array.isArray(mapTree)
? mapTree.flatMap((node, index) => flattenMapNode(node, [index]))
: flattenMapNode(mapTree, []);
}
function collectExplicitPotagerNodes(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): MapNode[] {
return flattenMapTree(mapTree).filter(
(node) => node.name === POTAGER_MAP_NAME,
);
}
function ensurePotagerMapTree(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): HierarchicalMapNode | HierarchicalMapNode[] {
const nextTree = cloneMapTree(mapTree);
const explicitPotagers = collectExplicitPotagerNodes(nextTree);
function visit(node: HierarchicalMapNode): void {
if (!node.children) return;
const nextChildren: HierarchicalMapNode[] = [];
node.children.forEach((child) => {
nextChildren.push(child);
visit(child);
if (!isPotagerSourceMapNode(child)) return;
const hasMatchingPotager = explicitPotagers.some((potager) =>
isSamePosition(potager, child),
);
if (hasMatchingPotager) return;
nextChildren.push(createPotagerMapNode(child));
});
node.children = nextChildren;
}
if (Array.isArray(nextTree)) {
nextTree.forEach((node) => visit(node));
} else {
visit(nextTree);
}
return nextTree;
}
function createPositionKey(node: MapNode): string {
@@ -124,7 +222,7 @@ async function loadMapModelUrls(
}
async function loadModelEntry(modelName: string): Promise<ModelEntry | null> {
for (const fileName of MODEL_FILE_NAMES) {
for (const fileName of [...MODEL_FILE_NAMES, `${modelName}.gltf`]) {
const modelUrl = `/models/${modelName}/${fileName}`;
try {
@@ -133,7 +231,12 @@ async function loadModelEntry(modelName: string): Promise<ModelEntry | null> {
if (response.ok && !contentType.includes(HTML_CONTENT_TYPE)) {
return [modelName, modelUrl];
}
} catch {
} catch (error) {
logger.warn("MapSceneData", "Failed to probe map model URL", {
modelName,
modelUrl,
error: error instanceof Error ? error : String(error),
});
continue;
}
}
+1 -8
View File
@@ -1,11 +1,4 @@
import type { MapNode } from "@/types/map/mapScene";
import type { Vector3Tuple } from "@/types/three/three";
export interface MapNodeInstanceTransform {
position: Vector3Tuple;
rotation: Vector3Tuple;
scale: Vector3Tuple;
}
import type { MapNode, MapNodeInstanceTransform } from "@/types/map/mapScene";
export function mapNodeToInstanceTransform(
node: MapNode,
@@ -8,6 +8,7 @@ const RUNTIME_VEGETATION_NODE_NAMES = new Set([
"champdeble",
"champdesoja",
"champsdetournesol",
"potager",
"sapin",
]);
+35
View File
@@ -0,0 +1,35 @@
import type { MapNode } from "@/types/map/mapScene";
export const POTAGER_MAP_NAME = "potager";
export const POTAGER_DEFAULT_ROTATION_OFFSET = [0, 0, 0] as const;
export const POTAGER_SOURCE_MAP_NAMES = new Set([
"champdeble",
"champdesoja",
"champsdetournesol",
]);
export function isPotagerSourceMapNode(node: MapNode): boolean {
const role = "role" in node ? node.role : undefined;
return (
node.type === "Object3D" &&
role !== "group" &&
POTAGER_SOURCE_MAP_NAMES.has(node.name) &&
!node.position.every((value) => Math.abs(value) < 0.0001)
);
}
export function createPotagerMapNode(sourceNode: MapNode): MapNode {
return {
name: POTAGER_MAP_NAME,
type: sourceNode.type,
position: sourceNode.position,
rotation: [
sourceNode.rotation[0] + POTAGER_DEFAULT_ROTATION_OFFSET[0],
sourceNode.rotation[1] + POTAGER_DEFAULT_ROTATION_OFFSET[1],
sourceNode.rotation[2] + POTAGER_DEFAULT_ROTATION_OFFSET[2],
],
scale: sourceNode.scale,
};
}