@pascal-app/editor 0.8.0 → 0.9.2
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 +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- 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-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -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-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -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/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -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 +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- 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/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -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 +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -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 +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- 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 +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -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 +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- 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/paint-panel.tsx +0 -163
- 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,51 @@
|
|
|
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'
|
|
48
|
+
import { BVHEcctrl, type BVHEcctrlApi, type MovementInput } from '@pascal-app/viewer'
|
|
10
49
|
import {
|
|
11
50
|
closeDoorOpenState,
|
|
12
51
|
DOOR_SWING_OPEN_ANGLE,
|
|
@@ -26,15 +65,23 @@ import {
|
|
|
26
65
|
type FirstPersonColliderWorld,
|
|
27
66
|
type FirstPersonSpawn,
|
|
28
67
|
} from './first-person/build-collider-world'
|
|
29
|
-
import type { BVHEcctrlApi } from './first-person/bvh-ecctrl'
|
|
30
|
-
import BVHEcctrl from './first-person/bvh-ecctrl'
|
|
31
68
|
|
|
32
69
|
const CAMERA_EYE_OFFSET = 0.45
|
|
33
70
|
const LOOK_SENSITIVITY = 0.002
|
|
34
71
|
const CONTROLLER_CENTER_FROM_EYE = 0.85
|
|
35
72
|
const DOOR_INTERACTION_DISTANCE = 2.5
|
|
36
73
|
const DOOR_LEAF_INTERACTION_DEPTH = 0.08
|
|
37
|
-
const
|
|
74
|
+
const ELEVATOR_RIDE_HORIZONTAL_PADDING = 0.18
|
|
75
|
+
const ELEVATOR_COLLIDER_HORIZONTAL_PADDING = 0.14
|
|
76
|
+
const ELEVATOR_COLLIDER_FLOOR_THICKNESS = 0.08
|
|
77
|
+
const ELEVATOR_COLLIDER_DOOR_DEPTH = 0.12
|
|
78
|
+
const ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD = 0.72
|
|
79
|
+
const DEFAULT_ELEVATOR_LEVEL_HEIGHT = 2.5
|
|
80
|
+
const VOID_FALL_RESPAWN_DEPTH = 12
|
|
81
|
+
|
|
82
|
+
type MovementKeyName = Exclude<keyof MovementInput, 'joystick'>
|
|
83
|
+
|
|
84
|
+
const movementKeyboardBindings: Array<{ name: MovementKeyName; keys: string[] }> = [
|
|
38
85
|
{ name: 'forward', keys: ['ArrowUp', 'KeyW'] },
|
|
39
86
|
{ name: 'backward', keys: ['ArrowDown', 'KeyS'] },
|
|
40
87
|
{ name: 'leftward', keys: ['ArrowLeft', 'KeyA'] },
|
|
@@ -42,6 +89,36 @@ const keyboardMap = [
|
|
|
42
89
|
{ name: 'jump', keys: ['Space'] },
|
|
43
90
|
{ name: 'run', keys: ['ShiftLeft', 'ShiftRight'] },
|
|
44
91
|
]
|
|
92
|
+
const keyboardMap = movementKeyboardBindings
|
|
93
|
+
const movementKeyToName = new Map<string, MovementKeyName>(
|
|
94
|
+
movementKeyboardBindings.flatMap(({ name, keys }) => keys.map((key) => [key, name] as const)),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const inactiveMovementInput: MovementInput = {
|
|
98
|
+
backward: false,
|
|
99
|
+
forward: false,
|
|
100
|
+
jump: false,
|
|
101
|
+
leftward: false,
|
|
102
|
+
rightward: false,
|
|
103
|
+
run: false,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getMovementInputForKey(code: string, active: boolean): MovementInput | null {
|
|
107
|
+
const name = movementKeyToName.get(code)
|
|
108
|
+
return name ? ({ [name]: active } as MovementInput) : null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function focusFirstPersonCanvas(canvas: HTMLCanvasElement) {
|
|
112
|
+
const activeElement = document.activeElement
|
|
113
|
+
if (activeElement instanceof HTMLElement && !canvas.contains(activeElement)) {
|
|
114
|
+
activeElement.blur()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!canvas.hasAttribute('tabindex')) {
|
|
118
|
+
canvas.tabIndex = -1
|
|
119
|
+
}
|
|
120
|
+
canvas.focus({ preventScroll: true })
|
|
121
|
+
}
|
|
45
122
|
|
|
46
123
|
const cameraOffset = new Vector3(0, CAMERA_EYE_OFFSET, 0)
|
|
47
124
|
const cameraEuler = new Euler(0, 0, 0, 'YXZ')
|
|
@@ -59,13 +136,421 @@ const doorOpeningLocalHit = new Vector3()
|
|
|
59
136
|
const doorOpeningLocalRay = new Ray()
|
|
60
137
|
const doorOpeningMatrix = new Matrix4()
|
|
61
138
|
const doorOpeningWorldHit = new Vector3()
|
|
139
|
+
const elevatorLocalControllerPosition = new Vector3()
|
|
140
|
+
const elevatorInteractionRaycaster = new Raycaster()
|
|
141
|
+
const elevatorColliderMatrix = new Matrix4()
|
|
142
|
+
const elevatorColliderLocalMatrix = new Matrix4()
|
|
143
|
+
const elevatorLocalEyePosition = new Vector3()
|
|
144
|
+
const elevatorWorldControllerPosition = new Vector3()
|
|
145
|
+
const elevatorColliderMaterial = new MeshBasicMaterial({ visible: false })
|
|
62
146
|
const spawnWorldPosition = new Vector3()
|
|
63
147
|
const spawnWorldEuler = new Euler(0, 0, 0, 'YXZ')
|
|
64
148
|
const windowInteractionRaycaster = new Raycaster()
|
|
65
149
|
|
|
66
|
-
type
|
|
67
|
-
|
|
68
|
-
|
|
150
|
+
type ElevatorColliderKind =
|
|
151
|
+
| 'cab-back'
|
|
152
|
+
| 'cab-ceiling'
|
|
153
|
+
| 'cab-door-left'
|
|
154
|
+
| 'cab-door-right'
|
|
155
|
+
| 'cab-door-gate'
|
|
156
|
+
| 'cab-floor'
|
|
157
|
+
| 'cab-left'
|
|
158
|
+
| 'cab-right'
|
|
159
|
+
| 'landing-door-gate'
|
|
160
|
+
| 'landing-door-left'
|
|
161
|
+
| 'landing-door-right'
|
|
162
|
+
| 'shaft-back'
|
|
163
|
+
| 'shaft-front-header'
|
|
164
|
+
| 'shaft-front-left'
|
|
165
|
+
| 'shaft-front-right'
|
|
166
|
+
| 'shaft-left'
|
|
167
|
+
| 'shaft-right'
|
|
168
|
+
| 'shaft-top'
|
|
169
|
+
|
|
170
|
+
type ElevatorColliderUserData = {
|
|
171
|
+
doorWidth?: number
|
|
172
|
+
dynamic?: boolean
|
|
173
|
+
elevatorId: AnyNodeId
|
|
174
|
+
kind: ElevatorColliderKind
|
|
175
|
+
levelId?: AnyNodeId
|
|
176
|
+
localPosition: [number, number, number]
|
|
177
|
+
matrixInitialized?: boolean
|
|
178
|
+
side?: ElevatorDoorSide
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
type ElevatorColliderMesh = Mesh & {
|
|
182
|
+
userData: Mesh['userData'] & ElevatorColliderUserData
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
type FirstPersonInteractableTarget =
|
|
186
|
+
| {
|
|
187
|
+
id: AnyNodeId
|
|
188
|
+
type: 'door' | 'window'
|
|
189
|
+
}
|
|
190
|
+
| {
|
|
191
|
+
action: 'open-door' | 'request-level'
|
|
192
|
+
buttonKind: 'cab' | 'landing'
|
|
193
|
+
id: AnyNodeId
|
|
194
|
+
levelId?: AnyNodeId
|
|
195
|
+
type: 'elevator'
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
type ElevatorButtonTarget = {
|
|
199
|
+
action: 'open-door' | 'request-level'
|
|
200
|
+
buttonKind: 'cab' | 'landing'
|
|
201
|
+
elevatorId: AnyNodeId
|
|
202
|
+
levelId?: AnyNodeId
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function resolveElevatorButtonTarget(object: Object3D): ElevatorButtonTarget | null {
|
|
206
|
+
let current: Object3D | null = object
|
|
207
|
+
|
|
208
|
+
while (current) {
|
|
209
|
+
const candidate = (
|
|
210
|
+
current.userData as {
|
|
211
|
+
elevatorButton?: {
|
|
212
|
+
action?: unknown
|
|
213
|
+
disabled?: unknown
|
|
214
|
+
elevatorId?: unknown
|
|
215
|
+
kind?: unknown
|
|
216
|
+
levelId?: unknown
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
).elevatorButton
|
|
220
|
+
|
|
221
|
+
if (candidate?.disabled === true) {
|
|
222
|
+
return null
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (typeof candidate?.elevatorId === 'string' && candidate.kind === 'cab') {
|
|
226
|
+
const action = candidate.action === 'open-door' ? 'open-door' : 'request-level'
|
|
227
|
+
if (action === 'open-door') {
|
|
228
|
+
return {
|
|
229
|
+
action,
|
|
230
|
+
buttonKind: candidate.kind,
|
|
231
|
+
elevatorId: candidate.elevatorId as AnyNodeId,
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (
|
|
237
|
+
typeof candidate?.elevatorId === 'string' &&
|
|
238
|
+
typeof candidate.levelId === 'string' &&
|
|
239
|
+
(candidate.kind === 'cab' || candidate.kind === 'landing')
|
|
240
|
+
) {
|
|
241
|
+
return {
|
|
242
|
+
action: 'request-level',
|
|
243
|
+
buttonKind: candidate.kind,
|
|
244
|
+
elevatorId: candidate.elevatorId as AnyNodeId,
|
|
245
|
+
levelId: candidate.levelId as AnyNodeId,
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
current = current.parent
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function getInteractableTargetKey(target: FirstPersonInteractableTarget | null) {
|
|
256
|
+
if (!target) return null
|
|
257
|
+
return target.type === 'elevator'
|
|
258
|
+
? `${target.type}:${target.id}:${target.levelId}`
|
|
259
|
+
: `${target.type}:${target.id}`
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function isDynamicElevatorCollider(kind: ElevatorColliderKind) {
|
|
263
|
+
return kind.startsWith('cab-') || kind.startsWith('landing-door')
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function isInsideElevatorCab(
|
|
267
|
+
elevator: ElevatorNode,
|
|
268
|
+
runtime: NonNullable<ReturnType<typeof useInteractive.getState>['elevators'][AnyNodeId]>,
|
|
269
|
+
localEyePosition: Vector3,
|
|
270
|
+
) {
|
|
271
|
+
const halfWidth = getElevatorCabWidth(elevator) / 2 - ELEVATOR_RIDE_HORIZONTAL_PADDING
|
|
272
|
+
const halfDepth = getElevatorCabDepth(elevator) / 2 - ELEVATOR_RIDE_HORIZONTAL_PADDING
|
|
273
|
+
const cabCenterZ = getElevatorCabCenterZ(elevator)
|
|
274
|
+
const cabHeight = Math.max(elevator.cabHeight, 1.4)
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
Math.abs(localEyePosition.x) <= Math.max(halfWidth, 0.24) &&
|
|
278
|
+
Math.abs(localEyePosition.z - cabCenterZ) <= Math.max(halfDepth, 0.24) &&
|
|
279
|
+
localEyePosition.y >= runtime.carY + 0.35 &&
|
|
280
|
+
localEyePosition.y <= runtime.carY + cabHeight + 0.7
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function getFirstPersonLevelHeight(levelId: string, nodes: Record<string, AnyNode>) {
|
|
285
|
+
const level = nodes[levelId as AnyNodeId]
|
|
286
|
+
if (level?.type !== 'level') return DEFAULT_ELEVATOR_LEVEL_HEIGHT
|
|
287
|
+
|
|
288
|
+
let maxTop = 0
|
|
289
|
+
for (const childId of level.children) {
|
|
290
|
+
const child = nodes[childId as AnyNodeId]
|
|
291
|
+
if (!child) continue
|
|
292
|
+
|
|
293
|
+
if (child.type === 'ceiling') {
|
|
294
|
+
maxTop = Math.max(maxTop, child.height ?? DEFAULT_ELEVATOR_LEVEL_HEIGHT)
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (child.type === 'wall') {
|
|
299
|
+
const meshY = Math.max(sceneRegistry.nodes.get(childId as AnyNodeId)?.position.y ?? 0, 0)
|
|
300
|
+
maxTop = Math.max(maxTop, meshY + (child.height ?? DEFAULT_ELEVATOR_LEVEL_HEIGHT))
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return maxTop > 0 ? maxTop : DEFAULT_ELEVATOR_LEVEL_HEIGHT
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function resolveElevatorColliderLevels(elevator: ElevatorNode, nodes: Record<string, AnyNode>) {
|
|
308
|
+
const allLevels = resolveElevatorBuildingLevels(elevator, nodes)
|
|
309
|
+
|
|
310
|
+
const baseYByLevelId = new Map<string, number>()
|
|
311
|
+
let cumulativeY = 0
|
|
312
|
+
for (const level of allLevels) {
|
|
313
|
+
baseYByLevelId.set(level.id, cumulativeY)
|
|
314
|
+
cumulativeY += getFirstPersonLevelHeight(level.id, nodes)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const serviceLevels = resolveElevatorServiceLevels(elevator, nodes)
|
|
318
|
+
const entries = serviceLevels.map((level) => ({
|
|
319
|
+
baseY: baseYByLevelId.get(level.id) ?? 0,
|
|
320
|
+
id: level.id as AnyNodeId,
|
|
321
|
+
}))
|
|
322
|
+
const firstServedLevel = serviceLevels[0] ?? null
|
|
323
|
+
const lastServedLevel = serviceLevels[serviceLevels.length - 1] ?? null
|
|
324
|
+
const shaftBaseY = firstServedLevel ? (baseYByLevelId.get(firstServedLevel.id) ?? 0) : 0
|
|
325
|
+
const lastServedIndex = lastServedLevel
|
|
326
|
+
? allLevels.findIndex((level) => level.id === lastServedLevel.id)
|
|
327
|
+
: -1
|
|
328
|
+
const nextLevel = lastServedIndex >= 0 ? allLevels[lastServedIndex + 1] : null
|
|
329
|
+
const shaftTopY = nextLevel
|
|
330
|
+
? (baseYByLevelId.get(nextLevel.id) ?? cumulativeY)
|
|
331
|
+
: lastServedLevel
|
|
332
|
+
? cumulativeY
|
|
333
|
+
: elevator.cabHeight + 0.3
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
entries,
|
|
337
|
+
shaftBaseY,
|
|
338
|
+
shaftTopY,
|
|
339
|
+
totalHeight: Math.max(shaftTopY - shaftBaseY, elevator.cabHeight + 0.3),
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function createElevatorColliderMesh(
|
|
344
|
+
elevatorId: AnyNodeId,
|
|
345
|
+
kind: ElevatorColliderKind,
|
|
346
|
+
size: [number, number, number],
|
|
347
|
+
localPosition: [number, number, number],
|
|
348
|
+
userData: Partial<ElevatorColliderUserData> = {},
|
|
349
|
+
) {
|
|
350
|
+
const geometry = new BoxGeometry(size[0], size[1], size[2])
|
|
351
|
+
|
|
352
|
+
const bvhGeometry = geometry as typeof geometry & {
|
|
353
|
+
computeBoundsTree?: typeof computeBoundsTree
|
|
354
|
+
disposeBoundsTree?: typeof disposeBoundsTree
|
|
355
|
+
}
|
|
356
|
+
;(bvhGeometry as any).computeBoundsTree = computeBoundsTree
|
|
357
|
+
;(bvhGeometry as any).disposeBoundsTree = disposeBoundsTree
|
|
358
|
+
bvhGeometry.computeBoundsTree?.({
|
|
359
|
+
maxLeafSize: 12,
|
|
360
|
+
strategy: 0,
|
|
361
|
+
} as never)
|
|
362
|
+
bvhGeometry.computeBoundingBox()
|
|
363
|
+
|
|
364
|
+
const mesh = new Mesh(bvhGeometry, elevatorColliderMaterial) as unknown as ElevatorColliderMesh
|
|
365
|
+
mesh.raycast = acceleratedRaycast
|
|
366
|
+
mesh.matrixAutoUpdate = false
|
|
367
|
+
mesh.visible = true
|
|
368
|
+
mesh.userData = {
|
|
369
|
+
...userData,
|
|
370
|
+
dynamic: isDynamicElevatorCollider(kind),
|
|
371
|
+
elevatorId,
|
|
372
|
+
excludeCollisionCheck: false,
|
|
373
|
+
excludeFloatHit: false,
|
|
374
|
+
friction: 0.8,
|
|
375
|
+
kind,
|
|
376
|
+
localPosition,
|
|
377
|
+
matrixInitialized: false,
|
|
378
|
+
restitution: 0.03,
|
|
379
|
+
type: 'ELEVATOR_COLLIDER',
|
|
380
|
+
}
|
|
381
|
+
return mesh
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function buildElevatorColliderMeshes(): ElevatorColliderMesh[] {
|
|
385
|
+
const nodes = useScene.getState().nodes
|
|
386
|
+
const meshes: ElevatorColliderMesh[] = []
|
|
387
|
+
|
|
388
|
+
for (const elevatorId of sceneRegistry.byType.elevator!) {
|
|
389
|
+
const typedElevatorId = elevatorId as AnyNodeId
|
|
390
|
+
const node = nodes[typedElevatorId]
|
|
391
|
+
if (node?.type !== 'elevator' || node.visible === false) continue
|
|
392
|
+
|
|
393
|
+
const { entries, shaftBaseY, shaftTopY, totalHeight } = resolveElevatorColliderLevels(
|
|
394
|
+
node,
|
|
395
|
+
nodes,
|
|
396
|
+
)
|
|
397
|
+
const cabWidth = getElevatorCabWidth(node)
|
|
398
|
+
const cabDepth = getElevatorCabDepth(node)
|
|
399
|
+
const shaftWidth = getElevatorShaftWidth(node, cabWidth)
|
|
400
|
+
const shaftDepth = getElevatorShaftDepth(node, cabDepth)
|
|
401
|
+
const cabHeight = Math.max(node.cabHeight, 1.4)
|
|
402
|
+
const doorWidth = Math.min(Math.max(node.doorWidth, 0.45), cabWidth - 0.18, shaftWidth - 0.18)
|
|
403
|
+
const doorHeight = Math.min(Math.max(node.doorHeight, 1.2), cabHeight - 0.1)
|
|
404
|
+
const doorStyle = getResolvedElevatorDoorStyle(node.doorStyle)
|
|
405
|
+
const shaftHeight = Math.max(totalHeight, cabHeight + 0.3)
|
|
406
|
+
const wallThickness = getElevatorShaftWallThickness(node)
|
|
407
|
+
const cabFloorWidth = Math.max(cabWidth - ELEVATOR_COLLIDER_HORIZONTAL_PADDING * 2, 0.48)
|
|
408
|
+
const cabFloorDepth = Math.max(cabDepth - ELEVATOR_COLLIDER_HORIZONTAL_PADDING * 2, 0.48)
|
|
409
|
+
const frontWallZ = -shaftDepth / 2 - wallThickness / 2
|
|
410
|
+
const frontZ = frontWallZ - wallThickness / 2 - 0.018
|
|
411
|
+
const cabCenterZ = -shaftDepth / 2 + cabDepth / 2
|
|
412
|
+
const leafWidth = getElevatorDoorLeafWidth(doorWidth, doorStyle)
|
|
413
|
+
const doorLeafSides = getElevatorDoorLeafSides(doorStyle)
|
|
414
|
+
const resolvedShaftTopY = Math.max(shaftTopY, shaftBaseY + shaftHeight)
|
|
415
|
+
|
|
416
|
+
meshes.push(
|
|
417
|
+
createElevatorColliderMesh(
|
|
418
|
+
typedElevatorId,
|
|
419
|
+
'shaft-back',
|
|
420
|
+
[shaftWidth + wallThickness * 2, shaftHeight, wallThickness],
|
|
421
|
+
[0, shaftBaseY + shaftHeight / 2, shaftDepth / 2 + wallThickness / 2],
|
|
422
|
+
),
|
|
423
|
+
createElevatorColliderMesh(
|
|
424
|
+
typedElevatorId,
|
|
425
|
+
'shaft-left',
|
|
426
|
+
[wallThickness, shaftHeight, shaftDepth + wallThickness * 2],
|
|
427
|
+
[-shaftWidth / 2 - wallThickness / 2, shaftBaseY + shaftHeight / 2, 0],
|
|
428
|
+
),
|
|
429
|
+
createElevatorColliderMesh(
|
|
430
|
+
typedElevatorId,
|
|
431
|
+
'shaft-right',
|
|
432
|
+
[wallThickness, shaftHeight, shaftDepth + wallThickness * 2],
|
|
433
|
+
[shaftWidth / 2 + wallThickness / 2, shaftBaseY + shaftHeight / 2, 0],
|
|
434
|
+
),
|
|
435
|
+
createElevatorColliderMesh(
|
|
436
|
+
typedElevatorId,
|
|
437
|
+
'shaft-top',
|
|
438
|
+
[shaftWidth + wallThickness * 2, wallThickness, shaftDepth + wallThickness * 2],
|
|
439
|
+
[0, shaftBaseY + shaftHeight - wallThickness / 2, 0],
|
|
440
|
+
),
|
|
441
|
+
createElevatorColliderMesh(
|
|
442
|
+
typedElevatorId,
|
|
443
|
+
'cab-floor',
|
|
444
|
+
[cabFloorWidth, ELEVATOR_COLLIDER_FLOOR_THICKNESS, cabFloorDepth],
|
|
445
|
+
[0, ELEVATOR_COLLIDER_FLOOR_THICKNESS / 2, cabCenterZ],
|
|
446
|
+
),
|
|
447
|
+
createElevatorColliderMesh(
|
|
448
|
+
typedElevatorId,
|
|
449
|
+
'cab-ceiling',
|
|
450
|
+
[cabWidth, wallThickness, cabDepth],
|
|
451
|
+
[0, cabHeight - wallThickness / 2, cabCenterZ],
|
|
452
|
+
),
|
|
453
|
+
createElevatorColliderMesh(
|
|
454
|
+
typedElevatorId,
|
|
455
|
+
'cab-back',
|
|
456
|
+
[cabWidth, cabHeight, wallThickness],
|
|
457
|
+
[0, cabHeight / 2, cabCenterZ + cabDepth / 2 - wallThickness / 2],
|
|
458
|
+
),
|
|
459
|
+
createElevatorColliderMesh(
|
|
460
|
+
typedElevatorId,
|
|
461
|
+
'cab-left',
|
|
462
|
+
[wallThickness, cabHeight, cabDepth],
|
|
463
|
+
[-cabWidth / 2 + wallThickness / 2, cabHeight / 2, cabCenterZ],
|
|
464
|
+
),
|
|
465
|
+
createElevatorColliderMesh(
|
|
466
|
+
typedElevatorId,
|
|
467
|
+
'cab-right',
|
|
468
|
+
[wallThickness, cabHeight, cabDepth],
|
|
469
|
+
[cabWidth / 2 - wallThickness / 2, cabHeight / 2, cabCenterZ],
|
|
470
|
+
),
|
|
471
|
+
createElevatorColliderMesh(
|
|
472
|
+
typedElevatorId,
|
|
473
|
+
'cab-door-gate',
|
|
474
|
+
[doorWidth, doorHeight, ELEVATOR_COLLIDER_DOOR_DEPTH],
|
|
475
|
+
[0, doorHeight / 2, frontZ],
|
|
476
|
+
{ doorWidth },
|
|
477
|
+
),
|
|
478
|
+
...doorLeafSides.map((side) =>
|
|
479
|
+
createElevatorColliderMesh(
|
|
480
|
+
typedElevatorId,
|
|
481
|
+
side === 'left' ? 'cab-door-left' : 'cab-door-right',
|
|
482
|
+
[leafWidth, doorHeight, ELEVATOR_COLLIDER_DOOR_DEPTH],
|
|
483
|
+
[0, doorHeight / 2, frontZ],
|
|
484
|
+
{ doorWidth, side },
|
|
485
|
+
),
|
|
486
|
+
),
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
const entrySpans = entries.map((entry, index) => {
|
|
490
|
+
const nextEntry = entries[index + 1]
|
|
491
|
+
return {
|
|
492
|
+
entry,
|
|
493
|
+
levelTopY: Math.max(nextEntry?.baseY ?? resolvedShaftTopY, entry.baseY + doorHeight + 0.24),
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
for (const { entry, levelTopY } of entrySpans) {
|
|
498
|
+
const wallDepth = wallThickness
|
|
499
|
+
const levelHeight = Math.max(levelTopY - entry.baseY, doorHeight + 0.24)
|
|
500
|
+
const jambWidth = Math.max((shaftWidth - doorWidth) / 2, 0.08)
|
|
501
|
+
const jambCenterOffset = doorWidth / 2 + jambWidth / 2
|
|
502
|
+
const headerHeight = Math.max(levelHeight - doorHeight, 0.14)
|
|
503
|
+
|
|
504
|
+
meshes.push(
|
|
505
|
+
createElevatorColliderMesh(
|
|
506
|
+
typedElevatorId,
|
|
507
|
+
'shaft-front-left',
|
|
508
|
+
[jambWidth, levelHeight, wallDepth],
|
|
509
|
+
[-jambCenterOffset, entry.baseY + levelHeight / 2, frontWallZ],
|
|
510
|
+
),
|
|
511
|
+
createElevatorColliderMesh(
|
|
512
|
+
typedElevatorId,
|
|
513
|
+
'shaft-front-right',
|
|
514
|
+
[jambWidth, levelHeight, wallDepth],
|
|
515
|
+
[jambCenterOffset, entry.baseY + levelHeight / 2, frontWallZ],
|
|
516
|
+
),
|
|
517
|
+
createElevatorColliderMesh(
|
|
518
|
+
typedElevatorId,
|
|
519
|
+
'shaft-front-header',
|
|
520
|
+
[shaftWidth, headerHeight, wallDepth],
|
|
521
|
+
[0, entry.baseY + doorHeight + headerHeight / 2, frontWallZ],
|
|
522
|
+
),
|
|
523
|
+
createElevatorColliderMesh(
|
|
524
|
+
typedElevatorId,
|
|
525
|
+
'landing-door-gate',
|
|
526
|
+
[doorWidth, doorHeight, ELEVATOR_COLLIDER_DOOR_DEPTH],
|
|
527
|
+
[0, entry.baseY + doorHeight / 2, frontZ - 0.02],
|
|
528
|
+
{ doorWidth, levelId: entry.id },
|
|
529
|
+
),
|
|
530
|
+
...doorLeafSides.map((side) =>
|
|
531
|
+
createElevatorColliderMesh(
|
|
532
|
+
typedElevatorId,
|
|
533
|
+
side === 'left' ? 'landing-door-left' : 'landing-door-right',
|
|
534
|
+
[leafWidth, doorHeight, ELEVATOR_COLLIDER_DOOR_DEPTH],
|
|
535
|
+
[0, entry.baseY + doorHeight / 2, frontZ - 0.02],
|
|
536
|
+
{ doorWidth, levelId: entry.id, side },
|
|
537
|
+
),
|
|
538
|
+
),
|
|
539
|
+
)
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return meshes
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function disposeElevatorColliderMeshes(meshes: ElevatorColliderMesh[]) {
|
|
547
|
+
for (const mesh of meshes) {
|
|
548
|
+
const geometry = mesh.geometry as typeof mesh.geometry & {
|
|
549
|
+
disposeBoundsTree?: typeof disposeBoundsTree
|
|
550
|
+
}
|
|
551
|
+
geometry.disposeBoundsTree?.()
|
|
552
|
+
geometry.dispose()
|
|
553
|
+
}
|
|
69
554
|
}
|
|
70
555
|
|
|
71
556
|
const resolvePlacedSpawnNode = (
|
|
@@ -83,11 +568,22 @@ export const FirstPersonControls = () => {
|
|
|
83
568
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
84
569
|
const placedSpawnNode = useScene((state) => resolvePlacedSpawnNode(state.nodes, selectedLevelId))
|
|
85
570
|
const controllerRef = useRef<BVHEcctrlApi | null>(null)
|
|
571
|
+
const movementInputRef = useRef<MovementInput>({ ...inactiveMovementInput })
|
|
572
|
+
const hadPointerLockRef = useRef(false)
|
|
86
573
|
const yawRef = useRef(0)
|
|
87
574
|
const pitchRef = useRef(0)
|
|
88
575
|
const interactableTargetRef = useRef<FirstPersonInteractableTarget | null>(null)
|
|
576
|
+
const [isElevatorRideLocked, setIsElevatorRideLocked] = useState(false)
|
|
577
|
+
const ridingElevatorRef = useRef<{
|
|
578
|
+
elevatorId: AnyNodeId
|
|
579
|
+
localControllerY: number | null
|
|
580
|
+
previousCarY: number
|
|
581
|
+
} | null>(null)
|
|
582
|
+
const rideLockedRef = useRef(false)
|
|
89
583
|
const worldRef = useRef<FirstPersonColliderWorld | null>(null)
|
|
584
|
+
const elevatorColliderMeshesRef = useRef<ElevatorColliderMesh[]>([])
|
|
90
585
|
const [world, setWorld] = useState<FirstPersonColliderWorld | null>(null)
|
|
586
|
+
const [elevatorColliderMeshes, setElevatorColliderMeshes] = useState<ElevatorColliderMesh[]>([])
|
|
91
587
|
const [controllerStart, setControllerStart] = useState<{
|
|
92
588
|
position: [number, number, number]
|
|
93
589
|
yaw: number
|
|
@@ -99,9 +595,29 @@ export const FirstPersonControls = () => {
|
|
|
99
595
|
setWorld(nextWorld)
|
|
100
596
|
}, [])
|
|
101
597
|
|
|
598
|
+
const replaceElevatorColliderMeshes = useCallback((nextMeshes: ElevatorColliderMesh[]) => {
|
|
599
|
+
disposeElevatorColliderMeshes(elevatorColliderMeshesRef.current)
|
|
600
|
+
elevatorColliderMeshesRef.current = nextMeshes
|
|
601
|
+
setElevatorColliderMeshes(nextMeshes)
|
|
602
|
+
}, [])
|
|
603
|
+
|
|
102
604
|
const rebuildColliderWorld = useCallback(() => {
|
|
103
605
|
replaceColliderWorld(buildFirstPersonColliderWorldFromRegistry())
|
|
104
|
-
|
|
606
|
+
replaceElevatorColliderMeshes(buildElevatorColliderMeshes())
|
|
607
|
+
}, [replaceColliderWorld, replaceElevatorColliderMeshes])
|
|
608
|
+
|
|
609
|
+
const setElevatorRideLocked = useCallback((locked: boolean) => {
|
|
610
|
+
if (rideLockedRef.current === locked) return
|
|
611
|
+
rideLockedRef.current = locked
|
|
612
|
+
setIsElevatorRideLocked(locked)
|
|
613
|
+
}, [])
|
|
614
|
+
|
|
615
|
+
const setControllerApi = useCallback((api: BVHEcctrlApi | null) => {
|
|
616
|
+
controllerRef.current = api
|
|
617
|
+
if (api) {
|
|
618
|
+
api.setMovement(movementInputRef.current)
|
|
619
|
+
}
|
|
620
|
+
}, [])
|
|
105
621
|
|
|
106
622
|
const resolveInteractableDoorId = useCallback((): AnyNodeId | null => {
|
|
107
623
|
const nodes = useScene.getState().nodes
|
|
@@ -111,7 +627,7 @@ export const FirstPersonControls = () => {
|
|
|
111
627
|
let closestDoorId: AnyNodeId | null = null
|
|
112
628
|
let closestDistance = DOOR_INTERACTION_DISTANCE
|
|
113
629
|
|
|
114
|
-
for (const doorId of sceneRegistry.byType.door) {
|
|
630
|
+
for (const doorId of sceneRegistry.byType.door!) {
|
|
115
631
|
const node = nodes[doorId as AnyNodeId]
|
|
116
632
|
if (node?.type !== 'door') continue
|
|
117
633
|
if (node.openingKind === 'opening') continue
|
|
@@ -209,7 +725,7 @@ export const FirstPersonControls = () => {
|
|
|
209
725
|
let closestWindowId: AnyNodeId | null = null
|
|
210
726
|
let closestDistance = DOOR_INTERACTION_DISTANCE
|
|
211
727
|
|
|
212
|
-
for (const windowId of sceneRegistry.byType.window) {
|
|
728
|
+
for (const windowId of sceneRegistry.byType.window!) {
|
|
213
729
|
const node = nodes[windowId as AnyNodeId]
|
|
214
730
|
if (node?.type !== 'window') continue
|
|
215
731
|
if (node.openingKind === 'opening') continue
|
|
@@ -230,7 +746,61 @@ export const FirstPersonControls = () => {
|
|
|
230
746
|
return closestWindowId
|
|
231
747
|
}, [camera])
|
|
232
748
|
|
|
749
|
+
const resolveInteractableElevatorTarget =
|
|
750
|
+
useCallback((): FirstPersonInteractableTarget | null => {
|
|
751
|
+
const nodes = useScene.getState().nodes
|
|
752
|
+
camera.updateMatrixWorld(true)
|
|
753
|
+
elevatorInteractionRaycaster.setFromCamera(centerScreenPoint, camera)
|
|
754
|
+
|
|
755
|
+
let closestTarget: FirstPersonInteractableTarget | null = null
|
|
756
|
+
let closestDistance = DOOR_INTERACTION_DISTANCE
|
|
757
|
+
|
|
758
|
+
for (const elevatorId of sceneRegistry.byType.elevator!) {
|
|
759
|
+
const typedElevatorId = elevatorId as AnyNodeId
|
|
760
|
+
const node = nodes[typedElevatorId]
|
|
761
|
+
if (node?.type !== 'elevator') continue
|
|
762
|
+
|
|
763
|
+
const object = sceneRegistry.nodes.get(typedElevatorId)
|
|
764
|
+
if (!object) continue
|
|
765
|
+
|
|
766
|
+
const runtime = useInteractive.getState().elevators[typedElevatorId]
|
|
767
|
+
object.updateWorldMatrix(true, true)
|
|
768
|
+
if (runtime) {
|
|
769
|
+
elevatorLocalEyePosition.copy(camera.position)
|
|
770
|
+
object.worldToLocal(elevatorLocalEyePosition)
|
|
771
|
+
}
|
|
772
|
+
const canUseCabButtons =
|
|
773
|
+
runtime && isInsideElevatorCab(node, runtime, elevatorLocalEyePosition)
|
|
774
|
+
|
|
775
|
+
const intersections = elevatorInteractionRaycaster.intersectObject(object, true)
|
|
776
|
+
for (const intersection of intersections) {
|
|
777
|
+
if (intersection.distance > closestDistance) continue
|
|
778
|
+
|
|
779
|
+
const target = resolveElevatorButtonTarget(intersection.object)
|
|
780
|
+
if (!target || target.elevatorId !== elevatorId) continue
|
|
781
|
+
if (target.action === 'request-level') {
|
|
782
|
+
if (!target.levelId || nodes[target.levelId]?.type !== 'level') continue
|
|
783
|
+
}
|
|
784
|
+
if (target.buttonKind === 'cab' && !canUseCabButtons) continue
|
|
785
|
+
|
|
786
|
+
closestTarget = {
|
|
787
|
+
action: target.action,
|
|
788
|
+
buttonKind: target.buttonKind,
|
|
789
|
+
id: target.elevatorId,
|
|
790
|
+
levelId: target.levelId,
|
|
791
|
+
type: 'elevator',
|
|
792
|
+
}
|
|
793
|
+
closestDistance = intersection.distance
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return closestTarget
|
|
798
|
+
}, [camera])
|
|
799
|
+
|
|
233
800
|
const resolveInteractableTarget = useCallback((): FirstPersonInteractableTarget | null => {
|
|
801
|
+
const elevatorTarget = resolveInteractableElevatorTarget()
|
|
802
|
+
if (elevatorTarget) return elevatorTarget
|
|
803
|
+
|
|
234
804
|
const doorId = resolveInteractableDoorId()
|
|
235
805
|
if (doorId) return { id: doorId, type: 'door' }
|
|
236
806
|
|
|
@@ -238,12 +808,42 @@ export const FirstPersonControls = () => {
|
|
|
238
808
|
if (windowId) return { id: windowId, type: 'window' }
|
|
239
809
|
|
|
240
810
|
return null
|
|
241
|
-
}, [resolveInteractableDoorId, resolveInteractableWindowId])
|
|
811
|
+
}, [resolveInteractableDoorId, resolveInteractableElevatorTarget, resolveInteractableWindowId])
|
|
242
812
|
|
|
243
813
|
const toggleInteractableTarget = useCallback(() => {
|
|
244
814
|
const target = interactableTargetRef.current ?? resolveInteractableTarget()
|
|
245
815
|
if (!target) return
|
|
246
816
|
|
|
817
|
+
if (target.type === 'elevator') {
|
|
818
|
+
if (target.buttonKind === 'cab') {
|
|
819
|
+
const state = useInteractive.getState().elevators[target.id]
|
|
820
|
+
if (state) {
|
|
821
|
+
ridingElevatorRef.current = {
|
|
822
|
+
elevatorId: target.id,
|
|
823
|
+
localControllerY: null,
|
|
824
|
+
previousCarY: state.carY,
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
if (target.action === 'open-door') {
|
|
829
|
+
openElevatorDoor(target.id)
|
|
830
|
+
return
|
|
831
|
+
}
|
|
832
|
+
if (target.levelId) {
|
|
833
|
+
const targetElevatorId =
|
|
834
|
+
target.buttonKind === 'landing'
|
|
835
|
+
? resolveElevatorDispatchTarget({
|
|
836
|
+
elevators: useInteractive.getState().elevators,
|
|
837
|
+
levelId: target.levelId,
|
|
838
|
+
nodes: useScene.getState().nodes,
|
|
839
|
+
requestedElevatorId: target.id,
|
|
840
|
+
})
|
|
841
|
+
: target.id
|
|
842
|
+
requestElevatorLevel(targetElevatorId, target.levelId)
|
|
843
|
+
}
|
|
844
|
+
return
|
|
845
|
+
}
|
|
846
|
+
|
|
247
847
|
if (target.type === 'window') {
|
|
248
848
|
const node = useScene.getState().nodes[target.id]
|
|
249
849
|
if (
|
|
@@ -270,6 +870,8 @@ export const FirstPersonControls = () => {
|
|
|
270
870
|
const target = interactableTargetRef.current ?? resolveInteractableTarget()
|
|
271
871
|
if (!target) return
|
|
272
872
|
|
|
873
|
+
if (target.type === 'elevator') return
|
|
874
|
+
|
|
273
875
|
if (target.type === 'window') {
|
|
274
876
|
const node = useScene.getState().nodes[target.id]
|
|
275
877
|
if (
|
|
@@ -325,6 +927,9 @@ export const FirstPersonControls = () => {
|
|
|
325
927
|
return () => {
|
|
326
928
|
worldRef.current?.dispose()
|
|
327
929
|
worldRef.current = null
|
|
930
|
+
disposeElevatorColliderMeshes(elevatorColliderMeshesRef.current)
|
|
931
|
+
elevatorColliderMeshesRef.current = []
|
|
932
|
+
setElevatorColliderMeshes([])
|
|
328
933
|
setWorld(null)
|
|
329
934
|
}
|
|
330
935
|
}, [rebuildColliderWorld])
|
|
@@ -352,6 +957,14 @@ export const FirstPersonControls = () => {
|
|
|
352
957
|
})
|
|
353
958
|
}, [camera, controllerStart, placedSpawn, world])
|
|
354
959
|
|
|
960
|
+
useEffect(() => {
|
|
961
|
+
const canvas = gl.domElement
|
|
962
|
+
focusFirstPersonCanvas(canvas)
|
|
963
|
+
|
|
964
|
+
const frame = window.requestAnimationFrame(() => focusFirstPersonCanvas(canvas))
|
|
965
|
+
return () => window.cancelAnimationFrame(frame)
|
|
966
|
+
}, [gl])
|
|
967
|
+
|
|
355
968
|
useEffect(() => {
|
|
356
969
|
const canvas = gl.domElement
|
|
357
970
|
const handleMouseMove = (e: MouseEvent) => {
|
|
@@ -373,22 +986,65 @@ export const FirstPersonControls = () => {
|
|
|
373
986
|
}
|
|
374
987
|
}
|
|
375
988
|
|
|
989
|
+
const handleMouseDown = (event: MouseEvent) => {
|
|
990
|
+
if (document.pointerLockElement !== canvas) return
|
|
991
|
+
if (event.button !== 0) return
|
|
992
|
+
|
|
993
|
+
event.preventDefault()
|
|
994
|
+
event.stopPropagation()
|
|
995
|
+
toggleInteractableTarget()
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
const handlePointerLockChange = () => {
|
|
999
|
+
const isLocked = document.pointerLockElement === canvas
|
|
1000
|
+
if (isLocked) {
|
|
1001
|
+
hadPointerLockRef.current = true
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (hadPointerLockRef.current && useEditor.getState().isFirstPersonMode) {
|
|
1006
|
+
useEditor.getState().setFirstPersonMode(false)
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
handlePointerLockChange()
|
|
376
1011
|
document.addEventListener('mousemove', handleMouseMove)
|
|
377
1012
|
document.addEventListener('click', handleClick)
|
|
1013
|
+
document.addEventListener('mousedown', handleMouseDown, true)
|
|
1014
|
+
document.addEventListener('pointerlockchange', handlePointerLockChange)
|
|
378
1015
|
|
|
379
1016
|
return () => {
|
|
380
1017
|
document.removeEventListener('mousemove', handleMouseMove)
|
|
381
1018
|
document.removeEventListener('click', handleClick)
|
|
1019
|
+
document.removeEventListener('mousedown', handleMouseDown, true)
|
|
1020
|
+
document.removeEventListener('pointerlockchange', handlePointerLockChange)
|
|
382
1021
|
if (document.pointerLockElement === canvas) {
|
|
383
1022
|
document.exitPointerLock()
|
|
384
1023
|
}
|
|
385
1024
|
}
|
|
386
|
-
}, [gl])
|
|
1025
|
+
}, [gl, toggleInteractableTarget])
|
|
387
1026
|
|
|
388
1027
|
useEffect(() => {
|
|
389
1028
|
const canvas = gl.domElement
|
|
390
1029
|
|
|
1030
|
+
const applyMovementKey = (event: KeyboardEvent, active: boolean) => {
|
|
1031
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
1032
|
+
return false
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const movement = getMovementInputForKey(event.code, active)
|
|
1036
|
+
if (!movement) return false
|
|
1037
|
+
|
|
1038
|
+
event.preventDefault()
|
|
1039
|
+
Object.assign(movementInputRef.current, movement)
|
|
1040
|
+
controllerRef.current?.setMovement(movement)
|
|
1041
|
+
return true
|
|
1042
|
+
}
|
|
1043
|
+
|
|
391
1044
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
1045
|
+
const handledMovement = applyMovementKey(event, true)
|
|
1046
|
+
if (handledMovement) return
|
|
1047
|
+
|
|
392
1048
|
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
393
1049
|
return
|
|
394
1050
|
}
|
|
@@ -411,32 +1067,286 @@ export const FirstPersonControls = () => {
|
|
|
411
1067
|
}
|
|
412
1068
|
}
|
|
413
1069
|
|
|
1070
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
1071
|
+
applyMovementKey(event, false)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
414
1074
|
document.addEventListener('keydown', handleKeyDown, true)
|
|
1075
|
+
document.addEventListener('keyup', handleKeyUp, true)
|
|
415
1076
|
return () => {
|
|
416
1077
|
document.removeEventListener('keydown', handleKeyDown, true)
|
|
1078
|
+
document.removeEventListener('keyup', handleKeyUp, true)
|
|
417
1079
|
}
|
|
418
1080
|
}, [closeInteractableTarget, gl, toggleInteractableTarget])
|
|
419
1081
|
|
|
420
|
-
|
|
1082
|
+
const syncElevatorColliderMeshes = useCallback(() => {
|
|
1083
|
+
const nodes = useScene.getState().nodes
|
|
1084
|
+
const interactive = useInteractive.getState()
|
|
1085
|
+
|
|
1086
|
+
for (const mesh of elevatorColliderMeshesRef.current) {
|
|
1087
|
+
const { doorWidth, dynamic, elevatorId, kind, levelId, localPosition, side } = mesh.userData
|
|
1088
|
+
const node = nodes[elevatorId]
|
|
1089
|
+
const runtime = interactive.elevators[elevatorId]
|
|
1090
|
+
const object = sceneRegistry.nodes.get(elevatorId)
|
|
1091
|
+
if (!(node?.type === 'elevator' && object && node.visible !== false)) {
|
|
1092
|
+
mesh.visible = false
|
|
1093
|
+
continue
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
if (!dynamic && mesh.userData.matrixInitialized && mesh.visible) {
|
|
1097
|
+
continue
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
let [localX, localY, localZ] = localPosition
|
|
1101
|
+
const isCabCollider = kind.startsWith('cab-')
|
|
1102
|
+
const isDoorCollider = kind === 'landing-door-left' || kind === 'landing-door-right'
|
|
1103
|
+
const isCabDoorGate = kind === 'cab-door-gate'
|
|
1104
|
+
const isLandingDoorGate = kind === 'landing-door-gate'
|
|
1105
|
+
|
|
1106
|
+
if (isCabCollider) {
|
|
1107
|
+
localY += runtime?.carY ?? 0
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (kind === 'cab-door-left' || kind === 'cab-door-right') {
|
|
1111
|
+
if (!runtime) {
|
|
1112
|
+
mesh.visible = false
|
|
1113
|
+
continue
|
|
1114
|
+
}
|
|
1115
|
+
localX = getElevatorDoorLeafX(
|
|
1116
|
+
side ?? 'left',
|
|
1117
|
+
doorWidth ?? node.doorWidth,
|
|
1118
|
+
runtime.doorOpen,
|
|
1119
|
+
node.doorStyle,
|
|
1120
|
+
)
|
|
1121
|
+
mesh.visible = true
|
|
1122
|
+
} else if (isCabDoorGate) {
|
|
1123
|
+
if (!runtime) {
|
|
1124
|
+
mesh.visible = false
|
|
1125
|
+
continue
|
|
1126
|
+
}
|
|
1127
|
+
mesh.visible = runtime.doorOpen < ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD
|
|
1128
|
+
} else if (isDoorCollider) {
|
|
1129
|
+
const doorOpen = runtime?.currentLevelId === levelId ? (runtime?.doorOpen ?? 0) : 0
|
|
1130
|
+
localX = getElevatorDoorLeafX(
|
|
1131
|
+
side ?? 'left',
|
|
1132
|
+
doorWidth ?? node.doorWidth,
|
|
1133
|
+
doorOpen,
|
|
1134
|
+
node.doorStyle,
|
|
1135
|
+
)
|
|
1136
|
+
mesh.visible = true
|
|
1137
|
+
} else if (isLandingDoorGate) {
|
|
1138
|
+
const doorOpen = runtime?.currentLevelId === levelId ? (runtime?.doorOpen ?? 0) : 0
|
|
1139
|
+
mesh.visible = doorOpen < ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD
|
|
1140
|
+
} else {
|
|
1141
|
+
mesh.visible = true
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
object.updateWorldMatrix(true, false)
|
|
1145
|
+
elevatorColliderMatrix.copy(object.matrixWorld)
|
|
1146
|
+
elevatorColliderMatrix.multiply(
|
|
1147
|
+
elevatorColliderLocalMatrix.makeTranslation(localX, localY, localZ),
|
|
1148
|
+
)
|
|
1149
|
+
mesh.matrix.copy(elevatorColliderMatrix)
|
|
1150
|
+
mesh.matrixWorld.copy(elevatorColliderMatrix)
|
|
1151
|
+
mesh.userData.matrixInitialized = true
|
|
1152
|
+
}
|
|
1153
|
+
}, [])
|
|
1154
|
+
|
|
1155
|
+
useFrame(() => {
|
|
1156
|
+
syncElevatorColliderMeshes()
|
|
1157
|
+
}, -1)
|
|
1158
|
+
|
|
1159
|
+
const syncElevatorRide = useCallback(
|
|
1160
|
+
(group: Group) => {
|
|
1161
|
+
const nodes = useScene.getState().nodes
|
|
1162
|
+
const interactive = useInteractive.getState()
|
|
1163
|
+
const activeRide = ridingElevatorRef.current
|
|
1164
|
+
let nextRide: {
|
|
1165
|
+
cabHeight: number
|
|
1166
|
+
cabCenterZ: number
|
|
1167
|
+
carY: number
|
|
1168
|
+
doorOpen: number
|
|
1169
|
+
elevatorId: AnyNodeId
|
|
1170
|
+
halfDepth: number
|
|
1171
|
+
halfWidth: number
|
|
1172
|
+
object: Object3D
|
|
1173
|
+
phase: NonNullable<(typeof interactive.elevators)[AnyNodeId]>['phase']
|
|
1174
|
+
} | null = null
|
|
1175
|
+
|
|
1176
|
+
const elevatorIds = activeRide
|
|
1177
|
+
? [
|
|
1178
|
+
activeRide.elevatorId,
|
|
1179
|
+
...Array.from(sceneRegistry.byType.elevator!).filter(
|
|
1180
|
+
(elevatorId) => elevatorId !== activeRide.elevatorId,
|
|
1181
|
+
),
|
|
1182
|
+
]
|
|
1183
|
+
: Array.from(sceneRegistry.byType.elevator!)
|
|
1184
|
+
|
|
1185
|
+
for (const elevatorId of elevatorIds) {
|
|
1186
|
+
const typedElevatorId = elevatorId as AnyNodeId
|
|
1187
|
+
const node = nodes[typedElevatorId]
|
|
1188
|
+
if (node?.type !== 'elevator') continue
|
|
1189
|
+
|
|
1190
|
+
const runtime = interactive.elevators[typedElevatorId]
|
|
1191
|
+
const object = sceneRegistry.nodes.get(typedElevatorId)
|
|
1192
|
+
if (!(runtime && object)) continue
|
|
1193
|
+
|
|
1194
|
+
object.updateWorldMatrix(true, true)
|
|
1195
|
+
elevatorLocalEyePosition.copy(camera.position)
|
|
1196
|
+
object.worldToLocal(elevatorLocalEyePosition)
|
|
1197
|
+
|
|
1198
|
+
const halfWidth = getElevatorCabWidth(node) / 2 - ELEVATOR_RIDE_HORIZONTAL_PADDING
|
|
1199
|
+
const halfDepth = getElevatorCabDepth(node) / 2 - ELEVATOR_RIDE_HORIZONTAL_PADDING
|
|
1200
|
+
const cabCenterZ = getElevatorCabCenterZ(node)
|
|
1201
|
+
const cabHeight = Math.max(node.cabHeight, 1.4)
|
|
1202
|
+
const insideFootprint =
|
|
1203
|
+
Math.abs(elevatorLocalEyePosition.x) <= Math.max(halfWidth, 0.24) &&
|
|
1204
|
+
Math.abs(elevatorLocalEyePosition.z - cabCenterZ) <= Math.max(halfDepth, 0.24)
|
|
1205
|
+
const insideCabHeight =
|
|
1206
|
+
elevatorLocalEyePosition.y >= runtime.carY + 0.35 &&
|
|
1207
|
+
elevatorLocalEyePosition.y <= runtime.carY + cabHeight + 0.7
|
|
1208
|
+
const continuingRide =
|
|
1209
|
+
activeRide?.elevatorId === typedElevatorId &&
|
|
1210
|
+
insideFootprint &&
|
|
1211
|
+
elevatorLocalEyePosition.y >= runtime.carY - 0.2 &&
|
|
1212
|
+
elevatorLocalEyePosition.y <= runtime.carY + cabHeight + 1.25
|
|
1213
|
+
|
|
1214
|
+
if ((insideFootprint && insideCabHeight) || continuingRide) {
|
|
1215
|
+
nextRide = {
|
|
1216
|
+
cabHeight,
|
|
1217
|
+
cabCenterZ,
|
|
1218
|
+
carY: runtime.carY,
|
|
1219
|
+
doorOpen: runtime.doorOpen,
|
|
1220
|
+
elevatorId: typedElevatorId,
|
|
1221
|
+
halfDepth: Math.max(halfDepth, 0.24),
|
|
1222
|
+
halfWidth: Math.max(halfWidth, 0.24),
|
|
1223
|
+
object,
|
|
1224
|
+
phase: runtime.phase,
|
|
1225
|
+
}
|
|
1226
|
+
break
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (!nextRide) {
|
|
1231
|
+
ridingElevatorRef.current = null
|
|
1232
|
+
setElevatorRideLocked(false)
|
|
1233
|
+
return
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
const previousCarY =
|
|
1237
|
+
activeRide?.elevatorId === nextRide.elevatorId ? activeRide.previousCarY : nextRide.carY
|
|
1238
|
+
const deltaY = nextRide.carY - previousCarY
|
|
1239
|
+
nextRide.object.updateWorldMatrix(true, true)
|
|
1240
|
+
elevatorLocalControllerPosition.copy(group.position)
|
|
1241
|
+
nextRide.object.worldToLocal(elevatorLocalControllerPosition)
|
|
1242
|
+
const localControllerY =
|
|
1243
|
+
activeRide?.elevatorId === nextRide.elevatorId && activeRide.localControllerY !== null
|
|
1244
|
+
? activeRide.localControllerY
|
|
1245
|
+
: elevatorLocalControllerPosition.y - nextRide.carY
|
|
1246
|
+
|
|
1247
|
+
if (Math.abs(deltaY) > 0.0001) {
|
|
1248
|
+
group.position.y += deltaY
|
|
1249
|
+
controllerRef.current?.resetLinVel()
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
const shouldLockToCab =
|
|
1253
|
+
nextRide.phase === 'closing' ||
|
|
1254
|
+
nextRide.phase === 'moving' ||
|
|
1255
|
+
(nextRide.phase === 'opening' && nextRide.doorOpen < ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD)
|
|
1256
|
+
if (shouldLockToCab) {
|
|
1257
|
+
elevatorLocalControllerPosition.copy(group.position)
|
|
1258
|
+
nextRide.object.worldToLocal(elevatorLocalControllerPosition)
|
|
1259
|
+
const desiredLocalY = nextRide.carY + localControllerY
|
|
1260
|
+
if (Math.abs(elevatorLocalControllerPosition.y - desiredLocalY) > 0.002) {
|
|
1261
|
+
elevatorLocalControllerPosition.y = desiredLocalY
|
|
1262
|
+
elevatorWorldControllerPosition.copy(elevatorLocalControllerPosition)
|
|
1263
|
+
nextRide.object.localToWorld(elevatorWorldControllerPosition)
|
|
1264
|
+
group.position.y = elevatorWorldControllerPosition.y
|
|
1265
|
+
controllerRef.current?.resetLinVel()
|
|
1266
|
+
elevatorLocalControllerPosition.copy(group.position)
|
|
1267
|
+
nextRide.object.worldToLocal(elevatorLocalControllerPosition)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
const clampedX = Math.max(
|
|
1271
|
+
-nextRide.halfWidth,
|
|
1272
|
+
Math.min(nextRide.halfWidth, elevatorLocalControllerPosition.x),
|
|
1273
|
+
)
|
|
1274
|
+
const clampedZ = Math.max(
|
|
1275
|
+
nextRide.cabCenterZ - nextRide.halfDepth,
|
|
1276
|
+
Math.min(nextRide.cabCenterZ + nextRide.halfDepth, elevatorLocalControllerPosition.z),
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
if (
|
|
1280
|
+
Math.abs(clampedX - elevatorLocalControllerPosition.x) > 0.0001 ||
|
|
1281
|
+
Math.abs(clampedZ - elevatorLocalControllerPosition.z) > 0.0001
|
|
1282
|
+
) {
|
|
1283
|
+
elevatorLocalControllerPosition.x = clampedX
|
|
1284
|
+
elevatorLocalControllerPosition.z = clampedZ
|
|
1285
|
+
elevatorWorldControllerPosition.copy(elevatorLocalControllerPosition)
|
|
1286
|
+
nextRide.object.localToWorld(elevatorWorldControllerPosition)
|
|
1287
|
+
group.position.x = elevatorWorldControllerPosition.x
|
|
1288
|
+
group.position.z = elevatorWorldControllerPosition.z
|
|
1289
|
+
controllerRef.current?.resetLinVel()
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
setElevatorRideLocked(shouldLockToCab)
|
|
1294
|
+
|
|
1295
|
+
ridingElevatorRef.current = {
|
|
1296
|
+
elevatorId: nextRide.elevatorId,
|
|
1297
|
+
localControllerY,
|
|
1298
|
+
previousCarY: nextRide.carY,
|
|
1299
|
+
}
|
|
1300
|
+
},
|
|
1301
|
+
[camera, setElevatorRideLocked],
|
|
1302
|
+
)
|
|
1303
|
+
|
|
1304
|
+
useFrame(() => {
|
|
421
1305
|
if (!controllerRef.current?.group) return
|
|
422
1306
|
|
|
423
1307
|
const group = controllerRef.current.group
|
|
1308
|
+
|
|
1309
|
+
// The site ground collider is effectively unbounded, but scenes without a
|
|
1310
|
+
// site node only have finite fallback floors — if the controller still ends
|
|
1311
|
+
// up below every collider it can never land, so put it back at the spawn.
|
|
1312
|
+
// Prefer the live spawn node over the mount-time start position so a spawn
|
|
1313
|
+
// moved mid-walkthrough doesn't respawn the player at stale coordinates.
|
|
1314
|
+
const worldBounds = worldRef.current?.bounds
|
|
1315
|
+
if (worldBounds && group.position.y < worldBounds.min.y - VOID_FALL_RESPAWN_DEPTH) {
|
|
1316
|
+
const respawnPosition = placedSpawn
|
|
1317
|
+
? [
|
|
1318
|
+
placedSpawn.position[0],
|
|
1319
|
+
placedSpawn.position[1] - CONTROLLER_CENTER_FROM_EYE,
|
|
1320
|
+
placedSpawn.position[2],
|
|
1321
|
+
]
|
|
1322
|
+
: controllerStart?.position
|
|
1323
|
+
if (respawnPosition) {
|
|
1324
|
+
group.position.set(respawnPosition[0]!, respawnPosition[1]!, respawnPosition[2]!)
|
|
1325
|
+
controllerRef.current.resetLinVel()
|
|
1326
|
+
ridingElevatorRef.current = null
|
|
1327
|
+
setElevatorRideLocked(false)
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
424
1331
|
group.rotation.y = 0
|
|
425
1332
|
camera.position.copy(group.position).add(cameraOffset)
|
|
426
1333
|
cameraEuler.set(pitchRef.current, yawRef.current, 0, 'YXZ')
|
|
427
1334
|
camera.quaternion.setFromEuler(cameraEuler)
|
|
428
1335
|
camera.updateMatrixWorld(true)
|
|
1336
|
+
syncElevatorRide(group)
|
|
1337
|
+
camera.position.copy(group.position).add(cameraOffset)
|
|
1338
|
+
camera.updateMatrixWorld(true)
|
|
429
1339
|
|
|
430
1340
|
const nextInteractableTarget = resolveInteractableTarget()
|
|
431
1341
|
const previousInteractableTarget = interactableTargetRef.current
|
|
432
1342
|
if (
|
|
433
|
-
previousInteractableTarget
|
|
434
|
-
|
|
1343
|
+
getInteractableTargetKey(previousInteractableTarget) !==
|
|
1344
|
+
getInteractableTargetKey(nextInteractableTarget)
|
|
435
1345
|
) {
|
|
436
1346
|
interactableTargetRef.current = nextInteractableTarget
|
|
437
1347
|
useViewer.getState().setHoveredId(nextInteractableTarget?.id ?? null)
|
|
438
1348
|
}
|
|
439
|
-
})
|
|
1349
|
+
}, 2.5)
|
|
440
1350
|
|
|
441
1351
|
useEffect(() => {
|
|
442
1352
|
return () => {
|
|
@@ -446,6 +1356,11 @@ export const FirstPersonControls = () => {
|
|
|
446
1356
|
}
|
|
447
1357
|
}, [])
|
|
448
1358
|
|
|
1359
|
+
const firstPersonColliderMeshes = useMemo(
|
|
1360
|
+
() => (world ? [world.mesh, ...elevatorColliderMeshes] : elevatorColliderMeshes),
|
|
1361
|
+
[world, elevatorColliderMeshes],
|
|
1362
|
+
)
|
|
1363
|
+
|
|
449
1364
|
if (!world) {
|
|
450
1365
|
return null
|
|
451
1366
|
}
|
|
@@ -458,7 +1373,7 @@ export const FirstPersonControls = () => {
|
|
|
458
1373
|
acceleration={26}
|
|
459
1374
|
airDragFactor={0.3}
|
|
460
1375
|
colliderCapsuleArgs={[0.25, 0.8, 4, 8]}
|
|
461
|
-
colliderMeshes={
|
|
1376
|
+
colliderMeshes={firstPersonColliderMeshes}
|
|
462
1377
|
collisionCheckIteration={3}
|
|
463
1378
|
collisionPushBackDamping={0.1}
|
|
464
1379
|
collisionPushBackThreshold={0.001}
|
|
@@ -473,13 +1388,14 @@ export const FirstPersonControls = () => {
|
|
|
473
1388
|
floatSensorRadius={0.15}
|
|
474
1389
|
floatSpringK={1200}
|
|
475
1390
|
gravity={9.81}
|
|
476
|
-
jumpVel={
|
|
1391
|
+
jumpVel={5}
|
|
477
1392
|
key="first-person-controller"
|
|
478
|
-
maxRunSpeed={5
|
|
1393
|
+
maxRunSpeed={5}
|
|
479
1394
|
maxSlope={1.2}
|
|
480
|
-
maxWalkSpeed={
|
|
1395
|
+
maxWalkSpeed={2}
|
|
1396
|
+
paused={isElevatorRideLocked}
|
|
481
1397
|
position={controllerStart.position}
|
|
482
|
-
ref={
|
|
1398
|
+
ref={setControllerApi}
|
|
483
1399
|
/>
|
|
484
1400
|
</KeyboardControls>
|
|
485
1401
|
)}
|
|
@@ -519,7 +1435,7 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
519
1435
|
return (
|
|
520
1436
|
<>
|
|
521
1437
|
{isLocked && (
|
|
522
|
-
<div className="pointer-events-none
|
|
1438
|
+
<div className="pointer-events-none absolute inset-0 z-40 flex items-center justify-center">
|
|
523
1439
|
<div className="relative h-7 w-7">
|
|
524
1440
|
<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
1441
|
<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 +1443,7 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
527
1443
|
</div>
|
|
528
1444
|
)}
|
|
529
1445
|
|
|
530
|
-
<div className="
|
|
1446
|
+
<div className="absolute top-4 right-4 z-50">
|
|
531
1447
|
<button
|
|
532
1448
|
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
1449
|
onClick={handleExit}
|
|
@@ -541,7 +1457,7 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
541
1457
|
</div>
|
|
542
1458
|
|
|
543
1459
|
{!hasPlacedSpawn && (
|
|
544
|
-
<div className="
|
|
1460
|
+
<div className="absolute top-4 left-1/2 z-50 -translate-x-1/2">
|
|
545
1461
|
<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
1462
|
Place a Spawn Point from the Build tab to control where walkthrough starts.
|
|
547
1463
|
</div>
|
|
@@ -549,7 +1465,7 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
549
1465
|
)}
|
|
550
1466
|
|
|
551
1467
|
{isLocked && (
|
|
552
|
-
<div className="pointer-events-none
|
|
1468
|
+
<div className="pointer-events-none absolute top-1/2 right-6 z-40 -translate-y-1/2">
|
|
553
1469
|
<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
1470
|
<ControlHint keys={['W', 'A', 'S', 'D']} label="Move" />
|
|
555
1471
|
<div className="h-px w-full bg-border/30" />
|