@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
|
@@ -2,15 +2,131 @@
|
|
|
2
2
|
|
|
3
3
|
import '../../../three-types'
|
|
4
4
|
|
|
5
|
+
import { Html } from '@react-three/drei'
|
|
5
6
|
import { useEffect, useMemo } from 'react'
|
|
6
7
|
import { PlaneGeometry } from 'three'
|
|
7
8
|
import { distance, smoothstep, uv, vec2 } from 'three/tsl'
|
|
8
9
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
9
10
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
11
|
+
import { formatLinearMeasurement, type LinearUnit } from '../../../lib/measurements'
|
|
10
12
|
import { createLineGeometry, getBoxEdgePoints } from './placement-box-geometry'
|
|
11
13
|
|
|
12
14
|
const VALID_COLOR = 0x22_c5_5e // green-500
|
|
13
15
|
const INVALID_COLOR = 0xef_44_44 // red-500
|
|
16
|
+
const MEASUREMENT_COLOR = 0x0f_17_2a
|
|
17
|
+
|
|
18
|
+
type PlacementBoxMeasurements = {
|
|
19
|
+
unit: LinearUnit
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function getMeasurementGuidePoints(width: number, height: number, depth: number) {
|
|
23
|
+
const minX = -width / 2
|
|
24
|
+
const maxX = width / 2
|
|
25
|
+
const minZ = -depth / 2
|
|
26
|
+
const maxZ = depth / 2
|
|
27
|
+
const guideOffset = 0.18
|
|
28
|
+
const tick = 0.08
|
|
29
|
+
const y = 0.02
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
depth: [
|
|
33
|
+
maxX + guideOffset,
|
|
34
|
+
y,
|
|
35
|
+
minZ,
|
|
36
|
+
maxX + guideOffset,
|
|
37
|
+
y,
|
|
38
|
+
maxZ,
|
|
39
|
+
|
|
40
|
+
maxX + guideOffset - tick,
|
|
41
|
+
y,
|
|
42
|
+
minZ,
|
|
43
|
+
maxX + guideOffset + tick,
|
|
44
|
+
y,
|
|
45
|
+
minZ,
|
|
46
|
+
|
|
47
|
+
maxX + guideOffset - tick,
|
|
48
|
+
y,
|
|
49
|
+
maxZ,
|
|
50
|
+
maxX + guideOffset + tick,
|
|
51
|
+
y,
|
|
52
|
+
maxZ,
|
|
53
|
+
],
|
|
54
|
+
height: [
|
|
55
|
+
minX - guideOffset,
|
|
56
|
+
0,
|
|
57
|
+
minZ,
|
|
58
|
+
minX - guideOffset,
|
|
59
|
+
height,
|
|
60
|
+
minZ,
|
|
61
|
+
|
|
62
|
+
minX - guideOffset - tick,
|
|
63
|
+
0,
|
|
64
|
+
minZ,
|
|
65
|
+
minX - guideOffset + tick,
|
|
66
|
+
0,
|
|
67
|
+
minZ,
|
|
68
|
+
|
|
69
|
+
minX - guideOffset - tick,
|
|
70
|
+
height,
|
|
71
|
+
minZ,
|
|
72
|
+
minX - guideOffset + tick,
|
|
73
|
+
height,
|
|
74
|
+
minZ,
|
|
75
|
+
],
|
|
76
|
+
width: [
|
|
77
|
+
minX,
|
|
78
|
+
y,
|
|
79
|
+
maxZ + guideOffset,
|
|
80
|
+
maxX,
|
|
81
|
+
y,
|
|
82
|
+
maxZ + guideOffset,
|
|
83
|
+
|
|
84
|
+
minX,
|
|
85
|
+
y,
|
|
86
|
+
maxZ + guideOffset - tick,
|
|
87
|
+
minX,
|
|
88
|
+
y,
|
|
89
|
+
maxZ + guideOffset + tick,
|
|
90
|
+
|
|
91
|
+
maxX,
|
|
92
|
+
y,
|
|
93
|
+
maxZ + guideOffset - tick,
|
|
94
|
+
maxX,
|
|
95
|
+
y,
|
|
96
|
+
maxZ + guideOffset + tick,
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function MeasurementPill({
|
|
102
|
+
label,
|
|
103
|
+
position,
|
|
104
|
+
}: {
|
|
105
|
+
label: string
|
|
106
|
+
position: [number, number, number]
|
|
107
|
+
}) {
|
|
108
|
+
return (
|
|
109
|
+
<Html center position={position} style={{ pointerEvents: 'none' }}>
|
|
110
|
+
<div
|
|
111
|
+
style={{
|
|
112
|
+
background: 'rgba(15, 23, 42, 0.86)',
|
|
113
|
+
border: '1px solid rgba(15, 23, 42, 0.65)',
|
|
114
|
+
borderRadius: '999px',
|
|
115
|
+
color: '#f8fafc',
|
|
116
|
+
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
|
|
117
|
+
fontSize: '11px',
|
|
118
|
+
fontWeight: 600,
|
|
119
|
+
lineHeight: 1,
|
|
120
|
+
padding: '4px 8px',
|
|
121
|
+
pointerEvents: 'none',
|
|
122
|
+
whiteSpace: 'nowrap',
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
{label}
|
|
126
|
+
</div>
|
|
127
|
+
</Html>
|
|
128
|
+
)
|
|
129
|
+
}
|
|
14
130
|
|
|
15
131
|
/**
|
|
16
132
|
* Green/red placement footprint shown while a node follows the cursor — the
|
|
@@ -27,12 +143,15 @@ const INVALID_COLOR = 0xef_44_44 // red-500
|
|
|
27
143
|
*/
|
|
28
144
|
export function PlacementBox({
|
|
29
145
|
dimensions,
|
|
146
|
+
measurements,
|
|
30
147
|
position,
|
|
31
148
|
rotationY = 0,
|
|
32
149
|
valid,
|
|
33
150
|
}: {
|
|
34
151
|
/** Footprint extent `[width, height, depth]` (unrotated). */
|
|
35
152
|
dimensions: [number, number, number]
|
|
153
|
+
/** Optional dimension guide labels matching the GLB item placement cursor. */
|
|
154
|
+
measurements?: PlacementBoxMeasurements
|
|
36
155
|
/** World-plan position of the footprint centre (floor level). */
|
|
37
156
|
position: [number, number, number]
|
|
38
157
|
/** Y-rotation in radians, applied to the whole box. */
|
|
@@ -61,6 +180,14 @@ export function PlacementBox({
|
|
|
61
180
|
geometry.translate(0, 0.01, 0)
|
|
62
181
|
return geometry
|
|
63
182
|
}, [width, depth])
|
|
183
|
+
const measurementGuideGeometries = useMemo(() => {
|
|
184
|
+
const points = getMeasurementGuidePoints(width, height, depth)
|
|
185
|
+
return {
|
|
186
|
+
depth: createLineGeometry(points.depth),
|
|
187
|
+
height: createLineGeometry(points.height),
|
|
188
|
+
width: createLineGeometry(points.width),
|
|
189
|
+
}
|
|
190
|
+
}, [width, height, depth])
|
|
64
191
|
|
|
65
192
|
const edgeMaterial = useMemo(
|
|
66
193
|
() => new LineBasicNodeMaterial({ linewidth: 3, depthTest: false, depthWrite: false }),
|
|
@@ -77,6 +204,16 @@ export function PlacementBox({
|
|
|
77
204
|
material.opacityNode = smoothstep(0, 0.7, distance(uv(), vec2(0.5, 0.5))).mul(0.6)
|
|
78
205
|
return material
|
|
79
206
|
}, [])
|
|
207
|
+
const measurementMaterial = useMemo(
|
|
208
|
+
() =>
|
|
209
|
+
new LineBasicNodeMaterial({
|
|
210
|
+
color: MEASUREMENT_COLOR,
|
|
211
|
+
linewidth: 2,
|
|
212
|
+
depthTest: false,
|
|
213
|
+
depthWrite: false,
|
|
214
|
+
}),
|
|
215
|
+
[],
|
|
216
|
+
)
|
|
80
217
|
|
|
81
218
|
useEffect(() => {
|
|
82
219
|
const color = valid ? VALID_COLOR : INVALID_COLOR
|
|
@@ -90,6 +227,14 @@ export function PlacementBox({
|
|
|
90
227
|
},
|
|
91
228
|
[edgeGeometry],
|
|
92
229
|
)
|
|
230
|
+
useEffect(
|
|
231
|
+
() => () => {
|
|
232
|
+
measurementGuideGeometries.depth.dispose()
|
|
233
|
+
measurementGuideGeometries.height.dispose()
|
|
234
|
+
measurementGuideGeometries.width.dispose()
|
|
235
|
+
},
|
|
236
|
+
[measurementGuideGeometries],
|
|
237
|
+
)
|
|
93
238
|
useEffect(
|
|
94
239
|
() => () => {
|
|
95
240
|
basePlaneGeometry.dispose()
|
|
@@ -100,10 +245,46 @@ export function PlacementBox({
|
|
|
100
245
|
() => () => {
|
|
101
246
|
edgeMaterial.dispose()
|
|
102
247
|
basePlaneMaterial.dispose()
|
|
248
|
+
measurementMaterial.dispose()
|
|
103
249
|
},
|
|
104
|
-
[edgeMaterial, basePlaneMaterial],
|
|
250
|
+
[edgeMaterial, basePlaneMaterial, measurementMaterial],
|
|
105
251
|
)
|
|
106
252
|
|
|
253
|
+
const measurementContent = measurements ? (
|
|
254
|
+
<>
|
|
255
|
+
<lineSegments
|
|
256
|
+
geometry={measurementGuideGeometries.width}
|
|
257
|
+
layers={EDITOR_LAYER}
|
|
258
|
+
material={measurementMaterial}
|
|
259
|
+
renderOrder={998}
|
|
260
|
+
/>
|
|
261
|
+
<lineSegments
|
|
262
|
+
geometry={measurementGuideGeometries.depth}
|
|
263
|
+
layers={EDITOR_LAYER}
|
|
264
|
+
material={measurementMaterial}
|
|
265
|
+
renderOrder={998}
|
|
266
|
+
/>
|
|
267
|
+
<lineSegments
|
|
268
|
+
geometry={measurementGuideGeometries.height}
|
|
269
|
+
layers={EDITOR_LAYER}
|
|
270
|
+
material={measurementMaterial}
|
|
271
|
+
renderOrder={998}
|
|
272
|
+
/>
|
|
273
|
+
<MeasurementPill
|
|
274
|
+
label={formatLinearMeasurement(width, measurements.unit)}
|
|
275
|
+
position={[0, 0.04, depth / 2 + 0.24]}
|
|
276
|
+
/>
|
|
277
|
+
<MeasurementPill
|
|
278
|
+
label={formatLinearMeasurement(depth, measurements.unit)}
|
|
279
|
+
position={[width / 2 + 0.24, 0.04, 0]}
|
|
280
|
+
/>
|
|
281
|
+
<MeasurementPill
|
|
282
|
+
label={formatLinearMeasurement(height, measurements.unit)}
|
|
283
|
+
position={[-width / 2 - 0.24, height / 2, -depth / 2]}
|
|
284
|
+
/>
|
|
285
|
+
</>
|
|
286
|
+
) : null
|
|
287
|
+
|
|
107
288
|
return (
|
|
108
289
|
<group position={position} rotation={[0, rotationY, 0]}>
|
|
109
290
|
<lineSegments
|
|
@@ -112,6 +293,7 @@ export function PlacementBox({
|
|
|
112
293
|
material={edgeMaterial}
|
|
113
294
|
renderOrder={999}
|
|
114
295
|
/>
|
|
296
|
+
{measurementContent}
|
|
115
297
|
<mesh
|
|
116
298
|
geometry={basePlaneGeometry}
|
|
117
299
|
layers={EDITOR_LAYER}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
sceneRegistry,
|
|
6
|
+
spatialGridManager,
|
|
7
|
+
useScene,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
10
|
+
import { BoxGeometry, Mesh, MeshBasicMaterial, PerspectiveCamera } from 'three'
|
|
11
|
+
import { resolvePointerSupportSurface } from './pointer-support-cap'
|
|
12
|
+
|
|
13
|
+
const LEVEL_ID = 'level_test' as AnyNodeId
|
|
14
|
+
const WALL_ID = 'wall_test' as AnyNodeId
|
|
15
|
+
|
|
16
|
+
describe('resolvePointerSupportSurface node tops', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
spatialGridManager.clear()
|
|
19
|
+
sceneRegistry.clear()
|
|
20
|
+
useViewer.setState({
|
|
21
|
+
selection: {
|
|
22
|
+
buildingId: null,
|
|
23
|
+
levelId: LEVEL_ID,
|
|
24
|
+
zoneId: null,
|
|
25
|
+
selectedIds: [],
|
|
26
|
+
},
|
|
27
|
+
} as never)
|
|
28
|
+
useScene.setState({
|
|
29
|
+
nodes: {
|
|
30
|
+
[LEVEL_ID]: {
|
|
31
|
+
id: LEVEL_ID,
|
|
32
|
+
type: 'level',
|
|
33
|
+
object: 'node',
|
|
34
|
+
parentId: null,
|
|
35
|
+
visible: true,
|
|
36
|
+
metadata: {},
|
|
37
|
+
children: [WALL_ID],
|
|
38
|
+
level: 0,
|
|
39
|
+
} as AnyNode,
|
|
40
|
+
[WALL_ID]: {
|
|
41
|
+
id: WALL_ID,
|
|
42
|
+
type: 'wall',
|
|
43
|
+
object: 'node',
|
|
44
|
+
parentId: LEVEL_ID,
|
|
45
|
+
visible: true,
|
|
46
|
+
metadata: {},
|
|
47
|
+
children: [],
|
|
48
|
+
assemblyLayers: [],
|
|
49
|
+
start: [-1, 0],
|
|
50
|
+
end: [1, 0],
|
|
51
|
+
frontSide: 'unknown',
|
|
52
|
+
backSide: 'unknown',
|
|
53
|
+
} as AnyNode,
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
afterEach(() => {
|
|
59
|
+
sceneRegistry.clear()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('prefers the nearest upward-facing registered node surface over the ground', () => {
|
|
63
|
+
const wallMesh = new Mesh(new BoxGeometry(2, 2, 0.2), new MeshBasicMaterial())
|
|
64
|
+
wallMesh.position.y = 1
|
|
65
|
+
wallMesh.updateMatrixWorld(true)
|
|
66
|
+
sceneRegistry.nodes.set(WALL_ID, wallMesh)
|
|
67
|
+
sceneRegistry.byType.wall!.add(WALL_ID)
|
|
68
|
+
|
|
69
|
+
const camera = new PerspectiveCamera()
|
|
70
|
+
camera.position.set(0, 5, 0)
|
|
71
|
+
camera.updateMatrixWorld(true)
|
|
72
|
+
|
|
73
|
+
const support = resolvePointerSupportSurface(camera, [0, 0, 0], {
|
|
74
|
+
includeNodeTopSurfaces: true,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
expect(support?.sourceNodeId).toBe(WALL_ID)
|
|
78
|
+
expect(support?.elevation).toBeCloseTo(2)
|
|
79
|
+
expect(support?.worldPoint).toEqual([0, 2, 0])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('keeps the ground result when node-top surfaces are not requested', () => {
|
|
83
|
+
const wallMesh = new Mesh(new BoxGeometry(2, 2, 0.2), new MeshBasicMaterial())
|
|
84
|
+
wallMesh.position.y = 1
|
|
85
|
+
wallMesh.updateMatrixWorld(true)
|
|
86
|
+
sceneRegistry.nodes.set(WALL_ID, wallMesh)
|
|
87
|
+
sceneRegistry.byType.wall!.add(WALL_ID)
|
|
88
|
+
|
|
89
|
+
const camera = new PerspectiveCamera()
|
|
90
|
+
camera.position.set(0, 5, 0)
|
|
91
|
+
camera.updateMatrixWorld(true)
|
|
92
|
+
|
|
93
|
+
const support = resolvePointerSupportSurface(camera, [0, 0, 0])
|
|
94
|
+
|
|
95
|
+
expect(support?.sourceNodeId).toBeNull()
|
|
96
|
+
expect(support?.elevation).toBe(0)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
canHostOnTop,
|
|
4
|
+
GROUND_SUPPORT_ID,
|
|
5
|
+
type ItemNode,
|
|
6
|
+
isLowProfileItemSurface,
|
|
7
|
+
sceneRegistry,
|
|
8
|
+
spatialGridManager,
|
|
9
|
+
useScene,
|
|
10
|
+
} from '@pascal-app/core'
|
|
11
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
12
|
+
import { type Camera, Matrix3, type Object3D, Raycaster, Vector3 } from 'three'
|
|
13
|
+
import { resolveTerrainGroundHit } from '../../../lib/ground-surface'
|
|
14
|
+
|
|
15
|
+
const originScratch = new Vector3()
|
|
16
|
+
const hitScratch = new Vector3()
|
|
17
|
+
const worldScratch = new Vector3()
|
|
18
|
+
const pointScratch = new Vector3()
|
|
19
|
+
const worldRayOrigin = new Vector3()
|
|
20
|
+
const worldRayDirection = new Vector3()
|
|
21
|
+
const nodeTopRaycaster = new Raycaster()
|
|
22
|
+
const nodeTopNormal = new Vector3()
|
|
23
|
+
const nodeTopNormalMatrix = new Matrix3()
|
|
24
|
+
|
|
25
|
+
const NODE_TOP_SURFACE_KINDS = ['wall', 'item', 'column'] as const
|
|
26
|
+
|
|
27
|
+
export type PointerSupportSurface = {
|
|
28
|
+
/** Level-local elevation of the pointed surface — the election cap. */
|
|
29
|
+
elevation: number
|
|
30
|
+
/** Slab host, or the explicit ground sentinel when the level base won. */
|
|
31
|
+
supportSlabId: string | null
|
|
32
|
+
/** Node whose upward-facing geometry supplied this surface, when applicable. */
|
|
33
|
+
sourceNodeId: AnyNodeId | null
|
|
34
|
+
/** World-space Y of the same surface, for grid-plane / preview placement. */
|
|
35
|
+
worldY: number
|
|
36
|
+
/**
|
|
37
|
+
* World-space point where the pointer ray meets the pointed surface's
|
|
38
|
+
* plane, or null when the ray never reaches it. Unlike the grid event's
|
|
39
|
+
* own hit — whose XZ is perspective-skewed whenever the event plane
|
|
40
|
+
* rides at a different storey than the aimed-at surface — this point
|
|
41
|
+
* depends only on the ray and the pointed surface, so a preview /
|
|
42
|
+
* election fed from it cannot flip with the event plane's height.
|
|
43
|
+
*/
|
|
44
|
+
worldPoint: [number, number, number] | null
|
|
45
|
+
/** {@link PointerSupportSurface.worldPoint} in the grid event's
|
|
46
|
+
* `localPosition` (building-local) frame — a drop-in replacement for
|
|
47
|
+
* the event's plane-hit XZ. */
|
|
48
|
+
localPoint: [number, number, number] | null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The walking surface the pointer actually points at: its level-local
|
|
53
|
+
* elevation (for use as the slab-support election cap, `maxElevation`),
|
|
54
|
+
* its world-space Y (for riding the grid event plane / draw preview on
|
|
55
|
+
* it), and the ray's crossing of that surface's plane (the plan point the
|
|
56
|
+
* cursor indicates).
|
|
57
|
+
*
|
|
58
|
+
* The grid event plane rides at the ghost's last height, so its hit point
|
|
59
|
+
* alone can't be trusted (that feedback loop is what made a ghost under an
|
|
60
|
+
* elevated deck blink between the deck top and the ground). But camera →
|
|
61
|
+
* hit reconstructs the true pointer ray regardless of the plane height,
|
|
62
|
+
* and the nearest slab plane that ray crosses inside its rendered polygon
|
|
63
|
+
* IS the surface under the cursor — the deck top when aiming at the deck,
|
|
64
|
+
* the floor/ground when aiming underneath it. The same reasoning applies
|
|
65
|
+
* to the cursor XZ: the event plane's hit is skewed along the ray whenever
|
|
66
|
+
* the plane sits on a different storey than the pointed surface, so
|
|
67
|
+
* callers should place at `localPoint` / `worldPoint`, not the event hit.
|
|
68
|
+
*
|
|
69
|
+
* When the ray crosses no slab, the surface is the level base — and on the
|
|
70
|
+
* storey that sits on the ground, that base is the sculpted terrain rather
|
|
71
|
+
* than a plane, so `elevation` varies with XZ. Everything downstream already
|
|
72
|
+
* treats it as a varying scalar, which is why terrain needs no new axis here.
|
|
73
|
+
*
|
|
74
|
+
* Returns null when no level is active (callers fall back to the
|
|
75
|
+
* uncapped max election and the raw event hit).
|
|
76
|
+
*/
|
|
77
|
+
export function resolvePointerSupportSurface(
|
|
78
|
+
camera: Camera,
|
|
79
|
+
worldHit: readonly [number, number, number],
|
|
80
|
+
options?: { includeNodeTopSurfaces?: boolean },
|
|
81
|
+
): PointerSupportSurface | null {
|
|
82
|
+
const levelId = useViewer.getState().selection.levelId
|
|
83
|
+
if (!levelId) return null
|
|
84
|
+
|
|
85
|
+
// The world ray, kept before the level conversion below: the terrain field is
|
|
86
|
+
// world-space (site geometry, not level-local), so the march needs this frame.
|
|
87
|
+
camera.getWorldPosition(worldRayOrigin)
|
|
88
|
+
worldRayDirection.set(worldHit[0], worldHit[1], worldHit[2]).sub(worldRayOrigin)
|
|
89
|
+
|
|
90
|
+
originScratch.copy(worldRayOrigin)
|
|
91
|
+
hitScratch.set(worldHit[0], worldHit[1], worldHit[2])
|
|
92
|
+
// Slab polygons/elevations live in the level frame; the level mesh
|
|
93
|
+
// carries the storey Y offset and any building rotation.
|
|
94
|
+
const levelMesh = sceneRegistry.nodes.get(levelId as AnyNodeId)
|
|
95
|
+
if (levelMesh) {
|
|
96
|
+
levelMesh.worldToLocal(originScratch)
|
|
97
|
+
levelMesh.worldToLocal(hitScratch)
|
|
98
|
+
}
|
|
99
|
+
hitScratch.sub(originScratch)
|
|
100
|
+
if (hitScratch.lengthSq() < 1e-12) return null
|
|
101
|
+
|
|
102
|
+
const pointed = spatialGridManager.getPointedSupportSurface(
|
|
103
|
+
levelId,
|
|
104
|
+
[originScratch.x, originScratch.y, originScratch.z],
|
|
105
|
+
[hitScratch.x, hitScratch.y, hitScratch.z],
|
|
106
|
+
)
|
|
107
|
+
let elevation = pointed.elevation
|
|
108
|
+
let point = pointed.point
|
|
109
|
+
|
|
110
|
+
// The level base is the second flat plane terrain has to displace (the first is
|
|
111
|
+
// `useGridEvents`'). The manager solves `t = -oy / dy` for it — a plane — which
|
|
112
|
+
// is still right for every storey whose base is a built floor, and wrong for the
|
|
113
|
+
// storey sitting on the ground, where the base IS the terrain.
|
|
114
|
+
//
|
|
115
|
+
// Substituted here rather than inside the manager for two reasons: the
|
|
116
|
+
// discrimination ("is this plane the site ground?") needs the level's *world*
|
|
117
|
+
// height, and this function is what owns the world↔level conversion; and
|
|
118
|
+
// `ground-surface` is meant to hold that rule once, shared with `useGridEvents`,
|
|
119
|
+
// rather than have `core` grow a terrain dependency it cannot express.
|
|
120
|
+
//
|
|
121
|
+
// Only the no-slab branch is replaced. A ray that crossed a slab inside its
|
|
122
|
+
// rendered polygon is aimed at a built floor, and terrain does not compete with
|
|
123
|
+
// one — that is the flat-floor invariant.
|
|
124
|
+
if (pointed.slabId === null) {
|
|
125
|
+
const baseWorldY = levelMesh ? levelMesh.localToWorld(worldScratch.set(0, 0, 0)).y : 0
|
|
126
|
+
const groundHit = resolveTerrainGroundHit(
|
|
127
|
+
[worldRayOrigin.x, worldRayOrigin.y, worldRayOrigin.z],
|
|
128
|
+
[worldRayDirection.x, worldRayDirection.y, worldRayDirection.z],
|
|
129
|
+
baseWorldY,
|
|
130
|
+
)
|
|
131
|
+
if (groundHit) {
|
|
132
|
+
pointScratch.set(groundHit.x, groundHit.y, groundHit.z)
|
|
133
|
+
if (levelMesh) levelMesh.worldToLocal(pointScratch)
|
|
134
|
+
elevation = pointScratch.y
|
|
135
|
+
point = [pointScratch.x, pointScratch.z]
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const worldY = levelMesh ? levelMesh.localToWorld(worldScratch.set(0, elevation, 0)).y : elevation
|
|
140
|
+
|
|
141
|
+
let worldPoint: [number, number, number] | null = null
|
|
142
|
+
let localPoint: [number, number, number] | null = null
|
|
143
|
+
if (point) {
|
|
144
|
+
pointScratch.set(point[0], elevation, point[1])
|
|
145
|
+
if (levelMesh) levelMesh.localToWorld(pointScratch)
|
|
146
|
+
worldPoint = [pointScratch.x, pointScratch.y, pointScratch.z]
|
|
147
|
+
// Same frame the grid events report `localPosition` in (use-grid-events).
|
|
148
|
+
const buildingId = useViewer.getState().selection.buildingId
|
|
149
|
+
const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
150
|
+
if (buildingMesh) buildingMesh.worldToLocal(pointScratch)
|
|
151
|
+
localPoint = [pointScratch.x, pointScratch.y, pointScratch.z]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (options?.includeNodeTopSurfaces) {
|
|
155
|
+
nodeTopRaycaster.set(worldRayOrigin, worldRayDirection.clone().normalize())
|
|
156
|
+
const nodes = useScene.getState().nodes
|
|
157
|
+
const registeredOwners = new Map(
|
|
158
|
+
[...sceneRegistry.nodes.entries()].map(([nodeId, object]) => [object, nodeId as AnyNodeId]),
|
|
159
|
+
)
|
|
160
|
+
const belongsToActiveLevel = (nodeId: AnyNodeId) => {
|
|
161
|
+
let current = nodes[nodeId]
|
|
162
|
+
const visited = new Set<AnyNodeId>()
|
|
163
|
+
while (current && !visited.has(current.id)) {
|
|
164
|
+
if (current.id === levelId) return true
|
|
165
|
+
visited.add(current.id)
|
|
166
|
+
current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
|
|
167
|
+
}
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const pointedDistance = worldPoint
|
|
172
|
+
? worldRayOrigin.distanceTo(pointScratch.set(...worldPoint))
|
|
173
|
+
: Number.POSITIVE_INFINITY
|
|
174
|
+
let nearest:
|
|
175
|
+
| {
|
|
176
|
+
distance: number
|
|
177
|
+
nodeId: AnyNodeId
|
|
178
|
+
point: Vector3
|
|
179
|
+
}
|
|
180
|
+
| undefined
|
|
181
|
+
|
|
182
|
+
for (const kind of NODE_TOP_SURFACE_KINDS) {
|
|
183
|
+
for (const rawId of sceneRegistry.byType[kind] ?? []) {
|
|
184
|
+
const nodeId = rawId as AnyNodeId
|
|
185
|
+
const node = nodes[nodeId]
|
|
186
|
+
const object = sceneRegistry.nodes.get(nodeId)
|
|
187
|
+
if (!(node?.visible && object?.visible && belongsToActiveLevel(nodeId))) continue
|
|
188
|
+
if (
|
|
189
|
+
node.type === 'item' &&
|
|
190
|
+
(!canHostOnTop(node) || isLowProfileItemSurface(node as ItemNode))
|
|
191
|
+
) {
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
for (const intersection of nodeTopRaycaster.intersectObject(object, true)) {
|
|
196
|
+
if (
|
|
197
|
+
intersection.distance >= (nearest?.distance ?? pointedDistance) ||
|
|
198
|
+
!intersection.face
|
|
199
|
+
) {
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
let ownerObject: Object3D | null = intersection.object
|
|
203
|
+
let belongsToNestedNode = false
|
|
204
|
+
while (ownerObject && ownerObject !== object) {
|
|
205
|
+
const ownerId = registeredOwners.get(ownerObject)
|
|
206
|
+
if (ownerId && ownerId !== nodeId) {
|
|
207
|
+
belongsToNestedNode = true
|
|
208
|
+
break
|
|
209
|
+
}
|
|
210
|
+
ownerObject = ownerObject.parent
|
|
211
|
+
}
|
|
212
|
+
if (belongsToNestedNode) continue
|
|
213
|
+
nodeTopNormal
|
|
214
|
+
.copy(intersection.face.normal)
|
|
215
|
+
.applyNormalMatrix(nodeTopNormalMatrix.getNormalMatrix(intersection.object.matrixWorld))
|
|
216
|
+
.normalize()
|
|
217
|
+
if (nodeTopNormal.y < 0.75) continue
|
|
218
|
+
nearest = {
|
|
219
|
+
distance: intersection.distance,
|
|
220
|
+
nodeId,
|
|
221
|
+
point: intersection.point.clone(),
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (nearest) {
|
|
228
|
+
const sourceNode = nodes[nearest.nodeId]
|
|
229
|
+
pointScratch.copy(nearest.point)
|
|
230
|
+
const nodeWorldPoint: [number, number, number] = [
|
|
231
|
+
pointScratch.x,
|
|
232
|
+
pointScratch.y,
|
|
233
|
+
pointScratch.z,
|
|
234
|
+
]
|
|
235
|
+
if (levelMesh) levelMesh.worldToLocal(pointScratch)
|
|
236
|
+
elevation = pointScratch.y
|
|
237
|
+
|
|
238
|
+
pointScratch.copy(nearest.point)
|
|
239
|
+
const buildingId = useViewer.getState().selection.buildingId
|
|
240
|
+
const buildingMesh = buildingId ? sceneRegistry.nodes.get(buildingId as AnyNodeId) : null
|
|
241
|
+
if (buildingMesh) buildingMesh.worldToLocal(pointScratch)
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
elevation,
|
|
245
|
+
supportSlabId:
|
|
246
|
+
sourceNode && 'supportSlabId' in sourceNode
|
|
247
|
+
? ((sourceNode.supportSlabId as string | undefined) ?? null)
|
|
248
|
+
: null,
|
|
249
|
+
sourceNodeId: nearest.nodeId,
|
|
250
|
+
worldY: nearest.point.y,
|
|
251
|
+
worldPoint: nodeWorldPoint,
|
|
252
|
+
localPoint: [pointScratch.x, pointScratch.y, pointScratch.z],
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
elevation,
|
|
259
|
+
supportSlabId: pointed.slabId ?? GROUND_SUPPORT_ID,
|
|
260
|
+
sourceNodeId: null,
|
|
261
|
+
worldY,
|
|
262
|
+
worldPoint,
|
|
263
|
+
localPoint,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** {@link resolvePointerSupportSurface}, elevation only — the election cap. */
|
|
268
|
+
export function resolvePointerSupportElevation(
|
|
269
|
+
camera: Camera,
|
|
270
|
+
worldHit: readonly [number, number, number],
|
|
271
|
+
): number | null {
|
|
272
|
+
return resolvePointerSupportSurface(camera, worldHit)?.elevation ?? null
|
|
273
|
+
}
|