@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,71 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode, WallNode } from '@pascal-app/core'
|
|
3
|
+
import { resolveSurfacePlanPointSnap } from './surface-plan-snap'
|
|
4
|
+
|
|
5
|
+
const wall = (id: string, start: [number, number], end: [number, number]): WallNode =>
|
|
6
|
+
({
|
|
7
|
+
id,
|
|
8
|
+
type: 'wall',
|
|
9
|
+
start,
|
|
10
|
+
end,
|
|
11
|
+
visible: true,
|
|
12
|
+
}) as WallNode
|
|
13
|
+
|
|
14
|
+
const nodesOf = (...walls: WallNode[]): Record<string, AnyNode> =>
|
|
15
|
+
Object.fromEntries(walls.map((node) => [node.id, node as AnyNode]))
|
|
16
|
+
|
|
17
|
+
// The measurement tool passes `magnetic` explicitly so its snapping never
|
|
18
|
+
// depends on the construction snapping-mode chip (whose default 'grid' mode
|
|
19
|
+
// turns `isMagneticSnapActive()` off). These tests pin that seam.
|
|
20
|
+
describe('resolveSurfacePlanPointSnap magnetic override', () => {
|
|
21
|
+
const walls = [wall('wall-a', [0, 0], [4, 0]), wall('wall-b', [0, 0], [0, 4])]
|
|
22
|
+
const nodes = nodesOf(...walls)
|
|
23
|
+
|
|
24
|
+
test('magnetic: true acquires a shared wall corner from the full endpoint radius', () => {
|
|
25
|
+
const result = resolveSurfacePlanPointSnap({
|
|
26
|
+
rawPoint: [0.3, 0.2],
|
|
27
|
+
nodes,
|
|
28
|
+
magnetic: true,
|
|
29
|
+
align: false,
|
|
30
|
+
})
|
|
31
|
+
expect(result.point).toEqual([0, 0])
|
|
32
|
+
expect(result.wallSnap).toBe('endpoint')
|
|
33
|
+
expect(result.wallIds.sort()).toEqual(['wall-a', 'wall-b'])
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('magnetic: true acquires a T-junction crossing on a wall body', () => {
|
|
37
|
+
const crossing = [wall('wall-a', [0, 0], [4, 0]), wall('wall-c', [3, -1], [3, 3])]
|
|
38
|
+
const result = resolveSurfacePlanPointSnap({
|
|
39
|
+
rawPoint: [3.05, 0.1],
|
|
40
|
+
nodes: nodesOf(...crossing),
|
|
41
|
+
magnetic: true,
|
|
42
|
+
align: false,
|
|
43
|
+
})
|
|
44
|
+
expect(result.point).toEqual([3, 0])
|
|
45
|
+
expect(result.wallSnap).toBe('intersection')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
test('magnetic: false keeps the fallback point outside the tight connect radius', () => {
|
|
49
|
+
const result = resolveSurfacePlanPointSnap({
|
|
50
|
+
rawPoint: [0.3, 0.2],
|
|
51
|
+
fallbackPoint: [0.3, 0.2],
|
|
52
|
+
nodes,
|
|
53
|
+
magnetic: false,
|
|
54
|
+
align: false,
|
|
55
|
+
})
|
|
56
|
+
expect(result.point).toEqual([0.3, 0.2])
|
|
57
|
+
expect(result.wallSnap).toBeNull()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('magnetic: false still sticks within the connect radius so ends can meet', () => {
|
|
61
|
+
const result = resolveSurfacePlanPointSnap({
|
|
62
|
+
rawPoint: [0.03, 0.02],
|
|
63
|
+
fallbackPoint: [0.03, 0.02],
|
|
64
|
+
nodes,
|
|
65
|
+
magnetic: false,
|
|
66
|
+
align: false,
|
|
67
|
+
})
|
|
68
|
+
expect(result.point).toEqual([0, 0])
|
|
69
|
+
expect(result.wallSnap).toBe('endpoint')
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AlignmentAnchor,
|
|
3
|
+
type AlignmentGuide,
|
|
4
|
+
type AnyNode,
|
|
5
|
+
collectAlignmentAnchors,
|
|
6
|
+
getWallCurveFrameAt,
|
|
7
|
+
getWallCurveLength,
|
|
8
|
+
isCurvedWall,
|
|
9
|
+
resolveLevelId,
|
|
10
|
+
useScene,
|
|
11
|
+
type WallNode,
|
|
12
|
+
} from '@pascal-app/core'
|
|
13
|
+
import {
|
|
14
|
+
getSegmentGridStep,
|
|
15
|
+
snapWallDraftPointDetailed,
|
|
16
|
+
WALL_CONNECT_SNAP_RADIUS,
|
|
17
|
+
type WallDraftSnapKind,
|
|
18
|
+
type WallPlanPoint,
|
|
19
|
+
type WallSnapRadii,
|
|
20
|
+
} from '../components/tools/wall/wall-drafting'
|
|
21
|
+
import useAlignmentGuides from '../store/use-alignment-guides'
|
|
22
|
+
import { isMagneticSnapActive } from '../store/use-editor'
|
|
23
|
+
import useWallSnapIndicator from '../store/use-wall-snap-indicator'
|
|
24
|
+
import { resolveAlignmentForFloorplanView } from './world-grid-snap'
|
|
25
|
+
|
|
26
|
+
const SURFACE_SNAP_MOVING_ID = '__surface_snap__'
|
|
27
|
+
export const SURFACE_ALIGNMENT_THRESHOLD_M = 0.08
|
|
28
|
+
const SURFACE_WALL_SNAP_RADII = {
|
|
29
|
+
endpoint: 0.38,
|
|
30
|
+
midpoint: 0.28,
|
|
31
|
+
intersection: 0.28,
|
|
32
|
+
wall: 0.18,
|
|
33
|
+
} satisfies WallSnapRadii
|
|
34
|
+
const WALL_SOURCE_MATCH_EPSILON = 0.035
|
|
35
|
+
|
|
36
|
+
export type SurfacePlanSnapInput = {
|
|
37
|
+
rawPoint: WallPlanPoint
|
|
38
|
+
fallbackPoint?: WallPlanPoint
|
|
39
|
+
levelId?: string | null
|
|
40
|
+
excludeId?: string | null
|
|
41
|
+
movingId?: string
|
|
42
|
+
nodes?: Readonly<Record<string, AnyNode>>
|
|
43
|
+
walls?: readonly WallNode[]
|
|
44
|
+
candidates?: readonly AlignmentAnchor[]
|
|
45
|
+
threshold?: number
|
|
46
|
+
magnetic?: boolean
|
|
47
|
+
align?: boolean
|
|
48
|
+
highlightWalls?: boolean
|
|
49
|
+
step?: number
|
|
50
|
+
snapRadii?: WallSnapRadii
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type SurfacePlanSnapResult = {
|
|
54
|
+
point: WallPlanPoint
|
|
55
|
+
wallSnap: WallDraftSnapKind | null
|
|
56
|
+
guides: AlignmentGuide[]
|
|
57
|
+
wallIds: string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getLevelWalls(
|
|
61
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
62
|
+
levelId: string | null | undefined,
|
|
63
|
+
walls?: readonly WallNode[],
|
|
64
|
+
): WallNode[] {
|
|
65
|
+
const source =
|
|
66
|
+
walls ?? Object.values(nodes).filter((node): node is WallNode => node.type === 'wall')
|
|
67
|
+
if (!levelId) return source.filter((wall) => wall.visible !== false)
|
|
68
|
+
|
|
69
|
+
return source.filter(
|
|
70
|
+
(wall) =>
|
|
71
|
+
wall.visible !== false && resolveLevelId(wall, nodes as Record<string, AnyNode>) === levelId,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function distanceSquared(a: WallPlanPoint, b: WallPlanPoint) {
|
|
76
|
+
const dx = a[0] - b[0]
|
|
77
|
+
const dz = a[1] - b[1]
|
|
78
|
+
return dx * dx + dz * dz
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function wallMidpoint(wall: WallNode): WallPlanPoint {
|
|
82
|
+
if (isCurvedWall(wall)) {
|
|
83
|
+
const frame = getWallCurveFrameAt(wall, 0.5)
|
|
84
|
+
return [frame.point.x, frame.point.y]
|
|
85
|
+
}
|
|
86
|
+
return [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function distanceToSegmentSquared(point: WallPlanPoint, start: WallPlanPoint, end: WallPlanPoint) {
|
|
90
|
+
const dx = end[0] - start[0]
|
|
91
|
+
const dz = end[1] - start[1]
|
|
92
|
+
const lengthSquared = dx * dx + dz * dz
|
|
93
|
+
if (lengthSquared < 1e-9) return distanceSquared(point, start)
|
|
94
|
+
|
|
95
|
+
const t = Math.max(
|
|
96
|
+
0,
|
|
97
|
+
Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dz) / lengthSquared),
|
|
98
|
+
)
|
|
99
|
+
const projected: WallPlanPoint = [start[0] + dx * t, start[1] + dz * t]
|
|
100
|
+
return distanceSquared(point, projected)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function distanceToWallSquared(point: WallPlanPoint, wall: WallNode) {
|
|
104
|
+
if (!isCurvedWall(wall)) {
|
|
105
|
+
return distanceToSegmentSquared(point, wall.start, wall.end)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sampleCount = Math.max(8, Math.ceil(getWallCurveLength(wall) / 0.3))
|
|
109
|
+
let bestDistanceSquared = Number.POSITIVE_INFINITY
|
|
110
|
+
let previous = getWallCurveFrameAt(wall, 0).point
|
|
111
|
+
for (let index = 1; index <= sampleCount; index += 1) {
|
|
112
|
+
const current = getWallCurveFrameAt(wall, index / sampleCount).point
|
|
113
|
+
const distance = distanceToSegmentSquared(
|
|
114
|
+
point,
|
|
115
|
+
[previous.x, previous.y],
|
|
116
|
+
[current.x, current.y],
|
|
117
|
+
)
|
|
118
|
+
bestDistanceSquared = Math.min(bestDistanceSquared, distance)
|
|
119
|
+
previous = current
|
|
120
|
+
}
|
|
121
|
+
return bestDistanceSquared
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function closestWallIds(point: WallPlanPoint, walls: readonly WallNode[], count: number) {
|
|
125
|
+
return walls
|
|
126
|
+
.map((wall) => ({ id: wall.id, distance: distanceToWallSquared(point, wall) }))
|
|
127
|
+
.sort((a, b) => a.distance - b.distance)
|
|
128
|
+
.slice(0, count)
|
|
129
|
+
.map(({ id }) => id)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function findSnapSourceWallIds(
|
|
133
|
+
point: WallPlanPoint,
|
|
134
|
+
kind: WallDraftSnapKind,
|
|
135
|
+
walls: readonly WallNode[],
|
|
136
|
+
): string[] {
|
|
137
|
+
const epsilonSquared = WALL_SOURCE_MATCH_EPSILON ** 2
|
|
138
|
+
|
|
139
|
+
if (kind === 'endpoint') {
|
|
140
|
+
const endpointMatches = walls.filter(
|
|
141
|
+
(wall) =>
|
|
142
|
+
distanceSquared(point, wall.start) <= epsilonSquared ||
|
|
143
|
+
distanceSquared(point, wall.end) <= epsilonSquared,
|
|
144
|
+
)
|
|
145
|
+
if (endpointMatches.length > 0) return endpointMatches.map((wall) => wall.id)
|
|
146
|
+
return closestWallIds(point, walls, 1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (kind === 'midpoint') {
|
|
150
|
+
const midpointMatches = walls.filter(
|
|
151
|
+
(wall) => distanceSquared(point, wallMidpoint(wall)) <= epsilonSquared,
|
|
152
|
+
)
|
|
153
|
+
if (midpointMatches.length > 0) return midpointMatches.map((wall) => wall.id)
|
|
154
|
+
return closestWallIds(point, walls, 1)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (kind === 'intersection') {
|
|
158
|
+
const crossingMatches = walls.filter(
|
|
159
|
+
(wall) => distanceToWallSquared(point, wall) <= epsilonSquared,
|
|
160
|
+
)
|
|
161
|
+
if (crossingMatches.length > 0) return crossingMatches.map((wall) => wall.id).slice(0, 2)
|
|
162
|
+
return closestWallIds(point, walls, 2)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return closestWallIds(point, walls, 1)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function clearSurfacePlanSnapFeedback() {
|
|
169
|
+
useAlignmentGuides.getState().clear()
|
|
170
|
+
useWallSnapIndicator.getState().clear()
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function resolveSurfacePlanPointSnap(input: SurfacePlanSnapInput): SurfacePlanSnapResult {
|
|
174
|
+
const nodes = input.nodes ?? useScene.getState().nodes
|
|
175
|
+
const walls = getLevelWalls(nodes, input.levelId, input.walls)
|
|
176
|
+
const fallbackPoint = input.fallbackPoint
|
|
177
|
+
const magnetic = input.magnetic ?? isMagneticSnapActive()
|
|
178
|
+
|
|
179
|
+
const wallSnap = snapWallDraftPointDetailed({
|
|
180
|
+
point: input.rawPoint,
|
|
181
|
+
walls,
|
|
182
|
+
step: input.step ?? getSegmentGridStep(),
|
|
183
|
+
magnetic,
|
|
184
|
+
snapRadii: input.snapRadii ?? SURFACE_WALL_SNAP_RADII,
|
|
185
|
+
gridSnap: fallbackPoint ? () => fallbackPoint : undefined,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
if (wallSnap.snap) {
|
|
189
|
+
const wallIds =
|
|
190
|
+
input.highlightWalls === false
|
|
191
|
+
? []
|
|
192
|
+
: findSnapSourceWallIds(wallSnap.point, wallSnap.snap, walls)
|
|
193
|
+
useWallSnapIndicator.getState().set({
|
|
194
|
+
x: wallSnap.point[0],
|
|
195
|
+
z: wallSnap.point[1],
|
|
196
|
+
kind: wallSnap.snap,
|
|
197
|
+
...(wallIds.length > 0 ? { wallIds } : {}),
|
|
198
|
+
})
|
|
199
|
+
useAlignmentGuides.getState().clear()
|
|
200
|
+
return { point: wallSnap.point, wallSnap: wallSnap.snap, guides: [], wallIds }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
useWallSnapIndicator.getState().clear()
|
|
204
|
+
|
|
205
|
+
// Alignment "lines" are DISPLAYED in every mode (grid / lines / angles /
|
|
206
|
+
// off); the magnetic pull onto an axis is applied only when magnetic
|
|
207
|
+
// ('lines'). `align: false` fully bypasses (no guides).
|
|
208
|
+
const basePoint = fallbackPoint ?? wallSnap.point
|
|
209
|
+
if (input.align === false) {
|
|
210
|
+
useAlignmentGuides.getState().clear()
|
|
211
|
+
return { point: basePoint, wallSnap: null, guides: [], wallIds: [] }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const movingId = input.movingId ?? SURFACE_SNAP_MOVING_ID
|
|
215
|
+
const allCandidates =
|
|
216
|
+
input.candidates ??
|
|
217
|
+
collectAlignmentAnchors(nodes, input.excludeId ?? movingId, input.levelId ?? null)
|
|
218
|
+
|
|
219
|
+
// In non-magnetic modes nothing pulls the point onto a guide, so restrict
|
|
220
|
+
// alignment to anchors already within connect distance — a corner then reads
|
|
221
|
+
// no differently from any other nearby point, and far corners stop lighting up
|
|
222
|
+
// from across the plan. Magnetic ('lines') keeps the full-range guides since
|
|
223
|
+
// its snap closes the gap. Mirrors the wall draft/endpoint tools. Filtering the
|
|
224
|
+
// candidates (both local-frame, like `basePoint`) rather than the resolved
|
|
225
|
+
// guides avoids the floor-plan view rotation baked into the guide coords.
|
|
226
|
+
const candidates = magnetic
|
|
227
|
+
? allCandidates
|
|
228
|
+
: allCandidates.filter(
|
|
229
|
+
(candidate) =>
|
|
230
|
+
distanceSquared(basePoint, [candidate.x, candidate.z]) <= WALL_CONNECT_SNAP_RADIUS ** 2,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if (candidates.length === 0) {
|
|
234
|
+
useAlignmentGuides.getState().clear()
|
|
235
|
+
return { point: basePoint, wallSnap: null, guides: [], wallIds: [] }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const alignment = resolveAlignmentForFloorplanView({
|
|
239
|
+
moving: [{ nodeId: movingId, kind: 'corner', x: basePoint[0], z: basePoint[1] }],
|
|
240
|
+
candidates,
|
|
241
|
+
threshold: input.threshold ?? SURFACE_ALIGNMENT_THRESHOLD_M,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
useAlignmentGuides.getState().set(alignment.guides)
|
|
245
|
+
|
|
246
|
+
if (!alignment.snap || !magnetic) {
|
|
247
|
+
return { point: basePoint, wallSnap: null, guides: alignment.guides, wallIds: [] }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
point: [basePoint[0] + alignment.snap.dx, basePoint[1] + alignment.snap.dz],
|
|
252
|
+
wallSnap: null,
|
|
253
|
+
guides: alignment.guides,
|
|
254
|
+
wallIds: [],
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { useCallback } from 'react'
|
|
5
|
+
import { getLinearUnitLabel, linearUnitToMeters, metersToLinearUnit } from './measurements'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared display/storage conversion for numeric property controls so that
|
|
9
|
+
* every length input honors the metric/imperial toggle identically.
|
|
10
|
+
*
|
|
11
|
+
* Values are always STORED in the field's own unit (meters for `unit === 'm'`).
|
|
12
|
+
* When the viewer preference is imperial AND the field is a meter length, the
|
|
13
|
+
* value is DISPLAYED (and edited) in feet; otherwise the conversions are the
|
|
14
|
+
* identity, so metric fields and non-length units (`'°'`, `'%'`, `'in'`, `''`,
|
|
15
|
+
* …) behave exactly as before.
|
|
16
|
+
*
|
|
17
|
+
* Used by both `SliderControl` and `MetricControl` — keep the two in sync via
|
|
18
|
+
* this single source of truth.
|
|
19
|
+
*/
|
|
20
|
+
export function useLinearDisplay(unit: string, precision: number) {
|
|
21
|
+
const viewerUnit = useViewer((state) => state.unit)
|
|
22
|
+
const isImperial = viewerUnit === 'imperial' && unit === 'm'
|
|
23
|
+
const displayUnit = isImperial ? getLinearUnitLabel('imperial') : unit
|
|
24
|
+
|
|
25
|
+
const toDisplay = useCallback(
|
|
26
|
+
(stored: number) => (isImperial ? metersToLinearUnit(stored, 'imperial') : stored),
|
|
27
|
+
[isImperial],
|
|
28
|
+
)
|
|
29
|
+
const toStored = useCallback(
|
|
30
|
+
(display: number) => (isImperial ? linearUnitToMeters(display, 'imperial') : display),
|
|
31
|
+
[isImperial],
|
|
32
|
+
)
|
|
33
|
+
// Round a stored value so it lands on a clean number of DISPLAY-unit digits.
|
|
34
|
+
const roundStored = useCallback(
|
|
35
|
+
(stored: number) => toStored(Number.parseFloat(toDisplay(stored).toFixed(precision))),
|
|
36
|
+
[toDisplay, toStored, precision],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
return { isImperial, displayUnit, toDisplay, toStored, roundStored }
|
|
40
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* World-grid snap for tools that consume `grid:move` / `grid:click`.
|
|
3
|
+
*
|
|
4
|
+
* Tools historically snapped on `event.localPosition` (the cursor in the
|
|
5
|
+
* active building's local frame). After the floor-plan grid was pulled
|
|
6
|
+
* out of the rotated scene group, snapping has to follow the WORLD XZ
|
|
7
|
+
* grid — otherwise a rotated building drags every placement off the
|
|
8
|
+
* visible grid lines. This helper resolves the active building's pose
|
|
9
|
+
* and projects the world snap back into local coords for storage.
|
|
10
|
+
*/
|
|
11
|
+
import {
|
|
12
|
+
type AlignmentAnchor,
|
|
13
|
+
type AlignmentGuide,
|
|
14
|
+
type AnyNodeId,
|
|
15
|
+
type BuildingPose,
|
|
16
|
+
type ResolveAlignmentInBuildingResult,
|
|
17
|
+
resolveAlignmentInBuildingWorld,
|
|
18
|
+
snapWorldXZToBuildingLocal,
|
|
19
|
+
useLiveTransforms,
|
|
20
|
+
useScene,
|
|
21
|
+
} from '@pascal-app/core'
|
|
22
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Look up the active building's pose, or null when we're at the site root.
|
|
26
|
+
* Used by tools that need to resolve alignment in world coords.
|
|
27
|
+
*
|
|
28
|
+
* Falls back through the active level's `parentId` because the 2D floor-plan
|
|
29
|
+
* often runs with `selection.buildingId === null` while still operating
|
|
30
|
+
* inside a specific building (the user is editing a level, not the building
|
|
31
|
+
* shell itself). Without the fallback, alignment in the floor plan saw a
|
|
32
|
+
* `buildingRotY === 0` pose and emitted world-axis guides — which appeared
|
|
33
|
+
* diagonal once the building was rotated.
|
|
34
|
+
*
|
|
35
|
+
* Honours `useLiveTransforms` overrides: while the building is being moved
|
|
36
|
+
* or rotated, the floor-plan scene <g> is driven by the live transform
|
|
37
|
+
* (see `use-floorplan-scene-data.ts`), so alignment has to read the same
|
|
38
|
+
* pose or the rotated anchors fall out of sync with the SVG transform and
|
|
39
|
+
* guides drift off the visible grid mid-drag (and through any post-drag
|
|
40
|
+
* frame where the live override is still set).
|
|
41
|
+
*/
|
|
42
|
+
export function getActiveBuildingPose(): BuildingPose | null {
|
|
43
|
+
const sel = useViewer.getState().selection
|
|
44
|
+
const nodes = useScene.getState().nodes
|
|
45
|
+
// Match `use-floorplan-scene-data.ts`: prefer the active level's
|
|
46
|
+
// owning building, fall back to selection.buildingId. If the user
|
|
47
|
+
// has a stale building selected from another scope while editing a
|
|
48
|
+
// different level, the level path is the authoritative one.
|
|
49
|
+
let buildingId: AnyNodeId | null = null
|
|
50
|
+
if (sel.levelId) {
|
|
51
|
+
const level = nodes[sel.levelId]
|
|
52
|
+
if (level && level.type === 'level' && level.parentId) {
|
|
53
|
+
buildingId = level.parentId as AnyNodeId
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!buildingId) buildingId = sel.buildingId ?? null
|
|
57
|
+
const building = buildingId ? nodes[buildingId] : null
|
|
58
|
+
if (building?.type !== 'building') return null
|
|
59
|
+
const live = useLiveTransforms.getState().transforms.get(buildingId as string)
|
|
60
|
+
return {
|
|
61
|
+
position: live?.position ?? building.position,
|
|
62
|
+
rotationY: live?.rotation ?? building.rotation[1] ?? 0,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve Figma-style alignment for tools whose anchors are in the active
|
|
68
|
+
* building's local frame, but where alignment must run on the WORLD axes
|
|
69
|
+
* (the frame the user sees the grid in). Wraps `resolveAlignmentInBuildingWorld`
|
|
70
|
+
* with the active building lookup so callers don't repeat the boilerplate.
|
|
71
|
+
*
|
|
72
|
+
* Returns:
|
|
73
|
+
* - `guides` in WORLD coords (renderer must live in a world-space group),
|
|
74
|
+
* - `snap` in BUILDING-LOCAL coords (ready to add to a local position).
|
|
75
|
+
*/
|
|
76
|
+
export function resolveAlignmentForActiveBuilding(args: {
|
|
77
|
+
moving: readonly AlignmentAnchor[]
|
|
78
|
+
candidates: readonly AlignmentAnchor[]
|
|
79
|
+
threshold: number
|
|
80
|
+
}): ResolveAlignmentInBuildingResult {
|
|
81
|
+
return resolveAlignmentInBuildingWorld({ ...args, pose: getActiveBuildingPose() })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function worldXZToPoseLocal(x: number, z: number, pose: BuildingPose | null): [number, number] {
|
|
85
|
+
if (!pose) return [x, z]
|
|
86
|
+
const cos = Math.cos(pose.rotationY)
|
|
87
|
+
const sin = Math.sin(pose.rotationY)
|
|
88
|
+
const dx = x - pose.position[0]
|
|
89
|
+
const dz = z - pose.position[2]
|
|
90
|
+
return [dx * cos - dz * sin, dx * sin + dz * cos]
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Project WORLD-frame alignment guides into the active building's LOCAL frame.
|
|
95
|
+
*
|
|
96
|
+
* The 3D alignment layer is still mounted inside the building-local tool group,
|
|
97
|
+
* so tools that resolve alignment on the world axes (item placement, slab move)
|
|
98
|
+
* need their guides converted before publishing to `useAlignmentGuides`.
|
|
99
|
+
*/
|
|
100
|
+
export function projectAlignmentGuidesWorldToActiveBuildingLocal(
|
|
101
|
+
guides: readonly AlignmentGuide[],
|
|
102
|
+
): AlignmentGuide[] {
|
|
103
|
+
const pose = getActiveBuildingPose()
|
|
104
|
+
return guides.map((guide) => {
|
|
105
|
+
const [fromX, fromZ] = worldXZToPoseLocal(guide.from.x, guide.from.z, pose)
|
|
106
|
+
const [toX, toZ] = worldXZToPoseLocal(guide.to.x, guide.to.z, pose)
|
|
107
|
+
return {
|
|
108
|
+
...guide,
|
|
109
|
+
from: { x: fromX, z: fromZ },
|
|
110
|
+
to: { x: toX, z: toZ },
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Baseline rotation the floor-plan view applies on top of the building
|
|
117
|
+
* rotation. Mirrors `FLOORPLAN_VIEW_ROTATION_DEG = 0` in lib/floorplan/geometry.ts —
|
|
118
|
+
* the scene group reads it via `floorplanSceneRotationDeg = FVR - buildingRot`.
|
|
119
|
+
*/
|
|
120
|
+
const FLOORPLAN_VIEW_ROTATION_RAD = 0
|
|
121
|
+
|
|
122
|
+
function rotateAnchorsBy(
|
|
123
|
+
anchors: readonly AlignmentAnchor[],
|
|
124
|
+
cos: number,
|
|
125
|
+
sin: number,
|
|
126
|
+
): AlignmentAnchor[] {
|
|
127
|
+
return anchors.map((a) => ({
|
|
128
|
+
nodeId: a.nodeId,
|
|
129
|
+
kind: a.kind,
|
|
130
|
+
x: a.x * cos - a.z * sin,
|
|
131
|
+
z: a.x * sin + a.z * cos,
|
|
132
|
+
}))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve alignment in the 2D floor-plan view frame — the frame the user
|
|
137
|
+
* sees the (always axis-aligned) grid lines in, regardless of how the
|
|
138
|
+
* building has been rotated. Use this from EVERY 2D floor-plan path so
|
|
139
|
+
* alignment guides stay parallel to the visible grid.
|
|
140
|
+
*
|
|
141
|
+
* Why it differs from `resolveAlignmentForActiveBuilding`: the 3D viewport
|
|
142
|
+
* shows the world XZ grid, so world-frame alignment matches the visible
|
|
143
|
+
* grid there. The 2D floor plan, however, rotates the scene `<g>` by
|
|
144
|
+
* `floorplanSceneRotationDeg = FVR − buildingRot` and renders the grid
|
|
145
|
+
* OUTSIDE that rotated group — so the visible axes are
|
|
146
|
+
* `R(FVR − buildingRot) · local`. World-frame alignment would land on
|
|
147
|
+
* world axes, which appear diagonal in this view; view-frame alignment
|
|
148
|
+
* lands on the SVG axes the user actually reads.
|
|
149
|
+
*
|
|
150
|
+
* Returns guides in view-frame coords (correct input for the floor-plan
|
|
151
|
+
* alignment-guide layer, which is mounted outside the rotated scene
|
|
152
|
+
* group) and a snap delta projected back into building-local (so callers
|
|
153
|
+
* can add it to a local position as-is).
|
|
154
|
+
*/
|
|
155
|
+
export function resolveAlignmentForFloorplanView(args: {
|
|
156
|
+
moving: readonly AlignmentAnchor[]
|
|
157
|
+
candidates: readonly AlignmentAnchor[]
|
|
158
|
+
threshold: number
|
|
159
|
+
}): ResolveAlignmentInBuildingResult {
|
|
160
|
+
const pose = getActiveBuildingPose()
|
|
161
|
+
const buildingRotY = pose?.rotationY ?? 0
|
|
162
|
+
const rot = FLOORPLAN_VIEW_ROTATION_RAD - buildingRotY
|
|
163
|
+
const cos = Math.cos(rot)
|
|
164
|
+
const sin = Math.sin(rot)
|
|
165
|
+
const result = resolveAlignmentInBuildingWorld({
|
|
166
|
+
moving: rotateAnchorsBy(args.moving, cos, sin),
|
|
167
|
+
candidates: rotateAnchorsBy(args.candidates, cos, sin),
|
|
168
|
+
threshold: args.threshold,
|
|
169
|
+
// Pose `null` keeps `resolveAlignmentInBuildingWorld` in its no-op
|
|
170
|
+
// path: it just runs `resolveAlignment` on the already-rotated
|
|
171
|
+
// anchors and returns the snap delta in the same view frame.
|
|
172
|
+
pose: null,
|
|
173
|
+
})
|
|
174
|
+
if (!result.snap) return result
|
|
175
|
+
// View-frame delta → local-frame delta (transpose of R(rot)).
|
|
176
|
+
const { dx, dz } = result.snap
|
|
177
|
+
return {
|
|
178
|
+
guides: result.guides,
|
|
179
|
+
snap: { dx: dx * cos + dz * sin, dz: -dx * sin + dz * cos },
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Snap a world XZ position to the grid, then express it in the active
|
|
185
|
+
* building's local frame. When no building is active, world == local.
|
|
186
|
+
*/
|
|
187
|
+
export function snapWorldXZForActiveBuilding(
|
|
188
|
+
worldX: number,
|
|
189
|
+
worldZ: number,
|
|
190
|
+
step: number,
|
|
191
|
+
): { world: [number, number]; local: [number, number] } {
|
|
192
|
+
const buildingId = useViewer.getState().selection.buildingId
|
|
193
|
+
const building = buildingId ? useScene.getState().nodes[buildingId] : null
|
|
194
|
+
if (building?.type !== 'building') {
|
|
195
|
+
if (step <= 0) return { world: [worldX, worldZ], local: [worldX, worldZ] }
|
|
196
|
+
const sx = Math.round(worldX / step) * step
|
|
197
|
+
const sz = Math.round(worldZ / step) * step
|
|
198
|
+
return { world: [sx, sz], local: [sx, sz] }
|
|
199
|
+
}
|
|
200
|
+
return snapWorldXZToBuildingLocal(
|
|
201
|
+
worldX,
|
|
202
|
+
worldZ,
|
|
203
|
+
building.position,
|
|
204
|
+
building.rotation[1] ?? 0,
|
|
205
|
+
step,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Snap a building-local plan point so the resulting position sits on the
|
|
211
|
+
* world XZ grid. The returned point is still in building-local coords —
|
|
212
|
+
* useful as a `gridSnap` callback for snapWallDraftPoint / snapFenceDraftPoint
|
|
213
|
+
* etc., which operate entirely in the local frame.
|
|
214
|
+
*/
|
|
215
|
+
export function snapBuildingLocalToWorldGrid(
|
|
216
|
+
local: readonly [number, number],
|
|
217
|
+
step: number,
|
|
218
|
+
): [number, number] {
|
|
219
|
+
const buildingId = useViewer.getState().selection.buildingId
|
|
220
|
+
const building = buildingId ? useScene.getState().nodes[buildingId] : null
|
|
221
|
+
if (building?.type !== 'building') {
|
|
222
|
+
if (step <= 0) return [local[0], local[1]]
|
|
223
|
+
return [Math.round(local[0] / step) * step, Math.round(local[1] / step) * step]
|
|
224
|
+
}
|
|
225
|
+
const rotY = building.rotation[1] ?? 0
|
|
226
|
+
const cos = Math.cos(rotY)
|
|
227
|
+
const sin = Math.sin(rotY)
|
|
228
|
+
const worldX = building.position[0] + local[0] * cos + local[1] * sin
|
|
229
|
+
const worldZ = building.position[2] - local[0] * sin + local[1] * cos
|
|
230
|
+
return snapWorldXZToBuildingLocal(worldX, worldZ, building.position, rotY, step).local
|
|
231
|
+
}
|