@pascal-app/editor 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +754 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +58 -51
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +201 -83
- package/src/components/editor/first-person/bvh-ecctrl.tsx +8 -8
- package/src/components/editor/first-person-controls.tsx +825 -20
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5829 -12005
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +178 -149
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +433 -156
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +136 -62
- package/src/components/editor/thumbnail-generator.tsx +9 -3
- package/src/components/editor/use-floorplan-background-placement.ts +119 -32
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +26 -3
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +41 -105
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +31 -5
- package/src/components/tools/item/placement-strategies.ts +190 -15
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +694 -191
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -538
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +16 -6
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/control-modes.tsx +5 -82
- package/src/components/ui/action-menu/index.tsx +1 -113
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +243 -6
- package/src/components/ui/command-palette/editor-commands.tsx +18 -13
- package/src/components/ui/command-palette/index.tsx +2 -1
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +11 -32
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/helper-manager.tsx +16 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1 -0
- package/src/components/ui/item-catalog/item-catalog.tsx +3 -0
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/panel-manager.tsx +35 -47
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +4 -6
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -36
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +10 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +115 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +237 -5
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +211 -19
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +23 -1
- package/src/lib/sfx-player.ts +70 -22
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +320 -37
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,12 +1,50 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
type ElevatorDoorSide,
|
|
7
|
+
type ElevatorNode,
|
|
8
|
+
emitter,
|
|
9
|
+
getElevatorCabCenterZ,
|
|
10
|
+
getElevatorCabDepth,
|
|
11
|
+
getElevatorCabWidth,
|
|
12
|
+
getElevatorDoorLeafSides,
|
|
13
|
+
getElevatorDoorLeafWidth,
|
|
14
|
+
getElevatorDoorLeafX,
|
|
15
|
+
getElevatorShaftDepth,
|
|
16
|
+
getElevatorShaftWallThickness,
|
|
17
|
+
getElevatorShaftWidth,
|
|
18
|
+
getResolvedElevatorDoorStyle,
|
|
19
|
+
openElevatorDoor,
|
|
20
|
+
requestElevatorLevel,
|
|
21
|
+
resolveElevatorBuildingLevels,
|
|
22
|
+
resolveElevatorDispatchTarget,
|
|
23
|
+
resolveElevatorServiceLevels,
|
|
24
|
+
sceneRegistry,
|
|
25
|
+
useInteractive,
|
|
26
|
+
useScene,
|
|
27
|
+
} from '@pascal-app/core'
|
|
5
28
|
import { useViewer } from '@pascal-app/viewer'
|
|
6
29
|
import { KeyboardControls } from '@react-three/drei'
|
|
7
30
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
8
31
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
9
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
Box3,
|
|
34
|
+
BoxGeometry,
|
|
35
|
+
Euler,
|
|
36
|
+
type Group,
|
|
37
|
+
Matrix4,
|
|
38
|
+
Mesh,
|
|
39
|
+
MeshBasicMaterial,
|
|
40
|
+
type Object3D,
|
|
41
|
+
Ray,
|
|
42
|
+
Raycaster,
|
|
43
|
+
Vector2,
|
|
44
|
+
Vector3,
|
|
45
|
+
} from 'three'
|
|
46
|
+
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh'
|
|
47
|
+
import '../../three-types'
|
|
10
48
|
import {
|
|
11
49
|
closeDoorOpenState,
|
|
12
50
|
DOOR_SWING_OPEN_ANGLE,
|
|
@@ -34,6 +72,12 @@ const LOOK_SENSITIVITY = 0.002
|
|
|
34
72
|
const CONTROLLER_CENTER_FROM_EYE = 0.85
|
|
35
73
|
const DOOR_INTERACTION_DISTANCE = 2.5
|
|
36
74
|
const DOOR_LEAF_INTERACTION_DEPTH = 0.08
|
|
75
|
+
const ELEVATOR_RIDE_HORIZONTAL_PADDING = 0.18
|
|
76
|
+
const ELEVATOR_COLLIDER_HORIZONTAL_PADDING = 0.14
|
|
77
|
+
const ELEVATOR_COLLIDER_FLOOR_THICKNESS = 0.08
|
|
78
|
+
const ELEVATOR_COLLIDER_DOOR_DEPTH = 0.12
|
|
79
|
+
const ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD = 0.72
|
|
80
|
+
const DEFAULT_ELEVATOR_LEVEL_HEIGHT = 2.5
|
|
37
81
|
const keyboardMap = [
|
|
38
82
|
{ name: 'forward', keys: ['ArrowUp', 'KeyW'] },
|
|
39
83
|
{ name: 'backward', keys: ['ArrowDown', 'KeyS'] },
|
|
@@ -59,13 +103,421 @@ const doorOpeningLocalHit = new Vector3()
|
|
|
59
103
|
const doorOpeningLocalRay = new Ray()
|
|
60
104
|
const doorOpeningMatrix = new Matrix4()
|
|
61
105
|
const doorOpeningWorldHit = new Vector3()
|
|
106
|
+
const elevatorLocalControllerPosition = new Vector3()
|
|
107
|
+
const elevatorInteractionRaycaster = new Raycaster()
|
|
108
|
+
const elevatorColliderMatrix = new Matrix4()
|
|
109
|
+
const elevatorColliderLocalMatrix = new Matrix4()
|
|
110
|
+
const elevatorLocalEyePosition = new Vector3()
|
|
111
|
+
const elevatorWorldControllerPosition = new Vector3()
|
|
112
|
+
const elevatorColliderMaterial = new MeshBasicMaterial({ visible: false })
|
|
62
113
|
const spawnWorldPosition = new Vector3()
|
|
63
114
|
const spawnWorldEuler = new Euler(0, 0, 0, 'YXZ')
|
|
64
115
|
const windowInteractionRaycaster = new Raycaster()
|
|
65
116
|
|
|
66
|
-
type
|
|
67
|
-
|
|
68
|
-
|
|
117
|
+
type ElevatorColliderKind =
|
|
118
|
+
| 'cab-back'
|
|
119
|
+
| 'cab-ceiling'
|
|
120
|
+
| 'cab-door-left'
|
|
121
|
+
| 'cab-door-right'
|
|
122
|
+
| 'cab-door-gate'
|
|
123
|
+
| 'cab-floor'
|
|
124
|
+
| 'cab-left'
|
|
125
|
+
| 'cab-right'
|
|
126
|
+
| 'landing-door-gate'
|
|
127
|
+
| 'landing-door-left'
|
|
128
|
+
| 'landing-door-right'
|
|
129
|
+
| 'shaft-back'
|
|
130
|
+
| 'shaft-front-header'
|
|
131
|
+
| 'shaft-front-left'
|
|
132
|
+
| 'shaft-front-right'
|
|
133
|
+
| 'shaft-left'
|
|
134
|
+
| 'shaft-right'
|
|
135
|
+
| 'shaft-top'
|
|
136
|
+
|
|
137
|
+
type ElevatorColliderUserData = {
|
|
138
|
+
doorWidth?: number
|
|
139
|
+
dynamic?: boolean
|
|
140
|
+
elevatorId: AnyNodeId
|
|
141
|
+
kind: ElevatorColliderKind
|
|
142
|
+
levelId?: AnyNodeId
|
|
143
|
+
localPosition: [number, number, number]
|
|
144
|
+
matrixInitialized?: boolean
|
|
145
|
+
side?: ElevatorDoorSide
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
type ElevatorColliderMesh = Mesh & {
|
|
149
|
+
userData: Mesh['userData'] & ElevatorColliderUserData
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
type FirstPersonInteractableTarget =
|
|
153
|
+
| {
|
|
154
|
+
id: AnyNodeId
|
|
155
|
+
type: 'door' | 'window'
|
|
156
|
+
}
|
|
157
|
+
| {
|
|
158
|
+
action: 'open-door' | 'request-level'
|
|
159
|
+
buttonKind: 'cab' | 'landing'
|
|
160
|
+
id: AnyNodeId
|
|
161
|
+
levelId?: AnyNodeId
|
|
162
|
+
type: 'elevator'
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type ElevatorButtonTarget = {
|
|
166
|
+
action: 'open-door' | 'request-level'
|
|
167
|
+
buttonKind: 'cab' | 'landing'
|
|
168
|
+
elevatorId: AnyNodeId
|
|
169
|
+
levelId?: AnyNodeId
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveElevatorButtonTarget(object: Object3D): ElevatorButtonTarget | null {
|
|
173
|
+
let current: Object3D | null = object
|
|
174
|
+
|
|
175
|
+
while (current) {
|
|
176
|
+
const candidate = (
|
|
177
|
+
current.userData as {
|
|
178
|
+
elevatorButton?: {
|
|
179
|
+
action?: unknown
|
|
180
|
+
disabled?: unknown
|
|
181
|
+
elevatorId?: unknown
|
|
182
|
+
kind?: unknown
|
|
183
|
+
levelId?: unknown
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
).elevatorButton
|
|
187
|
+
|
|
188
|
+
if (candidate?.disabled === true) {
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (typeof candidate?.elevatorId === 'string' && candidate.kind === 'cab') {
|
|
193
|
+
const action = candidate.action === 'open-door' ? 'open-door' : 'request-level'
|
|
194
|
+
if (action === 'open-door') {
|
|
195
|
+
return {
|
|
196
|
+
action,
|
|
197
|
+
buttonKind: candidate.kind,
|
|
198
|
+
elevatorId: candidate.elevatorId as AnyNodeId,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (
|
|
204
|
+
typeof candidate?.elevatorId === 'string' &&
|
|
205
|
+
typeof candidate.levelId === 'string' &&
|
|
206
|
+
(candidate.kind === 'cab' || candidate.kind === 'landing')
|
|
207
|
+
) {
|
|
208
|
+
return {
|
|
209
|
+
action: 'request-level',
|
|
210
|
+
buttonKind: candidate.kind,
|
|
211
|
+
elevatorId: candidate.elevatorId as AnyNodeId,
|
|
212
|
+
levelId: candidate.levelId as AnyNodeId,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
current = current.parent
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return null
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getInteractableTargetKey(target: FirstPersonInteractableTarget | null) {
|
|
223
|
+
if (!target) return null
|
|
224
|
+
return target.type === 'elevator'
|
|
225
|
+
? `${target.type}:${target.id}:${target.levelId}`
|
|
226
|
+
: `${target.type}:${target.id}`
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isDynamicElevatorCollider(kind: ElevatorColliderKind) {
|
|
230
|
+
return kind.startsWith('cab-') || kind.startsWith('landing-door')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isInsideElevatorCab(
|
|
234
|
+
elevator: ElevatorNode,
|
|
235
|
+
runtime: NonNullable<ReturnType<typeof useInteractive.getState>['elevators'][AnyNodeId]>,
|
|
236
|
+
localEyePosition: Vector3,
|
|
237
|
+
) {
|
|
238
|
+
const halfWidth = getElevatorCabWidth(elevator) / 2 - ELEVATOR_RIDE_HORIZONTAL_PADDING
|
|
239
|
+
const halfDepth = getElevatorCabDepth(elevator) / 2 - ELEVATOR_RIDE_HORIZONTAL_PADDING
|
|
240
|
+
const cabCenterZ = getElevatorCabCenterZ(elevator)
|
|
241
|
+
const cabHeight = Math.max(elevator.cabHeight, 1.4)
|
|
242
|
+
|
|
243
|
+
return (
|
|
244
|
+
Math.abs(localEyePosition.x) <= Math.max(halfWidth, 0.24) &&
|
|
245
|
+
Math.abs(localEyePosition.z - cabCenterZ) <= Math.max(halfDepth, 0.24) &&
|
|
246
|
+
localEyePosition.y >= runtime.carY + 0.35 &&
|
|
247
|
+
localEyePosition.y <= runtime.carY + cabHeight + 0.7
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function getFirstPersonLevelHeight(levelId: string, nodes: Record<string, AnyNode>) {
|
|
252
|
+
const level = nodes[levelId as AnyNodeId]
|
|
253
|
+
if (level?.type !== 'level') return DEFAULT_ELEVATOR_LEVEL_HEIGHT
|
|
254
|
+
|
|
255
|
+
let maxTop = 0
|
|
256
|
+
for (const childId of level.children) {
|
|
257
|
+
const child = nodes[childId as AnyNodeId]
|
|
258
|
+
if (!child) continue
|
|
259
|
+
|
|
260
|
+
if (child.type === 'ceiling') {
|
|
261
|
+
maxTop = Math.max(maxTop, child.height ?? DEFAULT_ELEVATOR_LEVEL_HEIGHT)
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (child.type === 'wall') {
|
|
266
|
+
const meshY = Math.max(sceneRegistry.nodes.get(childId as AnyNodeId)?.position.y ?? 0, 0)
|
|
267
|
+
maxTop = Math.max(maxTop, meshY + (child.height ?? DEFAULT_ELEVATOR_LEVEL_HEIGHT))
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return maxTop > 0 ? maxTop : DEFAULT_ELEVATOR_LEVEL_HEIGHT
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function resolveElevatorColliderLevels(elevator: ElevatorNode, nodes: Record<string, AnyNode>) {
|
|
275
|
+
const allLevels = resolveElevatorBuildingLevels(elevator, nodes)
|
|
276
|
+
|
|
277
|
+
const baseYByLevelId = new Map<string, number>()
|
|
278
|
+
let cumulativeY = 0
|
|
279
|
+
for (const level of allLevels) {
|
|
280
|
+
baseYByLevelId.set(level.id, cumulativeY)
|
|
281
|
+
cumulativeY += getFirstPersonLevelHeight(level.id, nodes)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const serviceLevels = resolveElevatorServiceLevels(elevator, nodes)
|
|
285
|
+
const entries = serviceLevels.map((level) => ({
|
|
286
|
+
baseY: baseYByLevelId.get(level.id) ?? 0,
|
|
287
|
+
id: level.id as AnyNodeId,
|
|
288
|
+
}))
|
|
289
|
+
const firstServedLevel = serviceLevels[0] ?? null
|
|
290
|
+
const lastServedLevel = serviceLevels[serviceLevels.length - 1] ?? null
|
|
291
|
+
const shaftBaseY = firstServedLevel ? (baseYByLevelId.get(firstServedLevel.id) ?? 0) : 0
|
|
292
|
+
const lastServedIndex = lastServedLevel
|
|
293
|
+
? allLevels.findIndex((level) => level.id === lastServedLevel.id)
|
|
294
|
+
: -1
|
|
295
|
+
const nextLevel = lastServedIndex >= 0 ? allLevels[lastServedIndex + 1] : null
|
|
296
|
+
const shaftTopY = nextLevel
|
|
297
|
+
? (baseYByLevelId.get(nextLevel.id) ?? cumulativeY)
|
|
298
|
+
: lastServedLevel
|
|
299
|
+
? cumulativeY
|
|
300
|
+
: elevator.cabHeight + 0.3
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
entries,
|
|
304
|
+
shaftBaseY,
|
|
305
|
+
shaftTopY,
|
|
306
|
+
totalHeight: Math.max(shaftTopY - shaftBaseY, elevator.cabHeight + 0.3),
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function createElevatorColliderMesh(
|
|
311
|
+
elevatorId: AnyNodeId,
|
|
312
|
+
kind: ElevatorColliderKind,
|
|
313
|
+
size: [number, number, number],
|
|
314
|
+
localPosition: [number, number, number],
|
|
315
|
+
userData: Partial<ElevatorColliderUserData> = {},
|
|
316
|
+
) {
|
|
317
|
+
const geometry = new BoxGeometry(size[0], size[1], size[2])
|
|
318
|
+
|
|
319
|
+
const bvhGeometry = geometry as typeof geometry & {
|
|
320
|
+
computeBoundsTree?: typeof computeBoundsTree
|
|
321
|
+
disposeBoundsTree?: typeof disposeBoundsTree
|
|
322
|
+
}
|
|
323
|
+
;(bvhGeometry as any).computeBoundsTree = computeBoundsTree
|
|
324
|
+
;(bvhGeometry as any).disposeBoundsTree = disposeBoundsTree
|
|
325
|
+
bvhGeometry.computeBoundsTree?.({
|
|
326
|
+
maxLeafSize: 12,
|
|
327
|
+
strategy: 0,
|
|
328
|
+
} as never)
|
|
329
|
+
bvhGeometry.computeBoundingBox()
|
|
330
|
+
|
|
331
|
+
const mesh = new Mesh(bvhGeometry, elevatorColliderMaterial) as unknown as ElevatorColliderMesh
|
|
332
|
+
mesh.raycast = acceleratedRaycast
|
|
333
|
+
mesh.matrixAutoUpdate = false
|
|
334
|
+
mesh.visible = true
|
|
335
|
+
mesh.userData = {
|
|
336
|
+
...userData,
|
|
337
|
+
dynamic: isDynamicElevatorCollider(kind),
|
|
338
|
+
elevatorId,
|
|
339
|
+
excludeCollisionCheck: false,
|
|
340
|
+
excludeFloatHit: false,
|
|
341
|
+
friction: 0.8,
|
|
342
|
+
kind,
|
|
343
|
+
localPosition,
|
|
344
|
+
matrixInitialized: false,
|
|
345
|
+
restitution: 0.03,
|
|
346
|
+
type: 'ELEVATOR_COLLIDER',
|
|
347
|
+
}
|
|
348
|
+
return mesh
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function buildElevatorColliderMeshes(): ElevatorColliderMesh[] {
|
|
352
|
+
const nodes = useScene.getState().nodes
|
|
353
|
+
const meshes: ElevatorColliderMesh[] = []
|
|
354
|
+
|
|
355
|
+
for (const elevatorId of sceneRegistry.byType.elevator!) {
|
|
356
|
+
const typedElevatorId = elevatorId as AnyNodeId
|
|
357
|
+
const node = nodes[typedElevatorId]
|
|
358
|
+
if (node?.type !== 'elevator' || node.visible === false) continue
|
|
359
|
+
|
|
360
|
+
const { entries, shaftBaseY, shaftTopY, totalHeight } = resolveElevatorColliderLevels(
|
|
361
|
+
node,
|
|
362
|
+
nodes,
|
|
363
|
+
)
|
|
364
|
+
const cabWidth = getElevatorCabWidth(node)
|
|
365
|
+
const cabDepth = getElevatorCabDepth(node)
|
|
366
|
+
const shaftWidth = getElevatorShaftWidth(node, cabWidth)
|
|
367
|
+
const shaftDepth = getElevatorShaftDepth(node, cabDepth)
|
|
368
|
+
const cabHeight = Math.max(node.cabHeight, 1.4)
|
|
369
|
+
const doorWidth = Math.min(Math.max(node.doorWidth, 0.45), cabWidth - 0.18, shaftWidth - 0.18)
|
|
370
|
+
const doorHeight = Math.min(Math.max(node.doorHeight, 1.2), cabHeight - 0.1)
|
|
371
|
+
const doorStyle = getResolvedElevatorDoorStyle(node.doorStyle)
|
|
372
|
+
const shaftHeight = Math.max(totalHeight, cabHeight + 0.3)
|
|
373
|
+
const wallThickness = getElevatorShaftWallThickness(node)
|
|
374
|
+
const cabFloorWidth = Math.max(cabWidth - ELEVATOR_COLLIDER_HORIZONTAL_PADDING * 2, 0.48)
|
|
375
|
+
const cabFloorDepth = Math.max(cabDepth - ELEVATOR_COLLIDER_HORIZONTAL_PADDING * 2, 0.48)
|
|
376
|
+
const frontWallZ = -shaftDepth / 2 - wallThickness / 2
|
|
377
|
+
const frontZ = frontWallZ - wallThickness / 2 - 0.018
|
|
378
|
+
const cabCenterZ = -shaftDepth / 2 + cabDepth / 2
|
|
379
|
+
const leafWidth = getElevatorDoorLeafWidth(doorWidth, doorStyle)
|
|
380
|
+
const doorLeafSides = getElevatorDoorLeafSides(doorStyle)
|
|
381
|
+
const resolvedShaftTopY = Math.max(shaftTopY, shaftBaseY + shaftHeight)
|
|
382
|
+
|
|
383
|
+
meshes.push(
|
|
384
|
+
createElevatorColliderMesh(
|
|
385
|
+
typedElevatorId,
|
|
386
|
+
'shaft-back',
|
|
387
|
+
[shaftWidth + wallThickness * 2, shaftHeight, wallThickness],
|
|
388
|
+
[0, shaftBaseY + shaftHeight / 2, shaftDepth / 2 + wallThickness / 2],
|
|
389
|
+
),
|
|
390
|
+
createElevatorColliderMesh(
|
|
391
|
+
typedElevatorId,
|
|
392
|
+
'shaft-left',
|
|
393
|
+
[wallThickness, shaftHeight, shaftDepth + wallThickness * 2],
|
|
394
|
+
[-shaftWidth / 2 - wallThickness / 2, shaftBaseY + shaftHeight / 2, 0],
|
|
395
|
+
),
|
|
396
|
+
createElevatorColliderMesh(
|
|
397
|
+
typedElevatorId,
|
|
398
|
+
'shaft-right',
|
|
399
|
+
[wallThickness, shaftHeight, shaftDepth + wallThickness * 2],
|
|
400
|
+
[shaftWidth / 2 + wallThickness / 2, shaftBaseY + shaftHeight / 2, 0],
|
|
401
|
+
),
|
|
402
|
+
createElevatorColliderMesh(
|
|
403
|
+
typedElevatorId,
|
|
404
|
+
'shaft-top',
|
|
405
|
+
[shaftWidth + wallThickness * 2, wallThickness, shaftDepth + wallThickness * 2],
|
|
406
|
+
[0, shaftBaseY + shaftHeight - wallThickness / 2, 0],
|
|
407
|
+
),
|
|
408
|
+
createElevatorColliderMesh(
|
|
409
|
+
typedElevatorId,
|
|
410
|
+
'cab-floor',
|
|
411
|
+
[cabFloorWidth, ELEVATOR_COLLIDER_FLOOR_THICKNESS, cabFloorDepth],
|
|
412
|
+
[0, ELEVATOR_COLLIDER_FLOOR_THICKNESS / 2, cabCenterZ],
|
|
413
|
+
),
|
|
414
|
+
createElevatorColliderMesh(
|
|
415
|
+
typedElevatorId,
|
|
416
|
+
'cab-ceiling',
|
|
417
|
+
[cabWidth, wallThickness, cabDepth],
|
|
418
|
+
[0, cabHeight - wallThickness / 2, cabCenterZ],
|
|
419
|
+
),
|
|
420
|
+
createElevatorColliderMesh(
|
|
421
|
+
typedElevatorId,
|
|
422
|
+
'cab-back',
|
|
423
|
+
[cabWidth, cabHeight, wallThickness],
|
|
424
|
+
[0, cabHeight / 2, cabCenterZ + cabDepth / 2 - wallThickness / 2],
|
|
425
|
+
),
|
|
426
|
+
createElevatorColliderMesh(
|
|
427
|
+
typedElevatorId,
|
|
428
|
+
'cab-left',
|
|
429
|
+
[wallThickness, cabHeight, cabDepth],
|
|
430
|
+
[-cabWidth / 2 + wallThickness / 2, cabHeight / 2, cabCenterZ],
|
|
431
|
+
),
|
|
432
|
+
createElevatorColliderMesh(
|
|
433
|
+
typedElevatorId,
|
|
434
|
+
'cab-right',
|
|
435
|
+
[wallThickness, cabHeight, cabDepth],
|
|
436
|
+
[cabWidth / 2 - wallThickness / 2, cabHeight / 2, cabCenterZ],
|
|
437
|
+
),
|
|
438
|
+
createElevatorColliderMesh(
|
|
439
|
+
typedElevatorId,
|
|
440
|
+
'cab-door-gate',
|
|
441
|
+
[doorWidth, doorHeight, ELEVATOR_COLLIDER_DOOR_DEPTH],
|
|
442
|
+
[0, doorHeight / 2, frontZ],
|
|
443
|
+
{ doorWidth },
|
|
444
|
+
),
|
|
445
|
+
...doorLeafSides.map((side) =>
|
|
446
|
+
createElevatorColliderMesh(
|
|
447
|
+
typedElevatorId,
|
|
448
|
+
side === 'left' ? 'cab-door-left' : 'cab-door-right',
|
|
449
|
+
[leafWidth, doorHeight, ELEVATOR_COLLIDER_DOOR_DEPTH],
|
|
450
|
+
[0, doorHeight / 2, frontZ],
|
|
451
|
+
{ doorWidth, side },
|
|
452
|
+
),
|
|
453
|
+
),
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
const entrySpans = entries.map((entry, index) => {
|
|
457
|
+
const nextEntry = entries[index + 1]
|
|
458
|
+
return {
|
|
459
|
+
entry,
|
|
460
|
+
levelTopY: Math.max(nextEntry?.baseY ?? resolvedShaftTopY, entry.baseY + doorHeight + 0.24),
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
for (const { entry, levelTopY } of entrySpans) {
|
|
465
|
+
const wallDepth = wallThickness
|
|
466
|
+
const levelHeight = Math.max(levelTopY - entry.baseY, doorHeight + 0.24)
|
|
467
|
+
const jambWidth = Math.max((shaftWidth - doorWidth) / 2, 0.08)
|
|
468
|
+
const jambCenterOffset = doorWidth / 2 + jambWidth / 2
|
|
469
|
+
const headerHeight = Math.max(levelHeight - doorHeight, 0.14)
|
|
470
|
+
|
|
471
|
+
meshes.push(
|
|
472
|
+
createElevatorColliderMesh(
|
|
473
|
+
typedElevatorId,
|
|
474
|
+
'shaft-front-left',
|
|
475
|
+
[jambWidth, levelHeight, wallDepth],
|
|
476
|
+
[-jambCenterOffset, entry.baseY + levelHeight / 2, frontWallZ],
|
|
477
|
+
),
|
|
478
|
+
createElevatorColliderMesh(
|
|
479
|
+
typedElevatorId,
|
|
480
|
+
'shaft-front-right',
|
|
481
|
+
[jambWidth, levelHeight, wallDepth],
|
|
482
|
+
[jambCenterOffset, entry.baseY + levelHeight / 2, frontWallZ],
|
|
483
|
+
),
|
|
484
|
+
createElevatorColliderMesh(
|
|
485
|
+
typedElevatorId,
|
|
486
|
+
'shaft-front-header',
|
|
487
|
+
[shaftWidth, headerHeight, wallDepth],
|
|
488
|
+
[0, entry.baseY + doorHeight + headerHeight / 2, frontWallZ],
|
|
489
|
+
),
|
|
490
|
+
createElevatorColliderMesh(
|
|
491
|
+
typedElevatorId,
|
|
492
|
+
'landing-door-gate',
|
|
493
|
+
[doorWidth, doorHeight, ELEVATOR_COLLIDER_DOOR_DEPTH],
|
|
494
|
+
[0, entry.baseY + doorHeight / 2, frontZ - 0.02],
|
|
495
|
+
{ doorWidth, levelId: entry.id },
|
|
496
|
+
),
|
|
497
|
+
...doorLeafSides.map((side) =>
|
|
498
|
+
createElevatorColliderMesh(
|
|
499
|
+
typedElevatorId,
|
|
500
|
+
side === 'left' ? 'landing-door-left' : 'landing-door-right',
|
|
501
|
+
[leafWidth, doorHeight, ELEVATOR_COLLIDER_DOOR_DEPTH],
|
|
502
|
+
[0, entry.baseY + doorHeight / 2, frontZ - 0.02],
|
|
503
|
+
{ doorWidth, levelId: entry.id, side },
|
|
504
|
+
),
|
|
505
|
+
),
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return meshes
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function disposeElevatorColliderMeshes(meshes: ElevatorColliderMesh[]) {
|
|
514
|
+
for (const mesh of meshes) {
|
|
515
|
+
const geometry = mesh.geometry as typeof mesh.geometry & {
|
|
516
|
+
disposeBoundsTree?: typeof disposeBoundsTree
|
|
517
|
+
}
|
|
518
|
+
geometry.disposeBoundsTree?.()
|
|
519
|
+
geometry.dispose()
|
|
520
|
+
}
|
|
69
521
|
}
|
|
70
522
|
|
|
71
523
|
const resolvePlacedSpawnNode = (
|
|
@@ -86,8 +538,17 @@ export const FirstPersonControls = () => {
|
|
|
86
538
|
const yawRef = useRef(0)
|
|
87
539
|
const pitchRef = useRef(0)
|
|
88
540
|
const interactableTargetRef = useRef<FirstPersonInteractableTarget | null>(null)
|
|
541
|
+
const [isElevatorRideLocked, setIsElevatorRideLocked] = useState(false)
|
|
542
|
+
const ridingElevatorRef = useRef<{
|
|
543
|
+
elevatorId: AnyNodeId
|
|
544
|
+
localControllerY: number | null
|
|
545
|
+
previousCarY: number
|
|
546
|
+
} | null>(null)
|
|
547
|
+
const rideLockedRef = useRef(false)
|
|
89
548
|
const worldRef = useRef<FirstPersonColliderWorld | null>(null)
|
|
549
|
+
const elevatorColliderMeshesRef = useRef<ElevatorColliderMesh[]>([])
|
|
90
550
|
const [world, setWorld] = useState<FirstPersonColliderWorld | null>(null)
|
|
551
|
+
const [elevatorColliderMeshes, setElevatorColliderMeshes] = useState<ElevatorColliderMesh[]>([])
|
|
91
552
|
const [controllerStart, setControllerStart] = useState<{
|
|
92
553
|
position: [number, number, number]
|
|
93
554
|
yaw: number
|
|
@@ -99,9 +560,22 @@ export const FirstPersonControls = () => {
|
|
|
99
560
|
setWorld(nextWorld)
|
|
100
561
|
}, [])
|
|
101
562
|
|
|
563
|
+
const replaceElevatorColliderMeshes = useCallback((nextMeshes: ElevatorColliderMesh[]) => {
|
|
564
|
+
disposeElevatorColliderMeshes(elevatorColliderMeshesRef.current)
|
|
565
|
+
elevatorColliderMeshesRef.current = nextMeshes
|
|
566
|
+
setElevatorColliderMeshes(nextMeshes)
|
|
567
|
+
}, [])
|
|
568
|
+
|
|
102
569
|
const rebuildColliderWorld = useCallback(() => {
|
|
103
570
|
replaceColliderWorld(buildFirstPersonColliderWorldFromRegistry())
|
|
104
|
-
|
|
571
|
+
replaceElevatorColliderMeshes(buildElevatorColliderMeshes())
|
|
572
|
+
}, [replaceColliderWorld, replaceElevatorColliderMeshes])
|
|
573
|
+
|
|
574
|
+
const setElevatorRideLocked = useCallback((locked: boolean) => {
|
|
575
|
+
if (rideLockedRef.current === locked) return
|
|
576
|
+
rideLockedRef.current = locked
|
|
577
|
+
setIsElevatorRideLocked(locked)
|
|
578
|
+
}, [])
|
|
105
579
|
|
|
106
580
|
const resolveInteractableDoorId = useCallback((): AnyNodeId | null => {
|
|
107
581
|
const nodes = useScene.getState().nodes
|
|
@@ -111,7 +585,7 @@ export const FirstPersonControls = () => {
|
|
|
111
585
|
let closestDoorId: AnyNodeId | null = null
|
|
112
586
|
let closestDistance = DOOR_INTERACTION_DISTANCE
|
|
113
587
|
|
|
114
|
-
for (const doorId of sceneRegistry.byType.door) {
|
|
588
|
+
for (const doorId of sceneRegistry.byType.door!) {
|
|
115
589
|
const node = nodes[doorId as AnyNodeId]
|
|
116
590
|
if (node?.type !== 'door') continue
|
|
117
591
|
if (node.openingKind === 'opening') continue
|
|
@@ -209,7 +683,7 @@ export const FirstPersonControls = () => {
|
|
|
209
683
|
let closestWindowId: AnyNodeId | null = null
|
|
210
684
|
let closestDistance = DOOR_INTERACTION_DISTANCE
|
|
211
685
|
|
|
212
|
-
for (const windowId of sceneRegistry.byType.window) {
|
|
686
|
+
for (const windowId of sceneRegistry.byType.window!) {
|
|
213
687
|
const node = nodes[windowId as AnyNodeId]
|
|
214
688
|
if (node?.type !== 'window') continue
|
|
215
689
|
if (node.openingKind === 'opening') continue
|
|
@@ -230,7 +704,61 @@ export const FirstPersonControls = () => {
|
|
|
230
704
|
return closestWindowId
|
|
231
705
|
}, [camera])
|
|
232
706
|
|
|
707
|
+
const resolveInteractableElevatorTarget =
|
|
708
|
+
useCallback((): FirstPersonInteractableTarget | null => {
|
|
709
|
+
const nodes = useScene.getState().nodes
|
|
710
|
+
camera.updateMatrixWorld(true)
|
|
711
|
+
elevatorInteractionRaycaster.setFromCamera(centerScreenPoint, camera)
|
|
712
|
+
|
|
713
|
+
let closestTarget: FirstPersonInteractableTarget | null = null
|
|
714
|
+
let closestDistance = DOOR_INTERACTION_DISTANCE
|
|
715
|
+
|
|
716
|
+
for (const elevatorId of sceneRegistry.byType.elevator!) {
|
|
717
|
+
const typedElevatorId = elevatorId as AnyNodeId
|
|
718
|
+
const node = nodes[typedElevatorId]
|
|
719
|
+
if (node?.type !== 'elevator') continue
|
|
720
|
+
|
|
721
|
+
const object = sceneRegistry.nodes.get(typedElevatorId)
|
|
722
|
+
if (!object) continue
|
|
723
|
+
|
|
724
|
+
const runtime = useInteractive.getState().elevators[typedElevatorId]
|
|
725
|
+
object.updateWorldMatrix(true, true)
|
|
726
|
+
if (runtime) {
|
|
727
|
+
elevatorLocalEyePosition.copy(camera.position)
|
|
728
|
+
object.worldToLocal(elevatorLocalEyePosition)
|
|
729
|
+
}
|
|
730
|
+
const canUseCabButtons =
|
|
731
|
+
runtime && isInsideElevatorCab(node, runtime, elevatorLocalEyePosition)
|
|
732
|
+
|
|
733
|
+
const intersections = elevatorInteractionRaycaster.intersectObject(object, true)
|
|
734
|
+
for (const intersection of intersections) {
|
|
735
|
+
if (intersection.distance > closestDistance) continue
|
|
736
|
+
|
|
737
|
+
const target = resolveElevatorButtonTarget(intersection.object)
|
|
738
|
+
if (!target || target.elevatorId !== elevatorId) continue
|
|
739
|
+
if (target.action === 'request-level') {
|
|
740
|
+
if (!target.levelId || nodes[target.levelId]?.type !== 'level') continue
|
|
741
|
+
}
|
|
742
|
+
if (target.buttonKind === 'cab' && !canUseCabButtons) continue
|
|
743
|
+
|
|
744
|
+
closestTarget = {
|
|
745
|
+
action: target.action,
|
|
746
|
+
buttonKind: target.buttonKind,
|
|
747
|
+
id: target.elevatorId,
|
|
748
|
+
levelId: target.levelId,
|
|
749
|
+
type: 'elevator',
|
|
750
|
+
}
|
|
751
|
+
closestDistance = intersection.distance
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return closestTarget
|
|
756
|
+
}, [camera])
|
|
757
|
+
|
|
233
758
|
const resolveInteractableTarget = useCallback((): FirstPersonInteractableTarget | null => {
|
|
759
|
+
const elevatorTarget = resolveInteractableElevatorTarget()
|
|
760
|
+
if (elevatorTarget) return elevatorTarget
|
|
761
|
+
|
|
234
762
|
const doorId = resolveInteractableDoorId()
|
|
235
763
|
if (doorId) return { id: doorId, type: 'door' }
|
|
236
764
|
|
|
@@ -238,12 +766,42 @@ export const FirstPersonControls = () => {
|
|
|
238
766
|
if (windowId) return { id: windowId, type: 'window' }
|
|
239
767
|
|
|
240
768
|
return null
|
|
241
|
-
}, [resolveInteractableDoorId, resolveInteractableWindowId])
|
|
769
|
+
}, [resolveInteractableDoorId, resolveInteractableElevatorTarget, resolveInteractableWindowId])
|
|
242
770
|
|
|
243
771
|
const toggleInteractableTarget = useCallback(() => {
|
|
244
772
|
const target = interactableTargetRef.current ?? resolveInteractableTarget()
|
|
245
773
|
if (!target) return
|
|
246
774
|
|
|
775
|
+
if (target.type === 'elevator') {
|
|
776
|
+
if (target.buttonKind === 'cab') {
|
|
777
|
+
const state = useInteractive.getState().elevators[target.id]
|
|
778
|
+
if (state) {
|
|
779
|
+
ridingElevatorRef.current = {
|
|
780
|
+
elevatorId: target.id,
|
|
781
|
+
localControllerY: null,
|
|
782
|
+
previousCarY: state.carY,
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
if (target.action === 'open-door') {
|
|
787
|
+
openElevatorDoor(target.id)
|
|
788
|
+
return
|
|
789
|
+
}
|
|
790
|
+
if (target.levelId) {
|
|
791
|
+
const targetElevatorId =
|
|
792
|
+
target.buttonKind === 'landing'
|
|
793
|
+
? resolveElevatorDispatchTarget({
|
|
794
|
+
elevators: useInteractive.getState().elevators,
|
|
795
|
+
levelId: target.levelId,
|
|
796
|
+
nodes: useScene.getState().nodes,
|
|
797
|
+
requestedElevatorId: target.id,
|
|
798
|
+
})
|
|
799
|
+
: target.id
|
|
800
|
+
requestElevatorLevel(targetElevatorId, target.levelId)
|
|
801
|
+
}
|
|
802
|
+
return
|
|
803
|
+
}
|
|
804
|
+
|
|
247
805
|
if (target.type === 'window') {
|
|
248
806
|
const node = useScene.getState().nodes[target.id]
|
|
249
807
|
if (
|
|
@@ -270,6 +828,8 @@ export const FirstPersonControls = () => {
|
|
|
270
828
|
const target = interactableTargetRef.current ?? resolveInteractableTarget()
|
|
271
829
|
if (!target) return
|
|
272
830
|
|
|
831
|
+
if (target.type === 'elevator') return
|
|
832
|
+
|
|
273
833
|
if (target.type === 'window') {
|
|
274
834
|
const node = useScene.getState().nodes[target.id]
|
|
275
835
|
if (
|
|
@@ -325,6 +885,9 @@ export const FirstPersonControls = () => {
|
|
|
325
885
|
return () => {
|
|
326
886
|
worldRef.current?.dispose()
|
|
327
887
|
worldRef.current = null
|
|
888
|
+
disposeElevatorColliderMeshes(elevatorColliderMeshesRef.current)
|
|
889
|
+
elevatorColliderMeshesRef.current = []
|
|
890
|
+
setElevatorColliderMeshes([])
|
|
328
891
|
setWorld(null)
|
|
329
892
|
}
|
|
330
893
|
}, [rebuildColliderWorld])
|
|
@@ -373,17 +936,28 @@ export const FirstPersonControls = () => {
|
|
|
373
936
|
}
|
|
374
937
|
}
|
|
375
938
|
|
|
939
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
940
|
+
if (document.pointerLockElement !== canvas) return
|
|
941
|
+
if (event.button !== 0) return
|
|
942
|
+
|
|
943
|
+
event.preventDefault()
|
|
944
|
+
event.stopPropagation()
|
|
945
|
+
toggleInteractableTarget()
|
|
946
|
+
}
|
|
947
|
+
|
|
376
948
|
document.addEventListener('mousemove', handleMouseMove)
|
|
377
949
|
document.addEventListener('click', handleClick)
|
|
950
|
+
document.addEventListener('mousedown', handleMouseDown, true)
|
|
378
951
|
|
|
379
952
|
return () => {
|
|
380
953
|
document.removeEventListener('mousemove', handleMouseMove)
|
|
381
954
|
document.removeEventListener('click', handleClick)
|
|
955
|
+
document.removeEventListener('mousedown', handleMouseDown, true)
|
|
382
956
|
if (document.pointerLockElement === canvas) {
|
|
383
957
|
document.exitPointerLock()
|
|
384
958
|
}
|
|
385
959
|
}
|
|
386
|
-
}, [gl])
|
|
960
|
+
}, [gl, toggleInteractableTarget])
|
|
387
961
|
|
|
388
962
|
useEffect(() => {
|
|
389
963
|
const canvas = gl.domElement
|
|
@@ -417,7 +991,229 @@ export const FirstPersonControls = () => {
|
|
|
417
991
|
}
|
|
418
992
|
}, [closeInteractableTarget, gl, toggleInteractableTarget])
|
|
419
993
|
|
|
420
|
-
|
|
994
|
+
const syncElevatorColliderMeshes = useCallback(() => {
|
|
995
|
+
const nodes = useScene.getState().nodes
|
|
996
|
+
const interactive = useInteractive.getState()
|
|
997
|
+
|
|
998
|
+
for (const mesh of elevatorColliderMeshesRef.current) {
|
|
999
|
+
const { doorWidth, dynamic, elevatorId, kind, levelId, localPosition, side } = mesh.userData
|
|
1000
|
+
const node = nodes[elevatorId]
|
|
1001
|
+
const runtime = interactive.elevators[elevatorId]
|
|
1002
|
+
const object = sceneRegistry.nodes.get(elevatorId)
|
|
1003
|
+
if (!(node?.type === 'elevator' && object && node.visible !== false)) {
|
|
1004
|
+
mesh.visible = false
|
|
1005
|
+
continue
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (!dynamic && mesh.userData.matrixInitialized && mesh.visible) {
|
|
1009
|
+
continue
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
let [localX, localY, localZ] = localPosition
|
|
1013
|
+
const isCabCollider = kind.startsWith('cab-')
|
|
1014
|
+
const isDoorCollider = kind === 'landing-door-left' || kind === 'landing-door-right'
|
|
1015
|
+
const isCabDoorGate = kind === 'cab-door-gate'
|
|
1016
|
+
const isLandingDoorGate = kind === 'landing-door-gate'
|
|
1017
|
+
|
|
1018
|
+
if (isCabCollider) {
|
|
1019
|
+
localY += runtime?.carY ?? 0
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (kind === 'cab-door-left' || kind === 'cab-door-right') {
|
|
1023
|
+
if (!runtime) {
|
|
1024
|
+
mesh.visible = false
|
|
1025
|
+
continue
|
|
1026
|
+
}
|
|
1027
|
+
localX = getElevatorDoorLeafX(
|
|
1028
|
+
side ?? 'left',
|
|
1029
|
+
doorWidth ?? node.doorWidth,
|
|
1030
|
+
runtime.doorOpen,
|
|
1031
|
+
node.doorStyle,
|
|
1032
|
+
)
|
|
1033
|
+
mesh.visible = true
|
|
1034
|
+
} else if (isCabDoorGate) {
|
|
1035
|
+
if (!runtime) {
|
|
1036
|
+
mesh.visible = false
|
|
1037
|
+
continue
|
|
1038
|
+
}
|
|
1039
|
+
mesh.visible = runtime.doorOpen < ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD
|
|
1040
|
+
} else if (isDoorCollider) {
|
|
1041
|
+
const doorOpen = runtime?.currentLevelId === levelId ? (runtime?.doorOpen ?? 0) : 0
|
|
1042
|
+
localX = getElevatorDoorLeafX(
|
|
1043
|
+
side ?? 'left',
|
|
1044
|
+
doorWidth ?? node.doorWidth,
|
|
1045
|
+
doorOpen,
|
|
1046
|
+
node.doorStyle,
|
|
1047
|
+
)
|
|
1048
|
+
mesh.visible = true
|
|
1049
|
+
} else if (isLandingDoorGate) {
|
|
1050
|
+
const doorOpen = runtime?.currentLevelId === levelId ? (runtime?.doorOpen ?? 0) : 0
|
|
1051
|
+
mesh.visible = doorOpen < ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD
|
|
1052
|
+
} else {
|
|
1053
|
+
mesh.visible = true
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
object.updateWorldMatrix(true, false)
|
|
1057
|
+
elevatorColliderMatrix.copy(object.matrixWorld)
|
|
1058
|
+
elevatorColliderMatrix.multiply(
|
|
1059
|
+
elevatorColliderLocalMatrix.makeTranslation(localX, localY, localZ),
|
|
1060
|
+
)
|
|
1061
|
+
mesh.matrix.copy(elevatorColliderMatrix)
|
|
1062
|
+
mesh.matrixWorld.copy(elevatorColliderMatrix)
|
|
1063
|
+
mesh.userData.matrixInitialized = true
|
|
1064
|
+
}
|
|
1065
|
+
}, [])
|
|
1066
|
+
|
|
1067
|
+
useFrame(() => {
|
|
1068
|
+
syncElevatorColliderMeshes()
|
|
1069
|
+
}, -1)
|
|
1070
|
+
|
|
1071
|
+
const syncElevatorRide = useCallback(
|
|
1072
|
+
(group: Group) => {
|
|
1073
|
+
const nodes = useScene.getState().nodes
|
|
1074
|
+
const interactive = useInteractive.getState()
|
|
1075
|
+
const activeRide = ridingElevatorRef.current
|
|
1076
|
+
let nextRide: {
|
|
1077
|
+
cabHeight: number
|
|
1078
|
+
cabCenterZ: number
|
|
1079
|
+
carY: number
|
|
1080
|
+
doorOpen: number
|
|
1081
|
+
elevatorId: AnyNodeId
|
|
1082
|
+
halfDepth: number
|
|
1083
|
+
halfWidth: number
|
|
1084
|
+
object: Object3D
|
|
1085
|
+
phase: NonNullable<(typeof interactive.elevators)[AnyNodeId]>['phase']
|
|
1086
|
+
} | null = null
|
|
1087
|
+
|
|
1088
|
+
const elevatorIds = activeRide
|
|
1089
|
+
? [
|
|
1090
|
+
activeRide.elevatorId,
|
|
1091
|
+
...Array.from(sceneRegistry.byType.elevator!).filter(
|
|
1092
|
+
(elevatorId) => elevatorId !== activeRide.elevatorId,
|
|
1093
|
+
),
|
|
1094
|
+
]
|
|
1095
|
+
: Array.from(sceneRegistry.byType.elevator!)
|
|
1096
|
+
|
|
1097
|
+
for (const elevatorId of elevatorIds) {
|
|
1098
|
+
const typedElevatorId = elevatorId as AnyNodeId
|
|
1099
|
+
const node = nodes[typedElevatorId]
|
|
1100
|
+
if (node?.type !== 'elevator') continue
|
|
1101
|
+
|
|
1102
|
+
const runtime = interactive.elevators[typedElevatorId]
|
|
1103
|
+
const object = sceneRegistry.nodes.get(typedElevatorId)
|
|
1104
|
+
if (!(runtime && object)) continue
|
|
1105
|
+
|
|
1106
|
+
object.updateWorldMatrix(true, true)
|
|
1107
|
+
elevatorLocalEyePosition.copy(camera.position)
|
|
1108
|
+
object.worldToLocal(elevatorLocalEyePosition)
|
|
1109
|
+
|
|
1110
|
+
const halfWidth = getElevatorCabWidth(node) / 2 - ELEVATOR_RIDE_HORIZONTAL_PADDING
|
|
1111
|
+
const halfDepth = getElevatorCabDepth(node) / 2 - ELEVATOR_RIDE_HORIZONTAL_PADDING
|
|
1112
|
+
const cabCenterZ = getElevatorCabCenterZ(node)
|
|
1113
|
+
const cabHeight = Math.max(node.cabHeight, 1.4)
|
|
1114
|
+
const insideFootprint =
|
|
1115
|
+
Math.abs(elevatorLocalEyePosition.x) <= Math.max(halfWidth, 0.24) &&
|
|
1116
|
+
Math.abs(elevatorLocalEyePosition.z - cabCenterZ) <= Math.max(halfDepth, 0.24)
|
|
1117
|
+
const insideCabHeight =
|
|
1118
|
+
elevatorLocalEyePosition.y >= runtime.carY + 0.35 &&
|
|
1119
|
+
elevatorLocalEyePosition.y <= runtime.carY + cabHeight + 0.7
|
|
1120
|
+
const continuingRide =
|
|
1121
|
+
activeRide?.elevatorId === typedElevatorId &&
|
|
1122
|
+
insideFootprint &&
|
|
1123
|
+
elevatorLocalEyePosition.y >= runtime.carY - 0.2 &&
|
|
1124
|
+
elevatorLocalEyePosition.y <= runtime.carY + cabHeight + 1.25
|
|
1125
|
+
|
|
1126
|
+
if ((insideFootprint && insideCabHeight) || continuingRide) {
|
|
1127
|
+
nextRide = {
|
|
1128
|
+
cabHeight,
|
|
1129
|
+
cabCenterZ,
|
|
1130
|
+
carY: runtime.carY,
|
|
1131
|
+
doorOpen: runtime.doorOpen,
|
|
1132
|
+
elevatorId: typedElevatorId,
|
|
1133
|
+
halfDepth: Math.max(halfDepth, 0.24),
|
|
1134
|
+
halfWidth: Math.max(halfWidth, 0.24),
|
|
1135
|
+
object,
|
|
1136
|
+
phase: runtime.phase,
|
|
1137
|
+
}
|
|
1138
|
+
break
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (!nextRide) {
|
|
1143
|
+
ridingElevatorRef.current = null
|
|
1144
|
+
setElevatorRideLocked(false)
|
|
1145
|
+
return
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
const previousCarY =
|
|
1149
|
+
activeRide?.elevatorId === nextRide.elevatorId ? activeRide.previousCarY : nextRide.carY
|
|
1150
|
+
const deltaY = nextRide.carY - previousCarY
|
|
1151
|
+
nextRide.object.updateWorldMatrix(true, true)
|
|
1152
|
+
elevatorLocalControllerPosition.copy(group.position)
|
|
1153
|
+
nextRide.object.worldToLocal(elevatorLocalControllerPosition)
|
|
1154
|
+
const localControllerY =
|
|
1155
|
+
activeRide?.elevatorId === nextRide.elevatorId && activeRide.localControllerY !== null
|
|
1156
|
+
? activeRide.localControllerY
|
|
1157
|
+
: elevatorLocalControllerPosition.y - nextRide.carY
|
|
1158
|
+
|
|
1159
|
+
if (Math.abs(deltaY) > 0.0001) {
|
|
1160
|
+
group.position.y += deltaY
|
|
1161
|
+
controllerRef.current?.resetLinVel()
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
const shouldLockToCab =
|
|
1165
|
+
nextRide.phase === 'closing' ||
|
|
1166
|
+
nextRide.phase === 'moving' ||
|
|
1167
|
+
(nextRide.phase === 'opening' && nextRide.doorOpen < ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD)
|
|
1168
|
+
if (shouldLockToCab) {
|
|
1169
|
+
elevatorLocalControllerPosition.copy(group.position)
|
|
1170
|
+
nextRide.object.worldToLocal(elevatorLocalControllerPosition)
|
|
1171
|
+
const desiredLocalY = nextRide.carY + localControllerY
|
|
1172
|
+
if (Math.abs(elevatorLocalControllerPosition.y - desiredLocalY) > 0.002) {
|
|
1173
|
+
elevatorLocalControllerPosition.y = desiredLocalY
|
|
1174
|
+
elevatorWorldControllerPosition.copy(elevatorLocalControllerPosition)
|
|
1175
|
+
nextRide.object.localToWorld(elevatorWorldControllerPosition)
|
|
1176
|
+
group.position.y = elevatorWorldControllerPosition.y
|
|
1177
|
+
controllerRef.current?.resetLinVel()
|
|
1178
|
+
elevatorLocalControllerPosition.copy(group.position)
|
|
1179
|
+
nextRide.object.worldToLocal(elevatorLocalControllerPosition)
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const clampedX = Math.max(
|
|
1183
|
+
-nextRide.halfWidth,
|
|
1184
|
+
Math.min(nextRide.halfWidth, elevatorLocalControllerPosition.x),
|
|
1185
|
+
)
|
|
1186
|
+
const clampedZ = Math.max(
|
|
1187
|
+
nextRide.cabCenterZ - nextRide.halfDepth,
|
|
1188
|
+
Math.min(nextRide.cabCenterZ + nextRide.halfDepth, elevatorLocalControllerPosition.z),
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
if (
|
|
1192
|
+
Math.abs(clampedX - elevatorLocalControllerPosition.x) > 0.0001 ||
|
|
1193
|
+
Math.abs(clampedZ - elevatorLocalControllerPosition.z) > 0.0001
|
|
1194
|
+
) {
|
|
1195
|
+
elevatorLocalControllerPosition.x = clampedX
|
|
1196
|
+
elevatorLocalControllerPosition.z = clampedZ
|
|
1197
|
+
elevatorWorldControllerPosition.copy(elevatorLocalControllerPosition)
|
|
1198
|
+
nextRide.object.localToWorld(elevatorWorldControllerPosition)
|
|
1199
|
+
group.position.x = elevatorWorldControllerPosition.x
|
|
1200
|
+
group.position.z = elevatorWorldControllerPosition.z
|
|
1201
|
+
controllerRef.current?.resetLinVel()
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
setElevatorRideLocked(shouldLockToCab)
|
|
1206
|
+
|
|
1207
|
+
ridingElevatorRef.current = {
|
|
1208
|
+
elevatorId: nextRide.elevatorId,
|
|
1209
|
+
localControllerY,
|
|
1210
|
+
previousCarY: nextRide.carY,
|
|
1211
|
+
}
|
|
1212
|
+
},
|
|
1213
|
+
[camera, setElevatorRideLocked],
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
useFrame(() => {
|
|
421
1217
|
if (!controllerRef.current?.group) return
|
|
422
1218
|
|
|
423
1219
|
const group = controllerRef.current.group
|
|
@@ -426,17 +1222,20 @@ export const FirstPersonControls = () => {
|
|
|
426
1222
|
cameraEuler.set(pitchRef.current, yawRef.current, 0, 'YXZ')
|
|
427
1223
|
camera.quaternion.setFromEuler(cameraEuler)
|
|
428
1224
|
camera.updateMatrixWorld(true)
|
|
1225
|
+
syncElevatorRide(group)
|
|
1226
|
+
camera.position.copy(group.position).add(cameraOffset)
|
|
1227
|
+
camera.updateMatrixWorld(true)
|
|
429
1228
|
|
|
430
1229
|
const nextInteractableTarget = resolveInteractableTarget()
|
|
431
1230
|
const previousInteractableTarget = interactableTargetRef.current
|
|
432
1231
|
if (
|
|
433
|
-
previousInteractableTarget
|
|
434
|
-
|
|
1232
|
+
getInteractableTargetKey(previousInteractableTarget) !==
|
|
1233
|
+
getInteractableTargetKey(nextInteractableTarget)
|
|
435
1234
|
) {
|
|
436
1235
|
interactableTargetRef.current = nextInteractableTarget
|
|
437
1236
|
useViewer.getState().setHoveredId(nextInteractableTarget?.id ?? null)
|
|
438
1237
|
}
|
|
439
|
-
})
|
|
1238
|
+
}, 2.5)
|
|
440
1239
|
|
|
441
1240
|
useEffect(() => {
|
|
442
1241
|
return () => {
|
|
@@ -446,6 +1245,11 @@ export const FirstPersonControls = () => {
|
|
|
446
1245
|
}
|
|
447
1246
|
}, [])
|
|
448
1247
|
|
|
1248
|
+
const firstPersonColliderMeshes = useMemo(
|
|
1249
|
+
() => (world ? [world.mesh, ...elevatorColliderMeshes] : elevatorColliderMeshes),
|
|
1250
|
+
[world, elevatorColliderMeshes],
|
|
1251
|
+
)
|
|
1252
|
+
|
|
449
1253
|
if (!world) {
|
|
450
1254
|
return null
|
|
451
1255
|
}
|
|
@@ -458,7 +1262,7 @@ export const FirstPersonControls = () => {
|
|
|
458
1262
|
acceleration={26}
|
|
459
1263
|
airDragFactor={0.3}
|
|
460
1264
|
colliderCapsuleArgs={[0.25, 0.8, 4, 8]}
|
|
461
|
-
colliderMeshes={
|
|
1265
|
+
colliderMeshes={firstPersonColliderMeshes}
|
|
462
1266
|
collisionCheckIteration={3}
|
|
463
1267
|
collisionPushBackDamping={0.1}
|
|
464
1268
|
collisionPushBackThreshold={0.001}
|
|
@@ -478,6 +1282,7 @@ export const FirstPersonControls = () => {
|
|
|
478
1282
|
maxRunSpeed={5.5}
|
|
479
1283
|
maxSlope={1.2}
|
|
480
1284
|
maxWalkSpeed={4}
|
|
1285
|
+
paused={isElevatorRideLocked}
|
|
481
1286
|
position={controllerStart.position}
|
|
482
1287
|
ref={controllerRef}
|
|
483
1288
|
/>
|
|
@@ -519,7 +1324,7 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
519
1324
|
return (
|
|
520
1325
|
<>
|
|
521
1326
|
{isLocked && (
|
|
522
|
-
<div className="pointer-events-none
|
|
1327
|
+
<div className="pointer-events-none absolute inset-0 z-40 flex items-center justify-center">
|
|
523
1328
|
<div className="relative h-7 w-7">
|
|
524
1329
|
<div className="absolute top-1/2 left-1/2 h-px w-7 -translate-x-1/2 -translate-y-1/2 bg-white/60" />
|
|
525
1330
|
<div className="absolute top-1/2 left-1/2 h-7 w-px -translate-x-1/2 -translate-y-1/2 bg-white/60" />
|
|
@@ -527,7 +1332,7 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
527
1332
|
</div>
|
|
528
1333
|
)}
|
|
529
1334
|
|
|
530
|
-
<div className="
|
|
1335
|
+
<div className="absolute top-4 right-4 z-50">
|
|
531
1336
|
<button
|
|
532
1337
|
className="pointer-events-auto flex items-center gap-2 rounded-xl border border-border/40 bg-background/90 px-4 py-2 font-medium text-foreground text-sm shadow-lg backdrop-blur-xl transition-colors hover:bg-background"
|
|
533
1338
|
onClick={handleExit}
|
|
@@ -541,7 +1346,7 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
541
1346
|
</div>
|
|
542
1347
|
|
|
543
1348
|
{!hasPlacedSpawn && (
|
|
544
|
-
<div className="
|
|
1349
|
+
<div className="absolute top-4 left-1/2 z-50 -translate-x-1/2">
|
|
545
1350
|
<div className="rounded-2xl border border-sky-300/35 bg-slate-950/88 px-4 py-2 text-center text-slate-100 text-sm shadow-lg backdrop-blur-xl">
|
|
546
1351
|
Place a Spawn Point from the Build tab to control where walkthrough starts.
|
|
547
1352
|
</div>
|
|
@@ -549,7 +1354,7 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
549
1354
|
)}
|
|
550
1355
|
|
|
551
1356
|
{isLocked && (
|
|
552
|
-
<div className="pointer-events-none
|
|
1357
|
+
<div className="pointer-events-none absolute top-1/2 right-6 z-40 -translate-y-1/2">
|
|
553
1358
|
<div className="flex min-w-[148px] flex-col gap-3 rounded-2xl border border-border/35 bg-background/80 px-4 py-4 shadow-lg backdrop-blur-xl">
|
|
554
1359
|
<ControlHint keys={['W', 'A', 'S', 'D']} label="Move" />
|
|
555
1360
|
<div className="h-px w-full bg-border/30" />
|