@pascal-app/editor 0.9.1 → 1.0.0-beta.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 +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -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 +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- 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-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- 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 +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- 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 +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- 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/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- 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 +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- 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 +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- 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/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- 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 +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- 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 +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -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 +44 -3
- 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/tab-bar.tsx +42 -28
- 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/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- 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 +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- 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 +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- 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 +188 -0
- package/src/lib/measurements.ts +210 -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/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- 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 +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -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 +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -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 +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- 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 +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -15,8 +15,11 @@ import {
|
|
|
15
15
|
getElevatorShaftDepth,
|
|
16
16
|
getElevatorShaftWallThickness,
|
|
17
17
|
getElevatorShaftWidth,
|
|
18
|
+
getLevelDisplayName,
|
|
19
|
+
getLevelElevations,
|
|
18
20
|
getResolvedElevatorDoorStyle,
|
|
19
21
|
openElevatorDoor,
|
|
22
|
+
pointInPolygon2D,
|
|
20
23
|
requestElevatorLevel,
|
|
21
24
|
resolveElevatorBuildingLevels,
|
|
22
25
|
resolveElevatorDispatchTarget,
|
|
@@ -25,7 +28,22 @@ import {
|
|
|
25
28
|
useInteractive,
|
|
26
29
|
useScene,
|
|
27
30
|
} from '@pascal-app/core'
|
|
28
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
BVHEcctrl,
|
|
33
|
+
type BVHEcctrlApi,
|
|
34
|
+
CROUCH_CAPSULE,
|
|
35
|
+
CROUCH_EYE_OFFSET,
|
|
36
|
+
CROUCH_FLOAT_HEIGHT,
|
|
37
|
+
CROUCH_RUN_SPEED,
|
|
38
|
+
CROUCH_WALK_SPEED,
|
|
39
|
+
EYE_LERP_SPEED,
|
|
40
|
+
type MovementInput,
|
|
41
|
+
STAND_CAPSULE,
|
|
42
|
+
STAND_CLEARANCE,
|
|
43
|
+
STAND_FLOAT_HEIGHT,
|
|
44
|
+
useViewer,
|
|
45
|
+
WALKTHROUGH_FOV,
|
|
46
|
+
} from '@pascal-app/viewer'
|
|
29
47
|
import { KeyboardControls } from '@react-three/drei'
|
|
30
48
|
import { useFrame, useThree } from '@react-three/fiber'
|
|
31
49
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
@@ -38,6 +56,7 @@ import {
|
|
|
38
56
|
Mesh,
|
|
39
57
|
MeshBasicMaterial,
|
|
40
58
|
type Object3D,
|
|
59
|
+
type PerspectiveCamera,
|
|
41
60
|
Ray,
|
|
42
61
|
Raycaster,
|
|
43
62
|
Vector2,
|
|
@@ -48,15 +67,19 @@ import '../../three-types'
|
|
|
48
67
|
import {
|
|
49
68
|
closeDoorOpenState,
|
|
50
69
|
DOOR_SWING_OPEN_ANGLE,
|
|
70
|
+
getDisplayedDoorValue,
|
|
51
71
|
isOperationDoorType,
|
|
52
72
|
toggleDoorOpenState,
|
|
53
73
|
} from '../../lib/door-interaction'
|
|
54
74
|
import {
|
|
55
75
|
closeWindowOpenState,
|
|
76
|
+
getDisplayedWindowValue,
|
|
56
77
|
isOperableWindowType,
|
|
57
78
|
toggleWindowOpenState,
|
|
58
79
|
} from '../../lib/window-interaction'
|
|
59
80
|
import useEditor from '../../store/use-editor'
|
|
81
|
+
import { useFirstPersonHud, type WalkthroughInteract } from '../../store/use-first-person-hud'
|
|
82
|
+
import { WalkthroughHud } from '../walkthrough-hud'
|
|
60
83
|
import {
|
|
61
84
|
buildFirstPersonColliderWorldFromRegistry,
|
|
62
85
|
deriveFirstPersonSpawn,
|
|
@@ -64,8 +87,6 @@ import {
|
|
|
64
87
|
type FirstPersonColliderWorld,
|
|
65
88
|
type FirstPersonSpawn,
|
|
66
89
|
} from './first-person/build-collider-world'
|
|
67
|
-
import type { BVHEcctrlApi } from './first-person/bvh-ecctrl'
|
|
68
|
-
import BVHEcctrl from './first-person/bvh-ecctrl'
|
|
69
90
|
|
|
70
91
|
const CAMERA_EYE_OFFSET = 0.45
|
|
71
92
|
const LOOK_SENSITIVITY = 0.002
|
|
@@ -77,8 +98,12 @@ const ELEVATOR_COLLIDER_HORIZONTAL_PADDING = 0.14
|
|
|
77
98
|
const ELEVATOR_COLLIDER_FLOOR_THICKNESS = 0.08
|
|
78
99
|
const ELEVATOR_COLLIDER_DOOR_DEPTH = 0.12
|
|
79
100
|
const ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD = 0.72
|
|
80
|
-
const
|
|
81
|
-
const
|
|
101
|
+
const VOID_FALL_RESPAWN_DEPTH = 12
|
|
102
|
+
const HUD_LABEL_SAMPLE_FRAMES = 10
|
|
103
|
+
|
|
104
|
+
type MovementKeyName = Exclude<keyof MovementInput, 'joystick'>
|
|
105
|
+
|
|
106
|
+
const movementKeyboardBindings: Array<{ name: MovementKeyName; keys: string[] }> = [
|
|
82
107
|
{ name: 'forward', keys: ['ArrowUp', 'KeyW'] },
|
|
83
108
|
{ name: 'backward', keys: ['ArrowDown', 'KeyS'] },
|
|
84
109
|
{ name: 'leftward', keys: ['ArrowLeft', 'KeyA'] },
|
|
@@ -86,9 +111,41 @@ const keyboardMap = [
|
|
|
86
111
|
{ name: 'jump', keys: ['Space'] },
|
|
87
112
|
{ name: 'run', keys: ['ShiftLeft', 'ShiftRight'] },
|
|
88
113
|
]
|
|
114
|
+
const keyboardMap = movementKeyboardBindings
|
|
115
|
+
const movementKeyToName = new Map<string, MovementKeyName>(
|
|
116
|
+
movementKeyboardBindings.flatMap(({ name, keys }) => keys.map((key) => [key, name] as const)),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
const inactiveMovementInput: MovementInput = {
|
|
120
|
+
backward: false,
|
|
121
|
+
forward: false,
|
|
122
|
+
jump: false,
|
|
123
|
+
leftward: false,
|
|
124
|
+
rightward: false,
|
|
125
|
+
run: false,
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function getMovementInputForKey(code: string, active: boolean): MovementInput | null {
|
|
129
|
+
const name = movementKeyToName.get(code)
|
|
130
|
+
return name ? ({ [name]: active } as MovementInput) : null
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function focusFirstPersonCanvas(canvas: HTMLCanvasElement) {
|
|
134
|
+
const activeElement = document.activeElement
|
|
135
|
+
if (activeElement instanceof HTMLElement && !canvas.contains(activeElement)) {
|
|
136
|
+
activeElement.blur()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!canvas.hasAttribute('tabindex')) {
|
|
140
|
+
canvas.tabIndex = -1
|
|
141
|
+
}
|
|
142
|
+
canvas.focus({ preventScroll: true })
|
|
143
|
+
}
|
|
89
144
|
|
|
90
|
-
const cameraOffset = new Vector3(
|
|
145
|
+
const cameraOffset = new Vector3()
|
|
91
146
|
const cameraEuler = new Euler(0, 0, 0, 'YXZ')
|
|
147
|
+
const standClearanceRaycaster = new Raycaster()
|
|
148
|
+
const standClearanceUp = new Vector3(0, 1, 0)
|
|
92
149
|
const centerScreenPoint = new Vector2(0, 0)
|
|
93
150
|
const doorInteractionRaycaster = new Raycaster()
|
|
94
151
|
const doorLeafBox = new Box3()
|
|
@@ -113,6 +170,9 @@ const elevatorColliderMaterial = new MeshBasicMaterial({ visible: false })
|
|
|
113
170
|
const spawnWorldPosition = new Vector3()
|
|
114
171
|
const spawnWorldEuler = new Euler(0, 0, 0, 'YXZ')
|
|
115
172
|
const windowInteractionRaycaster = new Raycaster()
|
|
173
|
+
const hudBuildingLocalEyePosition = new Vector3()
|
|
174
|
+
const hudWorldEyePosition = new Vector3()
|
|
175
|
+
const hudLevelBounds = new Box3()
|
|
116
176
|
|
|
117
177
|
type ElevatorColliderKind =
|
|
118
178
|
| 'cab-back'
|
|
@@ -169,6 +229,128 @@ type ElevatorButtonTarget = {
|
|
|
169
229
|
levelId?: AnyNodeId
|
|
170
230
|
}
|
|
171
231
|
|
|
232
|
+
function getLevelChildren(
|
|
233
|
+
level: Extract<AnyNode, { type: 'level' }>,
|
|
234
|
+
nodes: Record<string, AnyNode>,
|
|
235
|
+
) {
|
|
236
|
+
const childIds = new Set<string>(level.children)
|
|
237
|
+
return Object.values(nodes).filter((node) => node.parentId === level.id || childIds.has(node.id))
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function pointIsInLevelFootprint(
|
|
241
|
+
point: [number, number],
|
|
242
|
+
worldPoint: Vector3,
|
|
243
|
+
level: Extract<AnyNode, { type: 'level' }>,
|
|
244
|
+
nodes: Record<string, AnyNode>,
|
|
245
|
+
) {
|
|
246
|
+
const children = getLevelChildren(level, nodes)
|
|
247
|
+
const slabs = children.filter(
|
|
248
|
+
(node): node is Extract<AnyNode, { type: 'slab' }> =>
|
|
249
|
+
node.type === 'slab' && node.polygon.length >= 3,
|
|
250
|
+
)
|
|
251
|
+
const zones = children.filter(
|
|
252
|
+
(node): node is Extract<AnyNode, { type: 'zone' }> =>
|
|
253
|
+
node.type === 'zone' && node.polygon.length >= 3,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if (slabs.length > 0) {
|
|
257
|
+
return slabs.some(
|
|
258
|
+
(slab) =>
|
|
259
|
+
pointInPolygon2D(point, slab.polygon) &&
|
|
260
|
+
!slab.holes.some((hole) => pointInPolygon2D(point, hole)),
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (zones.length > 0) {
|
|
265
|
+
if (zones.some((zone) => pointInPolygon2D(point, zone.polygon))) return true
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const levelObject = sceneRegistry.nodes.get(level.id)
|
|
269
|
+
if (!levelObject) return false
|
|
270
|
+
hudLevelBounds.setFromObject(levelObject)
|
|
271
|
+
return (
|
|
272
|
+
!hudLevelBounds.isEmpty() &&
|
|
273
|
+
worldPoint.x >= hudLevelBounds.min.x &&
|
|
274
|
+
worldPoint.x <= hudLevelBounds.max.x &&
|
|
275
|
+
worldPoint.z >= hudLevelBounds.min.z &&
|
|
276
|
+
worldPoint.z <= hudLevelBounds.max.z
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function resolveFirstPersonHudLabels(worldPoint: Vector3) {
|
|
281
|
+
const nodes = useScene.getState().nodes
|
|
282
|
+
const levelElevations = getLevelElevations(nodes as Record<AnyNodeId, AnyNode>)
|
|
283
|
+
|
|
284
|
+
for (const building of Object.values(nodes)) {
|
|
285
|
+
if (building.type !== 'building') continue
|
|
286
|
+
const buildingObject = sceneRegistry.nodes.get(building.id)
|
|
287
|
+
if (!buildingObject) continue
|
|
288
|
+
|
|
289
|
+
buildingObject.updateWorldMatrix(true, true)
|
|
290
|
+
hudBuildingLocalEyePosition.copy(worldPoint)
|
|
291
|
+
buildingObject.worldToLocal(hudBuildingLocalEyePosition)
|
|
292
|
+
|
|
293
|
+
const levels = Object.values(nodes)
|
|
294
|
+
.filter((node) => node.type === 'level')
|
|
295
|
+
.filter((level) => levelElevations.get(level.id)?.buildingId === building.id)
|
|
296
|
+
.sort(
|
|
297
|
+
(left, right) =>
|
|
298
|
+
(levelElevations.get(left.id)?.baseY ?? 0) - (levelElevations.get(right.id)?.baseY ?? 0),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
let activeLevel: (typeof levels)[number] | null = null
|
|
302
|
+
for (const level of levels) {
|
|
303
|
+
const elevation = levelElevations.get(level.id)
|
|
304
|
+
if (!elevation) continue
|
|
305
|
+
if (
|
|
306
|
+
hudBuildingLocalEyePosition.y >= elevation.baseY - 0.5 &&
|
|
307
|
+
hudBuildingLocalEyePosition.y < elevation.baseY + elevation.height + 0.5
|
|
308
|
+
) {
|
|
309
|
+
activeLevel = level
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (!activeLevel) continue
|
|
313
|
+
|
|
314
|
+
const point: [number, number] = [hudBuildingLocalEyePosition.x, hudBuildingLocalEyePosition.z]
|
|
315
|
+
if (!pointIsInLevelFootprint(point, worldPoint, activeLevel, nodes)) continue
|
|
316
|
+
|
|
317
|
+
const zone = getLevelChildren(activeLevel, nodes).find(
|
|
318
|
+
(node) =>
|
|
319
|
+
node.type === 'zone' && node.polygon.length >= 3 && pointInPolygon2D(point, node.polygon),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
floorLabel: getLevelDisplayName(activeLevel),
|
|
324
|
+
zoneLabel: zone?.type === 'zone' ? zone.name : null,
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { floorLabel: null, zoneLabel: null }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function resolveHudInteract(target: FirstPersonInteractableTarget | null): WalkthroughInteract {
|
|
332
|
+
if (!target) return null
|
|
333
|
+
if (target.type === 'elevator') {
|
|
334
|
+
return {
|
|
335
|
+
label: target.action === 'open-door' ? 'door button' : 'elevator button',
|
|
336
|
+
verb: 'press',
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const node = useScene.getState().nodes[target.id]
|
|
341
|
+
if (target.type === 'window') {
|
|
342
|
+
if (node?.type !== 'window') return null
|
|
343
|
+
const isOpen = getDisplayedWindowValue(target.id, node.operationState) > 0
|
|
344
|
+
return { label: node.name || 'window', verb: isOpen ? 'close' : 'open' }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (node?.type !== 'door') return null
|
|
348
|
+
const isOpen = isOperationDoorType(node.doorType)
|
|
349
|
+
? getDisplayedDoorValue(target.id, 'operationState', node.operationState) > 0
|
|
350
|
+
: getDisplayedDoorValue(target.id, 'swingAngle', node.swingAngle) > 0
|
|
351
|
+
return { label: node.name || 'door', verb: isOpen ? 'close' : 'open' }
|
|
352
|
+
}
|
|
353
|
+
|
|
172
354
|
function resolveElevatorButtonTarget(object: Object3D): ElevatorButtonTarget | null {
|
|
173
355
|
let current: Object3D | null = object
|
|
174
356
|
|
|
@@ -248,37 +430,17 @@ function isInsideElevatorCab(
|
|
|
248
430
|
)
|
|
249
431
|
}
|
|
250
432
|
|
|
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
433
|
function resolveElevatorColliderLevels(elevator: ElevatorNode, nodes: Record<string, AnyNode>) {
|
|
275
434
|
const allLevels = resolveElevatorBuildingLevels(elevator, nodes)
|
|
435
|
+
const levelElevations = getLevelElevations(nodes as Record<AnyNodeId, AnyNode>)
|
|
276
436
|
|
|
277
437
|
const baseYByLevelId = new Map<string, number>()
|
|
278
438
|
let cumulativeY = 0
|
|
279
439
|
for (const level of allLevels) {
|
|
280
|
-
|
|
281
|
-
|
|
440
|
+
const elevation = levelElevations.get(level.id)
|
|
441
|
+
const baseY = elevation?.baseY ?? 0
|
|
442
|
+
baseYByLevelId.set(level.id, baseY)
|
|
443
|
+
cumulativeY = Math.max(cumulativeY, baseY + (elevation?.height ?? 0))
|
|
282
444
|
}
|
|
283
445
|
|
|
284
446
|
const serviceLevels = resolveElevatorServiceLevels(elevator, nodes)
|
|
@@ -535,9 +697,16 @@ export const FirstPersonControls = () => {
|
|
|
535
697
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
536
698
|
const placedSpawnNode = useScene((state) => resolvePlacedSpawnNode(state.nodes, selectedLevelId))
|
|
537
699
|
const controllerRef = useRef<BVHEcctrlApi | null>(null)
|
|
700
|
+
const movementInputRef = useRef<MovementInput>({ ...inactiveMovementInput })
|
|
701
|
+
const hadPointerLockRef = useRef(false)
|
|
538
702
|
const yawRef = useRef(0)
|
|
539
703
|
const pitchRef = useRef(0)
|
|
540
704
|
const interactableTargetRef = useRef<FirstPersonInteractableTarget | null>(null)
|
|
705
|
+
const hudLabelFrameRef = useRef(HUD_LABEL_SAMPLE_FRAMES - 1)
|
|
706
|
+
const crouchKeyRef = useRef(false)
|
|
707
|
+
const suspendRef = useRef(false)
|
|
708
|
+
const eyeOffsetRef = useRef(CAMERA_EYE_OFFSET)
|
|
709
|
+
const [crouched, setCrouched] = useState(false)
|
|
541
710
|
const [isElevatorRideLocked, setIsElevatorRideLocked] = useState(false)
|
|
542
711
|
const ridingElevatorRef = useRef<{
|
|
543
712
|
elevatorId: AnyNodeId
|
|
@@ -554,6 +723,35 @@ export const FirstPersonControls = () => {
|
|
|
554
723
|
yaw: number
|
|
555
724
|
} | null>(null)
|
|
556
725
|
|
|
726
|
+
useEffect(() => {
|
|
727
|
+
const previousCameraMode = useViewer.getState().cameraMode
|
|
728
|
+
if (previousCameraMode === 'orthographic') {
|
|
729
|
+
useViewer.getState().setCameraMode('perspective')
|
|
730
|
+
}
|
|
731
|
+
return () => {
|
|
732
|
+
if (previousCameraMode === 'orthographic') {
|
|
733
|
+
useViewer.getState().setCameraMode('orthographic')
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}, [])
|
|
737
|
+
|
|
738
|
+
useEffect(() => {
|
|
739
|
+
const perspectiveCamera = camera as PerspectiveCamera
|
|
740
|
+
if (!perspectiveCamera.isPerspectiveCamera) return
|
|
741
|
+
const previousFov = perspectiveCamera.fov
|
|
742
|
+
perspectiveCamera.fov = WALKTHROUGH_FOV
|
|
743
|
+
perspectiveCamera.updateProjectionMatrix()
|
|
744
|
+
return () => {
|
|
745
|
+
perspectiveCamera.fov = previousFov
|
|
746
|
+
perspectiveCamera.updateProjectionMatrix()
|
|
747
|
+
}
|
|
748
|
+
}, [camera])
|
|
749
|
+
|
|
750
|
+
useEffect(() => {
|
|
751
|
+
useFirstPersonHud.getState().reset()
|
|
752
|
+
return () => useFirstPersonHud.getState().reset()
|
|
753
|
+
}, [])
|
|
754
|
+
|
|
557
755
|
const replaceColliderWorld = useCallback((nextWorld: FirstPersonColliderWorld | null) => {
|
|
558
756
|
worldRef.current?.dispose()
|
|
559
757
|
worldRef.current = nextWorld
|
|
@@ -577,6 +775,13 @@ export const FirstPersonControls = () => {
|
|
|
577
775
|
setIsElevatorRideLocked(locked)
|
|
578
776
|
}, [])
|
|
579
777
|
|
|
778
|
+
const setControllerApi = useCallback((api: BVHEcctrlApi | null) => {
|
|
779
|
+
controllerRef.current = api
|
|
780
|
+
if (api) {
|
|
781
|
+
api.setMovement(movementInputRef.current)
|
|
782
|
+
}
|
|
783
|
+
}, [])
|
|
784
|
+
|
|
580
785
|
const resolveInteractableDoorId = useCallback((): AnyNodeId | null => {
|
|
581
786
|
const nodes = useScene.getState().nodes
|
|
582
787
|
camera.updateMatrixWorld(true)
|
|
@@ -915,6 +1120,28 @@ export const FirstPersonControls = () => {
|
|
|
915
1120
|
})
|
|
916
1121
|
}, [camera, controllerStart, placedSpawn, world])
|
|
917
1122
|
|
|
1123
|
+
useEffect(() => {
|
|
1124
|
+
const canvas = gl.domElement
|
|
1125
|
+
focusFirstPersonCanvas(canvas)
|
|
1126
|
+
|
|
1127
|
+
const frame = window.requestAnimationFrame(() => focusFirstPersonCanvas(canvas))
|
|
1128
|
+
return () => window.cancelAnimationFrame(frame)
|
|
1129
|
+
}, [gl])
|
|
1130
|
+
|
|
1131
|
+
// The pointer-lock effect below must be mount-stable. Its cleanup exits
|
|
1132
|
+
// pointer lock, and `toggleInteractableTarget` is recreated whenever the
|
|
1133
|
+
// camera object changes — which happens right after entry when the
|
|
1134
|
+
// persisted orthographic mode swaps to perspective. If the async lock
|
|
1135
|
+
// grant lands before that re-run, the cleanup's exitPointerLock fires an
|
|
1136
|
+
// unlock the handler reads as "user left walkthrough", instantly
|
|
1137
|
+
// cancelling a fresh entry (and arming the browser's ~1.25s re-lock
|
|
1138
|
+
// cooldown, so the next presses fail too). Route the callback through a
|
|
1139
|
+
// ref so the effect deps stay `[gl]`.
|
|
1140
|
+
const toggleInteractableTargetRef = useRef(toggleInteractableTarget)
|
|
1141
|
+
useEffect(() => {
|
|
1142
|
+
toggleInteractableTargetRef.current = toggleInteractableTarget
|
|
1143
|
+
}, [toggleInteractableTarget])
|
|
1144
|
+
|
|
918
1145
|
useEffect(() => {
|
|
919
1146
|
const canvas = gl.domElement
|
|
920
1147
|
const handleMouseMove = (e: MouseEvent) => {
|
|
@@ -942,32 +1169,75 @@ export const FirstPersonControls = () => {
|
|
|
942
1169
|
|
|
943
1170
|
event.preventDefault()
|
|
944
1171
|
event.stopPropagation()
|
|
945
|
-
|
|
1172
|
+
toggleInteractableTargetRef.current()
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
const handlePointerLockChange = () => {
|
|
1176
|
+
const isLocked = document.pointerLockElement === canvas
|
|
1177
|
+
if (isLocked) {
|
|
1178
|
+
hadPointerLockRef.current = true
|
|
1179
|
+
suspendRef.current = false
|
|
1180
|
+
useViewer.getState().setWalkthroughSuspended(false)
|
|
1181
|
+
return
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// Deliberately released (screenshot pause) — stay in first person;
|
|
1185
|
+
// clicking the canvas re-locks.
|
|
1186
|
+
if (suspendRef.current) return
|
|
1187
|
+
|
|
1188
|
+
if (hadPointerLockRef.current && useEditor.getState().isFirstPersonMode) {
|
|
1189
|
+
useEditor.getState().setFirstPersonMode(false)
|
|
1190
|
+
}
|
|
946
1191
|
}
|
|
947
1192
|
|
|
1193
|
+
handlePointerLockChange()
|
|
948
1194
|
document.addEventListener('mousemove', handleMouseMove)
|
|
949
1195
|
document.addEventListener('click', handleClick)
|
|
950
1196
|
document.addEventListener('mousedown', handleMouseDown, true)
|
|
1197
|
+
document.addEventListener('pointerlockchange', handlePointerLockChange)
|
|
951
1198
|
|
|
952
1199
|
return () => {
|
|
953
1200
|
document.removeEventListener('mousemove', handleMouseMove)
|
|
954
1201
|
document.removeEventListener('click', handleClick)
|
|
955
1202
|
document.removeEventListener('mousedown', handleMouseDown, true)
|
|
1203
|
+
document.removeEventListener('pointerlockchange', handlePointerLockChange)
|
|
1204
|
+
useViewer.getState().setWalkthroughSuspended(false)
|
|
956
1205
|
if (document.pointerLockElement === canvas) {
|
|
957
1206
|
document.exitPointerLock()
|
|
958
1207
|
}
|
|
959
1208
|
}
|
|
960
|
-
}, [gl
|
|
1209
|
+
}, [gl])
|
|
961
1210
|
|
|
962
1211
|
useEffect(() => {
|
|
963
1212
|
const canvas = gl.domElement
|
|
964
1213
|
|
|
1214
|
+
const applyMovementKey = (event: KeyboardEvent, active: boolean) => {
|
|
1215
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
1216
|
+
return false
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
const movement = getMovementInputForKey(event.code, active)
|
|
1220
|
+
if (!movement) return false
|
|
1221
|
+
|
|
1222
|
+
event.preventDefault()
|
|
1223
|
+
Object.assign(movementInputRef.current, movement)
|
|
1224
|
+
controllerRef.current?.setMovement(movement)
|
|
1225
|
+
return true
|
|
1226
|
+
}
|
|
1227
|
+
|
|
965
1228
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
1229
|
+
const handledMovement = applyMovementKey(event, true)
|
|
1230
|
+
if (handledMovement) return
|
|
1231
|
+
|
|
966
1232
|
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
967
1233
|
return
|
|
968
1234
|
}
|
|
969
1235
|
|
|
970
|
-
if (event.code === '
|
|
1236
|
+
if (event.code === 'ControlLeft' || event.code === 'ControlRight') {
|
|
1237
|
+
// While paused (P), crouch is frozen as-is — ⌃⇧⌘4 (clipboard
|
|
1238
|
+
// screenshot) must not toggle it under the user.
|
|
1239
|
+
if (!suspendRef.current) crouchKeyRef.current = true
|
|
1240
|
+
} else if (event.code === 'Escape') {
|
|
971
1241
|
event.preventDefault()
|
|
972
1242
|
event.stopPropagation()
|
|
973
1243
|
if (document.pointerLockElement === canvas) {
|
|
@@ -982,12 +1252,41 @@ export const FirstPersonControls = () => {
|
|
|
982
1252
|
event.preventDefault()
|
|
983
1253
|
event.stopPropagation()
|
|
984
1254
|
closeInteractableTarget()
|
|
1255
|
+
} else if (event.code === 'KeyP') {
|
|
1256
|
+
// P toggles a cursor pause (advertised in the HUD): frees the pointer
|
|
1257
|
+
// without leaving first person — e.g. for an OS screenshot, which
|
|
1258
|
+
// needs a movable cursor — and click or P resumes.
|
|
1259
|
+
event.preventDefault()
|
|
1260
|
+
event.stopPropagation()
|
|
1261
|
+
if (document.pointerLockElement === canvas) {
|
|
1262
|
+
suspendRef.current = true
|
|
1263
|
+
useViewer.getState().setWalkthroughSuspended(true)
|
|
1264
|
+
document.exitPointerLock()
|
|
1265
|
+
} else if (suspendRef.current) {
|
|
1266
|
+
const result = canvas.requestPointerLock?.() as Promise<void> | undefined
|
|
1267
|
+
if (result && typeof result.catch === 'function') result.catch(() => {})
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
1273
|
+
if ((event.code === 'ControlLeft' || event.code === 'ControlRight') && !suspendRef.current) {
|
|
1274
|
+
crouchKeyRef.current = false
|
|
985
1275
|
}
|
|
1276
|
+
applyMovementKey(event, false)
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
const handleBlur = () => {
|
|
1280
|
+
if (!suspendRef.current) crouchKeyRef.current = false
|
|
986
1281
|
}
|
|
987
1282
|
|
|
988
1283
|
document.addEventListener('keydown', handleKeyDown, true)
|
|
1284
|
+
document.addEventListener('keyup', handleKeyUp, true)
|
|
1285
|
+
window.addEventListener('blur', handleBlur)
|
|
989
1286
|
return () => {
|
|
990
1287
|
document.removeEventListener('keydown', handleKeyDown, true)
|
|
1288
|
+
document.removeEventListener('keyup', handleKeyUp, true)
|
|
1289
|
+
window.removeEventListener('blur', handleBlur)
|
|
991
1290
|
}
|
|
992
1291
|
}, [closeInteractableTarget, gl, toggleInteractableTarget])
|
|
993
1292
|
|
|
@@ -1213,10 +1512,55 @@ export const FirstPersonControls = () => {
|
|
|
1213
1512
|
[camera, setElevatorRideLocked],
|
|
1214
1513
|
)
|
|
1215
1514
|
|
|
1216
|
-
|
|
1515
|
+
const hasStandingClearance = useCallback((position: Vector3) => {
|
|
1516
|
+
standClearanceRaycaster.set(position, standClearanceUp)
|
|
1517
|
+
standClearanceRaycaster.far = STAND_CLEARANCE
|
|
1518
|
+
const meshes: Mesh[] = []
|
|
1519
|
+
if (worldRef.current) meshes.push(worldRef.current.mesh)
|
|
1520
|
+
for (const mesh of elevatorColliderMeshesRef.current) {
|
|
1521
|
+
if (mesh.visible) meshes.push(mesh)
|
|
1522
|
+
}
|
|
1523
|
+
return standClearanceRaycaster.intersectObjects(meshes, false).length === 0
|
|
1524
|
+
}, [])
|
|
1525
|
+
|
|
1526
|
+
useFrame((_, delta) => {
|
|
1217
1527
|
if (!controllerRef.current?.group) return
|
|
1218
1528
|
|
|
1219
1529
|
const group = controllerRef.current.group
|
|
1530
|
+
|
|
1531
|
+
// Crouch follows the held key; standing back up waits for headroom.
|
|
1532
|
+
// Frozen while the cursor pause is active.
|
|
1533
|
+
if (!suspendRef.current && crouchKeyRef.current !== crouched) {
|
|
1534
|
+
if (crouchKeyRef.current) setCrouched(true)
|
|
1535
|
+
else if (hasStandingClearance(group.position)) setCrouched(false)
|
|
1536
|
+
}
|
|
1537
|
+
const targetEyeOffset = crouched ? CROUCH_EYE_OFFSET : CAMERA_EYE_OFFSET
|
|
1538
|
+
eyeOffsetRef.current +=
|
|
1539
|
+
(targetEyeOffset - eyeOffsetRef.current) * Math.min(1, delta * EYE_LERP_SPEED)
|
|
1540
|
+
cameraOffset.set(0, eyeOffsetRef.current, 0)
|
|
1541
|
+
|
|
1542
|
+
// The site ground collider is effectively unbounded, but scenes without a
|
|
1543
|
+
// site node only have finite fallback floors — if the controller still ends
|
|
1544
|
+
// up below every collider it can never land, so put it back at the spawn.
|
|
1545
|
+
// Prefer the live spawn node over the mount-time start position so a spawn
|
|
1546
|
+
// moved mid-walkthrough doesn't respawn the player at stale coordinates.
|
|
1547
|
+
const worldBounds = worldRef.current?.bounds
|
|
1548
|
+
if (worldBounds && group.position.y < worldBounds.min.y - VOID_FALL_RESPAWN_DEPTH) {
|
|
1549
|
+
const respawnPosition = placedSpawn
|
|
1550
|
+
? [
|
|
1551
|
+
placedSpawn.position[0],
|
|
1552
|
+
placedSpawn.position[1] - CONTROLLER_CENTER_FROM_EYE,
|
|
1553
|
+
placedSpawn.position[2],
|
|
1554
|
+
]
|
|
1555
|
+
: controllerStart?.position
|
|
1556
|
+
if (respawnPosition) {
|
|
1557
|
+
group.position.set(respawnPosition[0]!, respawnPosition[1]!, respawnPosition[2]!)
|
|
1558
|
+
controllerRef.current.resetLinVel()
|
|
1559
|
+
ridingElevatorRef.current = null
|
|
1560
|
+
setElevatorRideLocked(false)
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1220
1564
|
group.rotation.y = 0
|
|
1221
1565
|
camera.position.copy(group.position).add(cameraOffset)
|
|
1222
1566
|
cameraEuler.set(pitchRef.current, yawRef.current, 0, 'YXZ')
|
|
@@ -1235,6 +1579,17 @@ export const FirstPersonControls = () => {
|
|
|
1235
1579
|
interactableTargetRef.current = nextInteractableTarget
|
|
1236
1580
|
useViewer.getState().setHoveredId(nextInteractableTarget?.id ?? null)
|
|
1237
1581
|
}
|
|
1582
|
+
|
|
1583
|
+
useFirstPersonHud.getState().setHud({
|
|
1584
|
+
interact: resolveHudInteract(nextInteractableTarget),
|
|
1585
|
+
})
|
|
1586
|
+
|
|
1587
|
+
hudLabelFrameRef.current += 1
|
|
1588
|
+
if (hudLabelFrameRef.current >= HUD_LABEL_SAMPLE_FRAMES) {
|
|
1589
|
+
hudLabelFrameRef.current = 0
|
|
1590
|
+
camera.getWorldPosition(hudWorldEyePosition)
|
|
1591
|
+
useFirstPersonHud.getState().setHud(resolveFirstPersonHudLabels(hudWorldEyePosition))
|
|
1592
|
+
}
|
|
1238
1593
|
}, 2.5)
|
|
1239
1594
|
|
|
1240
1595
|
useEffect(() => {
|
|
@@ -1261,7 +1616,7 @@ export const FirstPersonControls = () => {
|
|
|
1261
1616
|
<BVHEcctrl
|
|
1262
1617
|
acceleration={26}
|
|
1263
1618
|
airDragFactor={0.3}
|
|
1264
|
-
colliderCapsuleArgs={
|
|
1619
|
+
colliderCapsuleArgs={crouched ? CROUCH_CAPSULE : STAND_CAPSULE}
|
|
1265
1620
|
colliderMeshes={firstPersonColliderMeshes}
|
|
1266
1621
|
collisionCheckIteration={3}
|
|
1267
1622
|
collisionPushBackDamping={0.1}
|
|
@@ -1272,19 +1627,19 @@ export const FirstPersonControls = () => {
|
|
|
1272
1627
|
fallGravityFactor={4}
|
|
1273
1628
|
floatCheckType="BOTH"
|
|
1274
1629
|
floatDampingC={36}
|
|
1275
|
-
floatHeight={
|
|
1630
|
+
floatHeight={crouched ? CROUCH_FLOAT_HEIGHT : STAND_FLOAT_HEIGHT}
|
|
1276
1631
|
floatPullBackHeight={0.35}
|
|
1277
1632
|
floatSensorRadius={0.15}
|
|
1278
1633
|
floatSpringK={1200}
|
|
1279
1634
|
gravity={9.81}
|
|
1280
|
-
jumpVel={
|
|
1635
|
+
jumpVel={5}
|
|
1281
1636
|
key="first-person-controller"
|
|
1282
|
-
maxRunSpeed={5
|
|
1637
|
+
maxRunSpeed={crouched ? CROUCH_RUN_SPEED : 5}
|
|
1283
1638
|
maxSlope={1.2}
|
|
1284
|
-
maxWalkSpeed={
|
|
1639
|
+
maxWalkSpeed={crouched ? CROUCH_WALK_SPEED : 2}
|
|
1285
1640
|
paused={isElevatorRideLocked}
|
|
1286
1641
|
position={controllerStart.position}
|
|
1287
|
-
ref={
|
|
1642
|
+
ref={setControllerApi}
|
|
1288
1643
|
/>
|
|
1289
1644
|
</KeyboardControls>
|
|
1290
1645
|
)}
|
|
@@ -1292,27 +1647,14 @@ export const FirstPersonControls = () => {
|
|
|
1292
1647
|
)
|
|
1293
1648
|
}
|
|
1294
1649
|
|
|
1295
|
-
/**
|
|
1296
|
-
* Overlay UI for first-person mode: crosshair, controls hint, exit button.
|
|
1297
|
-
* Rendered as a regular DOM overlay (not inside the Canvas).
|
|
1298
|
-
*/
|
|
1299
1650
|
export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
1300
|
-
const [isLocked, setIsLocked] = useState(false)
|
|
1301
1651
|
const hasPlacedSpawn = useScene((state) =>
|
|
1302
1652
|
Object.values(state.nodes).some((node) => node.type === 'spawn'),
|
|
1303
1653
|
)
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
handlePointerLockChange()
|
|
1311
|
-
document.addEventListener('pointerlockchange', handlePointerLockChange)
|
|
1312
|
-
return () => {
|
|
1313
|
-
document.removeEventListener('pointerlockchange', handlePointerLockChange)
|
|
1314
|
-
}
|
|
1315
|
-
}, [])
|
|
1654
|
+
const floorLabel = useFirstPersonHud((state) => state.floorLabel)
|
|
1655
|
+
const zoneLabel = useFirstPersonHud((state) => state.zoneLabel)
|
|
1656
|
+
const interact = useFirstPersonHud((state) => state.interact)
|
|
1657
|
+
const suspended = useViewer((state) => state.walkthroughSuspended)
|
|
1316
1658
|
|
|
1317
1659
|
const handleExit = useCallback(() => {
|
|
1318
1660
|
if (document.pointerLockElement) {
|
|
@@ -1322,86 +1664,18 @@ export const FirstPersonOverlay = ({ onExit }: { onExit: () => void }) => {
|
|
|
1322
1664
|
}, [onExit])
|
|
1323
1665
|
|
|
1324
1666
|
return (
|
|
1325
|
-
|
|
1326
|
-
{
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
</div>
|
|
1333
|
-
)}
|
|
1334
|
-
|
|
1335
|
-
<div className="absolute top-4 right-4 z-50">
|
|
1336
|
-
<button
|
|
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"
|
|
1338
|
-
onClick={handleExit}
|
|
1339
|
-
type="button"
|
|
1340
|
-
>
|
|
1341
|
-
<kbd className="rounded border border-border/50 bg-accent/50 px-1.5 py-0.5 font-mono text-[10px] text-muted-foreground">
|
|
1342
|
-
ESC
|
|
1343
|
-
</kbd>
|
|
1344
|
-
Exit Street View
|
|
1345
|
-
</button>
|
|
1346
|
-
</div>
|
|
1347
|
-
|
|
1667
|
+
<WalkthroughHud
|
|
1668
|
+
floorLabel={floorLabel}
|
|
1669
|
+
interact={interact}
|
|
1670
|
+
onExit={handleExit}
|
|
1671
|
+
suspended={suspended}
|
|
1672
|
+
zoneLabel={zoneLabel}
|
|
1673
|
+
>
|
|
1348
1674
|
{!hasPlacedSpawn && (
|
|
1349
|
-
<div className="
|
|
1350
|
-
|
|
1351
|
-
Place a Spawn Point from the Build tab to control where walkthrough starts.
|
|
1352
|
-
</div>
|
|
1353
|
-
</div>
|
|
1354
|
-
)}
|
|
1355
|
-
|
|
1356
|
-
{isLocked && (
|
|
1357
|
-
<div className="pointer-events-none absolute top-1/2 right-6 z-40 -translate-y-1/2">
|
|
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">
|
|
1359
|
-
<ControlHint keys={['W', 'A', 'S', 'D']} label="Move" />
|
|
1360
|
-
<div className="h-px w-full bg-border/30" />
|
|
1361
|
-
<InlineControlHint keyLabel="Space" label="Jump" />
|
|
1362
|
-
<InlineControlHint keyLabel="Shift" label="Sprint" />
|
|
1363
|
-
<InlineControlHint keyLabel="E / R" label="Interact" />
|
|
1364
|
-
<InlineControlHint keyLabel="T" label="Close" />
|
|
1365
|
-
<div className="h-px w-full bg-border/30" />
|
|
1366
|
-
<span className="text-center text-muted-foreground/60 text-xs">
|
|
1367
|
-
Click to look around
|
|
1368
|
-
</span>
|
|
1369
|
-
</div>
|
|
1675
|
+
<div className="corner-smooth rounded-full border border-border/40 bg-background/80 px-3 py-1 text-center text-muted-foreground text-xs shadow-elevation-3 backdrop-blur-xl">
|
|
1676
|
+
Place a spawn point from the Build tab to control where walkthrough starts.
|
|
1370
1677
|
</div>
|
|
1371
1678
|
)}
|
|
1372
|
-
|
|
1373
|
-
)
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
function ControlHint({ label, keys }: { label: string; keys: string[] }) {
|
|
1377
|
-
return (
|
|
1378
|
-
<div className="flex flex-col items-center gap-1.5 text-center">
|
|
1379
|
-
<span className="font-medium text-[10px] text-muted-foreground/60 tracking-[0.03em]">
|
|
1380
|
-
{label}
|
|
1381
|
-
</span>
|
|
1382
|
-
<div className="flex flex-wrap items-center justify-center gap-1">
|
|
1383
|
-
{keys.map((key) => (
|
|
1384
|
-
<kbd
|
|
1385
|
-
className="flex h-5 min-w-5 items-center justify-center rounded border border-border/50 bg-accent/40 px-1 font-mono text-[10px] text-foreground/80 leading-none"
|
|
1386
|
-
key={key}
|
|
1387
|
-
>
|
|
1388
|
-
{key}
|
|
1389
|
-
</kbd>
|
|
1390
|
-
))}
|
|
1391
|
-
</div>
|
|
1392
|
-
</div>
|
|
1393
|
-
)
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
function InlineControlHint({ label, keyLabel }: { label: string; keyLabel: string }) {
|
|
1397
|
-
return (
|
|
1398
|
-
<div className="flex items-center justify-between gap-3">
|
|
1399
|
-
<span className="font-medium text-[10px] text-muted-foreground/60 uppercase tracking-[0.03em]">
|
|
1400
|
-
{label}
|
|
1401
|
-
</span>
|
|
1402
|
-
<kbd className="flex h-5 min-w-5 items-center justify-center rounded border border-border/50 bg-accent/40 px-1.5 font-mono text-[10px] text-foreground/80 leading-none">
|
|
1403
|
-
{keyLabel}
|
|
1404
|
-
</kbd>
|
|
1405
|
-
</div>
|
|
1679
|
+
</WalkthroughHud>
|
|
1406
1680
|
)
|
|
1407
1681
|
}
|