Files
editor/packages/viewer/src/systems/level/level-system.tsx
T
Open Pascal 8eab9979ab fix: address 2nd-round adversarial review of PR #402 fixes
- 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
2026-06-16 19:56:31 +00:00

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
}