- core: decouple drag-follow from distributionRole — add portConnectivityFollow flag to NodeDefinition; pipe-trap opts out (portConnectivityFollow: false) so dragging a connected pipe endpoint stretches the trap arm instead of yanking the anchored trap fixture - core: remove the module-level getLevelHeight cache entirely — it was keyed only by nodes-object identity, which could return stale heights for in-place mutations by pure/headless callers. The function is now fully pure and deterministic; viewer hot path recomputes per frame as before (the cache only ever skipped the resolver-free branch) - test: harden port-connectivity-pipe.test.ts — real DuctSegmentNode cross-family isolation case (was waste-vs-vent), new pipe-trap anchor case (run drag doesn't move trap; trap drag still stretches run), and beforeEach/afterEach registry reset instead of leaky beforeAll - nit: biome format/import-order on all touched files
51 lines
1.9 KiB
TypeScript
51 lines
1.9 KiB
TypeScript
import { getLevelHeight, type LevelNode, sceneRegistry, useScene } from '@pascal-app/core'
|
|
import { useFrame } from '@react-three/fiber'
|
|
import { lerp } from 'three/src/math/MathUtils.js'
|
|
import useViewer from '../../store/use-viewer'
|
|
|
|
const EXPLODED_GAP = 5
|
|
|
|
export const LevelSystem = () => {
|
|
useFrame((_, delta) => {
|
|
const nodes = useScene.getState().nodes
|
|
const levelMode = useViewer.getState().levelMode
|
|
const selectedLevel = useViewer.getState().selection.levelId
|
|
|
|
// Collect and sort levels by floor index so we can compute cumulative offsets.
|
|
// Level 0 → Y=0, Level 1 → Y=height(0), Level 2 → Y=height(0)+height(1), etc.
|
|
type LevelEntry = {
|
|
levelId: string
|
|
index: number
|
|
obj: NonNullable<ReturnType<typeof sceneRegistry.nodes.get>>
|
|
}
|
|
const entries: LevelEntry[] = []
|
|
sceneRegistry.byType.level!.forEach((levelId) => {
|
|
const obj = sceneRegistry.nodes.get(levelId)
|
|
const level = nodes[levelId as LevelNode['id']]
|
|
if (obj && level) {
|
|
entries.push({ levelId, index: (level as any).level ?? 0, obj })
|
|
}
|
|
})
|
|
entries.sort((a, b) => a.index - b.index)
|
|
|
|
// Walk sorted levels, accumulating base Y offsets
|
|
let cumulativeY = 0
|
|
for (const { levelId, index, obj } of entries) {
|
|
const level = nodes[levelId as LevelNode['id']]
|
|
const baseY = cumulativeY
|
|
const explodedExtra = levelMode === 'exploded' ? index * EXPLODED_GAP : 0
|
|
const targetY = baseY + explodedExtra
|
|
|
|
obj.position.y = lerp(obj.position.y, targetY, delta * 12) // Smoothly animate to new Y position
|
|
obj.visible = levelMode !== 'solo' || level?.id === selectedLevel || !selectedLevel
|
|
|
|
cumulativeY += getLevelHeight(
|
|
levelId,
|
|
nodes,
|
|
(wallId) => sceneRegistry.nodes.get(wallId)?.position.y,
|
|
)
|
|
}
|
|
}, 5) // Using a lower priority so it runs after transforms from other systems have settled
|
|
return null
|
|
}
|