166 lines
4.6 KiB
TypeScript

import { cloneLevelSubtree } from '@pascal-app/core/clone-scene-graph'
import type { AnyNode, AnyNodeId, LevelNode } from '@pascal-app/core/schema'
export type LevelDuplicatePreset =
| 'everything'
| 'structure'
| 'structure-materials'
| 'structure-furniture'
const NON_DUPLICABLE_NODE_TYPES = new Set<AnyNode['type']>(['scan', 'guide', 'spawn'])
const STRUCTURAL_NODE_TYPES = new Set<AnyNode['type']>([
'level',
'wall',
'fence',
'zone',
'slab',
'ceiling',
'roof',
'roof-segment',
'stair',
'stair-segment',
'window',
'door',
])
function shouldKeepNode(node: AnyNode, preset: LevelDuplicatePreset) {
if (NON_DUPLICABLE_NODE_TYPES.has(node.type)) return false
if (preset === 'everything') return true
if (preset === 'structure-furniture') return true
if (preset === 'structure' || preset === 'structure-materials') {
return STRUCTURAL_NODE_TYPES.has(node.type)
}
return true
}
/**
* Material field keys per kind, used by the `structure` duplicate preset
* to strip materials from the cloned subtree. Lookup table replaces the
* legacy per-kind switch — the Phase 6 grep gate flagged `case '<kind>':`
* in this file as the remaining per-kind dispatch outside the registry.
*
* Future: move this to a `capabilities.materialFields` declaration on
* each kind's `NodeDefinition` so adding a new kind with materials is a
* registry-only edit. Today the registry doesn't surface material fields
* in a uniform way (each kind's panel reads / writes them directly), so
* this map mirrors the legacy behavior 1:1.
*/
const MATERIAL_FIELDS_BY_KIND: Record<string, ReadonlyArray<string>> = {
wall: [
'material',
'materialPreset',
'interiorMaterial',
'interiorMaterialPreset',
'exteriorMaterial',
'exteriorMaterialPreset',
],
slab: ['material', 'materialPreset'],
ceiling: ['material', 'materialPreset'],
fence: ['material', 'materialPreset'],
shelf: ['material', 'materialPreset'],
'roof-segment': ['material', 'materialPreset'],
'stair-segment': ['material', 'materialPreset'],
window: ['material', 'materialPreset'],
door: ['material', 'materialPreset'],
roof: [
'material',
'materialPreset',
'topMaterial',
'topMaterialPreset',
'edgeMaterial',
'edgeMaterialPreset',
'wallMaterial',
'wallMaterialPreset',
],
stair: [
'material',
'materialPreset',
'railingMaterial',
'railingMaterialPreset',
'treadMaterial',
'treadMaterialPreset',
'sideMaterial',
'sideMaterialPreset',
],
}
function stripMaterials(node: AnyNode): AnyNode {
const fields = MATERIAL_FIELDS_BY_KIND[node.type]
if (!fields) return node
const next = { ...node } as Record<string, unknown>
for (const field of fields) delete next[field]
return next as AnyNode
}
function findLevelBuildingId(nodes: Record<AnyNodeId, AnyNode>, levelId: AnyNodeId) {
for (const node of Object.values(nodes)) {
if (node.type !== 'building' || !('children' in node) || !Array.isArray(node.children)) {
continue
}
if ((node.children as AnyNodeId[]).includes(levelId)) {
return node.id as AnyNodeId
}
}
return undefined
}
export function buildLevelDuplicateCreateOps({
nodes,
level,
levels,
preset,
}: {
nodes: Record<AnyNodeId, AnyNode>
level: LevelNode
levels: LevelNode[]
preset: LevelDuplicatePreset
}) {
const { clonedNodes, newLevelId } = cloneLevelSubtree(nodes, level.id)
const parentBuildingId =
(level.parentId as AnyNodeId | null) ?? findLevelBuildingId(nodes, level.id)
const nextLevelNumber = level.level + 1
const shiftedLevels = levels
.filter((entry) => entry.id !== level.id && entry.level >= nextLevelNumber)
.map((entry) => ({
id: entry.id,
level: entry.level + 1,
}))
const filteredNodes = clonedNodes
.filter((node) => shouldKeepNode(node, preset))
.map((node) => (preset === 'structure' ? stripMaterials(node) : node))
const keptIds = new Set(filteredNodes.map((node) => node.id))
const cleanedNodes = filteredNodes.map((node) => {
if (!('children' in node && Array.isArray(node.children))) {
return node
}
return {
...node,
children: node.children.filter((childId) => keptIds.has(childId as AnyNodeId)),
} as AnyNode
})
return {
createOps: cleanedNodes.map((node) => ({
node:
node.id === newLevelId
? ({
...node,
level: nextLevelNumber,
} as AnyNode)
: node,
parentId:
node.id === newLevelId
? parentBuildingId
: ((node.parentId as AnyNodeId | null) ?? undefined),
})),
newLevelId,
shiftedLevels,
}
}