fix(review): address audit findings before merge
🔍 Lint / 🪄 Check lint (pull_request) Has been cancelled
🔍 Lint / 🎨 Check format (pull_request) Has been cancelled
🔍 Lint / 🔎 Typecheck (pull_request) Has been cancelled
📊 Quality / 🔒 Security Audit (pull_request) Has been cancelled
📊 Quality / 📋 Dependency Freshness (pull_request) Has been cancelled
📊 Quality / 📦 Bundle Size (pull_request) Has been cancelled
🔍 Lint / 🏗 Build (pull_request) Has been cancelled

This commit is contained in:
Tom Boullay
2026-05-29 01:23:08 +02:00
parent 4728690a11
commit 093ffd726d
45 changed files with 823 additions and 785 deletions
+259
View File
@@ -0,0 +1,259 @@
import type {
HierarchicalMapNode,
MapNode,
SceneData,
} from "@/types/editor/editor";
const DEFAULT_NEW_NODE_NAME = "new-model";
export function serializeMapNodes(sceneData: SceneData): string {
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(".");
}
export function removeEditorMetadata(node: MapNode): MapNode {
return {
...(node.id ? { id: node.id } : {}),
name: node.name,
type: node.type,
position: node.position,
rotation: node.rotation,
scale: node.scale,
};
}
export 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 = {
...((updatedNode?.id ?? node.id)
? { id: updatedNode?.id ?? node.id }
: {}),
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 cloneMapTree(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): HierarchicalMapNode | HierarchicalMapNode[] {
return JSON.parse(JSON.stringify(mapTree)) as
| HierarchicalMapNode
| HierarchicalMapNode[];
}
function collectEditableMapNodes(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): MapNode[] {
const nodes: MapNode[] = [];
function visit(node: HierarchicalMapNode, path: number[]): void {
if (node.role !== "group" && node.type !== "Mesh") {
nodes.push({
...(node.id ? { id: node.id } : {}),
name: node.name,
position: node.position,
rotation: node.rotation,
scale: node.scale,
sourcePath: path,
type: node.type,
});
}
node.children?.forEach((child, index) => visit(child, [...path, index]));
}
if (Array.isArray(mapTree)) {
mapTree.forEach((node, index) => visit(node, [index]));
} else {
visit(mapTree, []);
}
return nodes;
}
export function updateTreeNodeAtPath(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
path: number[],
update: (node: HierarchicalMapNode) => HierarchicalMapNode,
): HierarchicalMapNode | HierarchicalMapNode[] {
const nextTree = cloneMapTree(mapTree);
const rootNodes = Array.isArray(nextTree) ? nextTree : [nextTree];
const targetIndex = path[path.length - 1] ?? 0;
const isRootTarget = Array.isArray(nextTree)
? path.length === 1
: path.length === 0;
if (isRootTarget) {
const targetNode = rootNodes[targetIndex];
if (targetNode) {
rootNodes[targetIndex] = update(targetNode);
}
return nextTree;
}
const parentPath = path.slice(0, -1);
let parent = Array.isArray(nextTree)
? rootNodes[parentPath[0] ?? 0]
: rootNodes[0];
const childPath = Array.isArray(nextTree) ? parentPath.slice(1) : parentPath;
for (const index of childPath) {
parent = parent?.children?.[index];
}
if (!parent?.children?.[targetIndex]) return nextTree;
parent.children[targetIndex] = update(parent.children[targetIndex]);
return nextTree;
}
export function removeTreeNodeAtPath(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
path: number[],
): HierarchicalMapNode | HierarchicalMapNode[] {
const nextTree = cloneMapTree(mapTree);
const rootNodes = Array.isArray(nextTree) ? nextTree : [nextTree];
const targetIndex = path[path.length - 1];
if (targetIndex === undefined) return nextTree;
if (Array.isArray(nextTree) && path.length === 1) {
nextTree.splice(targetIndex, 1);
return nextTree;
}
const parentPath = path.slice(0, -1);
let parent = Array.isArray(nextTree)
? rootNodes[parentPath[0] ?? 0]
: rootNodes[0];
const childPath = Array.isArray(nextTree) ? parentPath.slice(1) : parentPath;
for (const index of childPath) {
parent = parent?.children?.[index];
}
parent?.children?.splice(targetIndex, 1);
return nextTree;
}
export function updateSceneDataTree(
sceneData: SceneData,
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
): SceneData {
return {
...sceneData,
mapNodes: collectEditableMapNodes(mapTree),
mapTree,
};
}
function findNodePathByName(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
name: string,
): number[] | null {
function visit(node: HierarchicalMapNode, path: number[]): number[] | null {
if (node.name === name) return path;
for (let index = 0; index < (node.children?.length ?? 0); index++) {
const child = node.children?.[index];
if (!child) continue;
const result = visit(child, [...path, index]);
if (result) return result;
}
return null;
}
if (Array.isArray(mapTree)) {
for (let index = 0; index < mapTree.length; index++) {
const node = mapTree[index];
if (!node) continue;
const result = visit(node, [index]);
if (result) return result;
}
return null;
}
return visit(mapTree, []);
}
export function addTreeNode(
mapTree: HierarchicalMapNode | HierarchicalMapNode[],
node: HierarchicalMapNode,
): HierarchicalMapNode | HierarchicalMapNode[] {
const blockingPath = findNodePathByName(mapTree, "blocking");
if (!blockingPath) return mapTree;
return updateTreeNodeAtPath(mapTree, blockingPath, (blockingNode) => ({
...blockingNode,
children: [...(blockingNode.children ?? []), node],
}));
}
export function createNewMapNode(name: string): HierarchicalMapNode {
const safeName = name.trim() || DEFAULT_NEW_NODE_NAME;
return {
name: safeName,
type: "Object3D",
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
children: [
{
name: safeName,
type: "Mesh",
position: [0, 0, 0],
rotation: [0, 0, 0],
scale: [1, 1, 1],
},
],
};
}
+1 -1
View File
@@ -22,7 +22,7 @@ export async function createSceneDataFromFiles(
const models = new Map<string, string>();
for (const [path, file] of fileMap.entries()) {
const modelMatch = path.match(/^\/models\/(.+)\/model\.(glb|gltf)$/);
const modelMatch = path.match(/^\/models\/(.+)\/(?:model|\1)\.(glb|gltf)$/);
const modelName = modelMatch?.[1];
const modelExtension = modelMatch?.[2];
@@ -0,0 +1,17 @@
import { REPAIR_MISSION_POSITION_ENTRIES } from "@/data/gameplay/repairMissionAnchors";
import type { RepairMissionId } from "@/types/gameplay/repairMission";
import type { Vector3Tuple } from "@/types/three/three";
const FALLBACK_REPAIR_MISSION_POSITIONS = new Map(
REPAIR_MISSION_POSITION_ENTRIES.map(({ mission, position }) => [
mission,
position,
]),
);
export function getRepairMissionPosition(
mission: RepairMissionId,
anchors: Partial<Record<RepairMissionId, Vector3Tuple>>,
): Vector3Tuple | undefined {
return anchors[mission] ?? FALLBACK_REPAIR_MISSION_POSITIONS.get(mission);
}
+55
View File
@@ -0,0 +1,55 @@
import { CHUNK_CONFIG } from "@/data/world/chunkStreamingConfig";
import type { Vector3Tuple } from "@/types/three/three";
interface PositionedInstance {
position: Vector3Tuple;
}
export interface WorldInstanceChunk<TInstance extends PositionedInstance> {
centerX: number;
centerZ: number;
chunkKey: string;
instances: TInstance[];
}
function getWorldChunkKey(instance: PositionedInstance): string {
const [x, , z] = instance.position;
const chunkX = Math.floor(x / CHUNK_CONFIG.chunkSize);
const chunkZ = Math.floor(z / CHUNK_CONFIG.chunkSize);
return `${chunkX}:${chunkZ}`;
}
export function createWorldInstanceChunks<TInstance extends PositionedInstance>(
instances: TInstance[],
): WorldInstanceChunk<TInstance>[] {
const chunks = new Map<string, TInstance[]>();
for (const instance of instances) {
const chunkKey = getWorldChunkKey(instance);
const chunk = chunks.get(chunkKey);
if (chunk) {
chunk.push(instance);
} else {
chunks.set(chunkKey, [instance]);
}
}
return [...chunks.entries()].map(([chunkKey, chunkInstances]) => {
const center = chunkInstances.reduce(
(sum, instance) => {
sum.x += instance.position[0];
sum.z += instance.position[2];
return sum;
},
{ x: 0, z: 0 },
);
return {
centerX: center.x / chunkInstances.length,
centerZ: center.z / chunkInstances.length,
chunkKey,
instances: chunkInstances,
};
});
}