@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
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// Pure geometry for "magnetic" wall-draft snapping — no store / viewer / React
|
|
2
|
+
// deps, so it's unit-testable in isolation. Coordinates are XZ plan points
|
|
3
|
+
// (building-local meters). `wall-drafting.ts` layers grid/angle snapping and
|
|
4
|
+
// scene access on top of these primitives.
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getWallCurveFrameAt,
|
|
8
|
+
getWallCurveLength,
|
|
9
|
+
isCurvedWall,
|
|
10
|
+
type WallNode,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
|
|
13
|
+
export type WallPlanPoint = [number, number]
|
|
14
|
+
|
|
15
|
+
/** Which kind of existing-geometry snap produced a drafted point. */
|
|
16
|
+
export type WallDraftSnapKind = 'endpoint' | 'midpoint' | 'intersection' | 'wall'
|
|
17
|
+
|
|
18
|
+
export type WallSnapRadii = Partial<Record<WallDraftSnapKind, number>>
|
|
19
|
+
|
|
20
|
+
export type WallDraftSnapResult = {
|
|
21
|
+
point: WallPlanPoint
|
|
22
|
+
/**
|
|
23
|
+
* Set when `point` locked onto existing wall geometry (a corner, midpoint,
|
|
24
|
+
* crossing, or wall body) rather than a plain grid/angle position. This is
|
|
25
|
+
* the "magnetic" snap the beacon visualises; `null` for grid/angle-only.
|
|
26
|
+
*/
|
|
27
|
+
snap: WallDraftSnapKind | null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const WALL_JOIN_SNAP_RADIUS = 0.35
|
|
31
|
+
// Tight capture radius for the connectivity snap that runs in NON-magnetic modes
|
|
32
|
+
// (grid / off / angles). Joining an existing wall is treated as connectivity —
|
|
33
|
+
// separate from the 'lines' magnetic alignment — so a room can still close in
|
|
34
|
+
// those modes: within this distance of a wall the endpoint sticks onto it (and
|
|
35
|
+
// the beacon shows); beyond it, positioning is left to the mode. Kept small so
|
|
36
|
+
// only the last few cm near a wall stick, well above the room-detection junction
|
|
37
|
+
// tolerance so a captured endpoint always registers as connected.
|
|
38
|
+
export const WALL_CONNECT_SNAP_RADIUS = 0.05
|
|
39
|
+
// Generous radius for snapping to an *existing* wall's endpoint while
|
|
40
|
+
// drafting. Larger than `WALL_JOIN_SNAP_RADIUS` because endpoint snap
|
|
41
|
+
// is the strongest user intent (closing a polygon, attaching to a
|
|
42
|
+
// corner) and the cursor never lands pixel-perfect on a corner.
|
|
43
|
+
export const WALL_ENDPOINT_SNAP_RADIUS = 0.7
|
|
44
|
+
// Discrete "special point" snaps taken from the raw cursor (like the
|
|
45
|
+
// endpoint snap) but slightly tighter — a corner is the strongest intent,
|
|
46
|
+
// a midpoint / crossing is the next tier down.
|
|
47
|
+
export const WALL_MIDPOINT_SNAP_RADIUS = 0.5
|
|
48
|
+
export const WALL_INTERSECTION_SNAP_RADIUS = 0.5
|
|
49
|
+
|
|
50
|
+
export function distanceSquared(a: WallPlanPoint, b: WallPlanPoint): number {
|
|
51
|
+
const dx = a[0] - b[0]
|
|
52
|
+
const dz = a[1] - b[1]
|
|
53
|
+
return dx * dx + dz * dz
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function projectPointOntoWall(point: WallPlanPoint, wall: WallNode): WallPlanPoint | null {
|
|
57
|
+
const [x1, z1] = wall.start
|
|
58
|
+
const [x2, z2] = wall.end
|
|
59
|
+
const dx = x2 - x1
|
|
60
|
+
const dz = z2 - z1
|
|
61
|
+
const lengthSquared = dx * dx + dz * dz
|
|
62
|
+
if (lengthSquared < 1e-9) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const t = ((point[0] - x1) * dx + (point[1] - z1) * dz) / lengthSquared
|
|
67
|
+
if (t <= 0 || t >= 1) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return [x1 + dx * t, z1 + dz * t]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function findWallSnapTarget(
|
|
75
|
+
point: WallPlanPoint,
|
|
76
|
+
walls: WallNode[],
|
|
77
|
+
options?: { ignoreWallIds?: string[]; radius?: number },
|
|
78
|
+
): WallPlanPoint | null {
|
|
79
|
+
const ignoreWallIds = new Set(options?.ignoreWallIds ?? [])
|
|
80
|
+
const radiusSquared = (options?.radius ?? WALL_JOIN_SNAP_RADIUS) ** 2
|
|
81
|
+
let bestTarget: WallPlanPoint | null = null
|
|
82
|
+
let bestDistanceSquared = Number.POSITIVE_INFINITY
|
|
83
|
+
|
|
84
|
+
for (const wall of walls) {
|
|
85
|
+
if (ignoreWallIds.has(wall.id)) {
|
|
86
|
+
continue
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const candidates: Array<WallPlanPoint | null> = [wall.start, wall.end]
|
|
90
|
+
|
|
91
|
+
if (isCurvedWall(wall)) {
|
|
92
|
+
const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(wall) / 0.3))
|
|
93
|
+
for (let index = 0; index <= sampleCount; index += 1) {
|
|
94
|
+
const frame = getWallCurveFrameAt(wall, index / sampleCount)
|
|
95
|
+
candidates.push([frame.point.x, frame.point.y])
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
candidates.push(projectPointOntoWall(point, wall))
|
|
99
|
+
}
|
|
100
|
+
for (const candidate of candidates) {
|
|
101
|
+
if (!candidate) {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const candidateDistanceSquared = distanceSquared(point, candidate)
|
|
106
|
+
if (
|
|
107
|
+
candidateDistanceSquared > radiusSquared ||
|
|
108
|
+
candidateDistanceSquared >= bestDistanceSquared
|
|
109
|
+
) {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
bestTarget = candidate
|
|
114
|
+
bestDistanceSquared = candidateDistanceSquared
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return bestTarget
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Endpoint-only snap from the *raw* cursor (no grid pre-snap), with a
|
|
123
|
+
* generous radius. Use this before `findWallSnapTarget` so the strong
|
|
124
|
+
* "attach to an existing wall corner" intent isn't accidentally pushed
|
|
125
|
+
* out of range by an interim grid snap that moved the cursor away from
|
|
126
|
+
* the endpoint.
|
|
127
|
+
*/
|
|
128
|
+
export function findWallEndpointFromRaw(
|
|
129
|
+
point: WallPlanPoint,
|
|
130
|
+
walls: WallNode[],
|
|
131
|
+
ignoreWallIds?: string[],
|
|
132
|
+
radius = WALL_ENDPOINT_SNAP_RADIUS,
|
|
133
|
+
): WallPlanPoint | null {
|
|
134
|
+
const ignored = new Set(ignoreWallIds ?? [])
|
|
135
|
+
const radiusSquared = radius ** 2
|
|
136
|
+
let best: WallPlanPoint | null = null
|
|
137
|
+
let bestDistSq = Number.POSITIVE_INFINITY
|
|
138
|
+
|
|
139
|
+
for (const wall of walls) {
|
|
140
|
+
if (ignored.has(wall.id)) continue
|
|
141
|
+
for (const corner of [wall.start, wall.end] as WallPlanPoint[]) {
|
|
142
|
+
const d = distanceSquared(point, corner)
|
|
143
|
+
if (d <= radiusSquared && d < bestDistSq) {
|
|
144
|
+
best = corner
|
|
145
|
+
bestDistSq = d
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return best
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Midpoint of a wall — curve midpoint for curved walls, segment midpoint otherwise. */
|
|
153
|
+
function wallMidpoint(wall: WallNode): WallPlanPoint {
|
|
154
|
+
if (isCurvedWall(wall)) {
|
|
155
|
+
const frame = getWallCurveFrameAt(wall, 0.5)
|
|
156
|
+
return [frame.point.x, frame.point.y]
|
|
157
|
+
}
|
|
158
|
+
return [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Nearest wall midpoint to the raw cursor, within `WALL_MIDPOINT_SNAP_RADIUS`. */
|
|
162
|
+
export function findWallMidpointFromRaw(
|
|
163
|
+
point: WallPlanPoint,
|
|
164
|
+
walls: WallNode[],
|
|
165
|
+
ignoreWallIds?: string[],
|
|
166
|
+
radius = WALL_MIDPOINT_SNAP_RADIUS,
|
|
167
|
+
): WallPlanPoint | null {
|
|
168
|
+
const ignored = new Set(ignoreWallIds ?? [])
|
|
169
|
+
const radiusSquared = radius ** 2
|
|
170
|
+
let best: WallPlanPoint | null = null
|
|
171
|
+
let bestDistSq = Number.POSITIVE_INFINITY
|
|
172
|
+
|
|
173
|
+
for (const wall of walls) {
|
|
174
|
+
if (ignored.has(wall.id)) continue
|
|
175
|
+
const mid = wallMidpoint(wall)
|
|
176
|
+
const d = distanceSquared(point, mid)
|
|
177
|
+
if (d <= radiusSquared && d < bestDistSq) {
|
|
178
|
+
best = mid
|
|
179
|
+
bestDistSq = d
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return best
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Crossing point of two straight segments, or null if they don't intersect within both. */
|
|
186
|
+
function segmentIntersection(
|
|
187
|
+
a1: WallPlanPoint,
|
|
188
|
+
a2: WallPlanPoint,
|
|
189
|
+
b1: WallPlanPoint,
|
|
190
|
+
b2: WallPlanPoint,
|
|
191
|
+
): WallPlanPoint | null {
|
|
192
|
+
const rx = a2[0] - a1[0]
|
|
193
|
+
const rz = a2[1] - a1[1]
|
|
194
|
+
const sx = b2[0] - b1[0]
|
|
195
|
+
const sz = b2[1] - b1[1]
|
|
196
|
+
const denom = rx * sz - rz * sx
|
|
197
|
+
if (Math.abs(denom) < 1e-9) return null // parallel / collinear
|
|
198
|
+
|
|
199
|
+
const qpx = b1[0] - a1[0]
|
|
200
|
+
const qpz = b1[1] - a1[1]
|
|
201
|
+
const t = (qpx * sz - qpz * sx) / denom
|
|
202
|
+
const u = (qpx * rz - qpz * rx) / denom
|
|
203
|
+
if (t < 0 || t > 1 || u < 0 || u > 1) return null
|
|
204
|
+
|
|
205
|
+
return [a1[0] + t * rx, a1[1] + t * rz]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Nearest point where two existing straight walls cross, within
|
|
210
|
+
* `WALL_INTERSECTION_SNAP_RADIUS`. Curved walls are skipped. O(n²) over the
|
|
211
|
+
* level's walls — fine at editor scale.
|
|
212
|
+
*/
|
|
213
|
+
export function findWallIntersectionFromRaw(
|
|
214
|
+
point: WallPlanPoint,
|
|
215
|
+
walls: WallNode[],
|
|
216
|
+
ignoreWallIds?: string[],
|
|
217
|
+
radius = WALL_INTERSECTION_SNAP_RADIUS,
|
|
218
|
+
): WallPlanPoint | null {
|
|
219
|
+
const ignored = new Set(ignoreWallIds ?? [])
|
|
220
|
+
const straight = walls.filter((wall) => !ignored.has(wall.id) && !isCurvedWall(wall))
|
|
221
|
+
const radiusSquared = radius ** 2
|
|
222
|
+
let best: WallPlanPoint | null = null
|
|
223
|
+
let bestDistSq = Number.POSITIVE_INFINITY
|
|
224
|
+
|
|
225
|
+
for (let i = 0; i < straight.length; i += 1) {
|
|
226
|
+
for (let j = i + 1; j < straight.length; j += 1) {
|
|
227
|
+
const crossing = segmentIntersection(
|
|
228
|
+
straight[i]!.start,
|
|
229
|
+
straight[i]!.end,
|
|
230
|
+
straight[j]!.start,
|
|
231
|
+
straight[j]!.end,
|
|
232
|
+
)
|
|
233
|
+
if (!crossing) continue
|
|
234
|
+
const d = distanceSquared(point, crossing)
|
|
235
|
+
if (d <= radiusSquared && d < bestDistSq) {
|
|
236
|
+
best = crossing
|
|
237
|
+
bestDistSq = d
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return best
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Pick the candidate nearest to `point`, ignoring nulls. */
|
|
245
|
+
function nearestCandidate(
|
|
246
|
+
point: WallPlanPoint,
|
|
247
|
+
candidates: Array<WallDraftSnapResult | null | false>,
|
|
248
|
+
): WallDraftSnapResult | null {
|
|
249
|
+
let best: WallDraftSnapResult | null = null
|
|
250
|
+
let bestDistSq = Number.POSITIVE_INFINITY
|
|
251
|
+
for (const candidate of candidates) {
|
|
252
|
+
if (!candidate) continue
|
|
253
|
+
const d = distanceSquared(point, candidate.point)
|
|
254
|
+
if (d < bestDistSq) {
|
|
255
|
+
best = candidate
|
|
256
|
+
bestDistSq = d
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return best
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Tolerance for "the committed endpoint actually lies on existing wall
|
|
263
|
+
// geometry". Commit-time resolution (corner join, connect snap, split) puts
|
|
264
|
+
// the endpoint exactly on the geometry, so this only needs to absorb float
|
|
265
|
+
// drift — it is NOT a snap radius.
|
|
266
|
+
export const WALL_CHAIN_JOIN_TOLERANCE = 1e-3
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* True when a committed chain segment's resolved `end` lies on wall geometry
|
|
270
|
+
* (an endpoint, or a straight wall's interior) of a wall outside the current
|
|
271
|
+
* draft chain. The wall tools stop chaining there: a segment that tees into
|
|
272
|
+
* the existing network is a termination — continuing would draft the next
|
|
273
|
+
* segment on top of existing walls. `chainWallIds` excludes the chain's own
|
|
274
|
+
* segments (including the just-committed one) so edge/midpoint snaps onto a
|
|
275
|
+
* previous own segment don't read as a join. Curved wall interiors are
|
|
276
|
+
* skipped (their endpoints still count) — resolving an end onto a curve body
|
|
277
|
+
* is rare and continuing there matches the previous behaviour.
|
|
278
|
+
*/
|
|
279
|
+
export function chainEndJoinsExistingWall(
|
|
280
|
+
end: WallPlanPoint,
|
|
281
|
+
walls: WallNode[],
|
|
282
|
+
chainWallIds: string[],
|
|
283
|
+
tolerance = WALL_CHAIN_JOIN_TOLERANCE,
|
|
284
|
+
): boolean {
|
|
285
|
+
const ignored = new Set(chainWallIds)
|
|
286
|
+
const toleranceSquared = tolerance * tolerance
|
|
287
|
+
|
|
288
|
+
for (const wall of walls) {
|
|
289
|
+
if (ignored.has(wall.id)) continue
|
|
290
|
+
|
|
291
|
+
if (
|
|
292
|
+
distanceSquared(end, wall.start) <= toleranceSquared ||
|
|
293
|
+
distanceSquared(end, wall.end) <= toleranceSquared
|
|
294
|
+
) {
|
|
295
|
+
return true
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (isCurvedWall(wall)) continue
|
|
299
|
+
|
|
300
|
+
const projected = projectPointOntoWall(end, wall)
|
|
301
|
+
if (projected && distanceSquared(end, projected) <= toleranceSquared) {
|
|
302
|
+
return true
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return false
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Discrete "special point" snap from the raw cursor, in priority order:
|
|
311
|
+
* 1. corners (endpoints) — strongest intent, largest radius
|
|
312
|
+
* 2. midpoints / crossings — next tier; the nearer of the two wins
|
|
313
|
+
* A corner within range always wins over a midpoint/crossing. Returns null
|
|
314
|
+
* when no special point is in range (caller falls back to grid/edge snap).
|
|
315
|
+
*/
|
|
316
|
+
export function findWallSpecialPointSnap(
|
|
317
|
+
point: WallPlanPoint,
|
|
318
|
+
walls: WallNode[],
|
|
319
|
+
ignoreWallIds?: string[],
|
|
320
|
+
radii?: WallSnapRadii,
|
|
321
|
+
): WallDraftSnapResult | null {
|
|
322
|
+
const endpoint = findWallEndpointFromRaw(point, walls, ignoreWallIds, radii?.endpoint)
|
|
323
|
+
if (endpoint) return { point: endpoint, snap: 'endpoint' }
|
|
324
|
+
|
|
325
|
+
const midpoint = findWallMidpointFromRaw(point, walls, ignoreWallIds, radii?.midpoint)
|
|
326
|
+
const intersection = findWallIntersectionFromRaw(point, walls, ignoreWallIds, radii?.intersection)
|
|
327
|
+
return nearestCandidate(point, [
|
|
328
|
+
midpoint && { point: midpoint, snap: 'midpoint' },
|
|
329
|
+
intersection && { point: intersection, snap: 'intersection' },
|
|
330
|
+
])
|
|
331
|
+
}
|
|
@@ -18,7 +18,11 @@ export const ZoneBoundaryEditor: React.FC<ZoneBoundaryEditorProps> = ({ zoneId }
|
|
|
18
18
|
|
|
19
19
|
const handlePolygonChange = useCallback(
|
|
20
20
|
(newPolygon: Array<[number, number]>) => {
|
|
21
|
-
updateNode(zoneId, {
|
|
21
|
+
updateNode(zoneId, {
|
|
22
|
+
polygon: newPolygon,
|
|
23
|
+
autoFromWalls: false,
|
|
24
|
+
boundaryWallIds: [],
|
|
25
|
+
})
|
|
22
26
|
},
|
|
23
27
|
[zoneId, updateNode],
|
|
24
28
|
)
|
|
@@ -1,50 +1,27 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_ANGLE_STEP,
|
|
3
|
+
emitter,
|
|
4
|
+
type GridEvent,
|
|
5
|
+
type LevelNode,
|
|
6
|
+
snapPointAlongAngleRay,
|
|
7
|
+
useScene,
|
|
8
|
+
ZoneNode,
|
|
9
|
+
} from '@pascal-app/core'
|
|
2
10
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
11
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
4
12
|
import { BufferGeometry, DoubleSide, type Group, type Line, Shape, Vector3 } from 'three'
|
|
5
13
|
import { EDITOR_LAYER } from './../../../lib/constants'
|
|
6
14
|
import { sfxEmitter } from './../../../lib/sfx-bus'
|
|
7
|
-
import
|
|
15
|
+
import {
|
|
16
|
+
clearSurfacePlanSnapFeedback,
|
|
17
|
+
resolveSurfacePlanPointSnap,
|
|
18
|
+
} from './../../../lib/surface-plan-snap'
|
|
19
|
+
import { snapWorldXZForActiveBuilding } from './../../../lib/world-grid-snap'
|
|
20
|
+
import useEditor, { isAngleSnapActive, isGridSnapActive } from './../../../store/use-editor'
|
|
8
21
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
9
22
|
|
|
10
23
|
const Y_OFFSET = 0.02
|
|
11
24
|
|
|
12
|
-
/**
|
|
13
|
-
* Snaps a point to the nearest axis-aligned or 45-degree diagonal from the last point
|
|
14
|
-
*/
|
|
15
|
-
const calculateSnapPoint = (
|
|
16
|
-
lastPoint: [number, number],
|
|
17
|
-
currentPoint: [number, number],
|
|
18
|
-
): [number, number] => {
|
|
19
|
-
const [x1, y1] = lastPoint
|
|
20
|
-
const [x, y] = currentPoint
|
|
21
|
-
|
|
22
|
-
const dx = x - x1
|
|
23
|
-
const dy = y - y1
|
|
24
|
-
const absDx = Math.abs(dx)
|
|
25
|
-
const absDy = Math.abs(dy)
|
|
26
|
-
|
|
27
|
-
// Calculate distances to horizontal, vertical, and diagonal lines
|
|
28
|
-
const horizontalDist = absDy
|
|
29
|
-
const verticalDist = absDx
|
|
30
|
-
const diagonalDist = Math.abs(absDx - absDy)
|
|
31
|
-
|
|
32
|
-
// Find the minimum distance to determine which axis to snap to
|
|
33
|
-
const minDist = Math.min(horizontalDist, verticalDist, diagonalDist)
|
|
34
|
-
|
|
35
|
-
if (minDist === diagonalDist) {
|
|
36
|
-
// Snap to 45° diagonal
|
|
37
|
-
const diagonalLength = Math.min(absDx, absDy)
|
|
38
|
-
return [x1 + Math.sign(dx) * diagonalLength, y1 + Math.sign(dy) * diagonalLength]
|
|
39
|
-
}
|
|
40
|
-
if (minDist === horizontalDist) {
|
|
41
|
-
// Snap to horizontal
|
|
42
|
-
return [x, y1]
|
|
43
|
-
}
|
|
44
|
-
// Snap to vertical
|
|
45
|
-
return [x1, y]
|
|
46
|
-
}
|
|
47
|
-
|
|
48
25
|
/**
|
|
49
26
|
* Creates a zone with the given polygon points
|
|
50
27
|
*/
|
|
@@ -106,11 +83,35 @@ export const ZoneTool: React.FC = () => {
|
|
|
106
83
|
if (!currentLevelId) return
|
|
107
84
|
|
|
108
85
|
let cursorPosition: [number, number] = [0, 0]
|
|
86
|
+
let snappedCursorPosition: [number, number] | null = null
|
|
109
87
|
|
|
110
88
|
// Initialize line geometries
|
|
111
89
|
mainLineRef.current.geometry = new BufferGeometry()
|
|
112
90
|
closingLineRef.current.geometry = new BufferGeometry()
|
|
113
91
|
|
|
92
|
+
// Snapping follows the active mode: `angles` locks the ray to 15° from the
|
|
93
|
+
// last vertex (grid quantizes the distance along it), otherwise `grid`
|
|
94
|
+
// quantizes to the world-aligned grid; the shared surface snap then layers
|
|
95
|
+
// wall-corner/midpoint/crossing magnetics and alignment guides on top —
|
|
96
|
+
// the same pipeline slab/ceiling drafting uses. No held-Shift bypass —
|
|
97
|
+
// Shift cycles the mode (see interaction-scope.md).
|
|
98
|
+
const snapDraftPoint = (
|
|
99
|
+
lastPoint: [number, number] | undefined,
|
|
100
|
+
gridPoint: [number, number],
|
|
101
|
+
rawPoint: [number, number],
|
|
102
|
+
): [number, number] => {
|
|
103
|
+
const gridStep = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0
|
|
104
|
+
const orthoPoint: [number, number] =
|
|
105
|
+
isAngleSnapActive() && lastPoint
|
|
106
|
+
? [...snapPointAlongAngleRay(lastPoint, rawPoint, DEFAULT_ANGLE_STEP, gridStep)]
|
|
107
|
+
: gridPoint
|
|
108
|
+
return resolveSurfacePlanPointSnap({
|
|
109
|
+
rawPoint,
|
|
110
|
+
fallbackPoint: orthoPoint,
|
|
111
|
+
levelId: currentLevelId,
|
|
112
|
+
}).point
|
|
113
|
+
}
|
|
114
|
+
|
|
114
115
|
const updateLines = () => {
|
|
115
116
|
const points = pointsRef.current
|
|
116
117
|
const y = levelYRef.current + Y_OFFSET
|
|
@@ -126,11 +127,9 @@ export const ZoneTool: React.FC = () => {
|
|
|
126
127
|
|
|
127
128
|
// Add cursor point
|
|
128
129
|
const lastPoint = points[points.length - 1]
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
linePoints.push(new Vector3(snapped[0], y, snapped[1]))
|
|
133
|
-
}
|
|
130
|
+
const snapped = snappedCursorPosition ?? cursorPosition
|
|
131
|
+
if (lastPoint && isValidPoint(snapped)) {
|
|
132
|
+
linePoints.push(new Vector3(snapped[0], y, snapped[1]))
|
|
134
133
|
}
|
|
135
134
|
|
|
136
135
|
// Update main line geometry
|
|
@@ -144,17 +143,14 @@ export const ZoneTool: React.FC = () => {
|
|
|
144
143
|
|
|
145
144
|
// Update closing line (from cursor back to first point)
|
|
146
145
|
const firstPoint = points[0]
|
|
147
|
-
if (points.length >= 2 && lastPoint && isValidPoint(firstPoint)) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
|
|
156
|
-
closingLineRef.current.visible = true
|
|
157
|
-
}
|
|
146
|
+
if (points.length >= 2 && lastPoint && isValidPoint(firstPoint) && isValidPoint(snapped)) {
|
|
147
|
+
const closingPoints = [
|
|
148
|
+
new Vector3(snapped[0], y, snapped[1]),
|
|
149
|
+
new Vector3(firstPoint[0], y, firstPoint[1]),
|
|
150
|
+
]
|
|
151
|
+
closingLineRef.current.geometry.dispose()
|
|
152
|
+
closingLineRef.current.geometry = new BufferGeometry().setFromPoints(closingPoints)
|
|
153
|
+
closingLineRef.current.visible = true
|
|
158
154
|
} else {
|
|
159
155
|
closingLineRef.current.visible = false
|
|
160
156
|
}
|
|
@@ -162,36 +158,45 @@ export const ZoneTool: React.FC = () => {
|
|
|
162
158
|
|
|
163
159
|
const updatePreview = () => {
|
|
164
160
|
const points = pointsRef.current
|
|
165
|
-
const
|
|
161
|
+
const cursorPt = snappedCursorPosition ?? cursorPosition
|
|
166
162
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
cursorPt
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
setPreview({ points: [...points], cursorPoint: cursorPt, levelY: levelYRef.current })
|
|
163
|
+
setPreview({
|
|
164
|
+
points: [...points],
|
|
165
|
+
cursorPoint: isValidPoint(cursorPt) ? cursorPt : null,
|
|
166
|
+
levelY: levelYRef.current,
|
|
167
|
+
})
|
|
175
168
|
updateLines()
|
|
176
169
|
}
|
|
177
170
|
|
|
171
|
+
// World-grid snap projected into building-local; rotated buildings
|
|
172
|
+
// used to pull the snap off the visible grid lines. Grid quantize only
|
|
173
|
+
// in grid mode; off / lines / angles leave the raw cursor.
|
|
174
|
+
const gridPointOf = (event: GridEvent): [number, number] =>
|
|
175
|
+
isGridSnapActive()
|
|
176
|
+
? snapWorldXZForActiveBuilding(
|
|
177
|
+
event.position[0],
|
|
178
|
+
event.position[2],
|
|
179
|
+
useEditor.getState().gridSnapStep,
|
|
180
|
+
).local
|
|
181
|
+
: [event.localPosition[0], event.localPosition[2]]
|
|
182
|
+
|
|
178
183
|
const onGridMove = (event: GridEvent) => {
|
|
179
184
|
if (!cursorRef.current) return
|
|
180
185
|
|
|
181
|
-
|
|
182
|
-
const gridX = Math.round(event.localPosition[0] * 2) / 2
|
|
183
|
-
const gridZ = Math.round(event.localPosition[2] * 2) / 2
|
|
184
|
-
cursorPosition = [gridX, gridZ]
|
|
186
|
+
cursorPosition = gridPointOf(event)
|
|
185
187
|
levelYRef.current = event.localPosition[1]
|
|
186
188
|
|
|
187
|
-
// If we have points, snap to axis from last point
|
|
188
189
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1]
|
|
189
|
-
const displayPoint = lastPoint
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
190
|
+
const displayPoint = snapDraftPoint(lastPoint, cursorPosition, [
|
|
191
|
+
event.localPosition[0],
|
|
192
|
+
event.localPosition[2],
|
|
193
|
+
])
|
|
194
|
+
snappedCursorPosition = displayPoint
|
|
195
|
+
|
|
196
|
+
// Play snap sound when the snapped position changes during drawing — only
|
|
197
|
+
// when a quantizing mode is active (off / lines move continuously).
|
|
194
198
|
if (
|
|
199
|
+
(isGridSnapActive() || isAngleSnapActive()) &&
|
|
195
200
|
pointsRef.current.length > 0 &&
|
|
196
201
|
previousSnappedPointRef.current &&
|
|
197
202
|
(displayPoint[0] !== previousSnappedPointRef.current[0] ||
|
|
@@ -209,15 +214,11 @@ export const ZoneTool: React.FC = () => {
|
|
|
209
214
|
const onGridClick = (event: GridEvent) => {
|
|
210
215
|
if (!currentLevelId) return
|
|
211
216
|
|
|
212
|
-
const gridX = Math.round(event.localPosition[0] * 2) / 2
|
|
213
|
-
const gridZ = Math.round(event.localPosition[2] * 2) / 2
|
|
214
|
-
let clickPoint: [number, number] = [gridX, gridZ]
|
|
215
|
-
|
|
216
|
-
// Snap to axis from last point
|
|
217
217
|
const lastPoint = pointsRef.current[pointsRef.current.length - 1]
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
218
|
+
const clickPoint = snapDraftPoint(lastPoint, gridPointOf(event), [
|
|
219
|
+
event.localPosition[0],
|
|
220
|
+
event.localPosition[2],
|
|
221
|
+
])
|
|
221
222
|
|
|
222
223
|
// Check if clicking on the first point to close the shape
|
|
223
224
|
const firstPoint = pointsRef.current[0]
|
|
@@ -236,7 +237,9 @@ export const ZoneTool: React.FC = () => {
|
|
|
236
237
|
mainLineRef.current.visible = false
|
|
237
238
|
closingLineRef.current.visible = false
|
|
238
239
|
} else {
|
|
239
|
-
// Add point to polygon
|
|
240
|
+
// Add point to polygon. Every non-closing vertex is a "start" tick;
|
|
241
|
+
// closing the polygon above fires the structure-build (end) cue.
|
|
242
|
+
sfxEmitter.emit('sfx:structure-build-start')
|
|
240
243
|
pointsRef.current = [...pointsRef.current, clickPoint]
|
|
241
244
|
updatePreview()
|
|
242
245
|
}
|
|
@@ -269,6 +272,7 @@ export const ZoneTool: React.FC = () => {
|
|
|
269
272
|
|
|
270
273
|
// Reset state on unmount
|
|
271
274
|
pointsRef.current = []
|
|
275
|
+
clearSurfacePlanSnapFeedback()
|
|
272
276
|
}
|
|
273
277
|
}, [currentLevelId])
|
|
274
278
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
TooltipContent,
|
|
6
6
|
TooltipTrigger,
|
|
7
7
|
} from './../../../components/ui/primitives/tooltip'
|
|
8
|
+
import { triggerSFX } from './../../../lib/sfx-bus'
|
|
8
9
|
import { cn } from './../../../lib/utils'
|
|
9
10
|
|
|
10
11
|
interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
|
@@ -17,7 +18,18 @@ interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
|
|
17
18
|
|
|
18
19
|
export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>(
|
|
19
20
|
(
|
|
20
|
-
{
|
|
21
|
+
{
|
|
22
|
+
className,
|
|
23
|
+
children,
|
|
24
|
+
label,
|
|
25
|
+
shortcut,
|
|
26
|
+
isActive,
|
|
27
|
+
tooltipContent,
|
|
28
|
+
tooltipSide,
|
|
29
|
+
onClick,
|
|
30
|
+
onMouseEnter,
|
|
31
|
+
...props
|
|
32
|
+
},
|
|
21
33
|
ref,
|
|
22
34
|
) => {
|
|
23
35
|
return (
|
|
@@ -25,6 +37,14 @@ export const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProp
|
|
|
25
37
|
<TooltipTrigger asChild>
|
|
26
38
|
<Button
|
|
27
39
|
className={cn('relative h-11 w-11 transition-all', className)}
|
|
40
|
+
onClick={(event) => {
|
|
41
|
+
triggerSFX('sfx:menu-click')
|
|
42
|
+
onClick?.(event)
|
|
43
|
+
}}
|
|
44
|
+
onMouseEnter={(event) => {
|
|
45
|
+
triggerSFX('sfx:menu-hover')
|
|
46
|
+
onMouseEnter?.(event)
|
|
47
|
+
}}
|
|
28
48
|
ref={ref}
|
|
29
49
|
{...props}
|
|
30
50
|
>
|