@pascal-app/editor 0.8.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
DEFAULT_ANGLE_STEP,
|
|
4
5
|
type DoorNode,
|
|
5
6
|
getScaledDimensions,
|
|
6
|
-
getWallCurveFrameAt,
|
|
7
|
-
getWallCurveLength,
|
|
8
7
|
type ItemNode,
|
|
9
|
-
|
|
8
|
+
runAsSingleSceneHistoryStep,
|
|
9
|
+
snapPointAlongAngleRay,
|
|
10
10
|
useScene,
|
|
11
11
|
type WallNode,
|
|
12
12
|
WallNode as WallSchema,
|
|
@@ -14,38 +14,57 @@ import {
|
|
|
14
14
|
} from '@pascal-app/core'
|
|
15
15
|
import { useViewer } from '@pascal-app/viewer'
|
|
16
16
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
17
|
-
import
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import { resolveSnapFlags } from '../../../lib/snapping-mode'
|
|
18
|
+
import useEditor, { getActiveSnappingMode, isMagneticSnapActive } from '../../../store/use-editor'
|
|
19
|
+
import {
|
|
20
|
+
distanceSquared,
|
|
21
|
+
findWallSnapTarget,
|
|
22
|
+
findWallSpecialPointSnap,
|
|
23
|
+
projectPointOntoWall,
|
|
24
|
+
WALL_CONNECT_SNAP_RADIUS,
|
|
25
|
+
WALL_JOIN_SNAP_RADIUS,
|
|
26
|
+
type WallDraftSnapResult,
|
|
27
|
+
type WallPlanPoint,
|
|
28
|
+
type WallSnapRadii,
|
|
29
|
+
} from './wall-snap-geometry'
|
|
30
|
+
|
|
31
|
+
// The pure snap geometry lives in `./wall-snap-geometry`; re-exported here so
|
|
32
|
+
// existing importers (fence drafting, the editor barrel) keep their paths.
|
|
33
|
+
export {
|
|
34
|
+
chainEndJoinsExistingWall,
|
|
35
|
+
findWallSnapTarget,
|
|
36
|
+
WALL_CONNECT_SNAP_RADIUS,
|
|
37
|
+
WALL_JOIN_SNAP_RADIUS,
|
|
38
|
+
type WallDraftSnapKind,
|
|
39
|
+
type WallDraftSnapResult,
|
|
40
|
+
type WallPlanPoint,
|
|
41
|
+
type WallSnapRadii,
|
|
42
|
+
} from './wall-snap-geometry'
|
|
20
43
|
|
|
21
44
|
export const WALL_GRID_STEP = 0.5
|
|
22
|
-
export const WALL_JOIN_SNAP_RADIUS = 0.35
|
|
23
45
|
export const WALL_MIN_LENGTH = 0.01
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
0.1: Math.PI / 12,
|
|
30
|
-
0.05: Math.PI / 36,
|
|
31
|
-
}
|
|
46
|
+
// An endpoint projecting within this distance of an existing wall's corner
|
|
47
|
+
// resolves to the corner without splitting — splitting there would mint a
|
|
48
|
+
// sliver segment a hair longer than `WALL_MIN_LENGTH` that no snap radius
|
|
49
|
+
// can ever target again.
|
|
50
|
+
const WALL_SPLIT_ENDPOINT_EPSILON = 0.02
|
|
32
51
|
|
|
33
52
|
type WallSplitIntersection = {
|
|
34
|
-
|
|
53
|
+
/** `null` = snap-only outcome: resolve to `point` but split no wall. */
|
|
54
|
+
wallId: WallNode['id'] | null
|
|
35
55
|
point: WallPlanPoint
|
|
36
56
|
}
|
|
37
57
|
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
export function getWallGridStep(): number {
|
|
45
|
-
return useEditor.getState().gridSnapStep
|
|
58
|
+
export function getSegmentGridStep(): number {
|
|
59
|
+
// A 0 step means "no grid lattice" — every grid-snap consumer guards on
|
|
60
|
+
// `step <= 0` and returns the raw value, so disabling grid here suppresses
|
|
61
|
+
// the lattice for walls, fences, and every node move/affordance that reads
|
|
62
|
+
// this choke point, without retuning their snap math.
|
|
63
|
+
return resolveSnapFlags(getActiveSnappingMode()).grid ? useEditor.getState().gridSnapStep : 0
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
export function snapScalarToGrid(value: number, step = WALL_GRID_STEP): number {
|
|
67
|
+
if (step <= 0) return value
|
|
49
68
|
return Math.round(value / step) * step
|
|
50
69
|
}
|
|
51
70
|
|
|
@@ -53,46 +72,6 @@ export function snapPointToGrid(point: WallPlanPoint, step = WALL_GRID_STEP): Wa
|
|
|
53
72
|
return [snapScalarToGrid(point[0], step), snapScalarToGrid(point[1], step)]
|
|
54
73
|
}
|
|
55
74
|
|
|
56
|
-
export function snapPointTo45Degrees(
|
|
57
|
-
start: WallPlanPoint,
|
|
58
|
-
cursor: WallPlanPoint,
|
|
59
|
-
step = WALL_GRID_STEP,
|
|
60
|
-
angleStep = DEFAULT_WALL_ANGLE_SNAP_STEP,
|
|
61
|
-
): WallPlanPoint {
|
|
62
|
-
const dx = cursor[0] - start[0]
|
|
63
|
-
const dz = cursor[1] - start[1]
|
|
64
|
-
const angle = Math.atan2(dz, dx)
|
|
65
|
-
const snappedAngle = Math.round(angle / angleStep) * angleStep
|
|
66
|
-
const distance = Math.sqrt(dx * dx + dz * dz)
|
|
67
|
-
|
|
68
|
-
return snapPointToGrid(
|
|
69
|
-
[start[0] + Math.cos(snappedAngle) * distance, start[1] + Math.sin(snappedAngle) * distance],
|
|
70
|
-
step,
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function getWallAngleSnapStep(step = getWallGridStep()): number {
|
|
75
|
-
return WALL_ANGLE_SNAP_BY_GRID_STEP[step] ?? DEFAULT_WALL_ANGLE_SNAP_STEP
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoint | null {
|
|
79
|
-
const [x1, z1] = wall.start
|
|
80
|
-
const [x2, z2] = wall.end
|
|
81
|
-
const dx = x2 - x1
|
|
82
|
-
const dz = z2 - z1
|
|
83
|
-
const lengthSquared = dx * dx + dz * dz
|
|
84
|
-
if (lengthSquared < 1e-9) {
|
|
85
|
-
return null
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
|
|
89
|
-
if (t <= 0 || t >= 1) {
|
|
90
|
-
return null
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return [x1 + dx * t, z1 + dz * t]
|
|
94
|
-
}
|
|
95
|
-
|
|
96
75
|
function splitWallAtPoint(wall: WallNode, splitPoint: WallPlanPoint): [WallNode, WallNode] {
|
|
97
76
|
const { id: _id, parentId: _parentId, children, ...rest } = wall
|
|
98
77
|
|
|
@@ -119,6 +98,7 @@ function pointsEqual(a: WallPlanPoint, b: WallPlanPoint, tolerance = 1e-6): bool
|
|
|
119
98
|
function findWallIntersection(
|
|
120
99
|
point: WallPlanPoint,
|
|
121
100
|
walls: WallNode[],
|
|
101
|
+
radius: number,
|
|
122
102
|
ignoreWallIds?: string[],
|
|
123
103
|
): WallSplitIntersection | null {
|
|
124
104
|
const ignore = new Set(ignoreWallIds ?? [])
|
|
@@ -133,13 +113,20 @@ function findWallIntersection(
|
|
|
133
113
|
|
|
134
114
|
const candidateDistanceSquared = distanceSquared(point, projected)
|
|
135
115
|
if (
|
|
136
|
-
candidateDistanceSquared >
|
|
116
|
+
candidateDistanceSquared > radius * radius ||
|
|
137
117
|
candidateDistanceSquared >= bestDistanceSquared
|
|
138
118
|
) {
|
|
139
119
|
continue
|
|
140
120
|
}
|
|
141
121
|
|
|
142
|
-
|
|
122
|
+
const nearCorner = ([wall.start, wall.end] as WallPlanPoint[]).find(
|
|
123
|
+
(corner) =>
|
|
124
|
+
distanceSquared(projected, corner) <=
|
|
125
|
+
WALL_SPLIT_ENDPOINT_EPSILON * WALL_SPLIT_ENDPOINT_EPSILON,
|
|
126
|
+
)
|
|
127
|
+
best = nearCorner
|
|
128
|
+
? { wallId: null, point: [nearCorner[0], nearCorner[1]] }
|
|
129
|
+
: { wallId: wall.id, point: projected }
|
|
143
130
|
bestDistanceSquared = candidateDistanceSquared
|
|
144
131
|
}
|
|
145
132
|
|
|
@@ -291,6 +278,10 @@ function splitWallIfNeeded(
|
|
|
291
278
|
): { walls: WallNode[]; point: WallPlanPoint } | null {
|
|
292
279
|
if (!intersection) return null
|
|
293
280
|
|
|
281
|
+
if (!intersection.wallId) {
|
|
282
|
+
return { walls, point: intersection.point }
|
|
283
|
+
}
|
|
284
|
+
|
|
294
285
|
const wallToSplit = walls.find((wall) => wall.id === intersection.wallId)
|
|
295
286
|
if (!wallToSplit) {
|
|
296
287
|
return { walls, point: intersection.point }
|
|
@@ -324,76 +315,141 @@ function splitWallIfNeeded(
|
|
|
324
315
|
}
|
|
325
316
|
}
|
|
326
317
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const candidateDistanceSquared = distanceSquared(point, candidate)
|
|
359
|
-
if (
|
|
360
|
-
candidateDistanceSquared > radiusSquared ||
|
|
361
|
-
candidateDistanceSquared >= bestDistanceSquared
|
|
362
|
-
) {
|
|
363
|
-
continue
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
bestTarget = candidate
|
|
367
|
-
bestDistanceSquared = candidateDistanceSquared
|
|
368
|
-
}
|
|
369
|
-
}
|
|
318
|
+
/**
|
|
319
|
+
* Commit-time split resolution for an endpoint MOVE — the sibling of the
|
|
320
|
+
* inline resolution in `createWallOnCurrentLevel`: when a moved endpoint is
|
|
321
|
+
* dropped on another wall's interior, split that host exactly like the draw
|
|
322
|
+
* path (duplicate props, migrate attachments by span, skip the split when an
|
|
323
|
+
* opening straddles the point). Mutates the scene store (create halves /
|
|
324
|
+
* migrate attachments / delete host), so callers MUST run it inside the same
|
|
325
|
+
* `runAsSingleSceneHistoryStep` as their endpoint write.
|
|
326
|
+
*
|
|
327
|
+
* Returns the resolved endpoint (projection onto the host, or a nearby corner
|
|
328
|
+
* when the drop is within `WALL_SPLIT_ENDPOINT_EPSILON` of one — corner joins
|
|
329
|
+
* are not splits), or `null` when the point lands on no wall.
|
|
330
|
+
*/
|
|
331
|
+
export function resolveEndpointWallSplit(args: {
|
|
332
|
+
point: WallPlanPoint
|
|
333
|
+
/** Level the moved wall lives on — only its walls are split candidates. */
|
|
334
|
+
levelId: string | null
|
|
335
|
+
/** The moved wall + every wall receiving an endpoint update in the same commit. */
|
|
336
|
+
ignoreWallIds: string[]
|
|
337
|
+
/**
|
|
338
|
+
* Capture radius. The endpoint already snapped onto the wall body during
|
|
339
|
+
* the drag, so the tight connect radius (drop genuinely on the wall) is
|
|
340
|
+
* the default.
|
|
341
|
+
*/
|
|
342
|
+
radius?: number
|
|
343
|
+
}): WallPlanPoint | null {
|
|
344
|
+
const { point, levelId, ignoreWallIds, radius = WALL_CONNECT_SNAP_RADIUS } = args
|
|
345
|
+
const { nodes, createNodes, updateNodes, deleteNode } = useScene.getState()
|
|
346
|
+
const walls = Object.values(nodes).filter(
|
|
347
|
+
(node): node is WallNode => node?.type === 'wall' && (node.parentId ?? null) === levelId,
|
|
348
|
+
)
|
|
370
349
|
|
|
371
|
-
|
|
350
|
+
const intersection = findWallIntersection(point, walls, radius, ignoreWallIds)
|
|
351
|
+
const split = splitWallIfNeeded(intersection, walls, nodes, createNodes, updateNodes, deleteNode)
|
|
352
|
+
return split ? split.point : null
|
|
372
353
|
}
|
|
373
354
|
|
|
374
|
-
|
|
355
|
+
type SnapWallDraftArgs = {
|
|
375
356
|
point: WallPlanPoint
|
|
376
357
|
walls: WallNode[]
|
|
377
358
|
start?: WallPlanPoint
|
|
378
359
|
angleSnap?: boolean
|
|
379
360
|
ignoreWallIds?: string[]
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
361
|
+
bypassSnap?: boolean
|
|
362
|
+
/** Override the grid step. */
|
|
363
|
+
step?: number
|
|
364
|
+
/**
|
|
365
|
+
* Magnetic snapping to existing wall geometry (corners, midpoints,
|
|
366
|
+
* crossings, wall bodies). When `false`, only grid/angle snap applies and
|
|
367
|
+
* `snap` is always `null`. Defaults to `true` so callers that don't care
|
|
368
|
+
* keep the prior behaviour.
|
|
369
|
+
*/
|
|
370
|
+
magnetic?: boolean
|
|
371
|
+
/**
|
|
372
|
+
* Optional grid-snap override. Lets the caller route grid snapping
|
|
373
|
+
* through a world-XZ aligned snap (so a rotated building's draft
|
|
374
|
+
* lands on the visible grid). When omitted, falls back to the
|
|
375
|
+
* local-axis grid at `step`.
|
|
376
|
+
*/
|
|
377
|
+
gridSnap?: (point: WallPlanPoint) => WallPlanPoint
|
|
378
|
+
/** Optional magnetic snap radii. Omitted means wall tools keep their defaults. */
|
|
379
|
+
snapRadii?: WallSnapRadii
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function snapWallDraftPointDetailed(args: SnapWallDraftArgs): WallDraftSnapResult {
|
|
383
|
+
const {
|
|
384
|
+
point,
|
|
385
|
+
walls,
|
|
386
|
+
start,
|
|
387
|
+
angleSnap = false,
|
|
388
|
+
ignoreWallIds,
|
|
389
|
+
bypassSnap = false,
|
|
390
|
+
step: overrideStep,
|
|
391
|
+
magnetic = true,
|
|
392
|
+
gridSnap,
|
|
393
|
+
snapRadii,
|
|
394
|
+
} = args
|
|
395
|
+
|
|
396
|
+
if (bypassSnap) return { point, snap: null }
|
|
397
|
+
|
|
398
|
+
// Discrete special points (corner / midpoint / crossing) are taken from the
|
|
399
|
+
// raw cursor so an interim grid snap can't mask them. A corner always wins,
|
|
400
|
+
// then the nearer of midpoint / crossing — see `findWallSpecialPointSnap`.
|
|
401
|
+
if (magnetic) {
|
|
402
|
+
const special = findWallSpecialPointSnap(point, walls, ignoreWallIds, snapRadii)
|
|
403
|
+
if (special) return special
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const step = overrideStep ?? getSegmentGridStep()
|
|
407
|
+
// The angle path snaps the distance ALONG the 15° ray — a scalar, the
|
|
408
|
+
// same in world and local frames — so the `gridSnap` world-grid override
|
|
409
|
+
// only applies when the angle lock is off.
|
|
410
|
+
const basePoint: WallPlanPoint =
|
|
385
411
|
start && angleSnap
|
|
386
|
-
?
|
|
387
|
-
:
|
|
412
|
+
? [...snapPointAlongAngleRay(start, point, DEFAULT_ANGLE_STEP, step)]
|
|
413
|
+
: gridSnap
|
|
414
|
+
? gridSnap(point)
|
|
415
|
+
: snapPointToGrid(point, step)
|
|
388
416
|
|
|
389
|
-
|
|
390
|
-
findWallSnapTarget(basePoint, walls, {
|
|
417
|
+
if (magnetic) {
|
|
418
|
+
const wallSnap = findWallSnapTarget(basePoint, walls, {
|
|
391
419
|
ignoreWallIds,
|
|
392
|
-
|
|
393
|
-
|
|
420
|
+
radius: snapRadii?.wall,
|
|
421
|
+
})
|
|
422
|
+
if (wallSnap) return { point: wallSnap, snap: 'wall' }
|
|
423
|
+
return { point: basePoint, snap: null }
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Non-magnetic modes (grid / off / angles): connectivity still sticks so a
|
|
427
|
+
// room can close, but only within a tight radius — placement elsewhere is left
|
|
428
|
+
// to the mode (grid quantise / angle lock / free). Snap from the already
|
|
429
|
+
// positioned `basePoint` so the mode's placement is respected right up to the
|
|
430
|
+
// wall, then the last few cm stick onto it (and the beacon shows).
|
|
431
|
+
const connectRadii: WallSnapRadii = {
|
|
432
|
+
endpoint: WALL_CONNECT_SNAP_RADIUS,
|
|
433
|
+
midpoint: WALL_CONNECT_SNAP_RADIUS,
|
|
434
|
+
intersection: WALL_CONNECT_SNAP_RADIUS,
|
|
435
|
+
wall: WALL_CONNECT_SNAP_RADIUS,
|
|
436
|
+
}
|
|
437
|
+
const connectSpecial = findWallSpecialPointSnap(basePoint, walls, ignoreWallIds, connectRadii)
|
|
438
|
+
if (connectSpecial) return connectSpecial
|
|
439
|
+
const connectWall = findWallSnapTarget(basePoint, walls, {
|
|
440
|
+
ignoreWallIds,
|
|
441
|
+
radius: WALL_CONNECT_SNAP_RADIUS,
|
|
442
|
+
})
|
|
443
|
+
if (connectWall) return { point: connectWall, snap: 'wall' }
|
|
444
|
+
|
|
445
|
+
return { point: basePoint, snap: null }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function snapWallDraftPoint(args: SnapWallDraftArgs): WallPlanPoint {
|
|
449
|
+
return snapWallDraftPointDetailed(args).point
|
|
394
450
|
}
|
|
395
451
|
|
|
396
|
-
export function
|
|
452
|
+
export function isSegmentLongEnough(start: WallPlanPoint, end: WallPlanPoint): boolean {
|
|
397
453
|
return distanceSquared(start, end) >= WALL_MIN_LENGTH * WALL_MIN_LENGTH
|
|
398
454
|
}
|
|
399
455
|
|
|
@@ -405,7 +461,7 @@ export function createWallOnCurrentLevel(
|
|
|
405
461
|
const { createNode, createNodes, deleteNode, nodes } = useScene.getState()
|
|
406
462
|
const { updateNodes } = useScene.getState()
|
|
407
463
|
|
|
408
|
-
if (!(currentLevelId &&
|
|
464
|
+
if (!(currentLevelId && isSegmentLongEnough(start, end))) {
|
|
409
465
|
return null
|
|
410
466
|
}
|
|
411
467
|
|
|
@@ -416,56 +472,77 @@ export function createWallOnCurrentLevel(
|
|
|
416
472
|
let resolvedStart = start
|
|
417
473
|
let resolvedEnd = end
|
|
418
474
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
475
|
+
// The corner-join / wall-split resolution follows the snapping mode like the
|
|
476
|
+
// draft preview does: magnetic ('lines') keeps the generous join radius,
|
|
477
|
+
// every other mode uses the same tight connect radius the draft path already
|
|
478
|
+
// sticks endpoints with. So an endpoint the user saw connect to a wall body
|
|
479
|
+
// actually splits that wall (and redistributes its attachments) in every
|
|
480
|
+
// mode, while `'off'` / `'angles'` gain no residual long-range snap.
|
|
481
|
+
const joinRadius = isMagneticSnapActive() ? WALL_JOIN_SNAP_RADIUS : WALL_CONNECT_SNAP_RADIUS
|
|
482
|
+
|
|
483
|
+
// One undo step for the whole commit: the split ops (create halves, migrate
|
|
484
|
+
// attachments, delete host) plus the new wall each push their own history
|
|
485
|
+
// entry, and a single Ctrl-Z must not strand a half-split wall network.
|
|
486
|
+
return runAsSingleSceneHistoryStep(useScene, () => {
|
|
487
|
+
const endIntersection = findWallIntersection(resolvedEnd, workingWalls, joinRadius)
|
|
488
|
+
const splitEnd = splitWallIfNeeded(
|
|
489
|
+
endIntersection,
|
|
490
|
+
workingWalls,
|
|
491
|
+
nodes,
|
|
492
|
+
createNodes,
|
|
493
|
+
updateNodes,
|
|
494
|
+
deleteNode,
|
|
495
|
+
)
|
|
496
|
+
if (splitEnd) {
|
|
497
|
+
workingWalls = splitEnd.walls
|
|
498
|
+
resolvedEnd = splitEnd.point
|
|
499
|
+
}
|
|
432
500
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
501
|
+
const startIntersection = findWallIntersection(resolvedStart, workingWalls, joinRadius)
|
|
502
|
+
const splitStart = splitWallIfNeeded(
|
|
503
|
+
startIntersection,
|
|
504
|
+
workingWalls,
|
|
505
|
+
nodes,
|
|
506
|
+
createNodes,
|
|
507
|
+
updateNodes,
|
|
508
|
+
deleteNode,
|
|
509
|
+
)
|
|
510
|
+
if (splitStart) {
|
|
511
|
+
workingWalls = splitStart.walls
|
|
512
|
+
resolvedStart = splitStart.point
|
|
513
|
+
}
|
|
446
514
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
515
|
+
if (
|
|
516
|
+
!isSegmentLongEnough(resolvedStart, resolvedEnd) ||
|
|
517
|
+
pointsEqual(resolvedStart, resolvedEnd)
|
|
518
|
+
) {
|
|
519
|
+
return null
|
|
520
|
+
}
|
|
450
521
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
522
|
+
const duplicateWall = workingWalls.some(
|
|
523
|
+
(wall) =>
|
|
524
|
+
(pointsEqual(wall.start, resolvedStart) && pointsEqual(wall.end, resolvedEnd)) ||
|
|
525
|
+
(pointsEqual(wall.start, resolvedEnd) && pointsEqual(wall.end, resolvedStart)),
|
|
526
|
+
)
|
|
527
|
+
if (duplicateWall) {
|
|
528
|
+
return null
|
|
529
|
+
}
|
|
459
530
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
531
|
+
const wallCount = Object.values(nodes).filter((node) => node.type === 'wall').length
|
|
532
|
+
// A placed wall preset seeds `toolDefaults.wall` (thickness, height,
|
|
533
|
+
// materials, sides) before the tool activates; merge those first so the
|
|
534
|
+
// drawn wall reproduces the preset. Identity + endpoints always win.
|
|
535
|
+
const defaults = useEditor.getState().toolDefaults.wall ?? {}
|
|
536
|
+
const wall = WallSchema.parse({
|
|
537
|
+
...defaults,
|
|
538
|
+
name: `Wall ${wallCount + 1}`,
|
|
539
|
+
start: resolvedStart,
|
|
540
|
+
end: resolvedEnd,
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
createNode(wall, currentLevelId)
|
|
544
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
545
|
+
|
|
546
|
+
return wall
|
|
465
547
|
})
|
|
466
|
-
|
|
467
|
-
createNode(wall, currentLevelId)
|
|
468
|
-
sfxEmitter.emit('sfx:structure-build')
|
|
469
|
-
|
|
470
|
-
return wall
|
|
471
548
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { WallNode } from '@pascal-app/core'
|
|
3
|
+
import {
|
|
4
|
+
chainEndJoinsExistingWall,
|
|
5
|
+
findWallSnapTarget,
|
|
6
|
+
findWallSpecialPointSnap,
|
|
7
|
+
type WallPlanPoint,
|
|
8
|
+
} from './wall-snap-geometry'
|
|
9
|
+
|
|
10
|
+
function makeWall(start: WallPlanPoint, end: WallPlanPoint, id?: string): WallNode {
|
|
11
|
+
return {
|
|
12
|
+
object: 'node',
|
|
13
|
+
id: (id ?? `wall_${start.join('_')}_${end.join('_')}`) as WallNode['id'],
|
|
14
|
+
type: 'wall',
|
|
15
|
+
name: 'Wall',
|
|
16
|
+
parentId: null,
|
|
17
|
+
visible: true,
|
|
18
|
+
metadata: {},
|
|
19
|
+
children: [],
|
|
20
|
+
start,
|
|
21
|
+
end,
|
|
22
|
+
thickness: 0.1,
|
|
23
|
+
frontSide: 'unknown',
|
|
24
|
+
backSide: 'unknown',
|
|
25
|
+
} as unknown as WallNode
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('findWallSpecialPointSnap', () => {
|
|
29
|
+
test('snaps to a wall corner (endpoint) when near it', () => {
|
|
30
|
+
const walls = [makeWall([0, 0], [4, 0])]
|
|
31
|
+
const result = findWallSpecialPointSnap([0.1, 0.1], walls)
|
|
32
|
+
expect(result?.snap).toBe('endpoint')
|
|
33
|
+
expect(result?.point).toEqual([0, 0])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('snaps to a wall midpoint when near it (not a corner)', () => {
|
|
37
|
+
const walls = [makeWall([0, 0], [4, 0])]
|
|
38
|
+
const result = findWallSpecialPointSnap([2.1, 0.2], walls)
|
|
39
|
+
expect(result?.snap).toBe('midpoint')
|
|
40
|
+
expect(result?.point).toEqual([2, 0])
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('snaps to the crossing of two walls (intersection)', () => {
|
|
44
|
+
// A runs along z=0; B crosses it at x=1. B's own midpoint is [1,1], far
|
|
45
|
+
// from the crossing, so the snap is the intersection, not a midpoint.
|
|
46
|
+
const walls = [makeWall([0, 0], [4, 0], 'a'), makeWall([1, -1], [1, 3], 'b')]
|
|
47
|
+
const result = findWallSpecialPointSnap([1.1, 0.1], walls)
|
|
48
|
+
expect(result?.snap).toBe('intersection')
|
|
49
|
+
expect(result?.point[0]).toBeCloseTo(1, 6)
|
|
50
|
+
expect(result?.point[1]).toBeCloseTo(0, 6)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('corner wins over a midpoint when both are in range', () => {
|
|
54
|
+
const walls = [makeWall([0, 0], [1, 0])]
|
|
55
|
+
const result = findWallSpecialPointSnap([0.9, 0.1], walls)
|
|
56
|
+
expect(result?.snap).toBe('endpoint')
|
|
57
|
+
expect(result?.point).toEqual([1, 0])
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('returns null when no special point is in range', () => {
|
|
61
|
+
const walls = [makeWall([0, 0], [4, 0])]
|
|
62
|
+
// Near the wall body but far from corner/midpoint — that's an edge snap,
|
|
63
|
+
// handled separately by findWallSnapTarget, not a special point.
|
|
64
|
+
expect(findWallSpecialPointSnap([1.2, 0.1], walls)).toBeNull()
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('honors tighter per-call radii without changing defaults', () => {
|
|
68
|
+
const walls = [makeWall([0, 0], [4, 0])]
|
|
69
|
+
|
|
70
|
+
expect(findWallSpecialPointSnap([0.34, 0], walls)?.snap).toBe('endpoint')
|
|
71
|
+
expect(findWallSpecialPointSnap([0.34, 0], walls, undefined, { endpoint: 0.3 })).toBeNull()
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('chainEndJoinsExistingWall (chain termination)', () => {
|
|
76
|
+
test('true when the end lies on a non-chain wall interior (T junction)', () => {
|
|
77
|
+
const walls = [makeWall([0, 0], [4, 0], 'host'), makeWall([2, 2], [2, 0], 'chain_1')]
|
|
78
|
+
expect(chainEndJoinsExistingWall([2, 0], walls, ['chain_1'])).toBe(true)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('true when the end lands on a non-chain wall endpoint', () => {
|
|
82
|
+
const walls = [makeWall([0, 0], [4, 0], 'host'), makeWall([6, 2], [4, 0], 'chain_1')]
|
|
83
|
+
expect(chainEndJoinsExistingWall([4, 0], walls, ['chain_1'])).toBe(true)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('false when the end only touches the chain walls themselves', () => {
|
|
87
|
+
// Second segment doubles back onto the first one's midpoint — own-chain
|
|
88
|
+
// geometry is excluded, so the chain keeps going.
|
|
89
|
+
const walls = [makeWall([0, 0], [4, 0], 'chain_1'), makeWall([4, 0], [2, 0], 'chain_2')]
|
|
90
|
+
expect(chainEndJoinsExistingWall([2, 0], walls, ['chain_1', 'chain_2'])).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('false for a dead end in free space', () => {
|
|
94
|
+
const walls = [makeWall([0, 0], [4, 0], 'host'), makeWall([0, 2], [2, 2], 'chain_1')]
|
|
95
|
+
expect(chainEndJoinsExistingWall([2, 2], walls, ['chain_1'])).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('a near miss beyond the tolerance is not a join', () => {
|
|
99
|
+
const walls = [makeWall([0, 0], [4, 0], 'host')]
|
|
100
|
+
expect(chainEndJoinsExistingWall([2, 0.01], walls, [])).toBe(false)
|
|
101
|
+
expect(chainEndJoinsExistingWall([2, 0.0005], walls, [])).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('findWallSnapTarget (edge / along-wall)', () => {
|
|
106
|
+
test('projects onto a wall body within range', () => {
|
|
107
|
+
const walls = [makeWall([0, 0], [4, 0])]
|
|
108
|
+
const result = findWallSnapTarget([1.2, 0.1], walls)
|
|
109
|
+
expect(result?.[0]).toBeCloseTo(1.2, 6)
|
|
110
|
+
expect(result?.[1]).toBeCloseTo(0, 6)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
test('returns null when too far from any wall', () => {
|
|
114
|
+
const walls = [makeWall([0, 0], [4, 0])]
|
|
115
|
+
expect(findWallSnapTarget([1.2, 2], walls)).toBeNull()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('honors a tighter wall-body radius', () => {
|
|
119
|
+
const walls = [makeWall([0, 0], [4, 0])]
|
|
120
|
+
|
|
121
|
+
expect(findWallSnapTarget([1.2, 0.1], walls, { radius: 0.08 })).toBeNull()
|
|
122
|
+
})
|
|
123
|
+
})
|