Refactor elevator systems across core, viewer, and editor
This commit is contained in:
@@ -22,6 +22,13 @@ export function ElevatorAnimationSystem() {
|
||||
const nodes = useScene.getState().nodes
|
||||
const now = clock.getElapsedTime() * 1000
|
||||
|
||||
for (const elevatorId of Object.keys(interactive.elevators)) {
|
||||
const typedElevatorId = elevatorId as AnyNodeId
|
||||
if (nodes[typedElevatorId]?.type !== 'elevator') {
|
||||
interactive.removeElevator(typedElevatorId)
|
||||
}
|
||||
}
|
||||
|
||||
for (const elevatorId of sceneRegistry.byType.elevator) {
|
||||
const typedElevatorId = elevatorId as AnyNodeId
|
||||
const node = nodes[typedElevatorId]
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
type AnyNodeId,
|
||||
resolveElevatorDispatchTarget,
|
||||
useInteractive,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useThree } from '@react-three/fiber'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { type Object3D, Raycaster, Vector2 } from 'three'
|
||||
|
||||
type ElevatorButtonAction = 'open-door' | 'request-level'
|
||||
|
||||
type ElevatorButtonUserData = {
|
||||
action: ElevatorButtonAction
|
||||
disabled: boolean
|
||||
elevatorId: AnyNodeId
|
||||
kind: 'cab' | 'landing'
|
||||
levelId?: AnyNodeId
|
||||
}
|
||||
|
||||
function getElevatorButtonData(object: Object3D): ElevatorButtonUserData | null {
|
||||
let current: Object3D | null = object
|
||||
|
||||
while (current) {
|
||||
const data = (current.userData as { elevatorButton?: ElevatorButtonUserData }).elevatorButton
|
||||
if (data) return data
|
||||
current = current.parent
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function ElevatorInteractionSystem() {
|
||||
const camera = useThree((state) => state.camera)
|
||||
const gl = useThree((state) => state.gl)
|
||||
const scene = useThree((state) => state.scene)
|
||||
const raycasterRef = useRef(new Raycaster())
|
||||
const pointerRef = useRef(new Vector2())
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = gl.domElement
|
||||
|
||||
const handlePointerDown = (event: PointerEvent) => {
|
||||
if (event.button !== 0) return
|
||||
|
||||
const rect = canvas.getBoundingClientRect()
|
||||
const pointer = pointerRef.current
|
||||
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1
|
||||
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1
|
||||
|
||||
const raycaster = raycasterRef.current
|
||||
raycaster.setFromCamera(pointer, camera)
|
||||
|
||||
const button = raycaster
|
||||
.intersectObjects(scene.children, true)
|
||||
.map((intersection) => getElevatorButtonData(intersection.object))
|
||||
.find((data): data is ElevatorButtonUserData => data !== null)
|
||||
|
||||
if (!button || button.disabled) return
|
||||
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
|
||||
if (button.action === 'open-door') {
|
||||
useInteractive.getState().openElevatorDoor(button.elevatorId)
|
||||
return
|
||||
}
|
||||
|
||||
if (!button.levelId) return
|
||||
|
||||
const targetElevatorId =
|
||||
button.kind === 'landing'
|
||||
? resolveElevatorDispatchTarget({
|
||||
elevators: useInteractive.getState().elevators,
|
||||
levelId: button.levelId,
|
||||
nodes: useScene.getState().nodes,
|
||||
requestedElevatorId: button.elevatorId,
|
||||
})
|
||||
: button.elevatorId
|
||||
|
||||
useInteractive.getState().requestElevator(targetElevatorId, button.levelId)
|
||||
}
|
||||
|
||||
canvas.addEventListener('pointerdown', handlePointerDown, true)
|
||||
return () => {
|
||||
canvas.removeEventListener('pointerdown', handlePointerDown, true)
|
||||
}
|
||||
}, [camera, gl, scene])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import { type AnyNode, syncAutoElevatorOpenings, useScene } from '@pascal-app/core'
|
||||
import { useEffect, useRef } from 'react'
|
||||
|
||||
function isOpeningRelevantNode(node: AnyNode | undefined) {
|
||||
return (
|
||||
node?.type === 'building' ||
|
||||
node?.type === 'ceiling' ||
|
||||
node?.type === 'elevator' ||
|
||||
node?.type === 'level' ||
|
||||
node?.type === 'slab'
|
||||
)
|
||||
}
|
||||
|
||||
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 ElevatorOpeningSystem = () => {
|
||||
const syncingAutoOpeningsRef = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
const applyUpdates = (updates: ReturnType<typeof syncAutoElevatorOpenings>) => {
|
||||
if (updates.length === 0) return
|
||||
syncingAutoOpeningsRef.current = true
|
||||
useScene.getState().updateNodes(updates)
|
||||
queueMicrotask(() => {
|
||||
syncingAutoOpeningsRef.current = false
|
||||
})
|
||||
}
|
||||
|
||||
applyUpdates(syncAutoElevatorOpenings(useScene.getState().nodes))
|
||||
|
||||
return useScene.subscribe((state, prevState) => {
|
||||
if (syncingAutoOpeningsRef.current) return
|
||||
if (!hasOpeningRelevantNodeChange(state.nodes, prevState.nodes)) return
|
||||
applyUpdates(syncAutoElevatorOpenings(state.nodes))
|
||||
})
|
||||
}, [])
|
||||
|
||||
return null
|
||||
}
|
||||
Reference in New Issue
Block a user