feat: implement StairOpeningSystem to handle automated stair opening updates and adjust camera distance constraints

This commit is contained in:
sudhir
2026-05-20 00:53:10 +00:00
committed by open-pascal
parent 8b73d7986b
commit c31c08fd90
5 changed files with 68 additions and 25 deletions
+1
View File
@@ -130,6 +130,7 @@ export {
resolveElevatorServiceLevels, resolveElevatorServiceLevels,
} from './systems/elevator/elevator-service' } from './systems/elevator/elevator-service'
export { syncAutoStairOpenings } from './systems/stair/stair-opening-sync' export { syncAutoStairOpenings } from './systems/stair/stair-opening-sync'
export { StairOpeningSystem } from './systems/stair/stair-opening-system'
export { export {
getClampedWallCurveOffset, getClampedWallCurveOffset,
getMaxWallCurveOffset, getMaxWallCurveOffset,
@@ -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<string, AnyNode>,
prevNodes: Record<string, AnyNode>,
) {
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<typeof syncAutoStairOpenings>) => {
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
}
@@ -7,7 +7,7 @@ import {
sceneRegistry, sceneRegistry,
useScene, useScene,
} from '@pascal-app/core' } 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 { CameraControls, CameraControlsImpl } from '@react-three/drei'
import { useThree } from '@react-three/fiber' import { useThree } from '@react-three/fiber'
import { useCallback, useEffect, useMemo, useRef } from 'react' import { useCallback, useEffect, useMemo, useRef } from 'react'
@@ -440,9 +440,9 @@ export const CustomCameraControls = () => {
return ( return (
<CameraControls <CameraControls
makeDefault makeDefault
maxDistance={250} maxDistance={100}
maxPolarAngle={maxPolarAngle} maxPolarAngle={maxPolarAngle}
minDistance={2} minDistance={10}
minPolarAngle={0} minPolarAngle={0}
mouseButtons={mouseButtons} mouseButtons={mouseButtons}
onRest={onRest} onRest={onRest}
@@ -1,5 +1,6 @@
'use client' 'use client'
import { StairOpeningSystem } from '@pascal-app/core'
import { Canvas, extend, type ThreeToJSXElements, useFrame, useThree } from '@react-three/fiber' import { Canvas, extend, type ThreeToJSXElements, useFrame, useThree } from '@react-three/fiber'
import { useEffect, useMemo, useRef } from 'react' import { useEffect, useMemo, useRef } from 'react'
import * as THREE from 'three/webgpu' import * as THREE from 'three/webgpu'
@@ -216,6 +217,9 @@ const Viewer: React.FC<ViewerProps> = ({
builder, swaps the registered group's children. See builder, swaps the registered group's children. See
wiki/architecture/node-definitions.md. */} wiki/architecture/node-definitions.md. */}
<GeometrySystem /> <GeometrySystem />
{/* Automated stair opening sync — updates slab/ceiling cutouts
whenever stairs, slabs, or levels change. */}
<StairOpeningSystem />
{/* Mounts systems contributed by registry-backed kinds. Each {/* Mounts systems contributed by registry-backed kinds. Each
kind's `def.system` is loaded via lazy() and rendered here, kind's `def.system` is loaded via lazy() and rendered here,
ordered by `system.priority`. */} ordered by `system.priority`. */}
@@ -6,11 +6,10 @@ import {
type StairSegmentNode, type StairSegmentNode,
sceneRegistry, sceneRegistry,
spatialGridManager, spatialGridManager,
syncAutoStairOpenings,
useScene, useScene,
} from '@pascal-app/core' } from '@pascal-app/core'
import { useFrame } from '@react-three/fiber' import { useFrame } from '@react-three/fiber'
import { useEffect, useRef } from 'react' import { useRef } from 'react'
import * as THREE from 'three' import * as THREE from 'three'
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js' import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
@@ -30,26 +29,6 @@ export const StairSystem = () => {
const dirtyNodes = useScene((state) => state.dirtyNodes) const dirtyNodes = useScene((state) => state.dirtyNodes)
const clearDirty = useScene((state) => state.clearDirty) const clearDirty = useScene((state) => state.clearDirty)
const rootNodeIds = useScene((state) => state.rootNodeIds) const rootNodeIds = useScene((state) => state.rootNodeIds)
const syncingAutoOpeningsRef = useRef(false)
useEffect(() => {
const applyUpdates = (updates: ReturnType<typeof syncAutoStairOpenings>) => {
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(() => { useFrame(() => {
if (rootNodeIds.length === 0) { if (rootNodeIds.length === 0) {