import type { EditableMapDocument } from './editor/types'; import { normalizePhysicalMapConfig } from './physicalMap'; import { mapSelectionTransform, updatePlacedMapAsset, type MapInstanceTransform, type PlacedMapAsset, } from './types'; function sameAsset(left: PlacedMapAsset | undefined, right: PlacedMapAsset | undefined): boolean { return Boolean( left && right && left.id === right.id && left.name === right.name && JSON.stringify(left.selection) === JSON.stringify(right.selection), ); } function cloneAsset(asset: PlacedMapAsset): PlacedMapAsset { return { ...asset, selection: structuredClone(asset.selection) }; } export interface MapSceneDraftSummary { dirty: boolean; changeCount: number; /** 当前仍存在且相对应用基线新增或发生修改的实例 ID。 */ changedIds: string[]; /** 可直接在 Three.js 中预览的新增或已修改程序地图。 */ previewAssets: PlacedMapAsset[]; } export interface MapEditorDraftPreviewInstance { id: string; document: EditableMapDocument; transform: MapInstanceTransform; } /** * 当前激活的认证场景由交互编辑层绘制;其余待提交工程地图必须保留只读预览, * 否则切换到参数地形时会因为清空交互文档而从画布消失。 */ export function mapEditorDraftPreviewInstances( assets: readonly PlacedMapAsset[], pendingIds: readonly string[], documents: ReadonlyMap, activeId?: string, ): MapEditorDraftPreviewInstance[] { const pending = new Set(pendingIds); return assets.flatMap((asset) => { if (asset.id === activeId || !pending.has(asset.id) || asset.selection.kind !== 'project') return []; const document = documents.get(asset.selection.descriptorPath); return document ? [{ id: asset.id, document, transform: mapSelectionTransform(asset.selection) }] : []; }); } /** * 所有地图实例共享同一个场景草稿边界。新增、删除、替换和实例参数修改只更新 * `current`,显式应用成功后才推进 `applied` 基线。 */ export function summarizeMapSceneDraft( current: readonly PlacedMapAsset[], applied: readonly PlacedMapAsset[], ): MapSceneDraftSummary { const currentById = new Map(current.map((asset) => [asset.id, asset])); const appliedById = new Map(applied.map((asset) => [asset.id, asset])); const changed = current.filter((asset) => !sameAsset(asset, appliedById.get(asset.id))); const removed = applied.filter((asset) => !currentById.has(asset.id)); return { dirty: changed.length + removed.length > 0, changeCount: changed.length + removed.length, changedIds: changed.map((asset) => asset.id), previewAssets: changed.filter((asset) => asset.selection.kind === 'builtin').map(cloneAsset), }; } /** 放弃完整场景草稿,严格恢复上一次成功编译的实例顺序和状态。 */ export function restoreAppliedMapScene(applied: readonly PlacedMapAsset[]): PlacedMapAsset[] { return applied.map(cloneAsset); } export function clonePlacedMapAssets(assets: readonly PlacedMapAsset[]): PlacedMapAsset[] { return assets.map(cloneAsset); } /** 普通模型重载使用应用基线;只有显式场景提交才传入候选实例。 */ export function resolveMapSceneLoadAssets( applied: readonly PlacedMapAsset[], requested?: readonly PlacedMapAsset[], ): PlacedMapAsset[] { return clonePlacedMapAssets(requested ?? applied); } /** 视口操纵器只更新参数地形草稿,不触发 MuJoCo 编译。 */ export function transformParametricMapAsset( assets: readonly PlacedMapAsset[], id: string, position: readonly [number, number], yawDeg: number, ): PlacedMapAsset[] | readonly PlacedMapAsset[] { const target = assets.find((asset) => asset.id === id); if (!target || target.selection.kind !== 'builtin' || target.selection.config.preset === 'none') return assets; const clean = (value: number) => Math.round(value * 1_000_000) / 1_000_000; const config = normalizePhysicalMapConfig({ ...target.selection.config, positionX: clean(position[0]), positionY: clean(position[1]), yawDeg: clean(yawDeg), }); if ( config.positionX === target.selection.config.positionX && config.positionY === target.selection.config.positionY && config.yawDeg === target.selection.config.yawDeg ) return assets; return updatePlacedMapAsset(assets, id, { kind: 'builtin', config }); }