diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5a28397e..2d254f64 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -130,6 +130,7 @@ export { resolveElevatorServiceLevels, } from './systems/elevator/elevator-service' export { syncAutoStairOpenings } from './systems/stair/stair-opening-sync' +export { StairOpeningSystem } from './systems/stair/stair-opening-system' export { getClampedWallCurveOffset, getMaxWallCurveOffset, diff --git a/packages/core/src/systems/stair/stair-opening-system.tsx b/packages/core/src/systems/stair/stair-opening-system.tsx new file mode 100644 index 00000000..2558fcc5 --- /dev/null +++ b/packages/core/src/systems/stair/stair-opening-system.tsx @@ -0,0 +1,59 @@ +'use client' + +import { useEffect, useRef } from 'react' +import type { AnyNode } from '../../schema' +import useScene from '../../store/use-scene' +import { syncAutoStairOpenings } from './stair-opening-sync' + +function isOpeningRelevantNode(node: AnyNode | undefined) { + return ( + node?.type === 'building' || + node?.type === 'ceiling' || + node?.type === 'level' || + node?.type === 'slab' || + node?.type === 'stair' || + node?.type === 'stair-segment' + ) +} + +function hasOpeningRelevantNodeChange( + nextNodes: Record, + prevNodes: Record, +) { + if (nextNodes === prevNodes) return false + + const ids = new Set([...Object.keys(nextNodes), ...Object.keys(prevNodes)]) + for (const id of ids) { + const nextNode = nextNodes[id] + const prevNode = prevNodes[id] + if (nextNode === prevNode) continue + if (isOpeningRelevantNode(nextNode) || isOpeningRelevantNode(prevNode)) return true + } + + return false +} + +export const StairOpeningSystem = () => { + const syncingAutoOpeningsRef = useRef(false) + + useEffect(() => { + const applyUpdates = (updates: ReturnType) => { + if (updates.length === 0) return + syncingAutoOpeningsRef.current = true + useScene.getState().updateNodes(updates) + queueMicrotask(() => { + syncingAutoOpeningsRef.current = false + }) + } + + applyUpdates(syncAutoStairOpenings(useScene.getState().nodes)) + + return useScene.subscribe((state, prevState) => { + if (syncingAutoOpeningsRef.current) return + if (!hasOpeningRelevantNodeChange(state.nodes, prevState.nodes)) return + applyUpdates(syncAutoStairOpenings(state.nodes)) + }) + }, []) + + return null +} diff --git a/packages/editor/src/components/editor/custom-camera-controls.tsx b/packages/editor/src/components/editor/custom-camera-controls.tsx index 38214e30..7392da7b 100644 --- a/packages/editor/src/components/editor/custom-camera-controls.tsx +++ b/packages/editor/src/components/editor/custom-camera-controls.tsx @@ -7,7 +7,7 @@ import { sceneRegistry, useScene, } from '@pascal-app/core' -import { useViewer, ZONE_LAYER } from '@pascal-app/viewer' +import { useViewer, WalkthroughControls, ZONE_LAYER } from '@pascal-app/viewer' import { CameraControls, CameraControlsImpl } from '@react-three/drei' import { useThree } from '@react-three/fiber' import { useCallback, useEffect, useMemo, useRef } from 'react' @@ -440,9 +440,9 @@ export const CustomCameraControls = () => { return ( = ({ builder, swaps the registered group's children. See wiki/architecture/node-definitions.md. */} + {/* Automated stair opening sync — updates slab/ceiling cutouts + whenever stairs, slabs, or levels change. */} + {/* Mounts systems contributed by registry-backed kinds. Each kind's `def.system` is loaded via lazy() and rendered here, ordered by `system.priority`. */} diff --git a/packages/viewer/src/systems/stair/stair-system.tsx b/packages/viewer/src/systems/stair/stair-system.tsx index 99601d6a..fd2a74d6 100644 --- a/packages/viewer/src/systems/stair/stair-system.tsx +++ b/packages/viewer/src/systems/stair/stair-system.tsx @@ -6,11 +6,10 @@ import { type StairSegmentNode, sceneRegistry, spatialGridManager, - syncAutoStairOpenings, useScene, } from '@pascal-app/core' import { useFrame } from '@react-three/fiber' -import { useEffect, useRef } from 'react' +import { useRef } from 'react' import * as THREE from 'three' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' @@ -30,26 +29,6 @@ export const StairSystem = () => { const dirtyNodes = useScene((state) => state.dirtyNodes) const clearDirty = useScene((state) => state.clearDirty) const rootNodeIds = useScene((state) => state.rootNodeIds) - const syncingAutoOpeningsRef = useRef(false) - - useEffect(() => { - const applyUpdates = (updates: ReturnType) => { - if (updates.length === 0) return - syncingAutoOpeningsRef.current = true - useScene.getState().updateNodes(updates) - queueMicrotask(() => { - syncingAutoOpeningsRef.current = false - }) - } - - applyUpdates(syncAutoStairOpenings(useScene.getState().nodes)) - - return useScene.subscribe((state, prevState) => { - if (syncingAutoOpeningsRef.current) return - if (state.nodes === prevState.nodes) return - applyUpdates(syncAutoStairOpenings(state.nodes)) - }) - }, []) useFrame(() => { if (rootNodeIds.length === 0) {