@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
package/src/store/use-editor.tsx
CHANGED
|
@@ -4,11 +4,15 @@ import type { AssetInput } from '@pascal-app/core'
|
|
|
4
4
|
import {
|
|
5
5
|
type AnyNode,
|
|
6
6
|
type AnyNodeId,
|
|
7
|
+
type BrushSettings,
|
|
7
8
|
type BuildingNode,
|
|
9
|
+
type CabinetModuleNode,
|
|
10
|
+
type CabinetNode,
|
|
8
11
|
type CeilingNode,
|
|
9
12
|
type ChimneyMaterialRole,
|
|
10
13
|
type ChimneyNode,
|
|
11
14
|
type ColumnNode,
|
|
15
|
+
DEFAULT_BRUSH_SETTINGS,
|
|
12
16
|
type DoorNode,
|
|
13
17
|
type DormerNode,
|
|
14
18
|
type DormerSurfaceMaterialRole,
|
|
@@ -16,6 +20,7 @@ import {
|
|
|
16
20
|
type FenceNode,
|
|
17
21
|
type ItemNode,
|
|
18
22
|
type LevelNode,
|
|
23
|
+
nodeRegistry,
|
|
19
24
|
type RoofNode,
|
|
20
25
|
type RoofSegmentNode,
|
|
21
26
|
type RoofSurfaceMaterialRole,
|
|
@@ -25,6 +30,7 @@ import {
|
|
|
25
30
|
type StairNode,
|
|
26
31
|
type StairSegmentNode,
|
|
27
32
|
type StairSurfaceMaterialRole,
|
|
33
|
+
type TerrainVerb,
|
|
28
34
|
useScene,
|
|
29
35
|
type WallNode,
|
|
30
36
|
type WallSurfaceSide,
|
|
@@ -33,6 +39,13 @@ import {
|
|
|
33
39
|
import { useViewer } from '@pascal-app/viewer'
|
|
34
40
|
import { create } from 'zustand'
|
|
35
41
|
import { persist } from 'zustand/middleware'
|
|
42
|
+
import {
|
|
43
|
+
CONTINUATION_PROFILES,
|
|
44
|
+
type ContinuationContext,
|
|
45
|
+
type ContinuationMode,
|
|
46
|
+
continuationContextOf,
|
|
47
|
+
nextContinuation,
|
|
48
|
+
} from '../lib/continuation'
|
|
36
49
|
import {
|
|
37
50
|
type ActivePaintMaterial,
|
|
38
51
|
type PaintableMaterialTarget,
|
|
@@ -40,12 +53,58 @@ import {
|
|
|
40
53
|
resolvePaintTargetFromSelection,
|
|
41
54
|
type SingleSurfaceMaterialRole,
|
|
42
55
|
} from '../lib/material-paint'
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
import {
|
|
57
|
+
type CreatableMeasurementKind,
|
|
58
|
+
DEFAULT_CREATABLE_MEASUREMENT_KIND,
|
|
59
|
+
normalizeCreatableMeasurementKind,
|
|
60
|
+
} from '../lib/measurement-kind'
|
|
61
|
+
import {
|
|
62
|
+
cyclePaintScope as cyclePaintScopeValue,
|
|
63
|
+
type PaintHoverInfo,
|
|
64
|
+
type PaintScope,
|
|
65
|
+
} from '../lib/paint-scope'
|
|
66
|
+
import {
|
|
67
|
+
cycleSnappingModeIn,
|
|
68
|
+
defaultSnappingModeFor,
|
|
69
|
+
resolveSnapFlags,
|
|
70
|
+
type SnapContext,
|
|
71
|
+
type SnappingMode,
|
|
72
|
+
snapContextOf,
|
|
73
|
+
snappingModesFor,
|
|
74
|
+
} from '../lib/snapping-mode'
|
|
75
|
+
import { publishNavigationSyncPoseToStore } from './navigation-sync-pose-store'
|
|
76
|
+
import useInteractionScope from './use-interaction-scope'
|
|
77
|
+
|
|
78
|
+
const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'build'
|
|
45
79
|
const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
|
|
46
80
|
const MIN_FLOORPLAN_PANE_RATIO = 0.15
|
|
47
81
|
const MAX_FLOORPLAN_PANE_RATIO = 0.85
|
|
48
82
|
|
|
83
|
+
function resolveMovingNodeTarget(
|
|
84
|
+
node:
|
|
85
|
+
| ItemNode
|
|
86
|
+
| WindowNode
|
|
87
|
+
| DoorNode
|
|
88
|
+
| ElevatorNode
|
|
89
|
+
| CeilingNode
|
|
90
|
+
| ChimneyNode
|
|
91
|
+
| ColumnNode
|
|
92
|
+
| DormerNode
|
|
93
|
+
| SlabNode
|
|
94
|
+
| WallNode
|
|
95
|
+
| FenceNode
|
|
96
|
+
| RoofNode
|
|
97
|
+
| RoofSegmentNode
|
|
98
|
+
| SpawnNode
|
|
99
|
+
| StairNode
|
|
100
|
+
| StairSegmentNode
|
|
101
|
+
| BuildingNode
|
|
102
|
+
| CabinetNode
|
|
103
|
+
| CabinetModuleNode,
|
|
104
|
+
) {
|
|
105
|
+
return node
|
|
106
|
+
}
|
|
107
|
+
|
|
49
108
|
export type ViewMode = '3d' | '2d' | 'split'
|
|
50
109
|
export type SplitOrientation = 'horizontal' | 'vertical'
|
|
51
110
|
export type WorkspaceMode = 'edit' | 'studio'
|
|
@@ -59,9 +118,16 @@ export type WorkspaceMode = 'edit' | 'studio'
|
|
|
59
118
|
// — `ThumbnailGenerator` consults `captureMode.mode === 'preset'` and
|
|
60
119
|
// applies those constraints. Keeping it a discriminated union lets us
|
|
61
120
|
// add future modes without surfacing the choice to end users.
|
|
121
|
+
// How the captured pixels are cropped: full-frame 16:9, raw canvas viewport,
|
|
122
|
+
// or user-dragged area. Hosts (e.g. the studio capture bar) can preselect it
|
|
123
|
+
// when entering capture mode.
|
|
124
|
+
export type SnapshotCropMode = 'standard' | 'viewport' | 'area'
|
|
125
|
+
/** Aspect presets available to `standard` crops. */
|
|
126
|
+
export type SnapshotStandardAspect = '16:9' | '9:16' | '4:3' | '3:4' | '1:1'
|
|
127
|
+
|
|
62
128
|
export type CaptureMode =
|
|
63
129
|
| { mode: 'idle' }
|
|
64
|
-
| { mode: 'standard' }
|
|
130
|
+
| { mode: 'standard'; crop?: SnapshotCropMode; standardAspect?: SnapshotStandardAspect }
|
|
65
131
|
| {
|
|
66
132
|
mode: 'preset'
|
|
67
133
|
isolated: AnyNodeId[]
|
|
@@ -75,10 +141,24 @@ export type CaptureMode =
|
|
|
75
141
|
|
|
76
142
|
export type Phase = 'site' | 'structure' | 'furnish'
|
|
77
143
|
|
|
78
|
-
|
|
144
|
+
/**
|
|
145
|
+
* `terrain-sculpt` is a mode, not a build tool, and that is the whole answer to
|
|
146
|
+
* "how does terrain editing avoid conflicting with everything else".
|
|
147
|
+
*
|
|
148
|
+
* A build tool places a node and hands the pointer back. Sculpting is a
|
|
149
|
+
* sustained brush over the *ground* — the one surface every other tool uses as
|
|
150
|
+
* its reference plane — so while it is armed, clicks must not select a wall,
|
|
151
|
+
* drag a window, or arm a ghost. Modeling it as a mode gets that for free: it is
|
|
152
|
+
* mutually exclusive with `build`/`select`/`delete` by construction, every
|
|
153
|
+
* selection manager already early-returns unless `mode === 'select'`, and it
|
|
154
|
+
* holds a `sculpting` interaction scope for its whole lifetime so conflicting
|
|
155
|
+
* controls stay stepped back. `material-paint` is the existing precedent for
|
|
156
|
+
* exactly this shape.
|
|
157
|
+
*/
|
|
158
|
+
export type Mode = 'select' | 'edit' | 'delete' | 'build' | 'material-paint' | 'terrain-sculpt'
|
|
79
159
|
|
|
80
160
|
// Structure mode tools (building elements)
|
|
81
|
-
|
|
161
|
+
type BuiltInStructureTool =
|
|
82
162
|
| 'wall'
|
|
83
163
|
| 'fence'
|
|
84
164
|
| 'room'
|
|
@@ -87,6 +167,7 @@ export type StructureTool =
|
|
|
87
167
|
| 'ceiling'
|
|
88
168
|
| 'roof'
|
|
89
169
|
| 'column'
|
|
170
|
+
| 'structural-grid'
|
|
90
171
|
| 'elevator'
|
|
91
172
|
| 'stair'
|
|
92
173
|
| 'item'
|
|
@@ -106,9 +187,21 @@ export type StructureTool =
|
|
|
106
187
|
| 'dormer'
|
|
107
188
|
| 'gutter'
|
|
108
189
|
| 'downspout'
|
|
190
|
+
| 'duct-segment'
|
|
191
|
+
| 'duct-fitting'
|
|
192
|
+
| 'duct-terminal'
|
|
193
|
+
| 'hvac-equipment'
|
|
194
|
+
| 'lineset'
|
|
195
|
+
| 'liquid-line'
|
|
196
|
+
| 'pipe-segment'
|
|
197
|
+
| 'pipe-fitting'
|
|
198
|
+
| 'pipe-trap'
|
|
199
|
+
|
|
200
|
+
/** Registry node kinds are valid build tools without central union edits. */
|
|
201
|
+
export type StructureTool = BuiltInStructureTool | (string & {})
|
|
109
202
|
|
|
110
203
|
// Furnish mode tools (items and decoration)
|
|
111
|
-
export type FurnishTool = 'item'
|
|
204
|
+
export type FurnishTool = 'item' | 'cabinet'
|
|
112
205
|
|
|
113
206
|
// Site mode tools
|
|
114
207
|
export type SiteTool = 'property-line'
|
|
@@ -140,8 +233,14 @@ export type NavigationSyncPose = {
|
|
|
140
233
|
|
|
141
234
|
export type NavigationSyncPoseInput = Omit<NavigationSyncPose, 'revision'>
|
|
142
235
|
|
|
143
|
-
// Combined tool type
|
|
144
|
-
|
|
236
|
+
// Combined tool type. Known literals keep autocomplete; the `(string & {})`
|
|
237
|
+
// arm lets plugin-contributed tool ids (e.g. `'trees:tree'`) typecheck without
|
|
238
|
+
// the host enumerating every plugin kind. The runtime dispatch is already
|
|
239
|
+
// registry-first — `tool-manager` resolves `nodeRegistry.get(tool)?.tool` — so
|
|
240
|
+
// an unknown-to-the-host tool string flows straight through to the plugin's
|
|
241
|
+
// placement component.
|
|
242
|
+
export type KnownTool = SiteTool | StructureTool | FurnishTool
|
|
243
|
+
export type Tool = KnownTool | (string & {})
|
|
145
244
|
|
|
146
245
|
/**
|
|
147
246
|
* Starting parameters seeded into a draw tool before it mints a node.
|
|
@@ -151,16 +250,6 @@ export type Tool = SiteTool | StructureTool | FurnishTool
|
|
|
151
250
|
*/
|
|
152
251
|
export type ToolDefaults = Record<string, unknown>
|
|
153
252
|
|
|
154
|
-
export type MovingWallEndpoint = {
|
|
155
|
-
wall: WallNode
|
|
156
|
-
endpoint: 'start' | 'end'
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export type MovingFenceEndpoint = {
|
|
160
|
-
fence: FenceNode
|
|
161
|
-
endpoint: 'start' | 'end'
|
|
162
|
-
}
|
|
163
|
-
|
|
164
253
|
export type MaterialTargetRole =
|
|
165
254
|
| WallSurfaceSide
|
|
166
255
|
| StairSurfaceMaterialRole
|
|
@@ -168,6 +257,7 @@ export type MaterialTargetRole =
|
|
|
168
257
|
| ChimneyMaterialRole
|
|
169
258
|
| DormerSurfaceMaterialRole
|
|
170
259
|
| SingleSurfaceMaterialRole
|
|
260
|
+
| string
|
|
171
261
|
|
|
172
262
|
export type SelectedMaterialTarget = {
|
|
173
263
|
nodeId: AnyNodeId
|
|
@@ -203,31 +293,14 @@ type EditorState = {
|
|
|
203
293
|
*/
|
|
204
294
|
toolDefaults: Partial<Record<Tool, ToolDefaults>>
|
|
205
295
|
setToolDefaults: (tool: Tool, defaults: ToolDefaults | null) => void
|
|
296
|
+
lastMeasurementKind: CreatableMeasurementKind
|
|
297
|
+
setLastMeasurementKind: (kind: CreatableMeasurementKind) => void
|
|
206
298
|
structureLayer: StructureLayer
|
|
207
299
|
setStructureLayer: (layer: StructureLayer) => void
|
|
208
300
|
catalogCategory: CatalogCategory | null
|
|
209
301
|
setCatalogCategory: (category: CatalogCategory | null) => void
|
|
210
302
|
selectedItem: AssetInput | null
|
|
211
303
|
setSelectedItem: (item: AssetInput) => void
|
|
212
|
-
movingNode:
|
|
213
|
-
| ItemNode
|
|
214
|
-
| WindowNode
|
|
215
|
-
| DoorNode
|
|
216
|
-
| ElevatorNode
|
|
217
|
-
| CeilingNode
|
|
218
|
-
| ChimneyNode
|
|
219
|
-
| ColumnNode
|
|
220
|
-
| DormerNode
|
|
221
|
-
| SlabNode
|
|
222
|
-
| WallNode
|
|
223
|
-
| FenceNode
|
|
224
|
-
| RoofNode
|
|
225
|
-
| RoofSegmentNode
|
|
226
|
-
| SpawnNode
|
|
227
|
-
| StairNode
|
|
228
|
-
| StairSegmentNode
|
|
229
|
-
| BuildingNode
|
|
230
|
-
| null
|
|
231
304
|
/**
|
|
232
305
|
* True while a move was engaged by a press-drag gizmo (the on-canvas move
|
|
233
306
|
* cross) rather than a click-to-place flow. The placement coordinator reads
|
|
@@ -235,6 +308,8 @@ type EditorState = {
|
|
|
235
308
|
*/
|
|
236
309
|
placementDragMode: boolean
|
|
237
310
|
setPlacementDragMode: (dragMode: boolean) => void
|
|
311
|
+
roofHostDragArmedId: AnyNodeId | null
|
|
312
|
+
setRoofHostDragArmedId: (nodeId: AnyNodeId | null) => void
|
|
238
313
|
setMovingNode: (
|
|
239
314
|
node:
|
|
240
315
|
| ItemNode
|
|
@@ -254,6 +329,8 @@ type EditorState = {
|
|
|
254
329
|
| StairNode
|
|
255
330
|
| StairSegmentNode
|
|
256
331
|
| BuildingNode
|
|
332
|
+
| CabinetNode
|
|
333
|
+
| CabinetModuleNode
|
|
257
334
|
| null,
|
|
258
335
|
) => void
|
|
259
336
|
/**
|
|
@@ -275,43 +352,80 @@ type EditorState = {
|
|
|
275
352
|
*/
|
|
276
353
|
movingNodeOrigin: '2d' | '3d' | null
|
|
277
354
|
setMovingNodeOrigin: (origin: '2d' | '3d' | null) => void
|
|
278
|
-
movingWallEndpoint: MovingWallEndpoint | null
|
|
279
|
-
setMovingWallEndpoint: (value: MovingWallEndpoint | null) => void
|
|
280
|
-
movingFenceEndpoint: MovingFenceEndpoint | null
|
|
281
|
-
setMovingFenceEndpoint: (value: MovingFenceEndpoint | null) => void
|
|
282
355
|
/**
|
|
283
|
-
*
|
|
284
|
-
* (
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* overlays read it to render the right caption; the camera controls
|
|
288
|
-
* use the truthy value to suppress one-finger pan-rotate. Replaces
|
|
289
|
-
* the previous per-kind `resizing*` fields so adding a new resize
|
|
290
|
-
* handle doesn't require a new store field.
|
|
356
|
+
* World axis the R/T keyboard rotation turns around, for kinds with
|
|
357
|
+
* full 3D orientation (duct fittings). Alt cycles it Y → X → Z; the
|
|
358
|
+
* kind's tool / keyboard actions read it, and the floating action
|
|
359
|
+
* menu surfaces it in a pill above the selected node.
|
|
291
360
|
*/
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
curvingWall: WallNode | null
|
|
295
|
-
setCurvingWall: (wall: WallNode | null) => void
|
|
296
|
-
curvingFence: FenceNode | null
|
|
297
|
-
setCurvingFence: (fence: FenceNode | null) => void
|
|
361
|
+
rotationAxis: 'x' | 'y' | 'z'
|
|
362
|
+
cycleRotationAxis: () => 'x' | 'y' | 'z'
|
|
298
363
|
selectedMaterialTarget: SelectedMaterialTarget | null
|
|
299
364
|
setSelectedMaterialTarget: (target: SelectedMaterialTarget | null) => void
|
|
300
365
|
activePaintMaterial: ActivePaintMaterial | null
|
|
301
366
|
setActivePaintMaterial: (material: ActivePaintMaterial | null) => void
|
|
302
367
|
activePaintTarget: PaintableMaterialTarget
|
|
303
368
|
setActivePaintTarget: (target: PaintableMaterialTarget) => void
|
|
369
|
+
// Live vertex count of an in-progress polygon draft (slab / ceiling), so the
|
|
370
|
+
// contextual HUD can gate hints on it (e.g. "Finish" only once ≥ 3 points).
|
|
371
|
+
// 0 when not drafting. Not persisted.
|
|
372
|
+
draftVertexCount: number
|
|
373
|
+
setDraftVertexCount: (count: number) => void
|
|
374
|
+
// Painter application scope — how far one paint click spreads (this surface /
|
|
375
|
+
// whole item / all matching / room). One global mode, target-aware in the HUD
|
|
376
|
+
// (see `lib/paint-scope.ts`), defaulting to the narrowest `'single'`. Not
|
|
377
|
+
// persisted: a "paint everything" scope should reset each session.
|
|
378
|
+
paintScope: PaintScope
|
|
379
|
+
setPaintScope: (scope: PaintScope) => void
|
|
380
|
+
// Cycle the scope within the hovered node's available set and return the new
|
|
381
|
+
// value. Bound to Shift while in paint mode.
|
|
382
|
+
cyclePaintScope: () => PaintScope
|
|
304
383
|
// When true, clicking a surface in paint mode clears it back to its
|
|
305
384
|
// default material instead of applying `activePaintMaterial`.
|
|
306
385
|
paintEraser: boolean
|
|
307
386
|
setPaintEraser: (eraser: boolean) => void
|
|
308
387
|
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
388
|
+
/**
|
|
389
|
+
* Terrain sculpt state. Lives here rather than in the tool component so the
|
|
390
|
+
* bottom-bar HUD, the keyboard shortcuts, and the brush all read one source —
|
|
391
|
+
* the same reason the paint mode's material/scope/eraser live here.
|
|
392
|
+
*/
|
|
393
|
+
terrainVerb: TerrainVerb
|
|
394
|
+
setTerrainVerb: (verb: TerrainVerb) => void
|
|
395
|
+
terrainBrush: BrushSettings
|
|
396
|
+
setTerrainBrush: (settings: Partial<BrushSettings>) => void
|
|
397
|
+
/**
|
|
398
|
+
* Absolute height in metres the `flatten` verb aims at. Sampled by clicking
|
|
399
|
+
* the ground with the eyedropper, or typed. `null` means "sample on first
|
|
400
|
+
* click", which is what makes flatten usable without ever opening a number
|
|
401
|
+
* field.
|
|
402
|
+
*/
|
|
403
|
+
terrainFlattenTarget: number | null
|
|
404
|
+
setTerrainFlattenTarget: (metres: number | null) => void
|
|
405
|
+
/**
|
|
406
|
+
* True while the next click should sample a flatten target instead of
|
|
407
|
+
* sculpting. One-shot: sampling clears it.
|
|
408
|
+
*/
|
|
409
|
+
terrainSampling: boolean
|
|
410
|
+
setTerrainSampling: (sampling: boolean) => void
|
|
411
|
+
// What the cursor is over in paint mode: the scopes it offers + labels for the
|
|
412
|
+
// HUD chip. `null` when not over a paintable surface (drives the "hover a
|
|
413
|
+
// surface" hint). Set by the selection-manager paint hover; not persisted.
|
|
414
|
+
paintHover: PaintHoverInfo | null
|
|
415
|
+
setPaintHover: (info: PaintHoverInfo | null) => void
|
|
416
|
+
// Embedder capability: true when a host (e.g. community) can locate a selected
|
|
417
|
+
// node in its catalog browser. Gates the node action menu's "Find" button; the
|
|
418
|
+
// editor itself emits `selection:find-node` and lets the host fulfil it. Not
|
|
419
|
+
// persisted — it's a per-mount capability the host registers.
|
|
420
|
+
canFindNode: boolean
|
|
421
|
+
setCanFindNode: (canFind: boolean) => void
|
|
313
422
|
selectedReferenceId: string | null
|
|
314
423
|
setSelectedReferenceId: (id: string | null) => void
|
|
424
|
+
// Guide id with an in-flight reference-scale measurement (line drawing or
|
|
425
|
+
// length dialog). Owned by the floorplan panel; mirrored here so the
|
|
426
|
+
// reference panel can flip its Set Scale button into a Cancel.
|
|
427
|
+
referenceScaleActiveGuideId: string | null
|
|
428
|
+
setReferenceScaleActiveGuideId: (id: string | null) => void
|
|
315
429
|
guideUi: Record<string, GuideUiState>
|
|
316
430
|
setGuideLocked: (guideId: string, locked: boolean) => void
|
|
317
431
|
setGuideScaleReferenceVisible: (guideId: string, visible: boolean) => void
|
|
@@ -319,9 +433,6 @@ type EditorState = {
|
|
|
319
433
|
// Space detection for cutaway mode
|
|
320
434
|
spaces: Record<string, Space>
|
|
321
435
|
setSpaces: (spaces: Record<string, Space>) => void
|
|
322
|
-
// Generic hole editing (works for slabs, ceilings, and any future polygon nodes)
|
|
323
|
-
editingHole: SurfaceHoleTarget | null
|
|
324
|
-
setEditingHole: (hole: SurfaceHoleTarget | null) => void
|
|
325
436
|
hoveredHole: SurfaceHoleTarget | null
|
|
326
437
|
setHoveredHole: (hole: SurfaceHoleTarget | null) => void
|
|
327
438
|
// Preview mode (viewer-like experience inside the editor)
|
|
@@ -348,17 +459,38 @@ type EditorState = {
|
|
|
348
459
|
toggleFloorplanOpen: () => void
|
|
349
460
|
isFloorplanHovered: boolean
|
|
350
461
|
setFloorplanHovered: (hovered: boolean) => void
|
|
462
|
+
// Toggleable DWV riser-diagram (plumbing isometric) overlay.
|
|
463
|
+
isRiserOpen: boolean
|
|
464
|
+
setRiserOpen: (open: boolean) => void
|
|
465
|
+
toggleRiserOpen: () => void
|
|
351
466
|
navigationSyncPose: NavigationSyncPose | null
|
|
352
467
|
publishNavigationSyncPose: (pose: NavigationSyncPoseInput) => void
|
|
353
468
|
floorplanSelectionTool: FloorplanSelectionTool
|
|
354
469
|
setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
|
|
355
470
|
gridSnapStep: GridSnapStep
|
|
356
471
|
setGridSnapStep: (step: GridSnapStep) => void
|
|
472
|
+
// Cycles the grid step through GRID_SNAP_STEPS (0.5 → 0.25 → 0.1 → 0.05 →
|
|
473
|
+
// 0.5) and returns the new value. Bound to the measurement-step shortcut.
|
|
474
|
+
cycleGridSnapStep: () => GridSnapStep
|
|
357
475
|
// Magnetic snapping while drafting — snaps wall endpoints onto existing
|
|
358
476
|
// wall corners / wall bodies (the "magnetic" beacon). Independent of grid
|
|
359
477
|
// snap. On by default; toggled from the Display menu.
|
|
360
478
|
magneticSnap: boolean
|
|
361
479
|
setMagneticSnap: (enabled: boolean) => void
|
|
480
|
+
// Per-context, user-cyclable snapping mode (see `lib/snapping-mode.ts`). Each
|
|
481
|
+
// activity (wall / item / polygon) keeps its own mode + default, because they
|
|
482
|
+
// want different snapping — drawing a wall wants grid + angle, nudging an item
|
|
483
|
+
// wants free movement that only catches alignment lines. Resolved to the live
|
|
484
|
+
// context via `getActiveSnappingMode()`; maps onto `gridSnapStep`/`magneticSnap`
|
|
485
|
+
// via `resolveSnapFlags`. Persisted per context.
|
|
486
|
+
snappingModeByContext: Record<SnapContext, SnappingMode>
|
|
487
|
+
setSnappingMode: (context: SnapContext, mode: SnappingMode) => void
|
|
488
|
+
// Cycle the *active* context's mode within its own set; returns the new value.
|
|
489
|
+
cycleSnappingMode: () => SnappingMode
|
|
490
|
+
continuationByContext: Record<ContinuationContext, ContinuationMode>
|
|
491
|
+
setContinuation: (context: ContinuationContext, mode: ContinuationMode) => void
|
|
492
|
+
cycleContinuation: (context: ContinuationContext) => ContinuationMode
|
|
493
|
+
getContinuation: (context: ContinuationContext) => ContinuationMode
|
|
362
494
|
showReferenceFloor: boolean
|
|
363
495
|
toggleReferenceFloor: () => void
|
|
364
496
|
setShowReferenceFloor: (show: boolean) => void
|
|
@@ -369,6 +501,11 @@ type EditorState = {
|
|
|
369
501
|
// Development-only camera debug flag for inspecting underside geometry
|
|
370
502
|
allowUndergroundCamera: boolean
|
|
371
503
|
setAllowUndergroundCamera: (enabled: boolean) => void
|
|
504
|
+
// Development-only debug overlay: draw each wall's opening-snap hit area
|
|
505
|
+
// (the capsule of points within the snap radius of its centerline). Lets us
|
|
506
|
+
// see why a door/window snaps where it does.
|
|
507
|
+
show2dVoronoi: boolean
|
|
508
|
+
setShow2dVoronoi: (enabled: boolean) => void
|
|
372
509
|
// First-person walkthrough mode (street view)
|
|
373
510
|
isFirstPersonMode: boolean
|
|
374
511
|
_viewModeBeforeFirstPerson: ViewMode | null
|
|
@@ -402,6 +539,9 @@ type PersistedEditorLayoutState = Pick<
|
|
|
402
539
|
| 'floorplanSelectionTool'
|
|
403
540
|
| 'gridSnapStep'
|
|
404
541
|
| 'magneticSnap'
|
|
542
|
+
| 'lastMeasurementKind'
|
|
543
|
+
| 'snappingModeByContext'
|
|
544
|
+
| 'continuationByContext'
|
|
405
545
|
| 'showReferenceFloor'
|
|
406
546
|
| 'referenceFloorOffset'
|
|
407
547
|
| 'referenceFloorOpacity'
|
|
@@ -425,6 +565,18 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
425
565
|
floorplanSelectionTool: 'click',
|
|
426
566
|
gridSnapStep: 0.5,
|
|
427
567
|
magneticSnap: true,
|
|
568
|
+
lastMeasurementKind: DEFAULT_CREATABLE_MEASUREMENT_KIND,
|
|
569
|
+
snappingModeByContext: {
|
|
570
|
+
wall: defaultSnappingModeFor('wall'),
|
|
571
|
+
item: defaultSnappingModeFor('item'),
|
|
572
|
+
polygon: defaultSnappingModeFor('polygon'),
|
|
573
|
+
},
|
|
574
|
+
continuationByContext: {
|
|
575
|
+
wall: CONTINUATION_PROFILES.wall.default,
|
|
576
|
+
fence: CONTINUATION_PROFILES.fence.default,
|
|
577
|
+
point: CONTINUATION_PROFILES.point.default,
|
|
578
|
+
cabinet: CONTINUATION_PROFILES.cabinet.default,
|
|
579
|
+
},
|
|
428
580
|
showReferenceFloor: false,
|
|
429
581
|
referenceFloorOffset: 1,
|
|
430
582
|
referenceFloorOpacity: 0.35,
|
|
@@ -437,8 +589,12 @@ type SelectDefaultBuildingAndLevelOptions = {
|
|
|
437
589
|
}
|
|
438
590
|
|
|
439
591
|
function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
|
|
592
|
+
// The site phase used to hard-return `select`, which made a site-phase brush
|
|
593
|
+
// mode unrepresentable. It is an allowlist now rather than a free-for-all:
|
|
594
|
+
// `delete` and `material-paint` still have nothing to act on at site scope, so
|
|
595
|
+
// letting them survive here would restore a mode with no targets.
|
|
440
596
|
if (phase === 'site') {
|
|
441
|
-
return 'select'
|
|
597
|
+
return mode === 'terrain-sculpt' ? mode : 'select'
|
|
442
598
|
}
|
|
443
599
|
|
|
444
600
|
return mode === 'build' || mode === 'delete' || mode === 'material-paint' ? mode : 'select'
|
|
@@ -456,7 +612,7 @@ export function normalizePersistedEditorUiState(
|
|
|
456
612
|
state: Partial<PersistedEditorUiState> | null | undefined,
|
|
457
613
|
): PersistedEditorUiState {
|
|
458
614
|
const phase = state?.phase === 'structure' || state?.phase === 'furnish' ? state.phase : 'site'
|
|
459
|
-
|
|
615
|
+
let mode = normalizeModeForPhase(phase, state?.mode)
|
|
460
616
|
|
|
461
617
|
// Migrate old isFloorplanOpen to viewMode
|
|
462
618
|
let viewMode: ViewMode = '3d'
|
|
@@ -467,6 +623,13 @@ export function normalizePersistedEditorUiState(
|
|
|
467
623
|
}
|
|
468
624
|
const isFloorplanOpen = viewMode !== '3d'
|
|
469
625
|
|
|
626
|
+
// Both are persisted independently, and rehydrate goes through neither setter,
|
|
627
|
+
// so this is the third place the sculpt/2D pair has to be reconciled. The view
|
|
628
|
+
// wins here for the same reason it does in `setViewMode`: a brush armed over a
|
|
629
|
+
// hidden canvas is a mode the user cannot use, and reviving one on load is
|
|
630
|
+
// worse than reviving it mid-session — nothing on screen explains it.
|
|
631
|
+
if (mode === 'terrain-sculpt' && viewMode === '2d') mode = 'select'
|
|
632
|
+
|
|
470
633
|
if (phase === 'site') {
|
|
471
634
|
return {
|
|
472
635
|
...DEFAULT_PERSISTED_EDITOR_UI_STATE,
|
|
@@ -527,8 +690,51 @@ export function normalizePersistedEditorUiState(
|
|
|
527
690
|
}
|
|
528
691
|
}
|
|
529
692
|
|
|
693
|
+
// Validate a persisted per-context mode against that context's allowed set
|
|
694
|
+
// (so e.g. a stale `angles` for items resets), falling back to its default.
|
|
695
|
+
function migrateSnappingMode(value: unknown, context: SnapContext): SnappingMode {
|
|
696
|
+
return snappingModesFor(context).includes(value as SnappingMode)
|
|
697
|
+
? (value as SnappingMode)
|
|
698
|
+
: defaultSnappingModeFor(context)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
type LegacyContinuationState = {
|
|
702
|
+
continuationByContext?: Partial<Record<ContinuationContext, unknown>>
|
|
703
|
+
wallChainMode?: unknown
|
|
704
|
+
fenceChainMode?: unknown
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function migrateContinuationMode(
|
|
708
|
+
value: unknown,
|
|
709
|
+
context: ContinuationContext,
|
|
710
|
+
): ContinuationMode | null {
|
|
711
|
+
const profile = CONTINUATION_PROFILES[context]
|
|
712
|
+
return profile.options.includes(value as ContinuationMode) ? (value as ContinuationMode) : null
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function normalizeContinuationByContext(
|
|
716
|
+
state: LegacyContinuationState | null | undefined,
|
|
717
|
+
): Record<ContinuationContext, ContinuationMode> {
|
|
718
|
+
return {
|
|
719
|
+
wall:
|
|
720
|
+
migrateContinuationMode(state?.continuationByContext?.wall, 'wall') ??
|
|
721
|
+
migrateContinuationMode(state?.wallChainMode, 'wall') ??
|
|
722
|
+
CONTINUATION_PROFILES.wall.default,
|
|
723
|
+
fence:
|
|
724
|
+
migrateContinuationMode(state?.continuationByContext?.fence, 'fence') ??
|
|
725
|
+
migrateContinuationMode(state?.fenceChainMode, 'fence') ??
|
|
726
|
+
CONTINUATION_PROFILES.fence.default,
|
|
727
|
+
point:
|
|
728
|
+
migrateContinuationMode(state?.continuationByContext?.point, 'point') ??
|
|
729
|
+
CONTINUATION_PROFILES.point.default,
|
|
730
|
+
cabinet:
|
|
731
|
+
migrateContinuationMode(state?.continuationByContext?.cabinet, 'cabinet') ??
|
|
732
|
+
CONTINUATION_PROFILES.cabinet.default,
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
530
736
|
function normalizePersistedEditorLayoutState(
|
|
531
|
-
state: Partial<PersistedEditorLayoutState> | null | undefined,
|
|
737
|
+
state: (Partial<PersistedEditorLayoutState> & LegacyContinuationState) | null | undefined,
|
|
532
738
|
): PersistedEditorLayoutState {
|
|
533
739
|
return {
|
|
534
740
|
activeSidebarPanel:
|
|
@@ -543,6 +749,13 @@ function normalizePersistedEditorLayoutState(
|
|
|
543
749
|
: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
544
750
|
// Default on: only an explicit persisted `false` disables it.
|
|
545
751
|
magneticSnap: state?.magneticSnap !== false,
|
|
752
|
+
lastMeasurementKind: normalizeCreatableMeasurementKind(state?.lastMeasurementKind),
|
|
753
|
+
snappingModeByContext: {
|
|
754
|
+
wall: migrateSnappingMode(state?.snappingModeByContext?.wall, 'wall'),
|
|
755
|
+
item: migrateSnappingMode(state?.snappingModeByContext?.item, 'item'),
|
|
756
|
+
polygon: migrateSnappingMode(state?.snappingModeByContext?.polygon, 'polygon'),
|
|
757
|
+
},
|
|
758
|
+
continuationByContext: normalizeContinuationByContext(state),
|
|
546
759
|
showReferenceFloor: state?.showReferenceFloor === true,
|
|
547
760
|
referenceFloorOffset:
|
|
548
761
|
typeof state?.referenceFloorOffset === 'number' && state.referenceFloorOffset >= 1
|
|
@@ -662,6 +875,59 @@ export function selectSiteFloorplanContext() {
|
|
|
662
875
|
})
|
|
663
876
|
}
|
|
664
877
|
|
|
878
|
+
// Stashes the view mode the user was in before entering capture, so we can
|
|
879
|
+
// restore it on exit. Snapshot capture always frames in 3D — the 2D/split
|
|
880
|
+
// floorplan panes render nothing meaningful for a thumbnail.
|
|
881
|
+
let viewModeBeforeCapture: ViewMode | null = null
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Hold the interaction scope that belongs to a sustained brush mode.
|
|
885
|
+
*
|
|
886
|
+
* Paint and sculpt are the two modes whose scope lifetime is the *mode*, not a
|
|
887
|
+
* pointer gesture. Both must be released whenever the mode changes for any
|
|
888
|
+
* reason — including the phase switch that resets the mode without going through
|
|
889
|
+
* `setMode`. A stuck `sculpting` scope would leave selection disabled across the
|
|
890
|
+
* whole editor, so this is one function called from every mode transition rather
|
|
891
|
+
* than a rule each transition remembers.
|
|
892
|
+
*
|
|
893
|
+
* The eyedropper arm rides along for the same reason: it is one-shot state whose
|
|
894
|
+
* UI is unmounted the moment sculpt mode ends, so it has to be cleared on every
|
|
895
|
+
* exit path and not just the one through `setMode`.
|
|
896
|
+
*
|
|
897
|
+
* Every `set({ mode: ... })` in this store must be followed by a call to this —
|
|
898
|
+
* see the callers in `setPhase`, `setStructureLayer`, `setPreviewMode`,
|
|
899
|
+
* `setFirstPersonMode` and `setWorkspaceMode`. The four view-swapping ones are
|
|
900
|
+
* the dangerous class: they unmount `ToolManager` (via `noEditing`), so the
|
|
901
|
+
* sculpt tool that would otherwise release the scope on unmount is gone, and a
|
|
902
|
+
* scope leaked there is unrecoverable without a reload.
|
|
903
|
+
*/
|
|
904
|
+
function syncBrushModeScope(mode: Mode): void {
|
|
905
|
+
const scope = useInteractionScope.getState()
|
|
906
|
+
if (mode === 'material-paint') scope.begin({ kind: 'painting' })
|
|
907
|
+
else if (mode === 'terrain-sculpt') scope.begin({ kind: 'sculpting' })
|
|
908
|
+
// `isBrushMode` is the same set as the two branches above — kept as one
|
|
909
|
+
// predicate so an added brush mode cannot be handled here and missed there.
|
|
910
|
+
else {
|
|
911
|
+
scope.endIf((s) => s.kind === 'painting' || s.kind === 'sculpting')
|
|
912
|
+
if (useEditor.getState().terrainSampling) useEditor.getState().setTerrainSampling(false)
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Whether `mode` is one of the two sustained brush modes.
|
|
918
|
+
*
|
|
919
|
+
* Named because callers *outside* this store need the same test: a scope is
|
|
920
|
+
* single-owner, so anything that calls `begin()` while a brush mode holds its
|
|
921
|
+
* scope silently evicts it — the brush stays armed and painting while the rest of
|
|
922
|
+
* the editor believes a drag is running. Almost every producer is unreachable
|
|
923
|
+
* under a brush mode because it needs a selection and entering the mode clears
|
|
924
|
+
* one, but the clipboard paths read the clipboard instead (see
|
|
925
|
+
* `pasteSelectionAndPickUp`), so they have to ask.
|
|
926
|
+
*/
|
|
927
|
+
export function isBrushMode(mode: Mode): boolean {
|
|
928
|
+
return mode === 'material-paint' || mode === 'terrain-sculpt'
|
|
929
|
+
}
|
|
930
|
+
|
|
665
931
|
const useEditor = create<EditorState>()(
|
|
666
932
|
persist(
|
|
667
933
|
(set, get) => ({
|
|
@@ -690,6 +956,11 @@ const useEditor = create<EditorState>()(
|
|
|
690
956
|
set({ mode: 'select', tool: null, catalogCategory: null })
|
|
691
957
|
}
|
|
692
958
|
|
|
959
|
+
// Leaving the site phase must drop a held sculpt scope: this branch
|
|
960
|
+
// rewrites `mode` without going through `setMode`, so without this a
|
|
961
|
+
// stuck `sculpting` scope would disable selection in structure phase.
|
|
962
|
+
syncBrushModeScope(get().mode)
|
|
963
|
+
|
|
693
964
|
switch (phase) {
|
|
694
965
|
case 'site':
|
|
695
966
|
selectSiteFloorplanContext()
|
|
@@ -708,6 +979,23 @@ const useEditor = create<EditorState>()(
|
|
|
708
979
|
},
|
|
709
980
|
mode: DEFAULT_PERSISTED_EDITOR_UI_STATE.mode,
|
|
710
981
|
setMode: (mode) => {
|
|
982
|
+
// Sculpting is a site-phase mode. Arming it from structure/furnish moves
|
|
983
|
+
// the phase rather than failing silently: the user asked for the ground,
|
|
984
|
+
// and `normalizeModeForPhase` would otherwise reject the mode on the next
|
|
985
|
+
// rehydrate, leaving the UI showing a mode the store does not hold.
|
|
986
|
+
if (mode === 'terrain-sculpt' && get().phase !== 'site') {
|
|
987
|
+
get().setPhase('site')
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Same promotion, one axis over. The brush needs the 3D canvas: in `2d`
|
|
991
|
+
// the pane is only `display: none`, so the mode would arm, hold its
|
|
992
|
+
// scope and show its HUD while no pointer event could ever reach it.
|
|
993
|
+
// `split` is the smallest move that satisfies it — a plain `3d` would
|
|
994
|
+
// throw away a floorplan the user deliberately opened.
|
|
995
|
+
if (mode === 'terrain-sculpt' && get().viewMode === '2d') {
|
|
996
|
+
set({ viewMode: 'split', isFloorplanOpen: true })
|
|
997
|
+
}
|
|
998
|
+
|
|
711
999
|
set({ mode })
|
|
712
1000
|
|
|
713
1001
|
const { phase, structureLayer, tool } = get()
|
|
@@ -730,6 +1018,15 @@ const useEditor = create<EditorState>()(
|
|
|
730
1018
|
else if (tool) {
|
|
731
1019
|
set({ tool: null })
|
|
732
1020
|
}
|
|
1021
|
+
|
|
1022
|
+
if (mode === 'terrain-sculpt') {
|
|
1023
|
+
// Sculpting acts on the ground, never on a node. Clearing the
|
|
1024
|
+
// selection on entry is what stops the selected wall's gizmo from
|
|
1025
|
+
// sitting under the brush, competing for the same clicks.
|
|
1026
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
syncBrushModeScope(mode)
|
|
733
1030
|
},
|
|
734
1031
|
tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
|
|
735
1032
|
setTool: (tool) => set({ tool }),
|
|
@@ -744,6 +1041,8 @@ const useEditor = create<EditorState>()(
|
|
|
744
1041
|
}
|
|
745
1042
|
return { toolDefaults: next }
|
|
746
1043
|
}),
|
|
1044
|
+
lastMeasurementKind: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.lastMeasurementKind,
|
|
1045
|
+
setLastMeasurementKind: (kind) => set({ lastMeasurementKind: kind }),
|
|
747
1046
|
structureLayer: DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer,
|
|
748
1047
|
setStructureLayer: (layer) => {
|
|
749
1048
|
const { mode } = get()
|
|
@@ -753,6 +1052,7 @@ const useEditor = create<EditorState>()(
|
|
|
753
1052
|
set({ structureLayer: layer, tool })
|
|
754
1053
|
} else {
|
|
755
1054
|
set({ structureLayer: layer, mode: 'select', tool: null })
|
|
1055
|
+
syncBrushModeScope('select')
|
|
756
1056
|
}
|
|
757
1057
|
|
|
758
1058
|
const viewer = useViewer.getState()
|
|
@@ -765,48 +1065,57 @@ const useEditor = create<EditorState>()(
|
|
|
765
1065
|
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
|
766
1066
|
selectedItem: null,
|
|
767
1067
|
setSelectedItem: (item) => set({ selectedItem: item }),
|
|
768
|
-
movingNode: null as
|
|
769
|
-
| ItemNode
|
|
770
|
-
| WindowNode
|
|
771
|
-
| DoorNode
|
|
772
|
-
| ElevatorNode
|
|
773
|
-
| CeilingNode
|
|
774
|
-
| ColumnNode
|
|
775
|
-
| SlabNode
|
|
776
|
-
| WallNode
|
|
777
|
-
| FenceNode
|
|
778
|
-
| RoofNode
|
|
779
|
-
| RoofSegmentNode
|
|
780
|
-
| SpawnNode
|
|
781
|
-
| StairNode
|
|
782
|
-
| StairSegmentNode
|
|
783
|
-
| BuildingNode
|
|
784
|
-
| null,
|
|
785
1068
|
placementDragMode: false,
|
|
786
1069
|
setPlacementDragMode: (dragMode) => set({ placementDragMode: dragMode }),
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
1070
|
+
roofHostDragArmedId: null,
|
|
1071
|
+
setRoofHostDragArmedId: (nodeId) => set({ roofHostDragArmedId: nodeId }),
|
|
1072
|
+
// The node being placed/moved now lives inside the interaction scope
|
|
1073
|
+
// (`useMovingNode` / `getMovingNode`), not a `useEditor` flag. This setter
|
|
1074
|
+
// remains the single entry point: it drives the scope and still touches
|
|
1075
|
+
// `movingNodeOrigin` / `placementDragMode` so cross-store subscribers that
|
|
1076
|
+
// watch this store (community placement) keep firing on move start/end.
|
|
1077
|
+
setMovingNode: (node) => {
|
|
1078
|
+
const scope = useInteractionScope.getState()
|
|
1079
|
+
if (node === null) {
|
|
1080
|
+
scope.endIf((s) => s.kind === 'placing' || s.kind === 'moving')
|
|
1081
|
+
// Preserve `movingNodeOrigin` across the clear so the non-owning
|
|
1082
|
+
// side's effect cleanup — which fires after `setMovingNode(null)`
|
|
1083
|
+
// propagates — can still read who finalised. The next non-null
|
|
1084
|
+
// `setMovingNode` resets it. Always clear the press-drag flag.
|
|
1085
|
+
set({ placementDragMode: false })
|
|
1086
|
+
return
|
|
1087
|
+
}
|
|
1088
|
+
const targetNode = resolveMovingNodeTarget(node)
|
|
1089
|
+
const isNew = Boolean((targetNode as { metadata?: { isNew?: boolean } }).metadata?.isNew)
|
|
1090
|
+
if (isNew) {
|
|
1091
|
+
scope.begin({
|
|
1092
|
+
kind: 'placing',
|
|
1093
|
+
node: targetNode,
|
|
1094
|
+
nodeId: targetNode.id,
|
|
1095
|
+
nodeType: targetNode.type,
|
|
1096
|
+
view: '3d',
|
|
1097
|
+
pressDrag: get().placementDragMode,
|
|
1098
|
+
})
|
|
1099
|
+
} else {
|
|
1100
|
+
scope.begin({
|
|
1101
|
+
kind: 'moving',
|
|
1102
|
+
node: targetNode,
|
|
1103
|
+
nodeId: targetNode.id,
|
|
1104
|
+
nodeType: targetNode.type,
|
|
1105
|
+
view: '3d',
|
|
1106
|
+
})
|
|
1107
|
+
}
|
|
1108
|
+
set({ movingNodeOrigin: null })
|
|
1109
|
+
},
|
|
798
1110
|
movingNodeOrigin: null as '2d' | '3d' | null,
|
|
799
1111
|
setMovingNodeOrigin: (origin) => set({ movingNodeOrigin: origin }),
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
setCurvingWall: (wall) => set({ curvingWall: wall }),
|
|
808
|
-
curvingFence: null,
|
|
809
|
-
setCurvingFence: (fence) => set({ curvingFence: fence }),
|
|
1112
|
+
rotationAxis: 'y',
|
|
1113
|
+
cycleRotationAxis: () => {
|
|
1114
|
+
const order = ['y', 'x', 'z'] as const
|
|
1115
|
+
const next = order[(order.indexOf(get().rotationAxis as 'y' | 'x' | 'z') + 1) % 3]!
|
|
1116
|
+
set({ rotationAxis: next })
|
|
1117
|
+
return next
|
|
1118
|
+
},
|
|
810
1119
|
selectedMaterialTarget: null,
|
|
811
1120
|
setSelectedMaterialTarget: (target) => set({ selectedMaterialTarget: target }),
|
|
812
1121
|
activePaintMaterial: null,
|
|
@@ -819,6 +1128,19 @@ const useEditor = create<EditorState>()(
|
|
|
819
1128
|
set((state) =>
|
|
820
1129
|
state.activePaintTarget === target ? state : { activePaintTarget: target },
|
|
821
1130
|
),
|
|
1131
|
+
draftVertexCount: 0,
|
|
1132
|
+
setDraftVertexCount: (count) =>
|
|
1133
|
+
set((state) => (state.draftVertexCount === count ? state : { draftVertexCount: count })),
|
|
1134
|
+
paintScope: 'single',
|
|
1135
|
+
setPaintScope: (scope) => set({ paintScope: scope }),
|
|
1136
|
+
cyclePaintScope: () => {
|
|
1137
|
+
// Cycle within the hovered node's available scopes (what the click will
|
|
1138
|
+
// actually hit). With nothing paintable hovered there's only `single`.
|
|
1139
|
+
const scopes = get().paintHover?.scopes ?? (['single'] as PaintScope[])
|
|
1140
|
+
const next = cyclePaintScopeValue(get().paintScope, scopes)
|
|
1141
|
+
set({ paintScope: next })
|
|
1142
|
+
return next
|
|
1143
|
+
},
|
|
822
1144
|
paintEraser: false,
|
|
823
1145
|
setPaintEraser: (eraser) => set({ paintEraser: eraser }),
|
|
824
1146
|
primeMaterialPaintFromSelection: () => {
|
|
@@ -848,15 +1170,29 @@ const useEditor = create<EditorState>()(
|
|
|
848
1170
|
activePaintMaterial: activePaintMaterial ?? get().activePaintMaterial,
|
|
849
1171
|
}
|
|
850
1172
|
},
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1173
|
+
paintHover: null,
|
|
1174
|
+
setPaintHover: (info) => set({ paintHover: info }),
|
|
1175
|
+
terrainVerb: 'flatten',
|
|
1176
|
+
setTerrainVerb: (verb) =>
|
|
1177
|
+
set({
|
|
1178
|
+
terrainVerb: verb,
|
|
1179
|
+
// Switching away from flatten drops the sampling arm — an eyedropper
|
|
1180
|
+
// that survives into the raise brush would swallow its first click.
|
|
1181
|
+
terrainSampling: verb === 'flatten' ? get().terrainSampling : false,
|
|
1182
|
+
}),
|
|
1183
|
+
terrainBrush: DEFAULT_BRUSH_SETTINGS,
|
|
1184
|
+
setTerrainBrush: (settings) => set({ terrainBrush: { ...get().terrainBrush, ...settings } }),
|
|
1185
|
+
terrainFlattenTarget: null,
|
|
1186
|
+
setTerrainFlattenTarget: (metres) =>
|
|
1187
|
+
set({ terrainFlattenTarget: metres, terrainSampling: false }),
|
|
1188
|
+
terrainSampling: false,
|
|
1189
|
+
setTerrainSampling: (sampling) => set({ terrainSampling: sampling }),
|
|
1190
|
+
canFindNode: false,
|
|
1191
|
+
setCanFindNode: (canFind) => set({ canFindNode: canFind }),
|
|
858
1192
|
selectedReferenceId: null,
|
|
859
1193
|
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
|
|
1194
|
+
referenceScaleActiveGuideId: null,
|
|
1195
|
+
setReferenceScaleActiveGuideId: (id) => set({ referenceScaleActiveGuideId: id }),
|
|
860
1196
|
guideUi: {},
|
|
861
1197
|
setGuideLocked: (guideId, locked) =>
|
|
862
1198
|
set((state) => ({
|
|
@@ -889,8 +1225,6 @@ const useEditor = create<EditorState>()(
|
|
|
889
1225
|
}),
|
|
890
1226
|
spaces: {},
|
|
891
1227
|
setSpaces: (spaces) => set({ spaces }),
|
|
892
|
-
editingHole: null,
|
|
893
|
-
setEditingHole: (hole) => set({ editingHole: hole }),
|
|
894
1228
|
hoveredHole: null,
|
|
895
1229
|
setHoveredHole: (hole) =>
|
|
896
1230
|
set((state) =>
|
|
@@ -903,6 +1237,7 @@ const useEditor = create<EditorState>()(
|
|
|
903
1237
|
setPreviewMode: (preview) => {
|
|
904
1238
|
if (preview) {
|
|
905
1239
|
set({ isPreviewMode: true, mode: 'select', tool: null, catalogCategory: null })
|
|
1240
|
+
syncBrushModeScope('select')
|
|
906
1241
|
// Clear zone/item selection for clean viewer drill-down hierarchy
|
|
907
1242
|
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
908
1243
|
} else {
|
|
@@ -914,10 +1249,45 @@ const useEditor = create<EditorState>()(
|
|
|
914
1249
|
setCaptureMode: (next) => {
|
|
915
1250
|
const resolved: CaptureMode =
|
|
916
1251
|
typeof next === 'boolean' ? { mode: next ? 'standard' : 'idle' } : next
|
|
917
|
-
|
|
1252
|
+
const entering = resolved.mode !== 'idle'
|
|
1253
|
+
set((state) => {
|
|
1254
|
+
if (entering) {
|
|
1255
|
+
// Force 3D for the shot. Remember the prior mode only on the first
|
|
1256
|
+
// entry (viewMode is already '3d' on re-entry), so we restore the
|
|
1257
|
+
// user's real choice — not the forced '3d' — when capture ends.
|
|
1258
|
+
if (state.viewMode !== '3d') {
|
|
1259
|
+
viewModeBeforeCapture = state.viewMode
|
|
1260
|
+
return {
|
|
1261
|
+
captureMode: resolved,
|
|
1262
|
+
isCaptureMode: true,
|
|
1263
|
+
viewMode: '3d',
|
|
1264
|
+
isFloorplanOpen: false,
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
return { captureMode: resolved, isCaptureMode: true }
|
|
1268
|
+
}
|
|
1269
|
+
const restore = viewModeBeforeCapture
|
|
1270
|
+
viewModeBeforeCapture = null
|
|
1271
|
+
if (restore && restore !== '3d') {
|
|
1272
|
+
return {
|
|
1273
|
+
captureMode: resolved,
|
|
1274
|
+
isCaptureMode: false,
|
|
1275
|
+
viewMode: restore,
|
|
1276
|
+
isFloorplanOpen: true,
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return { captureMode: resolved, isCaptureMode: false }
|
|
1280
|
+
})
|
|
918
1281
|
},
|
|
919
1282
|
viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode,
|
|
920
|
-
setViewMode: (mode) =>
|
|
1283
|
+
setViewMode: (mode) => {
|
|
1284
|
+
set({ viewMode: mode, isFloorplanOpen: mode !== '3d' })
|
|
1285
|
+
// Going the other way, the view wins and the mode yields. Hiding the 3D
|
|
1286
|
+
// pane leaves the brush unreachable, and a held `sculpting` scope would
|
|
1287
|
+
// then keep selection suppressed in a floorplan the user is trying to
|
|
1288
|
+
// work in — a mode you cannot use and cannot see how to leave.
|
|
1289
|
+
if (mode === '2d' && get().mode === 'terrain-sculpt') get().setMode('select')
|
|
1290
|
+
},
|
|
921
1291
|
splitOrientation: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.splitOrientation,
|
|
922
1292
|
setSplitOrientation: (orientation) => set({ splitOrientation: orientation }),
|
|
923
1293
|
isFloorplanOpen: DEFAULT_PERSISTED_EDITOR_UI_STATE.isFloorplanOpen,
|
|
@@ -929,20 +1299,64 @@ const useEditor = create<EditorState>()(
|
|
|
929
1299
|
}),
|
|
930
1300
|
isFloorplanHovered: false,
|
|
931
1301
|
setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
|
|
1302
|
+
isRiserOpen: false,
|
|
1303
|
+
setRiserOpen: (open) => set({ isRiserOpen: open }),
|
|
1304
|
+
toggleRiserOpen: () => set((state) => ({ isRiserOpen: !state.isRiserOpen })),
|
|
932
1305
|
navigationSyncPose: null,
|
|
933
|
-
publishNavigationSyncPose: (pose) =>
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
})
|
|
1306
|
+
publishNavigationSyncPose: (pose) => {
|
|
1307
|
+
const navigationSyncPose = {
|
|
1308
|
+
...pose,
|
|
1309
|
+
revision: (get().navigationSyncPose?.revision ?? 0) + 1,
|
|
1310
|
+
}
|
|
1311
|
+
publishNavigationSyncPoseToStore(navigationSyncPose)
|
|
1312
|
+
set({ navigationSyncPose })
|
|
1313
|
+
},
|
|
940
1314
|
floorplanSelectionTool: 'click' as FloorplanSelectionTool,
|
|
941
1315
|
setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
|
|
942
1316
|
gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
943
1317
|
setGridSnapStep: (step) => set({ gridSnapStep: step }),
|
|
1318
|
+
cycleGridSnapStep: () => {
|
|
1319
|
+
const current = get().gridSnapStep
|
|
1320
|
+
const index = GRID_SNAP_STEPS.indexOf(current)
|
|
1321
|
+
const next = GRID_SNAP_STEPS[(index + 1) % GRID_SNAP_STEPS.length] ?? GRID_SNAP_STEPS[0]!
|
|
1322
|
+
set({ gridSnapStep: next })
|
|
1323
|
+
return next
|
|
1324
|
+
},
|
|
944
1325
|
magneticSnap: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.magneticSnap,
|
|
945
1326
|
setMagneticSnap: (enabled) => set({ magneticSnap: enabled }),
|
|
1327
|
+
snappingModeByContext: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.snappingModeByContext,
|
|
1328
|
+
setSnappingMode: (context, mode) =>
|
|
1329
|
+
set((state) => ({
|
|
1330
|
+
snappingModeByContext: { ...state.snappingModeByContext, [context]: mode },
|
|
1331
|
+
})),
|
|
1332
|
+
cycleSnappingMode: () => {
|
|
1333
|
+
const context = getActiveSnapContext() ?? 'item'
|
|
1334
|
+
const current = get().snappingModeByContext[context]
|
|
1335
|
+
const next = cycleSnappingModeIn(context, current)
|
|
1336
|
+
set((state) => ({
|
|
1337
|
+
snappingModeByContext: { ...state.snappingModeByContext, [context]: next },
|
|
1338
|
+
}))
|
|
1339
|
+
return next
|
|
1340
|
+
},
|
|
1341
|
+
continuationByContext: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.continuationByContext,
|
|
1342
|
+
setContinuation: (context, mode) => {
|
|
1343
|
+
const next =
|
|
1344
|
+
migrateContinuationMode(mode, context) ?? CONTINUATION_PROFILES[context].default
|
|
1345
|
+
set((state) => ({
|
|
1346
|
+
continuationByContext: { ...state.continuationByContext, [context]: next },
|
|
1347
|
+
}))
|
|
1348
|
+
},
|
|
1349
|
+
cycleContinuation: (context) => {
|
|
1350
|
+
const next = nextContinuation(context, get().getContinuation(context))
|
|
1351
|
+
set((state) => ({
|
|
1352
|
+
continuationByContext: { ...state.continuationByContext, [context]: next },
|
|
1353
|
+
}))
|
|
1354
|
+
return next
|
|
1355
|
+
},
|
|
1356
|
+
getContinuation: (context) => {
|
|
1357
|
+
const current = get().continuationByContext[context]
|
|
1358
|
+
return migrateContinuationMode(current, context) ?? CONTINUATION_PROFILES[context].default
|
|
1359
|
+
},
|
|
946
1360
|
showReferenceFloor: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showReferenceFloor,
|
|
947
1361
|
toggleReferenceFloor: () =>
|
|
948
1362
|
set((state) => ({ showReferenceFloor: !state.showReferenceFloor })),
|
|
@@ -955,6 +1369,8 @@ const useEditor = create<EditorState>()(
|
|
|
955
1369
|
set({ referenceFloorOpacity: Math.min(0.8, Math.max(0.1, opacity)) }),
|
|
956
1370
|
allowUndergroundCamera: false,
|
|
957
1371
|
setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
|
|
1372
|
+
show2dVoronoi: false,
|
|
1373
|
+
setShow2dVoronoi: (enabled) => set({ show2dVoronoi: enabled }),
|
|
958
1374
|
isFirstPersonMode: false,
|
|
959
1375
|
_viewModeBeforeFirstPerson: null as ViewMode | null,
|
|
960
1376
|
setFirstPersonMode: (enabled) => {
|
|
@@ -969,6 +1385,7 @@ const useEditor = create<EditorState>()(
|
|
|
969
1385
|
tool: null,
|
|
970
1386
|
catalogCategory: null,
|
|
971
1387
|
})
|
|
1388
|
+
syncBrushModeScope('select')
|
|
972
1389
|
} else {
|
|
973
1390
|
const prevMode = get()._viewModeBeforeFirstPerson
|
|
974
1391
|
set({
|
|
@@ -993,6 +1410,7 @@ const useEditor = create<EditorState>()(
|
|
|
993
1410
|
tool: null,
|
|
994
1411
|
catalogCategory: null,
|
|
995
1412
|
})
|
|
1413
|
+
syncBrushModeScope('select')
|
|
996
1414
|
// Clear selection so no edit affordances bleed into the clean canvas.
|
|
997
1415
|
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
998
1416
|
} else {
|
|
@@ -1014,11 +1432,35 @@ const useEditor = create<EditorState>()(
|
|
|
1014
1432
|
}),
|
|
1015
1433
|
{
|
|
1016
1434
|
name: 'pascal-editor-ui-preferences',
|
|
1017
|
-
merge: (persistedState, currentState) =>
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1435
|
+
merge: (persistedState, currentState) => {
|
|
1436
|
+
const uiState = normalizePersistedEditorUiState(
|
|
1437
|
+
persistedState as Partial<PersistedEditorState>,
|
|
1438
|
+
)
|
|
1439
|
+
const layoutState = normalizePersistedEditorLayoutState(
|
|
1440
|
+
persistedState as Partial<PersistedEditorState>,
|
|
1441
|
+
)
|
|
1442
|
+
|
|
1443
|
+
return {
|
|
1444
|
+
...currentState,
|
|
1445
|
+
...uiState,
|
|
1446
|
+
...layoutState,
|
|
1447
|
+
...(uiState.mode === 'build' && uiState.tool === 'measurement'
|
|
1448
|
+
? {
|
|
1449
|
+
toolDefaults: {
|
|
1450
|
+
...currentState.toolDefaults,
|
|
1451
|
+
measurement: { kind: layoutState.lastMeasurementKind },
|
|
1452
|
+
},
|
|
1453
|
+
}
|
|
1454
|
+
: {}),
|
|
1455
|
+
}
|
|
1456
|
+
},
|
|
1457
|
+
// `mode` is persisted, but the interaction scope a brush mode holds is not
|
|
1458
|
+
// — it lives in a separate, non-persisted store. Rehydrating into
|
|
1459
|
+
// `terrain-sculpt` (or paint) without re-claiming the scope would restore
|
|
1460
|
+
// the brush with selection still enabled, so every dab could grab a wall.
|
|
1461
|
+
onRehydrateStorage: () => (state) => {
|
|
1462
|
+
if (state) syncBrushModeScope(state.mode)
|
|
1463
|
+
},
|
|
1022
1464
|
partialize: (state) => ({
|
|
1023
1465
|
phase: state.phase,
|
|
1024
1466
|
mode: state.mode,
|
|
@@ -1033,12 +1475,109 @@ const useEditor = create<EditorState>()(
|
|
|
1033
1475
|
floorplanSelectionTool: state.floorplanSelectionTool,
|
|
1034
1476
|
gridSnapStep: state.gridSnapStep,
|
|
1035
1477
|
magneticSnap: state.magneticSnap,
|
|
1478
|
+
lastMeasurementKind: state.lastMeasurementKind,
|
|
1479
|
+
snappingModeByContext: state.snappingModeByContext,
|
|
1480
|
+
continuationByContext: state.continuationByContext,
|
|
1036
1481
|
showReferenceFloor: state.showReferenceFloor,
|
|
1037
1482
|
referenceFloorOffset: state.referenceFloorOffset,
|
|
1038
1483
|
referenceFloorOpacity: state.referenceFloorOpacity,
|
|
1039
1484
|
}),
|
|
1485
|
+
skipHydration: true,
|
|
1040
1486
|
},
|
|
1041
1487
|
),
|
|
1042
1488
|
)
|
|
1043
1489
|
|
|
1490
|
+
/**
|
|
1491
|
+
* Effective magnetic-snap state: the legacy `magneticSnap` flag AND the active
|
|
1492
|
+
* context's snapping mode. With exclusive modes, magnetic (alignment axes + wall
|
|
1493
|
+
* corner-join) is on only in `'lines'`. Read from the smallest magnetic choke
|
|
1494
|
+
* points so the mode is honoured without retuning any snap math.
|
|
1495
|
+
*/
|
|
1496
|
+
export function isMagneticSnapActive(): boolean {
|
|
1497
|
+
const state = useEditor.getState()
|
|
1498
|
+
return state.magneticSnap && resolveSnapFlags(getActiveSnappingMode()).magnetic
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Effective angle-lock state: the active context's snapping mode. With exclusive
|
|
1503
|
+
* modes the 15°/45° lock is on only in `'angles'`. Read from the smallest
|
|
1504
|
+
* angle-lock choke points (wall / fence draft call sites).
|
|
1505
|
+
*/
|
|
1506
|
+
export function isAngleSnapActive(): boolean {
|
|
1507
|
+
return resolveSnapFlags(getActiveSnappingMode()).angles
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Effective grid-lattice state: the active context's snapping mode. With
|
|
1512
|
+
* exclusive modes the grid quantize is on only in `'grid'`.
|
|
1513
|
+
*/
|
|
1514
|
+
export function isGridSnapActive(): boolean {
|
|
1515
|
+
return resolveSnapFlags(getActiveSnappingMode()).grid
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
/**
|
|
1519
|
+
* Whether alignment "lines" should be DISPLAYED for the active context.
|
|
1520
|
+
*
|
|
1521
|
+
* True whenever a snappable context is active — in EVERY snapping mode,
|
|
1522
|
+
* including `'off'`. The guides are passive reference feedback; this is
|
|
1523
|
+
* decoupled from the magnetic *pull*: a producer publishes guides whenever this
|
|
1524
|
+
* is true, but only applies the alignment delta when `isMagneticSnapActive()`
|
|
1525
|
+
* (i.e. `'lines'`). So the user always sees the same alignment lines while
|
|
1526
|
+
* snapping to grid / angles / off, and only snaps to them in `'lines'` mode.
|
|
1527
|
+
*/
|
|
1528
|
+
export function isAlignmentGuideActive(): boolean {
|
|
1529
|
+
return getActiveSnapContext() !== null
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
/**
|
|
1533
|
+
* The snapping context for what the user is currently doing (wall / item /
|
|
1534
|
+
* polygon), or null when nothing snappable is active. Derived from the
|
|
1535
|
+
* authoritative interaction scope, falling back to the armed build tool (the
|
|
1536
|
+
* `drafting` scope isn't wired). The single source every snap reader + the HUD
|
|
1537
|
+
* resolve their mode through.
|
|
1538
|
+
*/
|
|
1539
|
+
export function getActiveSnapContext(): SnapContext | null {
|
|
1540
|
+
const editor = useEditor.getState()
|
|
1541
|
+
return snapContextOf({
|
|
1542
|
+
scope: useInteractionScope.getState().scope,
|
|
1543
|
+
mode: editor.mode,
|
|
1544
|
+
tool: editor.tool,
|
|
1545
|
+
profileOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapProfile,
|
|
1546
|
+
profileOfNode: (nodeId) => {
|
|
1547
|
+
const node = useScene.getState().nodes[nodeId as AnyNodeId]
|
|
1548
|
+
return node ? nodeRegistry.get(node.type)?.snapProfile : undefined
|
|
1549
|
+
},
|
|
1550
|
+
draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
|
|
1551
|
+
})
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
export function getActiveContinuationContext(): ContinuationContext | null {
|
|
1555
|
+
const scope = useInteractionScope.getState().scope
|
|
1556
|
+
if (scope.kind === 'drafting') return continuationContextOf(scope.tool)
|
|
1557
|
+
if (scope.kind === 'placing') return continuationContextOf(scope.nodeType)
|
|
1558
|
+
if (scope.kind !== 'idle') return null
|
|
1559
|
+
|
|
1560
|
+
const editor = useEditor.getState()
|
|
1561
|
+
if (editor.mode !== 'build' || !editor.tool) return null
|
|
1562
|
+
return continuationContextOf(editor.tool)
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
export function getContinuation(context: ContinuationContext): ContinuationMode {
|
|
1566
|
+
return useEditor.getState().getContinuation(context)
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
/**
|
|
1570
|
+
* The effective snapping mode for the active context. Falls back to `'off'` when
|
|
1571
|
+
* no snappable context is active (select / idle, no armed tool) so grid /
|
|
1572
|
+
* magnetic / angle readers — including the snap-grid overlay — stay inert
|
|
1573
|
+
* outside an interaction. Per-context defaults (item is `'grid'`) only take
|
|
1574
|
+
* effect once a tool is armed or an interaction begins; otherwise the item
|
|
1575
|
+
* default would light up the snap grid at idle.
|
|
1576
|
+
*/
|
|
1577
|
+
export function getActiveSnappingMode(): SnappingMode {
|
|
1578
|
+
const context = getActiveSnapContext()
|
|
1579
|
+
if (!context) return 'off'
|
|
1580
|
+
return useEditor.getState().snappingModeByContext[context]
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1044
1583
|
export default useEditor
|