@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
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
DEFAULT_ANGLE_STEP,
|
|
5
|
+
DEFAULT_LEVEL_HEIGHT,
|
|
4
6
|
type DoorNode,
|
|
7
|
+
GROUND_SUPPORT_ID,
|
|
5
8
|
getScaledDimensions,
|
|
6
9
|
type ItemNode,
|
|
10
|
+
resolveWallSupportSlabPatch,
|
|
11
|
+
runAsSingleSceneHistoryStep,
|
|
12
|
+
snapPointAlongAngleRay,
|
|
13
|
+
spatialGridManager,
|
|
14
|
+
terrainSupportLift,
|
|
7
15
|
useScene,
|
|
8
16
|
type WallNode,
|
|
9
17
|
WallNode as WallSchema,
|
|
@@ -11,51 +19,58 @@ import {
|
|
|
11
19
|
} from '@pascal-app/core'
|
|
12
20
|
import { useViewer } from '@pascal-app/viewer'
|
|
13
21
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
14
|
-
import
|
|
22
|
+
import { resolveSnapFlags } from '../../../lib/snapping-mode'
|
|
23
|
+
import useEditor, { getActiveSnappingMode, isMagneticSnapActive } from '../../../store/use-editor'
|
|
15
24
|
import {
|
|
16
25
|
distanceSquared,
|
|
17
26
|
findWallSnapTarget,
|
|
18
27
|
findWallSpecialPointSnap,
|
|
19
28
|
projectPointOntoWall,
|
|
29
|
+
WALL_CONNECT_SNAP_RADIUS,
|
|
20
30
|
WALL_JOIN_SNAP_RADIUS,
|
|
21
31
|
type WallDraftSnapResult,
|
|
22
32
|
type WallPlanPoint,
|
|
33
|
+
type WallSnapRadii,
|
|
34
|
+
wallIdsAtSnapPoint,
|
|
23
35
|
} from './wall-snap-geometry'
|
|
24
36
|
|
|
25
37
|
// The pure snap geometry lives in `./wall-snap-geometry`; re-exported here so
|
|
26
38
|
// existing importers (fence drafting, the editor barrel) keep their paths.
|
|
27
39
|
export {
|
|
40
|
+
chainEndJoinsExistingWall,
|
|
28
41
|
findWallSnapTarget,
|
|
42
|
+
WALL_CONNECT_SNAP_RADIUS,
|
|
29
43
|
WALL_JOIN_SNAP_RADIUS,
|
|
30
44
|
type WallDraftSnapKind,
|
|
31
45
|
type WallDraftSnapResult,
|
|
32
46
|
type WallPlanPoint,
|
|
47
|
+
type WallSnapRadii,
|
|
33
48
|
} from './wall-snap-geometry'
|
|
34
49
|
|
|
35
50
|
export const WALL_GRID_STEP = 0.5
|
|
36
|
-
// Smallest available grid snap. Used as a precision-mode step (Shift +
|
|
37
|
-
// drag) so a drag can land on values the regular grid skips.
|
|
38
|
-
export const WALL_FINE_GRID_STEP = 0.05
|
|
39
51
|
export const WALL_MIN_LENGTH = 0.01
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
0.1: Math.PI / 12,
|
|
46
|
-
0.05: Math.PI / 36,
|
|
47
|
-
}
|
|
52
|
+
// An endpoint projecting within this distance of an existing wall's corner
|
|
53
|
+
// resolves to the corner without splitting — splitting there would mint a
|
|
54
|
+
// sliver segment a hair longer than `WALL_MIN_LENGTH` that no snap radius
|
|
55
|
+
// can ever target again.
|
|
56
|
+
const WALL_SPLIT_ENDPOINT_EPSILON = 0.02
|
|
48
57
|
|
|
49
58
|
type WallSplitIntersection = {
|
|
50
|
-
|
|
59
|
+
/** `null` = snap-only outcome: resolve to `point` but split no wall. */
|
|
60
|
+
wallId: WallNode['id'] | null
|
|
51
61
|
point: WallPlanPoint
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
export function getSegmentGridStep(): number {
|
|
55
|
-
|
|
65
|
+
// A 0 step means "no grid lattice" — every grid-snap consumer guards on
|
|
66
|
+
// `step <= 0` and returns the raw value, so disabling grid here suppresses
|
|
67
|
+
// the lattice for walls, fences, and every node move/affordance that reads
|
|
68
|
+
// this choke point, without retuning their snap math.
|
|
69
|
+
return resolveSnapFlags(getActiveSnappingMode()).grid ? useEditor.getState().gridSnapStep : 0
|
|
56
70
|
}
|
|
57
71
|
|
|
58
72
|
export function snapScalarToGrid(value: number, step = WALL_GRID_STEP): number {
|
|
73
|
+
if (step <= 0) return value
|
|
59
74
|
return Math.round(value / step) * step
|
|
60
75
|
}
|
|
61
76
|
|
|
@@ -63,36 +78,11 @@ export function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): Wa
|
|
|
63
78
|
return [snapScalarToGrid(point[0], step), snapScalarToGrid(point[1], step)]
|
|
64
79
|
}
|
|
65
80
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Optional grid-snap callback. Lets the caller route the final
|
|
73
|
-
* snap through a world-XZ grid (or any other axis system) instead
|
|
74
|
-
* of the local-axis grid `snapPointToGrid` uses. When omitted,
|
|
75
|
-
* falls back to the local-axis snap at `step`.
|
|
76
|
-
*/
|
|
77
|
-
gridSnap?: (point: WallPlanPoint) => WallPlanPoint,
|
|
78
|
-
): WallPlanPoint {
|
|
79
|
-
const dx = cursor[0] - start[0]
|
|
80
|
-
const dz = cursor[1] - start[1]
|
|
81
|
-
const angle = Math.atan2(dz, dx)
|
|
82
|
-
const snappedAngle = Math.round(angle / angleStep) * angleStep
|
|
83
|
-
const distance = Math.sqrt(dx * dx + dz * dz)
|
|
84
|
-
const point: WallPlanPoint = [
|
|
85
|
-
start[0] + Math.cos(snappedAngle) * distance,
|
|
86
|
-
start[1] + Math.sin(snappedAngle) * distance,
|
|
87
|
-
]
|
|
88
|
-
return gridSnap ? gridSnap(point) : snapPointToGrid(point, step)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function getWallAngleSnapStep(step = getSegmentGridStep()): number {
|
|
92
|
-
return WALL_ANGLE_SNAP_BY_GRID_STEP[step] ?? DEFAULT_WALL_ANGLE_SNAP_STEP
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function splitWallAtPoint(wall: WallNode, splitPoint: WallPlanPoint): [WallNode, WallNode] {
|
|
81
|
+
function splitWallAtPoint(
|
|
82
|
+
wall: WallNode,
|
|
83
|
+
splitPoint: WallPlanPoint,
|
|
84
|
+
nodes: ReturnType<typeof useScene.getState>['nodes'],
|
|
85
|
+
): [WallNode, WallNode] {
|
|
96
86
|
const { id: _id, parentId: _parentId, children, ...rest } = wall
|
|
97
87
|
|
|
98
88
|
const first = WallSchema.parse({
|
|
@@ -108,7 +98,24 @@ function splitWallAtPoint(wall: WallNode, splitPoint: WallPlanPoint): [WallNode,
|
|
|
108
98
|
children: [],
|
|
109
99
|
})
|
|
110
100
|
|
|
111
|
-
|
|
101
|
+
if (wall.supportSlabId !== GROUND_SUPPORT_ID || !wall.parentId) {
|
|
102
|
+
return [first, second]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const levelId = wall.parentId
|
|
106
|
+
const originalElevation =
|
|
107
|
+
(terrainSupportLift(nodes, levelId, wall.start[0], wall.start[1]) ?? 0) +
|
|
108
|
+
(wall.supportOffset ?? 0)
|
|
109
|
+
const rebase = (segment: WallNode): WallNode => {
|
|
110
|
+
const terrainElevation =
|
|
111
|
+
terrainSupportLift(nodes, levelId, segment.start[0], segment.start[1]) ?? 0
|
|
112
|
+
const supportOffset = originalElevation - terrainElevation
|
|
113
|
+
return {
|
|
114
|
+
...segment,
|
|
115
|
+
supportOffset: Math.abs(supportOffset) > 1e-6 ? supportOffset : undefined,
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return [rebase(first), rebase(second)]
|
|
112
119
|
}
|
|
113
120
|
|
|
114
121
|
function pointsEqual(a: WallPlanPoint, b: WallPlanPoint, tolerance = 1e-6): boolean {
|
|
@@ -118,6 +125,7 @@ function pointsEqual(a: WallPlanPoint, b: WallPlanPoint, tolerance = 1e-6): bool
|
|
|
118
125
|
function findWallIntersection(
|
|
119
126
|
point: WallPlanPoint,
|
|
120
127
|
walls: WallNode[],
|
|
128
|
+
radius: number,
|
|
121
129
|
ignoreWallIds?: string[],
|
|
122
130
|
): WallSplitIntersection | null {
|
|
123
131
|
const ignore = new Set(ignoreWallIds ?? [])
|
|
@@ -132,13 +140,20 @@ function findWallIntersection(
|
|
|
132
140
|
|
|
133
141
|
const candidateDistanceSquared = distanceSquared(point, projected)
|
|
134
142
|
if (
|
|
135
|
-
candidateDistanceSquared >
|
|
143
|
+
candidateDistanceSquared > radius * radius ||
|
|
136
144
|
candidateDistanceSquared >= bestDistanceSquared
|
|
137
145
|
) {
|
|
138
146
|
continue
|
|
139
147
|
}
|
|
140
148
|
|
|
141
|
-
|
|
149
|
+
const nearCorner = ([wall.start, wall.end] as WallPlanPoint[]).find(
|
|
150
|
+
(corner) =>
|
|
151
|
+
distanceSquared(projected, corner) <=
|
|
152
|
+
WALL_SPLIT_ENDPOINT_EPSILON * WALL_SPLIT_ENDPOINT_EPSILON,
|
|
153
|
+
)
|
|
154
|
+
best = nearCorner
|
|
155
|
+
? { wallId: null, point: [nearCorner[0], nearCorner[1]] }
|
|
156
|
+
: { wallId: wall.id, point: projected }
|
|
142
157
|
bestDistanceSquared = candidateDistanceSquared
|
|
143
158
|
}
|
|
144
159
|
|
|
@@ -290,12 +305,16 @@ function splitWallIfNeeded(
|
|
|
290
305
|
): { walls: WallNode[]; point: WallPlanPoint } | null {
|
|
291
306
|
if (!intersection) return null
|
|
292
307
|
|
|
308
|
+
if (!intersection.wallId) {
|
|
309
|
+
return { walls, point: intersection.point }
|
|
310
|
+
}
|
|
311
|
+
|
|
293
312
|
const wallToSplit = walls.find((wall) => wall.id === intersection.wallId)
|
|
294
313
|
if (!wallToSplit) {
|
|
295
314
|
return { walls, point: intersection.point }
|
|
296
315
|
}
|
|
297
316
|
|
|
298
|
-
const [first, second] = splitWallAtPoint(wallToSplit, intersection.point)
|
|
317
|
+
const [first, second] = splitWallAtPoint(wallToSplit, intersection.point, nodes)
|
|
299
318
|
const attachmentUpdates = buildAttachmentMigrationPlan(
|
|
300
319
|
wallToSplit,
|
|
301
320
|
intersection.point,
|
|
@@ -323,13 +342,51 @@ function splitWallIfNeeded(
|
|
|
323
342
|
}
|
|
324
343
|
}
|
|
325
344
|
|
|
345
|
+
/**
|
|
346
|
+
* Commit-time split resolution for an endpoint MOVE — the sibling of the
|
|
347
|
+
* inline resolution in `createWallOnCurrentLevel`: when a moved endpoint is
|
|
348
|
+
* dropped on another wall's interior, split that host exactly like the draw
|
|
349
|
+
* path (duplicate props, migrate attachments by span, skip the split when an
|
|
350
|
+
* opening straddles the point). Mutates the scene store (create halves /
|
|
351
|
+
* migrate attachments / delete host), so callers MUST run it inside the same
|
|
352
|
+
* `runAsSingleSceneHistoryStep` as their endpoint write.
|
|
353
|
+
*
|
|
354
|
+
* Returns the resolved endpoint (projection onto the host, or a nearby corner
|
|
355
|
+
* when the drop is within `WALL_SPLIT_ENDPOINT_EPSILON` of one — corner joins
|
|
356
|
+
* are not splits), or `null` when the point lands on no wall.
|
|
357
|
+
*/
|
|
358
|
+
export function resolveEndpointWallSplit(args: {
|
|
359
|
+
point: WallPlanPoint
|
|
360
|
+
/** Level the moved wall lives on — only its walls are split candidates. */
|
|
361
|
+
levelId: string | null
|
|
362
|
+
/** The moved wall + every wall receiving an endpoint update in the same commit. */
|
|
363
|
+
ignoreWallIds: string[]
|
|
364
|
+
/**
|
|
365
|
+
* Capture radius. The endpoint already snapped onto the wall body during
|
|
366
|
+
* the drag, so the tight connect radius (drop genuinely on the wall) is
|
|
367
|
+
* the default.
|
|
368
|
+
*/
|
|
369
|
+
radius?: number
|
|
370
|
+
}): WallPlanPoint | null {
|
|
371
|
+
const { point, levelId, ignoreWallIds, radius = WALL_CONNECT_SNAP_RADIUS } = args
|
|
372
|
+
const { nodes, createNodes, updateNodes, deleteNode } = useScene.getState()
|
|
373
|
+
const walls = Object.values(nodes).filter(
|
|
374
|
+
(node): node is WallNode => node?.type === 'wall' && (node.parentId ?? null) === levelId,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
const intersection = findWallIntersection(point, walls, radius, ignoreWallIds)
|
|
378
|
+
const split = splitWallIfNeeded(intersection, walls, nodes, createNodes, updateNodes, deleteNode)
|
|
379
|
+
return split ? split.point : null
|
|
380
|
+
}
|
|
381
|
+
|
|
326
382
|
type SnapWallDraftArgs = {
|
|
327
383
|
point: WallPlanPoint
|
|
328
384
|
walls: WallNode[]
|
|
329
385
|
start?: WallPlanPoint
|
|
330
386
|
angleSnap?: boolean
|
|
331
387
|
ignoreWallIds?: string[]
|
|
332
|
-
|
|
388
|
+
bypassSnap?: boolean
|
|
389
|
+
/** Override the grid step. */
|
|
333
390
|
step?: number
|
|
334
391
|
/**
|
|
335
392
|
* Magnetic snapping to existing wall geometry (corners, midpoints,
|
|
@@ -345,6 +402,8 @@ type SnapWallDraftArgs = {
|
|
|
345
402
|
* local-axis grid at `step`.
|
|
346
403
|
*/
|
|
347
404
|
gridSnap?: (point: WallPlanPoint) => WallPlanPoint
|
|
405
|
+
/** Optional magnetic snap radii. Omitted means wall tools keep their defaults. */
|
|
406
|
+
snapRadii?: WallSnapRadii
|
|
348
407
|
}
|
|
349
408
|
|
|
350
409
|
export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSnapResult {
|
|
@@ -354,34 +413,75 @@ export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSn
|
|
|
354
413
|
start,
|
|
355
414
|
angleSnap = false,
|
|
356
415
|
ignoreWallIds,
|
|
416
|
+
bypassSnap = false,
|
|
357
417
|
step: overrideStep,
|
|
358
418
|
magnetic = true,
|
|
359
419
|
gridSnap,
|
|
420
|
+
snapRadii,
|
|
360
421
|
} = args
|
|
361
422
|
|
|
423
|
+
if (bypassSnap) return { point, snap: null, targetWallIds: [] }
|
|
424
|
+
|
|
362
425
|
// Discrete special points (corner / midpoint / crossing) are taken from the
|
|
363
426
|
// raw cursor so an interim grid snap can't mask them. A corner always wins,
|
|
364
427
|
// then the nearer of midpoint / crossing — see `findWallSpecialPointSnap`.
|
|
365
428
|
if (magnetic) {
|
|
366
|
-
const special = findWallSpecialPointSnap(point, walls, ignoreWallIds)
|
|
429
|
+
const special = findWallSpecialPointSnap(point, walls, ignoreWallIds, snapRadii)
|
|
367
430
|
if (special) return special
|
|
368
431
|
}
|
|
369
432
|
|
|
370
433
|
const step = overrideStep ?? getSegmentGridStep()
|
|
371
|
-
|
|
372
|
-
|
|
434
|
+
// The angle path snaps the distance ALONG the 15° ray — a scalar, the
|
|
435
|
+
// same in world and local frames — so the `gridSnap` world-grid override
|
|
436
|
+
// only applies when the angle lock is off.
|
|
437
|
+
const basePoint: WallPlanPoint =
|
|
373
438
|
start && angleSnap
|
|
374
|
-
?
|
|
439
|
+
? [...snapPointAlongAngleRay(start, point, DEFAULT_ANGLE_STEP, step)]
|
|
375
440
|
: gridSnap
|
|
376
441
|
? gridSnap(point)
|
|
377
442
|
: snapPointToGrid(point, step)
|
|
378
443
|
|
|
379
444
|
if (magnetic) {
|
|
380
|
-
const wallSnap = findWallSnapTarget(basePoint, walls, {
|
|
381
|
-
|
|
445
|
+
const wallSnap = findWallSnapTarget(basePoint, walls, {
|
|
446
|
+
ignoreWallIds,
|
|
447
|
+
radius: snapRadii?.wall,
|
|
448
|
+
})
|
|
449
|
+
if (wallSnap) {
|
|
450
|
+
return {
|
|
451
|
+
point: wallSnap,
|
|
452
|
+
snap: 'wall',
|
|
453
|
+
targetWallIds: wallIdsAtSnapPoint(wallSnap, walls, ignoreWallIds),
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { point: basePoint, snap: null, targetWallIds: [] }
|
|
382
457
|
}
|
|
383
458
|
|
|
384
|
-
|
|
459
|
+
// Non-magnetic modes (grid / off / angles): connectivity still sticks so a
|
|
460
|
+
// room can close, but only within a tight radius — placement elsewhere is left
|
|
461
|
+
// to the mode (grid quantise / angle lock / free). Snap from the already
|
|
462
|
+
// positioned `basePoint` so the mode's placement is respected right up to the
|
|
463
|
+
// wall, then the last few cm stick onto it (and the beacon shows).
|
|
464
|
+
const connectRadii: WallSnapRadii = {
|
|
465
|
+
endpoint: WALL_CONNECT_SNAP_RADIUS,
|
|
466
|
+
midpoint: WALL_CONNECT_SNAP_RADIUS,
|
|
467
|
+
intersection: WALL_CONNECT_SNAP_RADIUS,
|
|
468
|
+
wall: WALL_CONNECT_SNAP_RADIUS,
|
|
469
|
+
}
|
|
470
|
+
const connectSpecial = findWallSpecialPointSnap(basePoint, walls, ignoreWallIds, connectRadii)
|
|
471
|
+
if (connectSpecial) return connectSpecial
|
|
472
|
+
const connectWall = findWallSnapTarget(basePoint, walls, {
|
|
473
|
+
ignoreWallIds,
|
|
474
|
+
radius: WALL_CONNECT_SNAP_RADIUS,
|
|
475
|
+
})
|
|
476
|
+
if (connectWall) {
|
|
477
|
+
return {
|
|
478
|
+
point: connectWall,
|
|
479
|
+
snap: 'wall',
|
|
480
|
+
targetWallIds: wallIdsAtSnapPoint(connectWall, walls, ignoreWallIds),
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return { point: basePoint, snap: null, targetWallIds: [] }
|
|
385
485
|
}
|
|
386
486
|
|
|
387
487
|
export function snapWallDraftPoint(args: SnapWallDraftArgs): WallPlanPoint {
|
|
@@ -392,9 +492,45 @@ export function isSegmentLongEnough(start: WallPlanPoint, end: WallPlanPoint): b
|
|
|
392
492
|
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
|
|
393
493
|
}
|
|
394
494
|
|
|
495
|
+
export type WallConstructionOptions = {
|
|
496
|
+
/** Pointer-decided maximum support elevation in level-local metres. */
|
|
497
|
+
supportCap?: number | null
|
|
498
|
+
/** Support source selected by the first click or inherited from a snapped wall. */
|
|
499
|
+
preferredSupportSlabId?: string | null
|
|
500
|
+
/** Frozen level-local Y shown by the draft ghost. */
|
|
501
|
+
constructionElevation?: number | null
|
|
502
|
+
/** Height shown by the draft ghost. */
|
|
503
|
+
constructionHeight?: number | null
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function resolveTerrainWallConstructionOptions(
|
|
507
|
+
nodes: Record<string, AnyNode>,
|
|
508
|
+
levelId: string,
|
|
509
|
+
point: WallPlanPoint,
|
|
510
|
+
defaults?: Record<string, unknown>,
|
|
511
|
+
): WallConstructionOptions | undefined {
|
|
512
|
+
const constructionElevation = terrainSupportLift(nodes, levelId, point[0], point[1])
|
|
513
|
+
if (constructionElevation == null) return undefined
|
|
514
|
+
|
|
515
|
+
const level = nodes[levelId]
|
|
516
|
+
const constructionHeight =
|
|
517
|
+
typeof defaults?.height === 'number'
|
|
518
|
+
? defaults.height
|
|
519
|
+
: level?.type === 'level'
|
|
520
|
+
? (level.height ?? DEFAULT_LEVEL_HEIGHT)
|
|
521
|
+
: DEFAULT_LEVEL_HEIGHT
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
constructionElevation,
|
|
525
|
+
constructionHeight,
|
|
526
|
+
supportCap: constructionElevation,
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
395
530
|
export function createWallOnCurrentLevel(
|
|
396
531
|
start: WallPlanPoint,
|
|
397
532
|
end: WallPlanPoint,
|
|
533
|
+
options?: WallConstructionOptions,
|
|
398
534
|
): WallNode | null {
|
|
399
535
|
const currentLevelId = useViewer.getState().selection.levelId
|
|
400
536
|
const { createNode, createNodes, deleteNode, nodes } = useScene.getState()
|
|
@@ -411,61 +547,121 @@ export function createWallOnCurrentLevel(
|
|
|
411
547
|
let resolvedStart = start
|
|
412
548
|
let resolvedEnd = end
|
|
413
549
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
resolvedStart = splitStart.point
|
|
440
|
-
}
|
|
550
|
+
// The corner-join / wall-split resolution follows the snapping mode like the
|
|
551
|
+
// draft preview does: magnetic ('lines') keeps the generous join radius,
|
|
552
|
+
// every other mode uses the same tight connect radius the draft path already
|
|
553
|
+
// sticks endpoints with. So an endpoint the user saw connect to a wall body
|
|
554
|
+
// actually splits that wall (and redistributes its attachments) in every
|
|
555
|
+
// mode, while `'off'` / `'angles'` gain no residual long-range snap.
|
|
556
|
+
const joinRadius = isMagneticSnapActive() ? WALL_JOIN_SNAP_RADIUS : WALL_CONNECT_SNAP_RADIUS
|
|
557
|
+
|
|
558
|
+
// One undo step for the whole commit: the split ops (create halves, migrate
|
|
559
|
+
// attachments, delete host) plus the new wall each push their own history
|
|
560
|
+
// entry, and a single Ctrl-Z must not strand a half-split wall network.
|
|
561
|
+
return runAsSingleSceneHistoryStep(useScene, () => {
|
|
562
|
+
const endIntersection = findWallIntersection(resolvedEnd, workingWalls, joinRadius)
|
|
563
|
+
const splitEnd = splitWallIfNeeded(
|
|
564
|
+
endIntersection,
|
|
565
|
+
workingWalls,
|
|
566
|
+
nodes,
|
|
567
|
+
createNodes,
|
|
568
|
+
updateNodes,
|
|
569
|
+
deleteNode,
|
|
570
|
+
)
|
|
571
|
+
if (splitEnd) {
|
|
572
|
+
workingWalls = splitEnd.walls
|
|
573
|
+
resolvedEnd = splitEnd.point
|
|
574
|
+
}
|
|
441
575
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
576
|
+
const startIntersection = findWallIntersection(resolvedStart, workingWalls, joinRadius)
|
|
577
|
+
const splitStart = splitWallIfNeeded(
|
|
578
|
+
startIntersection,
|
|
579
|
+
workingWalls,
|
|
580
|
+
nodes,
|
|
581
|
+
createNodes,
|
|
582
|
+
updateNodes,
|
|
583
|
+
deleteNode,
|
|
584
|
+
)
|
|
585
|
+
if (splitStart) {
|
|
586
|
+
workingWalls = splitStart.walls
|
|
587
|
+
resolvedStart = splitStart.point
|
|
588
|
+
}
|
|
445
589
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
return null
|
|
453
|
-
}
|
|
590
|
+
if (
|
|
591
|
+
!isSegmentLongEnough(resolvedStart, resolvedEnd) ||
|
|
592
|
+
pointsEqual(resolvedStart, resolvedEnd)
|
|
593
|
+
) {
|
|
594
|
+
return null
|
|
595
|
+
}
|
|
454
596
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
start: resolvedStart,
|
|
464
|
-
end: resolvedEnd,
|
|
465
|
-
})
|
|
597
|
+
const duplicateWall = workingWalls.some(
|
|
598
|
+
(wall) =>
|
|
599
|
+
(pointsEqual(wall.start, resolvedStart) && pointsEqual(wall.end, resolvedEnd)) ||
|
|
600
|
+
(pointsEqual(wall.start, resolvedEnd) && pointsEqual(wall.end, resolvedStart)),
|
|
601
|
+
)
|
|
602
|
+
if (duplicateWall) {
|
|
603
|
+
return null
|
|
604
|
+
}
|
|
466
605
|
|
|
467
|
-
|
|
468
|
-
|
|
606
|
+
const wallCount = Object.values(nodes).filter((node) => node.type === 'wall').length
|
|
607
|
+
// A placed wall preset seeds `toolDefaults.wall` (thickness, height,
|
|
608
|
+
// materials, sides) before the tool activates; merge those first so the
|
|
609
|
+
// drawn wall reproduces the preset. Identity + endpoints always win.
|
|
610
|
+
const defaults = useEditor.getState().toolDefaults.wall ?? {}
|
|
611
|
+
const wall = WallSchema.parse({
|
|
612
|
+
...defaults,
|
|
613
|
+
name: `Wall ${wallCount + 1}`,
|
|
614
|
+
start: resolvedStart,
|
|
615
|
+
end: resolvedEnd,
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
createNode(wall, currentLevelId)
|
|
619
|
+
const createdWall = useScene.getState().nodes[wall.id]
|
|
620
|
+
if (createdWall?.type === 'wall') {
|
|
621
|
+
const terrainBase = terrainSupportLift(
|
|
622
|
+
useScene.getState().nodes,
|
|
623
|
+
currentLevelId,
|
|
624
|
+
createdWall.start[0],
|
|
625
|
+
createdWall.start[1],
|
|
626
|
+
)
|
|
627
|
+
const preferredSupportSlabId =
|
|
628
|
+
options?.preferredSupportSlabId ??
|
|
629
|
+
(options?.constructionElevation != null && terrainBase != null ? GROUND_SUPPORT_ID : null)
|
|
630
|
+
const supportPatch = resolveWallSupportSlabPatch(createdWall, useScene.getState().nodes, {
|
|
631
|
+
maxElevation: options?.supportCap ?? null,
|
|
632
|
+
preferredSlabId: preferredSupportSlabId,
|
|
633
|
+
})
|
|
634
|
+
const supportSlabId = supportPatch.supportSlabId
|
|
635
|
+
const sourceSupport = spatialGridManager.getSlabSupportForWall(
|
|
636
|
+
currentLevelId,
|
|
637
|
+
createdWall.start,
|
|
638
|
+
createdWall.end,
|
|
639
|
+
createdWall.curveOffset,
|
|
640
|
+
createdWall.thickness,
|
|
641
|
+
supportSlabId,
|
|
642
|
+
options?.supportCap ?? null,
|
|
643
|
+
)
|
|
644
|
+
const supportOffset =
|
|
645
|
+
options?.constructionElevation == null
|
|
646
|
+
? undefined
|
|
647
|
+
: options.constructionElevation - sourceSupport.elevation
|
|
648
|
+
const preserveDraftHeight =
|
|
649
|
+
createdWall.height == null &&
|
|
650
|
+
options?.constructionHeight != null &&
|
|
651
|
+
options.constructionElevation != null &&
|
|
652
|
+
(terrainBase != null || Math.abs(options.constructionElevation) > 1e-6)
|
|
653
|
+
useScene.getState().updateNode(createdWall.id, {
|
|
654
|
+
...supportPatch,
|
|
655
|
+
height: preserveDraftHeight
|
|
656
|
+
? (options?.constructionHeight ?? createdWall.height)
|
|
657
|
+
: createdWall.height,
|
|
658
|
+
supportOffset:
|
|
659
|
+
supportOffset != null && Math.abs(supportOffset) > 1e-6 ? supportOffset : undefined,
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
469
663
|
|
|
470
|
-
|
|
664
|
+
const committedWall = useScene.getState().nodes[wall.id]
|
|
665
|
+
return committedWall?.type === 'wall' ? committedWall : wall
|
|
666
|
+
})
|
|
471
667
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import type { WallNode } from '@pascal-app/core'
|
|
3
3
|
import {
|
|
4
|
+
chainEndJoinsExistingWall,
|
|
4
5
|
findWallSnapTarget,
|
|
5
6
|
findWallSpecialPointSnap,
|
|
6
7
|
type WallPlanPoint,
|
|
@@ -47,6 +48,14 @@ describe('findWallSpecialPointSnap', () => {
|
|
|
47
48
|
expect(result?.snap).toBe('intersection')
|
|
48
49
|
expect(result?.point[0]).toBeCloseTo(1, 6)
|
|
49
50
|
expect(result?.point[1]).toBeCloseTo(0, 6)
|
|
51
|
+
expect(result?.targetWallIds).toEqual(['a', 'b'])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('keeps every wall that shares a snapped endpoint as provenance', () => {
|
|
55
|
+
const walls = [makeWall([0, 0], [4, 0], 'a'), makeWall([0, 0], [0, 4], 'b')]
|
|
56
|
+
const result = findWallSpecialPointSnap([0.1, 0.1], walls)
|
|
57
|
+
expect(result?.point).toEqual([0, 0])
|
|
58
|
+
expect(result?.targetWallIds).toEqual(['a', 'b'])
|
|
50
59
|
})
|
|
51
60
|
|
|
52
61
|
test('corner wins over a midpoint when both are in range', () => {
|
|
@@ -62,6 +71,43 @@ describe('findWallSpecialPointSnap', () => {
|
|
|
62
71
|
// handled separately by findWallSnapTarget, not a special point.
|
|
63
72
|
expect(findWallSpecialPointSnap([1.2, 0.1], walls)).toBeNull()
|
|
64
73
|
})
|
|
74
|
+
|
|
75
|
+
test('honors tighter per-call radii without changing defaults', () => {
|
|
76
|
+
const walls = [makeWall([0, 0], [4, 0])]
|
|
77
|
+
|
|
78
|
+
expect(findWallSpecialPointSnap([0.34, 0], walls)?.snap).toBe('endpoint')
|
|
79
|
+
expect(findWallSpecialPointSnap([0.34, 0], walls, undefined, { endpoint: 0.3 })).toBeNull()
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('chainEndJoinsExistingWall (chain termination)', () => {
|
|
84
|
+
test('true when the end lies on a non-chain wall interior (T junction)', () => {
|
|
85
|
+
const walls = [makeWall([0, 0], [4, 0], 'host'), makeWall([2, 2], [2, 0], 'chain_1')]
|
|
86
|
+
expect(chainEndJoinsExistingWall([2, 0], walls, ['chain_1'])).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('true when the end lands on a non-chain wall endpoint', () => {
|
|
90
|
+
const walls = [makeWall([0, 0], [4, 0], 'host'), makeWall([6, 2], [4, 0], 'chain_1')]
|
|
91
|
+
expect(chainEndJoinsExistingWall([4, 0], walls, ['chain_1'])).toBe(true)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('false when the end only touches the chain walls themselves', () => {
|
|
95
|
+
// Second segment doubles back onto the first one's midpoint — own-chain
|
|
96
|
+
// geometry is excluded, so the chain keeps going.
|
|
97
|
+
const walls = [makeWall([0, 0], [4, 0], 'chain_1'), makeWall([4, 0], [2, 0], 'chain_2')]
|
|
98
|
+
expect(chainEndJoinsExistingWall([2, 0], walls, ['chain_1', 'chain_2'])).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('false for a dead end in free space', () => {
|
|
102
|
+
const walls = [makeWall([0, 0], [4, 0], 'host'), makeWall([0, 2], [2, 2], 'chain_1')]
|
|
103
|
+
expect(chainEndJoinsExistingWall([2, 2], walls, ['chain_1'])).toBe(false)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('a near miss beyond the tolerance is not a join', () => {
|
|
107
|
+
const walls = [makeWall([0, 0], [4, 0], 'host')]
|
|
108
|
+
expect(chainEndJoinsExistingWall([2, 0.01], walls, [])).toBe(false)
|
|
109
|
+
expect(chainEndJoinsExistingWall([2, 0.0005], walls, [])).toBe(true)
|
|
110
|
+
})
|
|
65
111
|
})
|
|
66
112
|
|
|
67
113
|
describe('findWallSnapTarget (edge / along-wall)', () => {
|
|
@@ -76,4 +122,10 @@ describe('findWallSnapTarget (edge / along-wall)', () => {
|
|
|
76
122
|
const walls = [makeWall([0, 0], [4, 0])]
|
|
77
123
|
expect(findWallSnapTarget([1.2, 2], walls)).toBeNull()
|
|
78
124
|
})
|
|
125
|
+
|
|
126
|
+
test('honors a tighter wall-body radius', () => {
|
|
127
|
+
const walls = [makeWall([0, 0], [4, 0])]
|
|
128
|
+
|
|
129
|
+
expect(findWallSnapTarget([1.2, 0.1], walls, { radius: 0.08 })).toBeNull()
|
|
130
|
+
})
|
|
79
131
|
})
|