@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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode, AnyNodeId } from '@pascal-app/core'
|
|
3
|
+
import { collectQuickActionNodeScope } from './quick-action-nodes'
|
|
4
|
+
|
|
5
|
+
function fixtureNode({
|
|
6
|
+
id,
|
|
7
|
+
parentId,
|
|
8
|
+
children = [],
|
|
9
|
+
type = 'item',
|
|
10
|
+
}: {
|
|
11
|
+
id: string
|
|
12
|
+
parentId?: string
|
|
13
|
+
children?: string[]
|
|
14
|
+
type?: string
|
|
15
|
+
}) {
|
|
16
|
+
return {
|
|
17
|
+
id,
|
|
18
|
+
type,
|
|
19
|
+
parentId,
|
|
20
|
+
children,
|
|
21
|
+
} as unknown as AnyNode
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('collectQuickActionNodeScope', () => {
|
|
25
|
+
test('includes nested children of a selected node sibling', () => {
|
|
26
|
+
const run = fixtureNode({
|
|
27
|
+
id: 'run',
|
|
28
|
+
children: ['left-base', 'selected-base'],
|
|
29
|
+
})
|
|
30
|
+
const leftBase = fixtureNode({
|
|
31
|
+
id: 'left-base',
|
|
32
|
+
parentId: run.id,
|
|
33
|
+
children: ['expanded-wall'],
|
|
34
|
+
})
|
|
35
|
+
const selectedBase = fixtureNode({ id: 'selected-base', parentId: run.id })
|
|
36
|
+
const expandedWall = fixtureNode({ id: 'expanded-wall', parentId: leftBase.id })
|
|
37
|
+
const nodes = Object.fromEntries(
|
|
38
|
+
[run, leftBase, selectedBase, expandedWall].map((node) => [node.id, node]),
|
|
39
|
+
) as Record<AnyNodeId, AnyNode>
|
|
40
|
+
|
|
41
|
+
const collected = collectQuickActionNodeScope(nodes, selectedBase.id)
|
|
42
|
+
|
|
43
|
+
expect(collected?.[expandedWall.id as AnyNodeId]).toBe(expandedWall)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('includes other run subtrees when the provider declares level scope', () => {
|
|
47
|
+
const level = fixtureNode({
|
|
48
|
+
id: 'level',
|
|
49
|
+
type: 'level',
|
|
50
|
+
children: ['selected-run', 'other-run'],
|
|
51
|
+
})
|
|
52
|
+
const selectedRun = fixtureNode({
|
|
53
|
+
id: 'selected-run',
|
|
54
|
+
parentId: level.id,
|
|
55
|
+
children: ['selected-base'],
|
|
56
|
+
})
|
|
57
|
+
const selectedBase = fixtureNode({ id: 'selected-base', parentId: selectedRun.id })
|
|
58
|
+
const otherRun = fixtureNode({
|
|
59
|
+
id: 'other-run',
|
|
60
|
+
parentId: level.id,
|
|
61
|
+
children: ['other-base'],
|
|
62
|
+
})
|
|
63
|
+
const otherBase = fixtureNode({
|
|
64
|
+
id: 'other-base',
|
|
65
|
+
parentId: otherRun.id,
|
|
66
|
+
children: ['expanded-wall'],
|
|
67
|
+
})
|
|
68
|
+
const expandedWall = fixtureNode({ id: 'expanded-wall', parentId: otherBase.id })
|
|
69
|
+
const nodes = Object.fromEntries(
|
|
70
|
+
[level, selectedRun, selectedBase, otherRun, otherBase, expandedWall].map((node) => [
|
|
71
|
+
node.id,
|
|
72
|
+
node,
|
|
73
|
+
]),
|
|
74
|
+
) as Record<AnyNodeId, AnyNode>
|
|
75
|
+
|
|
76
|
+
expect(
|
|
77
|
+
collectQuickActionNodeScope(nodes, selectedBase.id)?.[expandedWall.id as AnyNodeId],
|
|
78
|
+
).toBeUndefined()
|
|
79
|
+
expect(
|
|
80
|
+
collectQuickActionNodeScope(nodes, selectedBase.id, 'level')?.[expandedWall.id as AnyNodeId],
|
|
81
|
+
).toBe(expandedWall)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('fails closed when a level-scoped provider has no level ancestor', () => {
|
|
85
|
+
const run = fixtureNode({ id: 'run', children: ['selected-base'] })
|
|
86
|
+
const selectedBase = fixtureNode({ id: 'selected-base', parentId: run.id })
|
|
87
|
+
const nodes = Object.fromEntries([run, selectedBase].map((node) => [node.id, node])) as Record<
|
|
88
|
+
AnyNodeId,
|
|
89
|
+
AnyNode
|
|
90
|
+
>
|
|
91
|
+
|
|
92
|
+
expect(collectQuickActionNodeScope(nodes, selectedBase.id, 'level')).toBeNull()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AnyNode, AnyNodeId, NodeQuickActionNodeScope } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
export function collectQuickActionNodeScope(
|
|
4
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
5
|
+
selectedId: string,
|
|
6
|
+
scope: NodeQuickActionNodeScope = 'family',
|
|
7
|
+
): Record<AnyNodeId, AnyNode> | null {
|
|
8
|
+
const selected = nodes[selectedId as AnyNodeId]
|
|
9
|
+
if (!selected) return null
|
|
10
|
+
|
|
11
|
+
const collected: Record<AnyNodeId, AnyNode> = {}
|
|
12
|
+
const addSubtree = (rootId: string | null | undefined) => {
|
|
13
|
+
if (!rootId) return
|
|
14
|
+
const pending = [rootId]
|
|
15
|
+
|
|
16
|
+
while (pending.length > 0) {
|
|
17
|
+
const id = pending.pop()
|
|
18
|
+
if (!id || collected[id as AnyNodeId]) continue
|
|
19
|
+
const node = nodes[id as AnyNodeId]
|
|
20
|
+
if (!node) continue
|
|
21
|
+
|
|
22
|
+
collected[node.id as AnyNodeId] = node
|
|
23
|
+
for (const childId of (node as { children?: readonly string[] }).children ?? []) {
|
|
24
|
+
pending.push(childId)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (scope === 'level') {
|
|
30
|
+
const visited = new Set<AnyNodeId>()
|
|
31
|
+
let current: AnyNode | undefined = selected
|
|
32
|
+
while (current && !visited.has(current.id as AnyNodeId)) {
|
|
33
|
+
visited.add(current.id as AnyNodeId)
|
|
34
|
+
if (current.type === 'level') {
|
|
35
|
+
addSubtree(current.id)
|
|
36
|
+
return collected
|
|
37
|
+
}
|
|
38
|
+
current = current.parentId ? nodes[current.parentId as AnyNodeId] : undefined
|
|
39
|
+
}
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
addSubtree(selected.id)
|
|
44
|
+
addSubtree(selected.parentId)
|
|
45
|
+
|
|
46
|
+
return collected
|
|
47
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createQuickMeasurementPointerScheduler } from './quick-measurement'
|
|
3
|
+
|
|
4
|
+
function pointer(clientX: number, clientY: number): PointerEvent {
|
|
5
|
+
return { clientX, clientY } as PointerEvent
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createFrameDriver() {
|
|
9
|
+
let nextId = 1
|
|
10
|
+
const callbacks = new Map<number, FrameRequestCallback>()
|
|
11
|
+
return {
|
|
12
|
+
driver: {
|
|
13
|
+
request: (callback: FrameRequestCallback) => {
|
|
14
|
+
const id = nextId++
|
|
15
|
+
callbacks.set(id, callback)
|
|
16
|
+
return id
|
|
17
|
+
},
|
|
18
|
+
cancel: (frameId: number) => callbacks.delete(frameId),
|
|
19
|
+
},
|
|
20
|
+
pending: () => callbacks.size,
|
|
21
|
+
runNext: (timestamp: number) => {
|
|
22
|
+
const next = callbacks.entries().next().value as [number, FrameRequestCallback] | undefined
|
|
23
|
+
if (!next) throw new Error('No animation frame scheduled')
|
|
24
|
+
callbacks.delete(next[0])
|
|
25
|
+
next[1](timestamp)
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe('createQuickMeasurementPointerScheduler', () => {
|
|
31
|
+
test('keeps only the latest pointer event and caps expensive work', () => {
|
|
32
|
+
const frames = createFrameDriver()
|
|
33
|
+
const processed: PointerEvent[] = []
|
|
34
|
+
const scheduler = createQuickMeasurementPointerScheduler(
|
|
35
|
+
(event) => processed.push(event),
|
|
36
|
+
frames.driver,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
scheduler.enqueue(pointer(1, 1))
|
|
40
|
+
scheduler.enqueue(pointer(8, 4))
|
|
41
|
+
expect(frames.pending()).toBe(1)
|
|
42
|
+
frames.runNext(0)
|
|
43
|
+
expect(processed.map((event) => [event.clientX, event.clientY])).toEqual([[8, 4]])
|
|
44
|
+
|
|
45
|
+
scheduler.enqueue(pointer(12, 4))
|
|
46
|
+
frames.runNext(16)
|
|
47
|
+
expect(processed).toHaveLength(1)
|
|
48
|
+
expect(frames.pending()).toBe(1)
|
|
49
|
+
|
|
50
|
+
scheduler.enqueue(pointer(20, 7))
|
|
51
|
+
frames.runNext(32)
|
|
52
|
+
expect(processed.map((event) => [event.clientX, event.clientY])).toEqual([
|
|
53
|
+
[8, 4],
|
|
54
|
+
[20, 7],
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('ignores sub-pixel jitter and cancels queued work', () => {
|
|
59
|
+
const frames = createFrameDriver()
|
|
60
|
+
const processed: PointerEvent[] = []
|
|
61
|
+
const scheduler = createQuickMeasurementPointerScheduler(
|
|
62
|
+
(event) => processed.push(event),
|
|
63
|
+
frames.driver,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
scheduler.enqueue(pointer(10, 10))
|
|
67
|
+
frames.runNext(0)
|
|
68
|
+
scheduler.enqueue(pointer(10.5, 10.5))
|
|
69
|
+
frames.runNext(32)
|
|
70
|
+
expect(processed).toHaveLength(1)
|
|
71
|
+
|
|
72
|
+
scheduler.enqueue(pointer(20, 20))
|
|
73
|
+
scheduler.clear()
|
|
74
|
+
expect(frames.pending()).toBe(0)
|
|
75
|
+
expect(processed).toHaveLength(1)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type AnyNodeId,
|
|
4
|
+
type GeometryContext,
|
|
5
|
+
nodeRegistry,
|
|
6
|
+
type QuickMeasurementReport,
|
|
7
|
+
} from '@pascal-app/core'
|
|
8
|
+
|
|
9
|
+
const QUICK_MEASUREMENT_QUERY_INTERVAL_MS = 30
|
|
10
|
+
const QUICK_MEASUREMENT_MIN_DISTANCE_SQ = 1
|
|
11
|
+
|
|
12
|
+
export function createQuickMeasurementPointerScheduler(
|
|
13
|
+
onPointerMove: (event: PointerEvent) => void,
|
|
14
|
+
frameDriver: {
|
|
15
|
+
request: (callback: FrameRequestCallback) => number
|
|
16
|
+
cancel: (frameId: number) => void
|
|
17
|
+
} = {
|
|
18
|
+
request: (callback) => requestAnimationFrame(callback),
|
|
19
|
+
cancel: (frameId) => cancelAnimationFrame(frameId),
|
|
20
|
+
},
|
|
21
|
+
): {
|
|
22
|
+
enqueue: (event: PointerEvent) => void
|
|
23
|
+
clear: () => void
|
|
24
|
+
} {
|
|
25
|
+
let latestEvent: PointerEvent | null = null
|
|
26
|
+
let frameId: number | null = null
|
|
27
|
+
let lastProcessedAt = Number.NEGATIVE_INFINITY
|
|
28
|
+
let lastClientX = Number.NaN
|
|
29
|
+
let lastClientY = Number.NaN
|
|
30
|
+
|
|
31
|
+
const schedule = () => {
|
|
32
|
+
if (frameId === null) frameId = frameDriver.request(flush)
|
|
33
|
+
}
|
|
34
|
+
const flush = (timestamp: number) => {
|
|
35
|
+
frameId = null
|
|
36
|
+
if (!latestEvent) return
|
|
37
|
+
if (timestamp - lastProcessedAt < QUICK_MEASUREMENT_QUERY_INTERVAL_MS) {
|
|
38
|
+
schedule()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const event = latestEvent
|
|
43
|
+
latestEvent = null
|
|
44
|
+
const deltaX = event.clientX - lastClientX
|
|
45
|
+
const deltaY = event.clientY - lastClientY
|
|
46
|
+
if (
|
|
47
|
+
Number.isFinite(lastClientX) &&
|
|
48
|
+
deltaX * deltaX + deltaY * deltaY < QUICK_MEASUREMENT_MIN_DISTANCE_SQ
|
|
49
|
+
) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
lastClientX = event.clientX
|
|
54
|
+
lastClientY = event.clientY
|
|
55
|
+
lastProcessedAt = timestamp
|
|
56
|
+
onPointerMove(event)
|
|
57
|
+
if (latestEvent) schedule()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
enqueue: (event) => {
|
|
62
|
+
latestEvent = event
|
|
63
|
+
schedule()
|
|
64
|
+
},
|
|
65
|
+
clear: () => {
|
|
66
|
+
if (frameId !== null) frameDriver.cancel(frameId)
|
|
67
|
+
latestEvent = null
|
|
68
|
+
frameId = null
|
|
69
|
+
lastProcessedAt = Number.NEGATIVE_INFINITY
|
|
70
|
+
lastClientX = Number.NaN
|
|
71
|
+
lastClientY = Number.NaN
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function quickMeasurementContext(
|
|
77
|
+
node: AnyNode,
|
|
78
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
79
|
+
): GeometryContext {
|
|
80
|
+
const resolve: GeometryContext['resolve'] = <N = AnyNode>(id: AnyNodeId) =>
|
|
81
|
+
nodes[id] as N | undefined
|
|
82
|
+
const childIds =
|
|
83
|
+
'children' in node && Array.isArray(node.children) ? (node.children as AnyNodeId[]) : []
|
|
84
|
+
const children = childIds
|
|
85
|
+
.map((id) => nodes[id])
|
|
86
|
+
.filter((child): child is AnyNode => child !== undefined)
|
|
87
|
+
const parent = node.parentId ? (nodes[node.parentId as AnyNodeId] ?? null) : null
|
|
88
|
+
const siblings =
|
|
89
|
+
parent && 'children' in parent && Array.isArray(parent.children)
|
|
90
|
+
? (parent.children as AnyNodeId[])
|
|
91
|
+
.map((id) => nodes[id])
|
|
92
|
+
.filter(
|
|
93
|
+
(sibling): sibling is AnyNode => sibling !== undefined && sibling.type === node.type,
|
|
94
|
+
)
|
|
95
|
+
: []
|
|
96
|
+
|
|
97
|
+
return { resolve, children, parent, siblings }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function resolveQuickMeasurementReport(
|
|
101
|
+
nodeId: string | null,
|
|
102
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
103
|
+
): QuickMeasurementReport | null {
|
|
104
|
+
if (!nodeId) return null
|
|
105
|
+
const node = nodes[nodeId as AnyNodeId]
|
|
106
|
+
if (!node || node.visible === false) return null
|
|
107
|
+
const quickMeasure = nodeRegistry.get(node.type)?.measurement?.quickMeasure
|
|
108
|
+
return quickMeasure ? quickMeasure(node, quickMeasurementContext(node, nodes)) : null
|
|
109
|
+
}
|
|
@@ -80,7 +80,7 @@ export function duplicateRoofSubtree(
|
|
|
80
80
|
const scene = useScene.getState()
|
|
81
81
|
const sourceRoof = scene.nodes[sourceRoofId]
|
|
82
82
|
|
|
83
|
-
if (
|
|
83
|
+
if (sourceRoof?.type !== 'roof') {
|
|
84
84
|
throw new Error(`Node "${sourceRoofId}" is not a roof`)
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -105,7 +105,7 @@ export function duplicateRoofSubtree(
|
|
|
105
105
|
const segmentClones: RoofSegmentNode[] = []
|
|
106
106
|
for (const childId of sourceRoof.children ?? []) {
|
|
107
107
|
const childNode = scene.nodes[childId as AnyNodeId]
|
|
108
|
-
if (
|
|
108
|
+
if (childNode?.type !== 'roof-segment') {
|
|
109
109
|
continue
|
|
110
110
|
}
|
|
111
111
|
|
|
@@ -125,7 +125,7 @@ export function duplicateRoofSubtree(
|
|
|
125
125
|
|
|
126
126
|
const nextScene = useScene.getState()
|
|
127
127
|
const createdRoof = nextScene.nodes[roofClone.id as AnyNodeId]
|
|
128
|
-
if (
|
|
128
|
+
if (createdRoof?.type !== 'roof') {
|
|
129
129
|
throw new Error(`Duplicated roof "${roofClone.id}" was not created`)
|
|
130
130
|
}
|
|
131
131
|
|
|
@@ -151,7 +151,7 @@ export function duplicateRoofSubtree(
|
|
|
151
151
|
|
|
152
152
|
const invalidSegment = segmentIds.find((segmentId) => {
|
|
153
153
|
const segment = nextScene.nodes[segmentId as AnyNodeId]
|
|
154
|
-
return
|
|
154
|
+
return segment?.type !== 'roof-segment' || segment.parentId !== createdRoof.id
|
|
155
155
|
})
|
|
156
156
|
if (invalidSegment) {
|
|
157
157
|
throw new Error(
|
|
@@ -179,7 +179,7 @@ export function clearRoofDuplicateMetadata(
|
|
|
179
179
|
) {
|
|
180
180
|
const scene = useScene.getState()
|
|
181
181
|
const roofNode = scene.nodes[roofId]
|
|
182
|
-
if (
|
|
182
|
+
if (roofNode?.type !== 'roof') {
|
|
183
183
|
return
|
|
184
184
|
}
|
|
185
185
|
|
|
@@ -198,7 +198,7 @@ export function clearRoofDuplicateMetadata(
|
|
|
198
198
|
|
|
199
199
|
for (const childId of roofNode.children ?? []) {
|
|
200
200
|
const childNode = scene.nodes[childId as AnyNodeId]
|
|
201
|
-
if (
|
|
201
|
+
if (childNode?.type !== 'roof-segment') {
|
|
202
202
|
continue
|
|
203
203
|
}
|
|
204
204
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type AnyNodeId, sceneRegistry, useScene } from '@pascal-app/core'
|
|
2
|
+
import type * as THREE from 'three'
|
|
3
|
+
|
|
4
|
+
export const HOVERED_ROOF_SEGMENT_OUTLINE_PROXY_NAME = '__roof-hover-outline-proxy__'
|
|
5
|
+
|
|
6
|
+
function hoveredRoofSegmentOutlineProxyName(segmentId: string) {
|
|
7
|
+
return `${HOVERED_ROOF_SEGMENT_OUTLINE_PROXY_NAME}:${segmentId}`
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getHoveredRoofSegmentOutlineProxy(segmentId: string): THREE.Object3D | null {
|
|
11
|
+
const segment = useScene.getState().nodes[segmentId as AnyNodeId]
|
|
12
|
+
if (!(segment?.type === 'roof-segment' && segment.parentId)) return null
|
|
13
|
+
return (
|
|
14
|
+
sceneRegistry.nodes
|
|
15
|
+
.get(segment.parentId)
|
|
16
|
+
?.getObjectByName(hoveredRoofSegmentOutlineProxyName(segmentId)) ?? null
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getHoveredRoofSegmentOutlineProxyName(segmentId: string): string {
|
|
21
|
+
return hoveredRoofSegmentOutlineProxyName(segmentId)
|
|
22
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNodeId,
|
|
3
|
+
getRoofSegmentWallFaces,
|
|
4
|
+
getScaledDimensions,
|
|
5
|
+
type ItemNode,
|
|
6
|
+
type RoofNode,
|
|
7
|
+
type RoofSegmentNode,
|
|
8
|
+
type RoofSegmentWallFace,
|
|
9
|
+
type RoofWallFaceId,
|
|
10
|
+
sceneRegistry,
|
|
11
|
+
segmentPointToRoofWallFace,
|
|
12
|
+
useScene,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
import * as THREE from 'three'
|
|
15
|
+
|
|
16
|
+
const worldPoint = new THREE.Vector3()
|
|
17
|
+
const worldNormal = new THREE.Vector3()
|
|
18
|
+
const localPoint = new THREE.Vector3()
|
|
19
|
+
const localNormal = new THREE.Vector3()
|
|
20
|
+
const inverseMatrix = new THREE.Matrix4()
|
|
21
|
+
|
|
22
|
+
export type RoofWallHit = {
|
|
23
|
+
segment: RoofSegmentNode
|
|
24
|
+
face: RoofSegmentWallFace
|
|
25
|
+
/** Face coords of the hit (u along the face, v above the segment base). */
|
|
26
|
+
u: number
|
|
27
|
+
v: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Pointer hits more than this far off the wall plane are not wall hits. */
|
|
31
|
+
const PLANE_TOLERANCE = 0.06
|
|
32
|
+
/** Reject faces whose normal disagrees with the hit normal (slope / soffit). */
|
|
33
|
+
const NORMAL_ALIGNMENT = 0.7
|
|
34
|
+
/** A wall face is vertical; slope faces on low pitches have |ny| ≫ 0. */
|
|
35
|
+
const MAX_NORMAL_Y = 0.4
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve a pointer hit on a roof to one of its segments' vertical wall
|
|
39
|
+
* faces (base walls under the roof + the coplanar gable/shed/gambrel end
|
|
40
|
+
* faces). Counterpart of `resolveRoofSegmentHit`, which resolves to the
|
|
41
|
+
* sloped top surface instead.
|
|
42
|
+
*
|
|
43
|
+
* `normal` must be the raw `NodeEvent.normal` (hit-object-local) together
|
|
44
|
+
* with the `object` it came from — roof events can originate from the
|
|
45
|
+
* merged-roof mesh (roof-local frame) or a painted segment mesh
|
|
46
|
+
* (segment-local frame), so the normal is normalised through world space
|
|
47
|
+
* here instead of trusting the event frame.
|
|
48
|
+
*
|
|
49
|
+
* Lives in `@pascal-app/editor` because both the kind-owned door/window
|
|
50
|
+
* tools (in `@pascal-app/nodes`, which depends on editor) and the item
|
|
51
|
+
* placement coordinator (in editor itself) consume it.
|
|
52
|
+
*/
|
|
53
|
+
export function resolveRoofWallHit(
|
|
54
|
+
roof: RoofNode,
|
|
55
|
+
position: [number, number, number],
|
|
56
|
+
normal: [number, number, number] | undefined,
|
|
57
|
+
object: THREE.Object3D | undefined,
|
|
58
|
+
): RoofWallHit | null {
|
|
59
|
+
if (!normal || !object) return null
|
|
60
|
+
|
|
61
|
+
worldPoint.set(position[0], position[1], position[2])
|
|
62
|
+
worldNormal.set(normal[0], normal[1], normal[2])
|
|
63
|
+
object.updateWorldMatrix(true, false)
|
|
64
|
+
worldNormal.transformDirection(object.matrixWorld)
|
|
65
|
+
|
|
66
|
+
const state = useScene.getState()
|
|
67
|
+
let best: { hit: RoofWallHit; score: number } | null = null
|
|
68
|
+
|
|
69
|
+
for (const childId of roof.children ?? []) {
|
|
70
|
+
const segment = state.nodes[childId as AnyNodeId] as RoofSegmentNode | undefined
|
|
71
|
+
if (segment?.type !== 'roof-segment') continue
|
|
72
|
+
const segObj = sceneRegistry.nodes.get(segment.id)
|
|
73
|
+
if (!segObj) continue
|
|
74
|
+
segObj.updateWorldMatrix(true, false)
|
|
75
|
+
|
|
76
|
+
localPoint.copy(worldPoint)
|
|
77
|
+
segObj.worldToLocal(localPoint)
|
|
78
|
+
inverseMatrix.copy(segObj.matrixWorld).invert()
|
|
79
|
+
localNormal.copy(worldNormal).transformDirection(inverseMatrix)
|
|
80
|
+
|
|
81
|
+
if (Math.abs(localNormal.y) > MAX_NORMAL_Y) continue
|
|
82
|
+
|
|
83
|
+
for (const face of getRoofSegmentWallFaces(segment)) {
|
|
84
|
+
const alignment =
|
|
85
|
+
localNormal.x * face.normal[0] +
|
|
86
|
+
localNormal.y * face.normal[1] +
|
|
87
|
+
localNormal.z * face.normal[2]
|
|
88
|
+
if (alignment < NORMAL_ALIGNMENT) continue
|
|
89
|
+
|
|
90
|
+
const { u, v, dist } = segmentPointToRoofWallFace(segment, face.id, [
|
|
91
|
+
localPoint.x,
|
|
92
|
+
localPoint.y,
|
|
93
|
+
localPoint.z,
|
|
94
|
+
])
|
|
95
|
+
if (Math.abs(dist) > PLANE_TOLERANCE) continue
|
|
96
|
+
if (u < -PLANE_TOLERANCE || u > face.length + PLANE_TOLERANCE) continue
|
|
97
|
+
if (v < -PLANE_TOLERANCE) continue
|
|
98
|
+
|
|
99
|
+
const score = Math.abs(dist)
|
|
100
|
+
if (!best || score < best.score) {
|
|
101
|
+
best = { hit: { segment, face, u, v }, score }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return best?.hit ?? null
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Overlap guard for nodes sharing a roof-segment wall face — the
|
|
111
|
+
* roof-host analogue of `hasWallChildOverlap`. Hosted children store
|
|
112
|
+
* FACE-LOCAL coords + an explicit `roofFace`, so siblings compare
|
|
113
|
+
* directly: doors/windows are center-anchored in v, wall items
|
|
114
|
+
* bottom-anchored.
|
|
115
|
+
*/
|
|
116
|
+
export function hasRoofFaceChildOverlap(
|
|
117
|
+
segment: RoofSegmentNode,
|
|
118
|
+
faceId: RoofWallFaceId,
|
|
119
|
+
u: number,
|
|
120
|
+
v: number,
|
|
121
|
+
width: number,
|
|
122
|
+
height: number,
|
|
123
|
+
ignoreId?: string,
|
|
124
|
+
): boolean {
|
|
125
|
+
const nodes = useScene.getState().nodes
|
|
126
|
+
const newLeft = u - width / 2
|
|
127
|
+
const newRight = u + width / 2
|
|
128
|
+
const newBottom = v - height / 2
|
|
129
|
+
const newTop = v + height / 2
|
|
130
|
+
|
|
131
|
+
for (const childId of segment.children ?? []) {
|
|
132
|
+
if (childId === ignoreId) continue
|
|
133
|
+
const child = nodes[childId as AnyNodeId]
|
|
134
|
+
if (!child) continue
|
|
135
|
+
if ((child as { roofFace?: RoofWallFaceId }).roofFace !== faceId) continue
|
|
136
|
+
const position = (child as { position?: [number, number, number] }).position
|
|
137
|
+
if (!position) continue
|
|
138
|
+
|
|
139
|
+
let childW: number
|
|
140
|
+
let childBottom: number
|
|
141
|
+
let childTop: number
|
|
142
|
+
if (child.type === 'door' || child.type === 'window') {
|
|
143
|
+
const opening = child as { width: number; height: number }
|
|
144
|
+
childW = opening.width
|
|
145
|
+
childBottom = position[1] - opening.height / 2
|
|
146
|
+
childTop = position[1] + opening.height / 2
|
|
147
|
+
} else if (child.type === 'item') {
|
|
148
|
+
const item = child as ItemNode
|
|
149
|
+
if (item.asset.attachTo !== 'wall' && item.asset.attachTo !== 'wall-side') continue
|
|
150
|
+
const [w, h] = getScaledDimensions(item)
|
|
151
|
+
childW = w
|
|
152
|
+
// Items anchor position[1] at their bottom edge.
|
|
153
|
+
childBottom = position[1]
|
|
154
|
+
childTop = position[1] + h
|
|
155
|
+
} else {
|
|
156
|
+
continue
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const xOverlap = newLeft < position[0] + childW / 2 && newRight > position[0] - childW / 2
|
|
160
|
+
const yOverlap = newBottom < childTop && newTop > childBottom
|
|
161
|
+
if (xOverlap && yOverlap) return true
|
|
162
|
+
}
|
|
163
|
+
return false
|
|
164
|
+
}
|