fix(editor): restore stable map editing behavior
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user