@pascal-app/editor 0.9.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +188 -0
- package/src/lib/measurements.ts +210 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
type FloorplanMoveTargetSession,
|
|
11
11
|
nodeRegistry,
|
|
12
12
|
pauseSceneHistory,
|
|
13
|
-
resolveAlignment,
|
|
14
13
|
resumeSceneHistory,
|
|
15
14
|
useLiveNodeOverrides,
|
|
16
15
|
useLiveTransforms,
|
|
@@ -19,11 +18,19 @@ import {
|
|
|
19
18
|
import { useViewer } from '@pascal-app/viewer'
|
|
20
19
|
import { useEffect } from 'react'
|
|
21
20
|
import { commitFreshPlacementSubtree } from '../../lib/fresh-planar-placement'
|
|
21
|
+
import { isHistoryShortcut } from '../../lib/history'
|
|
22
22
|
import { isFreshPlacementMetadata, stripPlacementMetadataFlags } from '../../lib/placement-metadata'
|
|
23
23
|
import { resolvePlanarCursorPosition } from '../../lib/planar-cursor-placement'
|
|
24
|
+
import { movementSfxStepKey } from '../../lib/sfx/movement-tick'
|
|
24
25
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
26
|
+
import { resolveAlignmentForFloorplanView } from '../../lib/world-grid-snap'
|
|
25
27
|
import useAlignmentGuides from '../../store/use-alignment-guides'
|
|
26
|
-
import useEditor
|
|
28
|
+
import useEditor, {
|
|
29
|
+
isAlignmentGuideActive,
|
|
30
|
+
isGridSnapActive,
|
|
31
|
+
isMagneticSnapActive,
|
|
32
|
+
} from '../../store/use-editor'
|
|
33
|
+
import { useMovingNode } from '../../store/use-interaction-scope'
|
|
27
34
|
import { useWallMoveGhosts } from '../../store/use-wall-move-ghosts'
|
|
28
35
|
|
|
29
36
|
// Figma-style alignment snap threshold. Meters in world space; 8cm gives
|
|
@@ -53,7 +60,7 @@ const ALIGNMENT_THRESHOLD_M = 0.08
|
|
|
53
60
|
* cursor → meters accounts for pan / zoom / building rotation.
|
|
54
61
|
*/
|
|
55
62
|
export function FloorplanRegistryMoveOverlay() {
|
|
56
|
-
const movingNode =
|
|
63
|
+
const movingNode = useMovingNode()
|
|
57
64
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
58
65
|
const setMovingNodeOrigin = useEditor((s) => s.setMovingNodeOrigin)
|
|
59
66
|
|
|
@@ -114,6 +121,17 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
114
121
|
pauseSceneHistory(useScene)
|
|
115
122
|
let historyPaused = true
|
|
116
123
|
|
|
124
|
+
const clearLivePreviews = () => {
|
|
125
|
+
const liveTransforms = useLiveTransforms.getState()
|
|
126
|
+
const liveOverrides = useLiveNodeOverrides.getState()
|
|
127
|
+
const scene = useScene.getState()
|
|
128
|
+
for (const id of session.affectedIds) {
|
|
129
|
+
liveTransforms.clear(id)
|
|
130
|
+
liveOverrides.clear(id)
|
|
131
|
+
scene.markDirty(id)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
117
135
|
// The registry action menu's Move button portals to `document.body`,
|
|
118
136
|
// so the trigger click's pointer-up happens OUTSIDE the floor-plan
|
|
119
137
|
// scene and never reaches `onPointerUp` here. That means: the very
|
|
@@ -124,6 +142,18 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
124
142
|
// to be consumed. That legacy flow is gone in the registry layer;
|
|
125
143
|
// all entries use the action menu now.
|
|
126
144
|
let hasMovedSinceStart = false
|
|
145
|
+
// Last resolved position of the moved node — drives the move "tick" SFX.
|
|
146
|
+
// Parity with the 3D move, which emits on any change of the resolved
|
|
147
|
+
// position (every snapping mode, not only grid).
|
|
148
|
+
let lastSnapKey: string | null = null
|
|
149
|
+
// Live cursor location — updated on EVERY pointermove (even over the 3D
|
|
150
|
+
// canvas) so R-key ownership can follow the pointer's CURRENT pane rather
|
|
151
|
+
// than the sticky `hasMovedSinceStart`. Without this, once the user touched
|
|
152
|
+
// the 2D pane the overlay claimed R forever and the 3D flip went dead.
|
|
153
|
+
let pointerOverFloorplan = false
|
|
154
|
+
const onPointerTrack = (event: PointerEvent) => {
|
|
155
|
+
pointerOverFloorplan = isPointerOverFloorplanScene(event.clientX, event.clientY)
|
|
156
|
+
}
|
|
127
157
|
|
|
128
158
|
const onMove = (event: PointerEvent) => {
|
|
129
159
|
// Skip 3D-canvas / other-UI cursor moves so the overlay only
|
|
@@ -146,10 +176,31 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
146
176
|
metaKey: event.metaKey,
|
|
147
177
|
},
|
|
148
178
|
})
|
|
179
|
+
// Move "tick" — same feedback the 3D move gives, which fires whenever the
|
|
180
|
+
// resolved position changes (any snapping mode, not just grid), so it
|
|
181
|
+
// ticks as the item lands on each new snapped/free position.
|
|
182
|
+
const movedId = session.affectedIds[0]
|
|
183
|
+
const moved = movedId ? useScene.getState().nodes[movedId] : undefined
|
|
184
|
+
const pos = (moved as { position?: [number, number, number] } | undefined)?.position
|
|
185
|
+
if (pos) {
|
|
186
|
+
const key = movementSfxStepKey({
|
|
187
|
+
coords: [pos[0], pos[2]],
|
|
188
|
+
gridSnapActive: isGridSnapActive(),
|
|
189
|
+
gridStep: useEditor.getState().gridSnapStep,
|
|
190
|
+
})
|
|
191
|
+
if (key !== lastSnapKey) {
|
|
192
|
+
lastSnapKey = key
|
|
193
|
+
sfxEmitter.emit('sfx:grid-snap')
|
|
194
|
+
}
|
|
195
|
+
}
|
|
149
196
|
}
|
|
150
197
|
|
|
151
198
|
const commitFinalStateOrRevert = () => {
|
|
152
199
|
const commitValid = session.canCommit()
|
|
200
|
+
const freshPlacement = isFreshPlacementMetadata(
|
|
201
|
+
(useScene.getState().nodes[movingNode.id] as { metadata?: unknown } | undefined)
|
|
202
|
+
?.metadata,
|
|
203
|
+
)
|
|
153
204
|
|
|
154
205
|
// Claim ownership of the drag teardown so the 3D move tool's
|
|
155
206
|
// unmount-time cleanup skips its restore-from-snapshot — see
|
|
@@ -163,6 +214,29 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
163
214
|
// planner). For those we still do Phase 1 (revert to baseline)
|
|
164
215
|
// and Phase 2's resume — but Phase 2's write is delegated, and
|
|
165
216
|
// we skip the snapshot-diff finalUpdates path.
|
|
217
|
+
if (commitValid && freshPlacement) {
|
|
218
|
+
session.commit?.()
|
|
219
|
+
const stagedNode = useScene.getState().nodes[movingNode.id]
|
|
220
|
+
const committedId = stagedNode
|
|
221
|
+
? commitFreshPlacementSubtree(
|
|
222
|
+
movingNode.id as AnyNodeId,
|
|
223
|
+
{
|
|
224
|
+
metadata: stripPlacementMetadataFlags(stagedNode.metadata),
|
|
225
|
+
visible: true,
|
|
226
|
+
} as Partial<AnyNode>,
|
|
227
|
+
)
|
|
228
|
+
: null
|
|
229
|
+
if (historyPaused) {
|
|
230
|
+
resumeSceneHistory(useScene)
|
|
231
|
+
historyPaused = false
|
|
232
|
+
}
|
|
233
|
+
if (committedId) {
|
|
234
|
+
sfxEmitter.emit('sfx:item-place')
|
|
235
|
+
useViewer.getState().setSelection({ selectedIds: [committedId] })
|
|
236
|
+
}
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
166
240
|
if (commitValid && session.commit) {
|
|
167
241
|
useScene.getState().updateNodes(snapshotsToUpdates(snapshots))
|
|
168
242
|
if (historyPaused) {
|
|
@@ -283,7 +357,42 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
283
357
|
}
|
|
284
358
|
|
|
285
359
|
const onKey = (event: KeyboardEvent) => {
|
|
286
|
-
|
|
360
|
+
// R flips a directional kind's facing mid-placement (door / window:
|
|
361
|
+
// front ↔ back). The session records the flip and re-runs its last
|
|
362
|
+
// apply so the 2D symbol updates immediately; kinds without a facing
|
|
363
|
+
// leave `flipSide` unset and R falls through to the global handler.
|
|
364
|
+
//
|
|
365
|
+
// Ownership follows the CURRENT pointer pane, not a sticky flag: the 3D
|
|
366
|
+
// move tool ALSO listens for R on `window`. We own R only while the
|
|
367
|
+
// cursor is over the 2D floor-plan pane (`pointerOverFloorplan`) AND the
|
|
368
|
+
// 2D mover has actually engaged (`hasMovedSinceStart`); then we
|
|
369
|
+
// `stopImmediatePropagation` (this handler is CAPTURE-phase, so it runs
|
|
370
|
+
// first) so the 3D tool can't also flip. When the cursor is over the 3D
|
|
371
|
+
// pane we yield — the 3D tool owns R there. (The old sticky
|
|
372
|
+
// `hasMovedSinceStart`-only gate made the overlay claim R forever after
|
|
373
|
+
// the first 2D move, killing the 3D flip.)
|
|
374
|
+
if (event.key === 'r' || event.key === 'R') {
|
|
375
|
+
// Yield Cmd/Ctrl+R to the browser reload instead of flipping the side.
|
|
376
|
+
if (event.metaKey || event.ctrlKey) return
|
|
377
|
+
if (!(session.flipSide && hasMovedSinceStart && pointerOverFloorplan)) return
|
|
378
|
+
if (event.repeat) return
|
|
379
|
+
const t = event.target as HTMLElement | null
|
|
380
|
+
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) {
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
event.preventDefault()
|
|
384
|
+
event.stopImmediatePropagation()
|
|
385
|
+
session.flipSide()
|
|
386
|
+
sfxEmitter.emit('sfx:item-rotate')
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
if (event.key !== 'Escape' && !isHistoryShortcut(event)) return
|
|
390
|
+
if (isHistoryShortcut(event)) {
|
|
391
|
+
// ⌘Z mid-move cancels like Escape — keep it from reaching the
|
|
392
|
+
// global undo arm (this handler is capture-phase, that one bubbles).
|
|
393
|
+
event.preventDefault()
|
|
394
|
+
event.stopImmediatePropagation()
|
|
395
|
+
}
|
|
287
396
|
// Claim teardown ownership so the 3D move tool's cleanup skips
|
|
288
397
|
// its own restore — without this, both sides would race to
|
|
289
398
|
// write the same baseline, harmless but wasteful.
|
|
@@ -295,12 +404,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
295
404
|
resumeSceneHistory(useScene)
|
|
296
405
|
historyPaused = false
|
|
297
406
|
}
|
|
298
|
-
|
|
299
|
-
const liveOverrides = useLiveNodeOverrides.getState()
|
|
300
|
-
for (const id of session.affectedIds) {
|
|
301
|
-
liveTransforms.clear(id)
|
|
302
|
-
liveOverrides.clear(id)
|
|
303
|
-
}
|
|
407
|
+
clearLivePreviews()
|
|
304
408
|
useAlignmentGuides.getState().clear()
|
|
305
409
|
setMovingNode(null)
|
|
306
410
|
return
|
|
@@ -317,24 +421,24 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
317
421
|
// `useLiveNodeOverrides`. Either way, leaving them in place
|
|
318
422
|
// after Esc would freeze the 2D / 3D view at the cancelled
|
|
319
423
|
// position.
|
|
320
|
-
|
|
321
|
-
const liveOverrides = useLiveNodeOverrides.getState()
|
|
322
|
-
for (const id of session.affectedIds) {
|
|
323
|
-
liveTransforms.clear(id)
|
|
324
|
-
liveOverrides.clear(id)
|
|
325
|
-
}
|
|
424
|
+
clearLivePreviews()
|
|
326
425
|
// Restore selection cleared by the action menu's Move click.
|
|
327
426
|
useViewer.getState().setSelection({ selectedIds: snapshots.map((s) => s.id) })
|
|
328
427
|
setMovingNode(null)
|
|
329
428
|
}
|
|
330
429
|
|
|
430
|
+
window.addEventListener('pointermove', onPointerTrack)
|
|
331
431
|
window.addEventListener('pointermove', onMove)
|
|
332
432
|
window.addEventListener('pointerup', onPointerUp)
|
|
333
|
-
|
|
433
|
+
// Capture phase so this runs BEFORE the 3D move tool's bubble-phase R
|
|
434
|
+
// listener — when the cursor is over the 2D pane, `stopImmediatePropagation`
|
|
435
|
+
// then pre-empts it so only one handler flips.
|
|
436
|
+
window.addEventListener('keydown', onKey, true)
|
|
334
437
|
return () => {
|
|
438
|
+
window.removeEventListener('pointermove', onPointerTrack)
|
|
335
439
|
window.removeEventListener('pointermove', onMove)
|
|
336
440
|
window.removeEventListener('pointerup', onPointerUp)
|
|
337
|
-
window.removeEventListener('keydown', onKey)
|
|
441
|
+
window.removeEventListener('keydown', onKey, true)
|
|
338
442
|
// Unmount cleanup. `historyPaused === true` here means none of
|
|
339
443
|
// our terminal paths (commit, Esc) ran in this overlay — they
|
|
340
444
|
// each call `resumeSceneHistory` and flip the flag.
|
|
@@ -365,12 +469,7 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
365
469
|
// `useLiveTransforms`; wall sessions write to
|
|
366
470
|
// `useLiveNodeOverrides`. In pure 2D view the corresponding 3D
|
|
367
471
|
// tool's cleanup isn't there to clear them for us.
|
|
368
|
-
|
|
369
|
-
const liveOverrides = useLiveNodeOverrides.getState()
|
|
370
|
-
for (const id of session.affectedIds) {
|
|
371
|
-
liveTransforms.clear(id)
|
|
372
|
-
liveOverrides.clear(id)
|
|
373
|
-
}
|
|
472
|
+
clearLivePreviews()
|
|
374
473
|
// Sessions that publish Figma-style alignment guides during `apply`
|
|
375
474
|
// (item / shelf / column) leave them in the store; this cleanup runs
|
|
376
475
|
// after every terminal path (commit + Esc both unmount via
|
|
@@ -388,12 +487,40 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
388
487
|
// ── Path 2 — generic free-floating translate ────────────────────
|
|
389
488
|
const entry = scene.querySelector(`[data-node-id="${movingNode.id}"]`) as SVGGElement | null
|
|
390
489
|
if (!entry) return
|
|
391
|
-
|
|
392
|
-
const
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
490
|
+
const sceneNodes = useScene.getState().nodes as Record<string, AnyNode>
|
|
491
|
+
const relatedEntryIds = collectFloorplanMoveEntryIds(movingNode.id as AnyNodeId, sceneNodes)
|
|
492
|
+
const relatedEntries = Array.from(relatedEntryIds)
|
|
493
|
+
.map((id) => scene.querySelector(`[data-node-id="${id}"]`) as SVGGElement | null)
|
|
494
|
+
.filter((value): value is SVGGElement => value != null)
|
|
495
|
+
|
|
496
|
+
// Polyline kinds (duct / pipe / lineset) carry a `path`, not a
|
|
497
|
+
// `position` — translating a `position` here would write a field their
|
|
498
|
+
// schema ignores and snap the run back. For those we move every path
|
|
499
|
+
// point by the cursor delta and commit the translated `path` instead.
|
|
500
|
+
// The reference origin is the path centre so the SVG `translate` delta
|
|
501
|
+
// matches the geometry's actual location (which isn't at [0,0,0]).
|
|
502
|
+
// Only 3D `[x, y, z]` polyline kinds (duct / pipe / lineset) are handled
|
|
503
|
+
// here. A spline fence also carries a `path`, but it is 2D (`[x, y]`) and
|
|
504
|
+
// moves through its own `floorplanMoveTarget`, so exclude shorter tuples.
|
|
505
|
+
const rawPath = (movingNode as { path?: unknown }).path
|
|
506
|
+
const originalPath =
|
|
507
|
+
Array.isArray(rawPath) && Array.isArray(rawPath[0]) && rawPath[0].length >= 3
|
|
508
|
+
? (rawPath as [number, number, number][]).map((p) => [...p] as [number, number, number])
|
|
509
|
+
: null
|
|
510
|
+
const originalPosition: [number, number, number] = originalPath
|
|
511
|
+
? (() => {
|
|
512
|
+
let cx = 0
|
|
513
|
+
let cz = 0
|
|
514
|
+
for (const p of originalPath) {
|
|
515
|
+
cx += p[0]
|
|
516
|
+
cz += p[2]
|
|
517
|
+
}
|
|
518
|
+
const n = originalPath.length || 1
|
|
519
|
+
return [cx / n, originalPath[0]?.[1] ?? 0, cz / n]
|
|
520
|
+
})()
|
|
521
|
+
: (((movingNode as unknown as { position?: [number, number, number] }).position ?? [
|
|
522
|
+
0, 0, 0,
|
|
523
|
+
]) as [number, number, number])
|
|
397
524
|
const isFreshPlacement = isFreshPlacementMetadata(
|
|
398
525
|
(movingNode as { metadata?: unknown }).metadata,
|
|
399
526
|
)
|
|
@@ -403,20 +530,41 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
403
530
|
// so its untransformed bbox IS the world-space footprint. Cache the
|
|
404
531
|
// moving entry's local bbox once (relative to originalPosition) and
|
|
405
532
|
// derive anchors at any proposed (sx, sz) by translating it.
|
|
406
|
-
const movingLocalBBox =
|
|
533
|
+
const movingLocalBBox = unionFloorplanEntryBBox(relatedEntries)
|
|
407
534
|
const candidateAnchors: AlignmentAnchor[] = []
|
|
408
535
|
const allEntries = scene.querySelectorAll('[data-node-id]')
|
|
409
536
|
for (const el of Array.from(allEntries)) {
|
|
410
537
|
const otherId = el.getAttribute('data-node-id')
|
|
411
|
-
if (!otherId || otherId
|
|
538
|
+
if (!otherId || relatedEntryIds.has(otherId as AnyNodeId)) continue
|
|
412
539
|
const b = (el as SVGGraphicsElement).getBBox()
|
|
413
|
-
|
|
540
|
+
// Skip only fully-degenerate (point) entries. A thin run (duct / pipe /
|
|
541
|
+
// lineset drawn as a line) has one zero dimension but is still a valid
|
|
542
|
+
// alignment target — its endpoints become line anchors.
|
|
543
|
+
if (b.width <= 0 && b.height <= 0) continue
|
|
414
544
|
candidateAnchors.push(...bboxAnchors(otherId, b.x, b.y, b.x + b.width, b.y + b.height))
|
|
415
545
|
}
|
|
416
546
|
|
|
417
547
|
let lastSnapped: [number, number] | null = null
|
|
418
548
|
let dragAnchor: [number, number] | null = null
|
|
419
549
|
|
|
550
|
+
// Footprint bounding box drawn around the dragged entry — the 2D
|
|
551
|
+
// counterpart of the 3D `DragBoundingBox`, so a moved / duplicated node
|
|
552
|
+
// reads the same in both views. Green wireframe rect over the entry's
|
|
553
|
+
// own bbox, translated in lockstep with it. The entry stays visible the
|
|
554
|
+
// whole drag (no hide-until-move) so it never appears to vanish.
|
|
555
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
556
|
+
const boxEl = document.createElementNS(SVG_NS, 'rect')
|
|
557
|
+
boxEl.setAttribute('x', String(movingLocalBBox.x))
|
|
558
|
+
boxEl.setAttribute('y', String(movingLocalBBox.y))
|
|
559
|
+
boxEl.setAttribute('width', String(movingLocalBBox.width))
|
|
560
|
+
boxEl.setAttribute('height', String(movingLocalBBox.height))
|
|
561
|
+
boxEl.setAttribute('fill', 'none')
|
|
562
|
+
boxEl.setAttribute('stroke', '#22c55e')
|
|
563
|
+
boxEl.setAttribute('stroke-width', '1.5')
|
|
564
|
+
boxEl.setAttribute('vector-effect', 'non-scaling-stroke')
|
|
565
|
+
boxEl.setAttribute('pointer-events', 'none')
|
|
566
|
+
scene.appendChild(boxEl)
|
|
567
|
+
|
|
420
568
|
const onMove = (event: PointerEvent) => {
|
|
421
569
|
// Same target guard as Path 1 — pointer must be over the floor
|
|
422
570
|
// plan scene; otherwise we'd react to 3D-canvas moves with garbage
|
|
@@ -426,9 +574,12 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
426
574
|
if (!m) return
|
|
427
575
|
|
|
428
576
|
// 1) Grid snap baseline. Fresh catalog placement is absolute under
|
|
429
|
-
// the cursor; existing moves preserve the cursor's grab offset.
|
|
577
|
+
// the cursor; existing moves preserve the cursor's grab offset. Grid
|
|
578
|
+
// follows the active snapping mode (Shift cycles it); raw cursor in
|
|
579
|
+
// any non-grid mode.
|
|
430
580
|
const gridStep = useEditor.getState().gridSnapStep
|
|
431
|
-
const snap = (value: number) =>
|
|
581
|
+
const snap = (value: number) =>
|
|
582
|
+
isGridSnapActive() ? Math.round(value / gridStep) * gridStep : value
|
|
432
583
|
const resolved = resolvePlanarCursorPosition({
|
|
433
584
|
cursor: [m[0], m[1]],
|
|
434
585
|
original: [originalPosition[0], originalPosition[2]],
|
|
@@ -441,12 +592,13 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
441
592
|
|
|
442
593
|
// 2) Alignment snap layered on top. Treat the grid-snapped point
|
|
443
594
|
// as the "proposed" position so alignment competes from a stable
|
|
444
|
-
// base rather than the raw cursor jitter.
|
|
445
|
-
//
|
|
446
|
-
//
|
|
595
|
+
// base rather than the raw cursor jitter. Alignment "lines" are
|
|
596
|
+
// DISPLAYED in every mode except Off (isAlignmentGuideActive); the
|
|
597
|
+
// magnetic pull toward them applies only in 'lines' mode. Alt is
|
|
598
|
+
// force-place, not a snap bypass.
|
|
447
599
|
let finalX = gridX
|
|
448
600
|
let finalZ = gridZ
|
|
449
|
-
if (
|
|
601
|
+
if (isAlignmentGuideActive() && candidateAnchors.length > 0) {
|
|
450
602
|
// Translate the cached local bbox to the proposed pos to get the
|
|
451
603
|
// moving anchors at that location. The entry's untransformed
|
|
452
604
|
// bbox is in world meters relative to the node's origin, so a
|
|
@@ -469,12 +621,12 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
469
621
|
// store, which the 2D FloorplanAlignmentGuideLayer renders
|
|
470
622
|
// inside the rotated scene <g>. The 3D pipeline uses a
|
|
471
623
|
// separate store, so frames stay isolated per surface.
|
|
472
|
-
const result =
|
|
624
|
+
const result = resolveAlignmentForFloorplanView({
|
|
473
625
|
moving: movingAnchors,
|
|
474
626
|
candidates: candidateAnchors,
|
|
475
627
|
threshold: ALIGNMENT_THRESHOLD_M,
|
|
476
628
|
})
|
|
477
|
-
if (result.snap) {
|
|
629
|
+
if (result.snap && isMagneticSnapActive()) {
|
|
478
630
|
finalX += result.snap.dx
|
|
479
631
|
finalZ += result.snap.dz
|
|
480
632
|
}
|
|
@@ -483,9 +635,34 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
483
635
|
useAlignmentGuides.getState().clear()
|
|
484
636
|
}
|
|
485
637
|
|
|
638
|
+
// 3) Kind-owned attachment snap (cabinet → wall) — 2D parity with the
|
|
639
|
+
// 3D move tool's `groupMoveSnap` pass. An attach behavior, not an
|
|
640
|
+
// alignment guide, so it runs in every snapping mode except Off.
|
|
641
|
+
const groupMoveSnap = def?.capabilities?.movable?.groupMoveSnap
|
|
642
|
+
if (groupMoveSnap && (isGridSnapActive() || isMagneticSnapActive())) {
|
|
643
|
+
const snappedPosition = groupMoveSnap({
|
|
644
|
+
node: movingNode,
|
|
645
|
+
candidatePosition: [finalX, originalPosition[1], finalZ],
|
|
646
|
+
movingIds: [movingNode.id as AnyNodeId],
|
|
647
|
+
nodes: useScene.getState().nodes as Record<string, AnyNode>,
|
|
648
|
+
levelId:
|
|
649
|
+
(useViewer.getState().selection.levelId as AnyNodeId | null) ??
|
|
650
|
+
(movingNode.parentId as AnyNodeId | undefined) ??
|
|
651
|
+
null,
|
|
652
|
+
})
|
|
653
|
+
if (snappedPosition) {
|
|
654
|
+
finalX = snappedPosition[0]
|
|
655
|
+
finalZ = snappedPosition[2]
|
|
656
|
+
useAlignmentGuides.getState().clear()
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
486
660
|
const dx = finalX - originalPosition[0]
|
|
487
661
|
const dz = finalZ - originalPosition[2]
|
|
488
|
-
|
|
662
|
+
for (const relatedEntry of relatedEntries) {
|
|
663
|
+
relatedEntry.setAttribute('transform', `translate(${dx} ${dz})`)
|
|
664
|
+
}
|
|
665
|
+
boxEl.setAttribute('transform', `translate(${dx} ${dz})`)
|
|
489
666
|
lastSnapped = [finalX, finalZ]
|
|
490
667
|
}
|
|
491
668
|
|
|
@@ -499,6 +676,35 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
499
676
|
const [, oldY] = originalPosition
|
|
500
677
|
setMovingNodeOrigin('2d')
|
|
501
678
|
let selectedId = movingNode.id as AnyNodeId
|
|
679
|
+
if (originalPath) {
|
|
680
|
+
// Polyline kinds: shift every point by the committed delta and
|
|
681
|
+
// write `path`. Strip the fresh-placement flags on first drop.
|
|
682
|
+
const dx = sx - originalPosition[0]
|
|
683
|
+
const dz = sz - originalPosition[2]
|
|
684
|
+
const nextPath = originalPath.map(
|
|
685
|
+
([x, y, z]) => [x + dx, y, z + dz] as [number, number, number],
|
|
686
|
+
)
|
|
687
|
+
useScene.getState().updateNode(
|
|
688
|
+
movingNode.id as AnyNodeId,
|
|
689
|
+
(isFreshPlacement
|
|
690
|
+
? {
|
|
691
|
+
path: nextPath,
|
|
692
|
+
metadata: stripPlacementMetadataFlags(
|
|
693
|
+
(movingNode as { metadata?: unknown }).metadata,
|
|
694
|
+
),
|
|
695
|
+
visible: true,
|
|
696
|
+
}
|
|
697
|
+
: { path: nextPath }) as Partial<AnyNode>,
|
|
698
|
+
)
|
|
699
|
+
useViewer.getState().setSelection({ selectedIds: [movingNode.id as AnyNodeId] })
|
|
700
|
+
for (const relatedEntry of relatedEntries) {
|
|
701
|
+
relatedEntry.removeAttribute('transform')
|
|
702
|
+
}
|
|
703
|
+
useAlignmentGuides.getState().clear()
|
|
704
|
+
setMovingNode(null)
|
|
705
|
+
swallowNextClick()
|
|
706
|
+
return
|
|
707
|
+
}
|
|
502
708
|
if (isFreshPlacement) {
|
|
503
709
|
selectedId =
|
|
504
710
|
commitFreshPlacementSubtree(
|
|
@@ -520,37 +726,54 @@ export function FloorplanRegistryMoveOverlay() {
|
|
|
520
726
|
)
|
|
521
727
|
}
|
|
522
728
|
useViewer.getState().setSelection({ selectedIds: [selectedId] })
|
|
523
|
-
|
|
729
|
+
for (const relatedEntry of relatedEntries) {
|
|
730
|
+
relatedEntry.removeAttribute('transform')
|
|
731
|
+
}
|
|
524
732
|
useAlignmentGuides.getState().clear()
|
|
525
733
|
setMovingNode(null)
|
|
526
734
|
swallowNextClick()
|
|
527
735
|
}
|
|
528
736
|
|
|
529
737
|
const onKey = (event: KeyboardEvent) => {
|
|
530
|
-
if (event.key
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
738
|
+
if (event.key !== 'Escape' && !isHistoryShortcut(event)) return
|
|
739
|
+
if (isHistoryShortcut(event)) {
|
|
740
|
+
// ⌘Z mid-move cancels like Escape — keep it from reaching the
|
|
741
|
+
// global undo arm (this handler is capture-phase, that one bubbles).
|
|
742
|
+
event.preventDefault()
|
|
743
|
+
event.stopImmediatePropagation()
|
|
744
|
+
}
|
|
745
|
+
setMovingNodeOrigin('2d')
|
|
746
|
+
if (isFreshPlacement) {
|
|
747
|
+
emitter.emit('tool:cancel')
|
|
748
|
+
const temporal = useScene.temporal.getState()
|
|
749
|
+
const wasTracking = (temporal as { isTracking?: boolean }).isTracking !== false
|
|
750
|
+
if (wasTracking) temporal.pause()
|
|
751
|
+
useScene.getState().deleteNode(movingNode.id as AnyNodeId)
|
|
752
|
+
if (wasTracking) temporal.resume()
|
|
543
753
|
}
|
|
754
|
+
for (const relatedEntry of relatedEntries) {
|
|
755
|
+
relatedEntry.removeAttribute('transform')
|
|
756
|
+
}
|
|
757
|
+
useAlignmentGuides.getState().clear()
|
|
758
|
+
setMovingNode(null)
|
|
544
759
|
}
|
|
545
760
|
|
|
546
761
|
window.addEventListener('pointermove', onMove)
|
|
547
762
|
window.addEventListener('pointerup', onPointerUp)
|
|
548
|
-
window.addEventListener('keydown', onKey)
|
|
763
|
+
window.addEventListener('keydown', onKey, true)
|
|
549
764
|
return () => {
|
|
550
765
|
window.removeEventListener('pointermove', onMove)
|
|
551
766
|
window.removeEventListener('pointerup', onPointerUp)
|
|
552
|
-
window.removeEventListener('keydown', onKey)
|
|
553
|
-
|
|
767
|
+
window.removeEventListener('keydown', onKey, true)
|
|
768
|
+
for (const relatedEntry of relatedEntries) {
|
|
769
|
+
relatedEntry.removeAttribute('transform')
|
|
770
|
+
}
|
|
771
|
+
// Always un-hide on teardown so a committed copy shows and a
|
|
772
|
+
// never-revealed entry doesn't leak a hidden style onto a reused node.
|
|
773
|
+
for (const relatedEntry of relatedEntries) {
|
|
774
|
+
relatedEntry.style.visibility = ''
|
|
775
|
+
}
|
|
776
|
+
boxEl.remove()
|
|
554
777
|
useAlignmentGuides.getState().clear()
|
|
555
778
|
}
|
|
556
779
|
}, [isActive, movingNode, setMovingNode, setMovingNodeOrigin, hasMoveTarget, def])
|
|
@@ -601,6 +824,51 @@ function deepEqual(a: unknown, b: unknown): boolean {
|
|
|
601
824
|
return false
|
|
602
825
|
}
|
|
603
826
|
|
|
827
|
+
function collectFloorplanMoveEntryIds(
|
|
828
|
+
rootId: AnyNodeId,
|
|
829
|
+
nodes: Record<string, AnyNode>,
|
|
830
|
+
): Set<AnyNodeId> {
|
|
831
|
+
const root = nodes[rootId]
|
|
832
|
+
if (root?.type !== 'cabinet' && root?.type !== 'cabinet-module') {
|
|
833
|
+
return new Set([rootId])
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const ids = new Set<AnyNodeId>()
|
|
837
|
+
const queue: AnyNodeId[] = [rootId]
|
|
838
|
+
while (queue.length > 0) {
|
|
839
|
+
const id = queue.pop()!
|
|
840
|
+
if (ids.has(id)) continue
|
|
841
|
+
const node = nodes[id]
|
|
842
|
+
if (node?.type !== 'cabinet' && node?.type !== 'cabinet-module') continue
|
|
843
|
+
ids.add(id)
|
|
844
|
+
for (const childId of node.children ?? []) {
|
|
845
|
+
const child = nodes[childId as AnyNodeId]
|
|
846
|
+
if (child?.type === 'cabinet' || child?.type === 'cabinet-module') {
|
|
847
|
+
queue.push(childId as AnyNodeId)
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
return ids
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
function unionFloorplanEntryBBox(entries: readonly SVGGraphicsElement[]): DOMRect {
|
|
855
|
+
const first = entries[0]?.getBBox()
|
|
856
|
+
if (!first) return new DOMRect(0, 0, 0, 0)
|
|
857
|
+
|
|
858
|
+
let minX = first.x
|
|
859
|
+
let minY = first.y
|
|
860
|
+
let maxX = first.x + first.width
|
|
861
|
+
let maxY = first.y + first.height
|
|
862
|
+
for (const entry of entries.slice(1)) {
|
|
863
|
+
const box = entry.getBBox()
|
|
864
|
+
minX = Math.min(minX, box.x)
|
|
865
|
+
minY = Math.min(minY, box.y)
|
|
866
|
+
maxX = Math.max(maxX, box.x + box.width)
|
|
867
|
+
maxY = Math.max(maxY, box.y + box.height)
|
|
868
|
+
}
|
|
869
|
+
return new DOMRect(minX, minY, Math.max(0, maxX - minX), Math.max(0, maxY - minY))
|
|
870
|
+
}
|
|
871
|
+
|
|
604
872
|
function swallowNextClick() {
|
|
605
873
|
const swallowClick = (e: MouseEvent) => {
|
|
606
874
|
e.stopPropagation()
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { createFloorplanRenderScaleReference } from './floorplan-render-context'
|
|
3
|
+
|
|
4
|
+
describe('floorplan render context', () => {
|
|
5
|
+
test('updates the live scale without changing the registry-facing reader', () => {
|
|
6
|
+
const scale = createFloorplanRenderScaleReference(0.02)
|
|
7
|
+
const read = scale.read
|
|
8
|
+
|
|
9
|
+
scale.update(0.01)
|
|
10
|
+
|
|
11
|
+
expect(scale.read).toBe(read)
|
|
12
|
+
expect(read()).toBe(0.01)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('notifies selected-handle renderers when a hidden floorplan becomes visible', () => {
|
|
16
|
+
const scale = createFloorplanRenderScaleReference(30)
|
|
17
|
+
let renderedCurveHandleRadius = 8 * scale.read()
|
|
18
|
+
const unsubscribe = scale.subscribe(() => {
|
|
19
|
+
renderedCurveHandleRadius = 8 * scale.read()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
scale.update(0.02)
|
|
23
|
+
|
|
24
|
+
expect(renderedCurveHandleRadius).toBe(0.16)
|
|
25
|
+
unsubscribe()
|
|
26
|
+
})
|
|
27
|
+
})
|