fix(editor): restore stable map editing behavior

This commit is contained in:
Tom Boullay
2026-05-29 00:52:44 +02:00
parent d5675fe82c
commit 343a122c06
28 changed files with 453 additions and 302 deletions
+1
View File
@@ -99,6 +99,7 @@ function flattenMapNode(node: HierarchicalMapNode, path: number[]): MapNode[] {
return [
{
...(node.id ? { id: node.id } : {}),
name: node.name,
type: node.type,
position: node.position,
+2
View File
@@ -23,6 +23,7 @@ function isMapNode(value: unknown): value is MapNode {
}
return (
(value.id === undefined || typeof value.id === "string") &&
typeof value.name === "string" &&
typeof value.type === "string" &&
isVector3Tuple(value.position) &&
@@ -53,6 +54,7 @@ function isHierarchicalMapNode(value: unknown): value is HierarchicalMapNode {
function flattenMapNode(node: HierarchicalMapNode, path: number[]): MapNode[] {
const mapNode: MapNode = {
...(node.id ? { id: node.id } : {}),
name: node.name,
type: node.type,
position: node.position,
+2 -2
View File
@@ -1,9 +1,9 @@
import type { MapNode } from "@/types/map/mapScene";
export const POTAGER_MAP_NAME = "potager";
export const POTAGER_DEFAULT_ROTATION_OFFSET = [0, 0, 0] as const;
const POTAGER_DEFAULT_ROTATION_OFFSET = [0, 0, 0] as const;
export const POTAGER_SOURCE_MAP_NAMES = new Set([
const POTAGER_SOURCE_MAP_NAMES = new Set([
"champdeble",
"champdesoja",
"champsdetournesol",
+62 -6
View File
@@ -1,3 +1,7 @@
import {
REPAIR_MISSION_ANCHOR_IDS,
REPAIR_MISSION_POSITION_ENTRIES,
} from "@/data/gameplay/repairMissionAnchors";
import type { RepairMissionId } from "@/types/gameplay/repairMission";
import type { MapNode } from "@/types/map/mapScene";
import type { Vector3Tuple } from "@/types/three/three";
@@ -8,10 +12,67 @@ const REPAIR_MISSION_MAP_NODE_NAMES = {
farm: "fermeverticale",
} as const satisfies Record<RepairMissionId, string>;
const REPAIR_MISSION_FALLBACK_POSITIONS = new Map(
REPAIR_MISSION_POSITION_ENTRIES.map(({ mission, position }) => [
mission,
position,
]),
);
function isOriginPosition(position: Vector3Tuple): boolean {
return position.every((value) => Math.abs(value) < 0.0001);
}
function hasDistinctTransform(node: MapNode): boolean {
return (
node.rotation.some((value) => Math.abs(value) > 0.0001) ||
node.scale.some((value) => Math.abs(value - 1) > 0.0001)
);
}
function distanceToPosition(node: MapNode, position: Vector3Tuple): number {
return Math.hypot(
node.position[0] - position[0],
node.position[2] - position[2],
);
}
function getAnchorNode(
mapNodes: readonly MapNode[],
mission: RepairMissionId,
mapName: string,
): MapNode | null {
const anchorId = REPAIR_MISSION_ANCHOR_IDS[mission];
if (anchorId) {
const nodeById = mapNodes.find((candidate) => candidate.id === anchorId);
if (nodeById) return nodeById;
}
const candidates = mapNodes.filter(
(candidate) =>
candidate.name === mapName &&
candidate.type === "Object3D" &&
!isOriginPosition(candidate.position),
);
if (mission !== "pylon") return candidates[0] ?? null;
const distinctCandidates = candidates.filter(hasDistinctTransform);
const pylonCandidates =
distinctCandidates.length > 0 ? distinctCandidates : candidates;
const fallbackPosition = REPAIR_MISSION_FALLBACK_POSITIONS.get(mission);
if (!fallbackPosition) return pylonCandidates[0] ?? null;
return (
[...pylonCandidates].sort(
(a, b) =>
distanceToPosition(a, fallbackPosition) -
distanceToPosition(b, fallbackPosition),
)[0] ?? null
);
}
export function getRepairMissionMapAnchors(
mapNodes: readonly MapNode[],
): Partial<Record<RepairMissionId, Vector3Tuple>> {
@@ -20,12 +81,7 @@ export function getRepairMissionMapAnchors(
for (const [mission, mapName] of Object.entries(
REPAIR_MISSION_MAP_NODE_NAMES,
) as [RepairMissionId, string][]) {
const node = mapNodes.find(
(candidate) =>
candidate.name === mapName &&
candidate.type === "Object3D" &&
!isOriginPosition(candidate.position),
);
const node = getAnchorNode(mapNodes, mission, mapName);
if (node) {
anchors[mission] = node.position;
+76
View File
@@ -0,0 +1,76 @@
import * as THREE from "three";
type TextureMaterialKey = Extract<
| keyof THREE.MeshBasicMaterial
| keyof THREE.MeshStandardMaterial
| keyof THREE.MeshPhysicalMaterial
| keyof THREE.MeshToonMaterial,
string
>;
type MaterialWithTextureSlots = THREE.Material &
Partial<Record<TextureMaterialKey, THREE.Texture | null>>;
interface DisposeObject3DOptions {
disposeTextures?: boolean;
}
const MATERIAL_TEXTURE_KEYS = [
"alphaMap",
"aoMap",
"bumpMap",
"clearcoatMap",
"clearcoatNormalMap",
"clearcoatRoughnessMap",
"displacementMap",
"emissiveMap",
"envMap",
"gradientMap",
"lightMap",
"map",
"metalnessMap",
"normalMap",
"roughnessMap",
"sheenColorMap",
"sheenRoughnessMap",
"specularColorMap",
"specularIntensityMap",
"specularMap",
"thicknessMap",
"transmissionMap",
] as const satisfies readonly TextureMaterialKey[];
export function disposeObject3D(
object: THREE.Object3D,
options: DisposeObject3DOptions = {},
): void {
object.traverse((child) => {
if (!(child instanceof THREE.Mesh)) return;
child.geometry?.dispose();
if (Array.isArray(child.material)) {
child.material.forEach((material) => disposeMaterial(material, options));
} else if (child.material) {
disposeMaterial(child.material, options);
}
});
}
function disposeMaterial(
material: THREE.Material,
options: DisposeObject3DOptions,
): void {
material.dispose();
if (!options.disposeTextures) return;
const materialWithTextures = material as MaterialWithTextureSlots;
for (const key of MATERIAL_TEXTURE_KEYS) {
const value = materialWithTextures[key];
if (value instanceof THREE.Texture) {
value.dispose();
}
}
}