Refactor elevator runtime into core
This commit is contained in:
@@ -1,7 +1,20 @@
|
||||
import {
|
||||
type AnyNode,
|
||||
type AnyNodeId,
|
||||
type ElevatorDoorSide,
|
||||
type ElevatorNode,
|
||||
getElevatorCabDepth,
|
||||
getElevatorCabWidth,
|
||||
getElevatorDoorLeafSides,
|
||||
getElevatorDoorLeafWidth,
|
||||
getElevatorDoorLeafX,
|
||||
getElevatorShaftDepth,
|
||||
getElevatorShaftWallThickness,
|
||||
getElevatorShaftWidth,
|
||||
getResolvedElevatorDoorPanelStyle as getResolvedDoorPanelStyle,
|
||||
getResolvedElevatorDoorStyle as getResolvedDoorStyle,
|
||||
getResolvedElevatorShaftStyle as getResolvedShaftStyle,
|
||||
resolveElevatorLevels,
|
||||
useInteractive,
|
||||
useLiveNodeOverrides,
|
||||
useLiveTransforms,
|
||||
@@ -21,7 +34,6 @@ import {
|
||||
} from 'three'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useNodeEvents } from '../../../hooks/use-node-events'
|
||||
import { resolveElevatorLevels } from '../../../systems/elevator/elevator-utils'
|
||||
|
||||
const SHAFT_WALL_COLOR = '#d7dce4'
|
||||
const SHAFT_SIDE_COLOR = '#4b5563'
|
||||
@@ -30,7 +42,6 @@ const CAB_COLOR = '#d7dde5'
|
||||
const GLASS_COLOR = '#f8fafc'
|
||||
const DOOR_COLOR = '#8e98a6'
|
||||
const PANEL_COLOR = '#1f2937'
|
||||
const DEFAULT_SHAFT_WALL_THICKNESS = 0.09
|
||||
|
||||
type Vector3Tuple = [number, number, number]
|
||||
|
||||
@@ -41,10 +52,8 @@ const BUTTON_RING_GEOMETRY = new TorusGeometry(1.12, 0.12, 8, 24)
|
||||
const LABEL_MATRIX_DUMMY = new Object3D()
|
||||
const SHAFT_TOP_FRAME_CLEARANCE = 0.006
|
||||
|
||||
type ElevatorDoorSide = 'left' | 'right'
|
||||
type ElevatorDoorPanelStyleValue = ElevatorNode['doorPanelStyle']
|
||||
type ElevatorDoorStyleValue = ElevatorNode['doorStyle']
|
||||
type ElevatorShaftStyleValue = ElevatorNode['shaftStyle']
|
||||
|
||||
const SHAFT_WALL_MATERIAL = new MeshStandardMaterial({
|
||||
color: SHAFT_WALL_COLOR,
|
||||
@@ -611,78 +620,6 @@ function ElevatorMeshButton({
|
||||
)
|
||||
}
|
||||
|
||||
function getResolvedDoorStyle(
|
||||
doorStyle: ElevatorDoorStyleValue | undefined,
|
||||
): ElevatorDoorStyleValue {
|
||||
return doorStyle ?? 'center-opening'
|
||||
}
|
||||
|
||||
function getResolvedDoorPanelStyle(
|
||||
doorPanelStyle: ElevatorDoorPanelStyleValue | undefined,
|
||||
): ElevatorDoorPanelStyleValue {
|
||||
return doorPanelStyle ?? 'glass-frame'
|
||||
}
|
||||
|
||||
function getResolvedShaftStyle(
|
||||
shaftStyle: ElevatorShaftStyleValue | undefined,
|
||||
): ElevatorShaftStyleValue {
|
||||
return shaftStyle ?? 'solid'
|
||||
}
|
||||
|
||||
function getElevatorDoorLeafSides(
|
||||
doorStyle: ElevatorDoorStyleValue | undefined,
|
||||
): ElevatorDoorSide[] {
|
||||
const resolvedDoorStyle = getResolvedDoorStyle(doorStyle)
|
||||
if (resolvedDoorStyle === 'single-left') return ['left']
|
||||
if (resolvedDoorStyle === 'single-right') return ['right']
|
||||
return ['left', 'right']
|
||||
}
|
||||
|
||||
function getElevatorDoorLeafX(
|
||||
side: ElevatorDoorSide,
|
||||
openingWidth: number,
|
||||
doorOpen: number,
|
||||
doorStyle: ElevatorDoorStyleValue | undefined,
|
||||
) {
|
||||
const resolvedDoorStyle = getResolvedDoorStyle(doorStyle)
|
||||
if (resolvedDoorStyle === 'center-opening') {
|
||||
const direction = side === 'left' ? -1 : 1
|
||||
return direction * (openingWidth / 4 + doorOpen * openingWidth * 0.34)
|
||||
}
|
||||
|
||||
const direction = resolvedDoorStyle === 'single-left' ? -1 : 1
|
||||
return direction * doorOpen * openingWidth * 0.68
|
||||
}
|
||||
|
||||
function getElevatorDoorLeafWidth(
|
||||
openingWidth: number,
|
||||
doorStyle: ElevatorDoorStyleValue | undefined,
|
||||
) {
|
||||
return getResolvedDoorStyle(doorStyle) === 'center-opening'
|
||||
? Math.max(openingWidth / 2 - 0.018, 0.12)
|
||||
: Math.max(openingWidth - 0.018, 0.18)
|
||||
}
|
||||
|
||||
function getElevatorCabWidth(node: ElevatorNode) {
|
||||
return Math.max(node.width, 0.8)
|
||||
}
|
||||
|
||||
function getElevatorCabDepth(node: ElevatorNode) {
|
||||
return Math.max(node.depth, 0.8)
|
||||
}
|
||||
|
||||
function getElevatorShaftWallThickness(node: ElevatorNode) {
|
||||
return Math.max(node.shaftWallThickness ?? DEFAULT_SHAFT_WALL_THICKNESS, 0.04)
|
||||
}
|
||||
|
||||
function getElevatorShaftWidth(node: ElevatorNode, cabWidth = getElevatorCabWidth(node)) {
|
||||
return Math.max(node.shaftWidth ?? cabWidth, cabWidth, 0.8)
|
||||
}
|
||||
|
||||
function getElevatorShaftDepth(node: ElevatorNode, cabDepth = getElevatorCabDepth(node)) {
|
||||
return Math.max(node.shaftDepth ?? cabDepth, cabDepth, 0.8)
|
||||
}
|
||||
|
||||
function getElevatorLevelContextNodes(
|
||||
elevator: ElevatorNode,
|
||||
nodes: ReturnType<typeof useScene.getState>['nodes'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { ElevatorOpeningSystem } from '@pascal-app/core'
|
||||
import { ElevatorOpeningSystem, ElevatorRuntimeSystem } from '@pascal-app/core'
|
||||
import { Canvas, extend, type ThreeToJSXElements, useFrame, useThree } from '@react-three/fiber'
|
||||
import { useEffect, useMemo, useRef } from 'react'
|
||||
import * as THREE from 'three/webgpu'
|
||||
@@ -8,7 +8,6 @@ import useViewer from '../../store/use-viewer'
|
||||
import { CeilingSystem } from '../../systems/ceiling/ceiling-system'
|
||||
import { DoorAnimationSystem } from '../../systems/door/door-animation-system'
|
||||
import { DoorSystem } from '../../systems/door/door-system'
|
||||
import { ElevatorAnimationSystem } from '../../systems/elevator/elevator-animation-system'
|
||||
import { ElevatorInteractionSystem } from '../../systems/elevator/elevator-interaction-system'
|
||||
import { FenceSystem } from '../../systems/fence/fence-system'
|
||||
import { GuideSystem } from '../../systems/guide/guide-system'
|
||||
@@ -225,7 +224,7 @@ const Viewer: React.FC<ViewerProps> = ({
|
||||
{/* Core systems */}
|
||||
<CeilingSystem />
|
||||
<DoorAnimationSystem />
|
||||
<ElevatorAnimationSystem />
|
||||
<ElevatorRuntimeSystem />
|
||||
<ElevatorInteractionSystem />
|
||||
<ElevatorOpeningSystem />
|
||||
<WindowAnimationSystem />
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import {
|
||||
type AnyNodeId,
|
||||
type ElevatorNode,
|
||||
sceneRegistry,
|
||||
useInteractive,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import { resolveElevatorLevels } from './elevator-utils'
|
||||
|
||||
const EPSILON = 0.001
|
||||
|
||||
function moveToward(current: number, target: number, maxDelta: number) {
|
||||
const delta = target - current
|
||||
if (Math.abs(delta) <= maxDelta) return target
|
||||
return current + Math.sign(delta) * maxDelta
|
||||
}
|
||||
|
||||
export function ElevatorAnimationSystem() {
|
||||
useFrame(({ clock }, delta) => {
|
||||
const interactive = useInteractive.getState()
|
||||
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]
|
||||
if (node?.type !== 'elevator') {
|
||||
interactive.removeElevator(typedElevatorId)
|
||||
continue
|
||||
}
|
||||
|
||||
const elevator = node as ElevatorNode
|
||||
const { entries, defaultEntry } = resolveElevatorLevels(elevator, nodes)
|
||||
if (!defaultEntry) continue
|
||||
|
||||
const state = interactive.elevators[typedElevatorId]
|
||||
if (!state) {
|
||||
interactive.initElevator(typedElevatorId, defaultEntry.id as AnyNodeId, defaultEntry.baseY)
|
||||
continue
|
||||
}
|
||||
|
||||
const currentEntry =
|
||||
entries.find((entry) => entry.id === state.currentLevelId) ?? defaultEntry
|
||||
if (currentEntry.id !== state.currentLevelId) {
|
||||
interactive.setElevatorState(typedElevatorId, {
|
||||
currentLevelId: currentEntry.id as AnyNodeId,
|
||||
carY: currentEntry.baseY,
|
||||
targetLevelId: null,
|
||||
phase: 'idle',
|
||||
phaseStartedAt: null,
|
||||
queue: [],
|
||||
doorOpen: 0,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const targetEntry = state.targetLevelId
|
||||
? entries.find((entry) => entry.id === state.targetLevelId)
|
||||
: state.queue[0]
|
||||
? entries.find((entry) => entry.id === state.queue[0])
|
||||
: null
|
||||
|
||||
const doorDurationMs = Math.max(elevator.doorDurationMs ?? 900, 1)
|
||||
const doorStep = (delta * 1000) / doorDurationMs
|
||||
|
||||
switch (state.phase) {
|
||||
case 'idle': {
|
||||
const nextLevelId = state.queue[0] ?? null
|
||||
if (!nextLevelId) {
|
||||
if (state.doorOpen > EPSILON) {
|
||||
interactive.setElevatorState(typedElevatorId, {
|
||||
doorOpen: Math.max(0, state.doorOpen - doorStep),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
interactive.setElevatorState(typedElevatorId, {
|
||||
targetLevelId: nextLevelId,
|
||||
phase:
|
||||
state.doorOpen > EPSILON
|
||||
? 'closing'
|
||||
: nextLevelId === state.currentLevelId
|
||||
? 'opening'
|
||||
: 'moving',
|
||||
phaseStartedAt: now,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'closing': {
|
||||
const doorOpen = Math.max(0, state.doorOpen - doorStep)
|
||||
interactive.setElevatorState(typedElevatorId, {
|
||||
doorOpen,
|
||||
phase: doorOpen <= EPSILON ? (state.targetLevelId ? 'moving' : 'idle') : 'closing',
|
||||
phaseStartedAt: doorOpen <= EPSILON ? now : state.phaseStartedAt,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'moving': {
|
||||
if (!targetEntry) {
|
||||
interactive.setElevatorState(typedElevatorId, {
|
||||
targetLevelId: null,
|
||||
phase: 'idle',
|
||||
queue: [],
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
const speed = Math.max(elevator.speed ?? 2.2, 0.1)
|
||||
const nextY = moveToward(state.carY, targetEntry.baseY, speed * delta)
|
||||
const arrived = Math.abs(nextY - targetEntry.baseY) <= EPSILON
|
||||
interactive.setElevatorState(typedElevatorId, {
|
||||
carY: nextY,
|
||||
currentLevelId: arrived ? (targetEntry.id as AnyNodeId) : state.currentLevelId,
|
||||
phase: arrived ? 'opening' : 'moving',
|
||||
phaseStartedAt: arrived ? now : state.phaseStartedAt,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'opening': {
|
||||
const doorOpen = Math.min(1, state.doorOpen + doorStep)
|
||||
interactive.setElevatorState(typedElevatorId, {
|
||||
doorOpen,
|
||||
phase: doorOpen >= 1 - EPSILON ? 'open' : 'opening',
|
||||
phaseStartedAt: doorOpen >= 1 - EPSILON ? now : state.phaseStartedAt,
|
||||
targetLevelId: doorOpen >= 1 - EPSILON ? null : state.targetLevelId,
|
||||
queue:
|
||||
doorOpen >= 1 - EPSILON && state.queue[0] === state.currentLevelId
|
||||
? state.queue.slice(1)
|
||||
: state.queue,
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'open': {
|
||||
const elapsed = now - (state.phaseStartedAt ?? now)
|
||||
if (elapsed < Math.max(elevator.dwellMs ?? 1400, 0)) break
|
||||
|
||||
interactive.setElevatorState(typedElevatorId, {
|
||||
phase: 'closing',
|
||||
phaseStartedAt: now,
|
||||
targetLevelId: state.queue[0] ?? null,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 2)
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
type AnyNodeId,
|
||||
openElevatorDoor,
|
||||
resolveElevatorDispatchTarget,
|
||||
requestElevatorLevel,
|
||||
useInteractive,
|
||||
useScene,
|
||||
} from '@pascal-app/core'
|
||||
@@ -62,7 +64,7 @@ export function ElevatorInteractionSystem() {
|
||||
event.stopPropagation()
|
||||
|
||||
if (button.action === 'open-door') {
|
||||
useInteractive.getState().openElevatorDoor(button.elevatorId)
|
||||
openElevatorDoor(button.elevatorId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -78,7 +80,7 @@ export function ElevatorInteractionSystem() {
|
||||
})
|
||||
: button.elevatorId
|
||||
|
||||
useInteractive.getState().requestElevator(targetElevatorId, button.levelId)
|
||||
requestElevatorLevel(targetElevatorId, button.levelId)
|
||||
}
|
||||
|
||||
canvas.addEventListener('pointerdown', handlePointerDown, true)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import {
|
||||
resolveElevatorBuildingLevels,
|
||||
resolveElevatorServiceLevels,
|
||||
type AnyNode,
|
||||
type AnyNodeId,
|
||||
type ElevatorNode,
|
||||
type LevelNode,
|
||||
} from '@pascal-app/core'
|
||||
import { getLevelHeight } from '../level/level-utils'
|
||||
|
||||
export type ElevatorLevelEntry = {
|
||||
id: LevelNode['id']
|
||||
label: string
|
||||
baseY: number
|
||||
}
|
||||
|
||||
export function resolveElevatorLevels(
|
||||
elevator: ElevatorNode,
|
||||
nodes: Record<AnyNodeId, AnyNode>,
|
||||
): {
|
||||
entries: ElevatorLevelEntry[]
|
||||
defaultEntry: ElevatorLevelEntry | null
|
||||
shaftBaseY: number
|
||||
shaftTopY: number
|
||||
totalHeight: number
|
||||
} {
|
||||
const allLevels = resolveElevatorBuildingLevels(elevator, nodes)
|
||||
|
||||
const baseYByLevelId = new Map<string, number>()
|
||||
let cumulativeY = 0
|
||||
for (const level of allLevels) {
|
||||
baseYByLevelId.set(level.id, cumulativeY)
|
||||
cumulativeY += getLevelHeight(level.id, nodes)
|
||||
}
|
||||
|
||||
const serviceLevels = resolveElevatorServiceLevels(elevator, nodes)
|
||||
const entries = serviceLevels.map((level) => ({
|
||||
id: level.id,
|
||||
label: String(level.level),
|
||||
baseY: baseYByLevelId.get(level.id) ?? 0,
|
||||
}))
|
||||
|
||||
const defaultEntry =
|
||||
entries.find((entry) => entry.id === elevator.defaultLevelId) ??
|
||||
entries.find((entry) => entry.id === elevator.fromLevelId) ??
|
||||
entries[0] ??
|
||||
null
|
||||
const firstServedLevel = serviceLevels[0] ?? null
|
||||
const lastServedLevel = serviceLevels[serviceLevels.length - 1] ?? null
|
||||
const shaftBaseY = firstServedLevel ? (baseYByLevelId.get(firstServedLevel.id) ?? 0) : 0
|
||||
const lastServedIndex = lastServedLevel
|
||||
? allLevels.findIndex((level) => level.id === lastServedLevel.id)
|
||||
: -1
|
||||
const nextLevel = lastServedIndex >= 0 ? allLevels[lastServedIndex + 1] : null
|
||||
const shaftTopY = nextLevel
|
||||
? (baseYByLevelId.get(nextLevel.id) ?? cumulativeY)
|
||||
: lastServedLevel
|
||||
? cumulativeY
|
||||
: elevator.cabHeight + 0.3
|
||||
|
||||
return {
|
||||
entries,
|
||||
defaultEntry,
|
||||
shaftBaseY,
|
||||
shaftTopY,
|
||||
totalHeight: Math.max(shaftTopY - shaftBaseY, elevator.cabHeight + 0.3),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user