feat: implement StairOpeningSystem to handle automated stair opening updates and adjust camera distance constraints
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user