@pascal-app/editor 0.8.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +168 -0
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +795 -28
- package/src/components/editor/editor-layout-mobile.tsx +8 -3
- package/src/components/editor/editor-layout-v2.tsx +76 -51
- package/src/components/editor/export-manager.tsx +69 -32
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +182 -0
- package/src/components/editor/first-person/build-collider-world.ts +228 -83
- package/src/components/editor/first-person-controls.tsx +943 -27
- package/src/components/editor/floating-action-menu.tsx +676 -208
- package/src/components/editor/floating-building-action-menu.tsx +2 -2
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +6916 -12137
- package/src/components/editor/grid.tsx +197 -36
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +420 -0
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +396 -0
- package/src/components/editor/group-transform-shared.ts +489 -0
- package/src/components/editor/handles/handle-arrow.tsx +581 -0
- package/src/components/editor/handles/use-handle-drag.ts +246 -0
- package/src/components/editor/index.tsx +412 -162
- package/src/components/editor/measurement-pill.tsx +115 -0
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +1646 -0
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +1106 -483
- package/src/components/editor/site-edge-labels.tsx +45 -21
- package/src/components/editor/slab-hole-highlights.tsx +474 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +369 -155
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +140 -41
- package/src/components/editor/use-floorplan-background-placement.ts +190 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +19 -7
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +14 -19
- package/src/components/editor/wall-move-side-handles.tsx +870 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +304 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +15 -6
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +392 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +857 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +77 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -66
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +62 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +3325 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +24 -9
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +439 -57
- package/src/components/systems/ceiling/ceiling-system.tsx +109 -12
- package/src/components/systems/roof/roof-edit-system.tsx +1851 -22
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/stair/stair-edit-system.tsx +1 -1
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +375 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +263 -0
- package/src/components/tools/fence/fence-drafting.ts +141 -27
- package/src/components/tools/item/move-tool.tsx +45 -105
- package/src/components/tools/item/placement-math.test.ts +22 -0
- package/src/components/tools/item/placement-math.ts +59 -7
- package/src/components/tools/item/placement-strategies.ts +432 -16
- package/src/components/tools/item/placement-types.ts +22 -1
- package/src/components/tools/item/use-draft-node.ts +51 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +1399 -256
- package/src/components/tools/registry/move-registry-node-tool.tsx +1064 -0
- package/src/components/tools/roof/roof-tool.tsx +350 -28
- package/src/components/tools/select/box-select-state.ts +88 -0
- package/src/components/tools/select/box-select-tool.tsx +442 -520
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +570 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +195 -0
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +305 -0
- package/src/components/tools/shared/polygon-editor.tsx +866 -175
- package/src/components/tools/shared/segment-angle.ts +56 -0
- package/src/components/tools/site/site-boundary-editor.tsx +520 -16
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +2 -1
- package/src/components/tools/stair/stair-tool.tsx +342 -37
- package/src/components/tools/tool-manager.tsx +239 -62
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +248 -171
- package/src/components/tools/wall/wall-snap-geometry.test.ts +123 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +331 -0
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +86 -82
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -170
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +3 -117
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +27 -93
- package/src/components/ui/action-menu/view-toggles.tsx +279 -90
- package/src/components/ui/command-palette/editor-commands.tsx +20 -15
- package/src/components/ui/command-palette/index.tsx +5 -3
- package/src/components/ui/controls/material-paint-panel.tsx +152 -0
- package/src/components/ui/controls/material-picker.tsx +87 -177
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +142 -46
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +47 -10
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +235 -23
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +59 -0
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +40 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +16 -36
- package/src/components/ui/level-duplicate-dialog.tsx +2 -1
- package/src/components/ui/panels/node-display.ts +17 -16
- package/src/components/ui/panels/panel-manager.tsx +55 -58
- package/src/components/ui/panels/panel-wrapper.tsx +238 -12
- package/src/components/ui/panels/parametric-inspector.tsx +482 -0
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +82 -8
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +122 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +66 -7
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +267 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +92 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -21
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +140 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +48 -8
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +132 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +9 -5
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +86 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +7 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +112 -39
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +51 -2
- package/src/components/ui/sidebar/tab-bar.tsx +75 -1
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +253 -61
- package/src/components/viewer-zone-system.tsx +7 -2
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +64 -10
- package/src/hooks/use-ceiling-events.ts +176 -0
- package/src/hooks/use-drag-action.ts +127 -0
- package/src/hooks/use-keyboard.ts +493 -95
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +451 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/constants.ts +6 -2
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +50 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +117 -0
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +10 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +339 -0
- package/src/lib/fresh-planar-placement.ts +130 -0
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +11 -1
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-duplication.ts +54 -42
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +227 -48
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +100 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +7 -7
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +332 -0
- package/src/lib/scene.ts +40 -8
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +69 -10
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +148 -55
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +231 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +767 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +39 -0
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +34 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -264
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -178
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -441
- package/src/components/tools/fence/move-fence-tool.tsx +0 -302
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -364
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -178
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -426
- package/src/components/tools/wall/move-wall-tool.tsx +0 -358
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -249
- package/src/components/ui/panels/column-panel.tsx +0 -759
- package/src/components/ui/panels/door-panel.tsx +0 -1299
- package/src/components/ui/panels/fence-panel.tsx +0 -222
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -280
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -161
- package/src/components/ui/panels/stair-panel.tsx +0 -577
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -273
- package/src/components/ui/panels/window-panel.tsx +0 -968
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
package/src/store/use-editor.tsx
CHANGED
|
@@ -2,14 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
import type { AssetInput } from '@pascal-app/core'
|
|
4
4
|
import {
|
|
5
|
+
type AnyNode,
|
|
5
6
|
type AnyNodeId,
|
|
6
7
|
type BuildingNode,
|
|
8
|
+
type CabinetModuleNode,
|
|
9
|
+
type CabinetNode,
|
|
7
10
|
type CeilingNode,
|
|
11
|
+
type ChimneyMaterialRole,
|
|
12
|
+
type ChimneyNode,
|
|
8
13
|
type ColumnNode,
|
|
9
14
|
type DoorNode,
|
|
15
|
+
type DormerNode,
|
|
16
|
+
type DormerSurfaceMaterialRole,
|
|
17
|
+
type ElevatorNode,
|
|
10
18
|
type FenceNode,
|
|
11
19
|
type ItemNode,
|
|
12
20
|
type LevelNode,
|
|
21
|
+
nodeRegistry,
|
|
13
22
|
type RoofNode,
|
|
14
23
|
type RoofSegmentNode,
|
|
15
24
|
type RoofSurfaceMaterialRole,
|
|
@@ -27,7 +36,13 @@ import {
|
|
|
27
36
|
import { useViewer } from '@pascal-app/viewer'
|
|
28
37
|
import { create } from 'zustand'
|
|
29
38
|
import { persist } from 'zustand/middleware'
|
|
30
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
CONTINUATION_PROFILES,
|
|
41
|
+
type ContinuationContext,
|
|
42
|
+
type ContinuationMode,
|
|
43
|
+
continuationContextOf,
|
|
44
|
+
nextContinuation,
|
|
45
|
+
} from '../lib/continuation'
|
|
31
46
|
import {
|
|
32
47
|
type ActivePaintMaterial,
|
|
33
48
|
type PaintableMaterialTarget,
|
|
@@ -35,14 +50,90 @@ import {
|
|
|
35
50
|
resolvePaintTargetFromSelection,
|
|
36
51
|
type SingleSurfaceMaterialRole,
|
|
37
52
|
} from '../lib/material-paint'
|
|
53
|
+
import {
|
|
54
|
+
type CreatableMeasurementKind,
|
|
55
|
+
DEFAULT_CREATABLE_MEASUREMENT_KIND,
|
|
56
|
+
normalizeCreatableMeasurementKind,
|
|
57
|
+
} from '../lib/measurement-kind'
|
|
58
|
+
import {
|
|
59
|
+
cyclePaintScope as cyclePaintScopeValue,
|
|
60
|
+
type PaintHoverInfo,
|
|
61
|
+
type PaintScope,
|
|
62
|
+
} from '../lib/paint-scope'
|
|
63
|
+
import {
|
|
64
|
+
cycleSnappingModeIn,
|
|
65
|
+
defaultSnappingModeFor,
|
|
66
|
+
resolveSnapFlags,
|
|
67
|
+
type SnapContext,
|
|
68
|
+
type SnappingMode,
|
|
69
|
+
snapContextOf,
|
|
70
|
+
snappingModesFor,
|
|
71
|
+
} from '../lib/snapping-mode'
|
|
72
|
+
import useInteractionScope from './use-interaction-scope'
|
|
38
73
|
|
|
39
|
-
const DEFAULT_ACTIVE_SIDEBAR_PANEL = '
|
|
74
|
+
const DEFAULT_ACTIVE_SIDEBAR_PANEL = 'build'
|
|
40
75
|
const DEFAULT_FLOORPLAN_PANE_RATIO = 0.5
|
|
41
76
|
const MIN_FLOORPLAN_PANE_RATIO = 0.15
|
|
42
77
|
const MAX_FLOORPLAN_PANE_RATIO = 0.85
|
|
43
78
|
|
|
79
|
+
function resolveMovingNodeTarget(
|
|
80
|
+
node:
|
|
81
|
+
| ItemNode
|
|
82
|
+
| WindowNode
|
|
83
|
+
| DoorNode
|
|
84
|
+
| ElevatorNode
|
|
85
|
+
| CeilingNode
|
|
86
|
+
| ChimneyNode
|
|
87
|
+
| ColumnNode
|
|
88
|
+
| DormerNode
|
|
89
|
+
| SlabNode
|
|
90
|
+
| WallNode
|
|
91
|
+
| FenceNode
|
|
92
|
+
| RoofNode
|
|
93
|
+
| RoofSegmentNode
|
|
94
|
+
| SpawnNode
|
|
95
|
+
| StairNode
|
|
96
|
+
| StairSegmentNode
|
|
97
|
+
| BuildingNode
|
|
98
|
+
| CabinetNode
|
|
99
|
+
| CabinetModuleNode,
|
|
100
|
+
) {
|
|
101
|
+
return node
|
|
102
|
+
}
|
|
103
|
+
|
|
44
104
|
export type ViewMode = '3d' | '2d' | 'split'
|
|
45
105
|
export type SplitOrientation = 'horizontal' | 'vertical'
|
|
106
|
+
export type WorkspaceMode = 'edit' | 'studio'
|
|
107
|
+
|
|
108
|
+
// Snapshot capture is invoked from two surfaces with different policies.
|
|
109
|
+
// `standard` mirrors the existing user-driven UX — pick region / viewport /
|
|
110
|
+
// area, save the blob as a project thumbnail. `preset` is the constrained
|
|
111
|
+
// variant for the unified preset capture flow (community save-as-preset
|
|
112
|
+
// modal): the overlay locks to a square crop, the renderer clears alpha
|
|
113
|
+
// (transparent background), and the rendered set is locked to `isolated`
|
|
114
|
+
// — `ThumbnailGenerator` consults `captureMode.mode === 'preset'` and
|
|
115
|
+
// applies those constraints. Keeping it a discriminated union lets us
|
|
116
|
+
// add future modes without surfacing the choice to end users.
|
|
117
|
+
// How the captured pixels are cropped: full-frame 16:9, raw canvas viewport,
|
|
118
|
+
// or user-dragged area. Hosts (e.g. the studio capture bar) can preselect it
|
|
119
|
+
// when entering capture mode.
|
|
120
|
+
export type SnapshotCropMode = 'standard' | 'viewport' | 'area'
|
|
121
|
+
/** Aspect presets available to `standard` crops. */
|
|
122
|
+
export type SnapshotStandardAspect = '16:9' | '9:16' | '4:3' | '3:4' | '1:1'
|
|
123
|
+
|
|
124
|
+
export type CaptureMode =
|
|
125
|
+
| { mode: 'idle' }
|
|
126
|
+
| { mode: 'standard'; crop?: SnapshotCropMode; standardAspect?: SnapshotStandardAspect }
|
|
127
|
+
| {
|
|
128
|
+
mode: 'preset'
|
|
129
|
+
isolated: AnyNodeId[]
|
|
130
|
+
framingBounds?: {
|
|
131
|
+
min: [number, number]
|
|
132
|
+
max: [number, number]
|
|
133
|
+
center: [number, number]
|
|
134
|
+
size: [number, number]
|
|
135
|
+
}
|
|
136
|
+
}
|
|
46
137
|
|
|
47
138
|
export type Phase = 'site' | 'structure' | 'furnish'
|
|
48
139
|
|
|
@@ -58,15 +149,37 @@ export type StructureTool =
|
|
|
58
149
|
| 'ceiling'
|
|
59
150
|
| 'roof'
|
|
60
151
|
| 'column'
|
|
152
|
+
| 'elevator'
|
|
61
153
|
| 'stair'
|
|
62
154
|
| 'item'
|
|
63
155
|
| 'zone'
|
|
64
156
|
| 'spawn'
|
|
65
157
|
| 'window'
|
|
66
158
|
| 'door'
|
|
159
|
+
| 'shelf'
|
|
160
|
+
| 'box-vent'
|
|
161
|
+
| 'ridge-vent'
|
|
162
|
+
| 'turbine-vent'
|
|
163
|
+
| 'cupola'
|
|
164
|
+
| 'eyebrow-vent'
|
|
165
|
+
| 'chimney'
|
|
166
|
+
| 'solar-panel'
|
|
167
|
+
| 'skylight'
|
|
168
|
+
| 'dormer'
|
|
169
|
+
| 'gutter'
|
|
170
|
+
| 'downspout'
|
|
171
|
+
| 'duct-segment'
|
|
172
|
+
| 'duct-fitting'
|
|
173
|
+
| 'duct-terminal'
|
|
174
|
+
| 'hvac-equipment'
|
|
175
|
+
| 'lineset'
|
|
176
|
+
| 'liquid-line'
|
|
177
|
+
| 'pipe-segment'
|
|
178
|
+
| 'pipe-fitting'
|
|
179
|
+
| 'pipe-trap'
|
|
67
180
|
|
|
68
181
|
// Furnish mode tools (items and decoration)
|
|
69
|
-
export type FurnishTool = 'item'
|
|
182
|
+
export type FurnishTool = 'item' | 'cabinet'
|
|
70
183
|
|
|
71
184
|
// Site mode tools
|
|
72
185
|
export type SiteTool = 'property-line'
|
|
@@ -86,24 +199,43 @@ export type StructureLayer = 'zones' | 'elements'
|
|
|
86
199
|
export type FloorplanSelectionTool = 'click' | 'marquee'
|
|
87
200
|
export type GridSnapStep = 0.5 | 0.25 | 0.1 | 0.05
|
|
88
201
|
|
|
89
|
-
|
|
90
|
-
export type Tool = SiteTool | StructureTool | FurnishTool
|
|
202
|
+
export type NavigationSyncSource = '2d' | '3d'
|
|
91
203
|
|
|
92
|
-
export type
|
|
93
|
-
|
|
94
|
-
|
|
204
|
+
export type NavigationSyncPose = {
|
|
205
|
+
source: NavigationSyncSource
|
|
206
|
+
revision: number
|
|
207
|
+
target: [number, number, number]
|
|
208
|
+
azimuth: number
|
|
209
|
+
viewWidth: number
|
|
95
210
|
}
|
|
96
211
|
|
|
97
|
-
export type
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
212
|
+
export type NavigationSyncPoseInput = Omit<NavigationSyncPose, 'revision'>
|
|
213
|
+
|
|
214
|
+
// Combined tool type. Known literals keep autocomplete; the `(string & {})`
|
|
215
|
+
// arm lets plugin-contributed tool ids (e.g. `'trees:tree'`) typecheck without
|
|
216
|
+
// the host enumerating every plugin kind. The runtime dispatch is already
|
|
217
|
+
// registry-first — `tool-manager` resolves `nodeRegistry.get(tool)?.tool` — so
|
|
218
|
+
// an unknown-to-the-host tool string flows straight through to the plugin's
|
|
219
|
+
// placement component.
|
|
220
|
+
export type KnownTool = SiteTool | StructureTool | FurnishTool
|
|
221
|
+
export type Tool = KnownTool | (string & {})
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Starting parameters seeded into a draw tool before it mints a node.
|
|
225
|
+
* A loose param bag — the tool's create path validates it through the
|
|
226
|
+
* kind's schema (`FenceNode.parse({ ...defaults, start, end })`), which
|
|
227
|
+
* is the real type gate, so unknown keys are simply ignored.
|
|
228
|
+
*/
|
|
229
|
+
export type ToolDefaults = Record<string, unknown>
|
|
101
230
|
|
|
102
231
|
export type MaterialTargetRole =
|
|
103
232
|
| WallSurfaceSide
|
|
104
233
|
| StairSurfaceMaterialRole
|
|
105
234
|
| RoofSurfaceMaterialRole
|
|
235
|
+
| ChimneyMaterialRole
|
|
236
|
+
| DormerSurfaceMaterialRole
|
|
106
237
|
| SingleSurfaceMaterialRole
|
|
238
|
+
| string
|
|
107
239
|
|
|
108
240
|
export type SelectedMaterialTarget = {
|
|
109
241
|
nodeId: AnyNodeId
|
|
@@ -116,6 +248,8 @@ type MaterialPaintSelectionSnapshot = {
|
|
|
116
248
|
activePaintMaterial: ActivePaintMaterial | null
|
|
117
249
|
}
|
|
118
250
|
|
|
251
|
+
export type SurfaceHoleTarget = { nodeId: string; holeIndex: number }
|
|
252
|
+
|
|
119
253
|
export type GuideUiState = {
|
|
120
254
|
locked?: boolean
|
|
121
255
|
scaleReferenceVisible?: boolean
|
|
@@ -128,35 +262,42 @@ type EditorState = {
|
|
|
128
262
|
setMode: (mode: Mode) => void
|
|
129
263
|
tool: Tool | null
|
|
130
264
|
setTool: (tool: Tool | null) => void
|
|
265
|
+
/**
|
|
266
|
+
* Per-tool starting parameters for the next node a draw tool mints.
|
|
267
|
+
* Transient (not persisted): host apps seed an entry just before
|
|
268
|
+
* activating the tool (placing a drawn preset, or a future dimension
|
|
269
|
+
* picker), the tool's create path merges it, and the tool clears its
|
|
270
|
+
* own entry on deactivation so a later manual draw isn't poisoned.
|
|
271
|
+
*/
|
|
272
|
+
toolDefaults: Partial<Record<Tool, ToolDefaults>>
|
|
273
|
+
setToolDefaults: (tool: Tool, defaults: ToolDefaults | null) => void
|
|
274
|
+
lastMeasurementKind: CreatableMeasurementKind
|
|
275
|
+
setLastMeasurementKind: (kind: CreatableMeasurementKind) => void
|
|
131
276
|
structureLayer: StructureLayer
|
|
132
277
|
setStructureLayer: (layer: StructureLayer) => void
|
|
133
278
|
catalogCategory: CatalogCategory | null
|
|
134
279
|
setCatalogCategory: (category: CatalogCategory | null) => void
|
|
135
280
|
selectedItem: AssetInput | null
|
|
136
281
|
setSelectedItem: (item: AssetInput) => void
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
| RoofNode
|
|
147
|
-
| RoofSegmentNode
|
|
148
|
-
| SpawnNode
|
|
149
|
-
| StairNode
|
|
150
|
-
| StairSegmentNode
|
|
151
|
-
| BuildingNode
|
|
152
|
-
| null
|
|
282
|
+
/**
|
|
283
|
+
* True while a move was engaged by a press-drag gizmo (the on-canvas move
|
|
284
|
+
* cross) rather than a click-to-place flow. The placement coordinator reads
|
|
285
|
+
* this to commit on pointer-release instead of waiting for a click.
|
|
286
|
+
*/
|
|
287
|
+
placementDragMode: boolean
|
|
288
|
+
setPlacementDragMode: (dragMode: boolean) => void
|
|
289
|
+
roofHostDragArmedId: AnyNodeId | null
|
|
290
|
+
setRoofHostDragArmedId: (nodeId: AnyNodeId | null) => void
|
|
153
291
|
setMovingNode: (
|
|
154
292
|
node:
|
|
155
293
|
| ItemNode
|
|
156
294
|
| WindowNode
|
|
157
295
|
| DoorNode
|
|
296
|
+
| ElevatorNode
|
|
158
297
|
| CeilingNode
|
|
298
|
+
| ChimneyNode
|
|
159
299
|
| ColumnNode
|
|
300
|
+
| DormerNode
|
|
160
301
|
| SlabNode
|
|
161
302
|
| WallNode
|
|
162
303
|
| FenceNode
|
|
@@ -166,29 +307,80 @@ type EditorState = {
|
|
|
166
307
|
| StairNode
|
|
167
308
|
| StairSegmentNode
|
|
168
309
|
| BuildingNode
|
|
310
|
+
| CabinetNode
|
|
311
|
+
| CabinetModuleNode
|
|
169
312
|
| null,
|
|
170
313
|
) => void
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
314
|
+
/**
|
|
315
|
+
* Which view (2D floor plan or 3D viewer) most recently completed
|
|
316
|
+
* the active move — set by the committing or cancelling side just
|
|
317
|
+
* before clearing `movingNode`. Lets the *other* side's effect
|
|
318
|
+
* cleanup skip its own restore-from-snapshot when the drag was
|
|
319
|
+
* already finalised elsewhere (split view mounts both the 2D
|
|
320
|
+
* overlay and the 3D move tool for the same `movingNode`).
|
|
321
|
+
*
|
|
322
|
+
* Reset to null when the next non-null `setMovingNode` starts a
|
|
323
|
+
* fresh drag (so stale values from the previous drag don't poison
|
|
324
|
+
* cleanups). Preserved across `setMovingNode(null)` so the
|
|
325
|
+
* non-owning side's cleanup — which fires after the clear
|
|
326
|
+
* propagates — can still read who finalised. Null while a drag
|
|
327
|
+
* is in progress means "no side has claimed it yet" — both
|
|
328
|
+
* cleanups then restore to their pre-drag snapshot, which is the
|
|
329
|
+
* same baseline, so the result is idempotent.
|
|
330
|
+
*/
|
|
331
|
+
movingNodeOrigin: '2d' | '3d' | null
|
|
332
|
+
setMovingNodeOrigin: (origin: '2d' | '3d' | null) => void
|
|
333
|
+
/**
|
|
334
|
+
* World axis the R/T keyboard rotation turns around, for kinds with
|
|
335
|
+
* full 3D orientation (duct fittings). Alt cycles it Y → X → Z; the
|
|
336
|
+
* kind's tool / keyboard actions read it, and the floating action
|
|
337
|
+
* menu surfaces it in a pill above the selected node.
|
|
338
|
+
*/
|
|
339
|
+
rotationAxis: 'x' | 'y' | 'z'
|
|
340
|
+
cycleRotationAxis: () => 'x' | 'y' | 'z'
|
|
179
341
|
selectedMaterialTarget: SelectedMaterialTarget | null
|
|
180
342
|
setSelectedMaterialTarget: (target: SelectedMaterialTarget | null) => void
|
|
181
343
|
activePaintMaterial: ActivePaintMaterial | null
|
|
182
344
|
setActivePaintMaterial: (material: ActivePaintMaterial | null) => void
|
|
183
345
|
activePaintTarget: PaintableMaterialTarget
|
|
184
346
|
setActivePaintTarget: (target: PaintableMaterialTarget) => void
|
|
347
|
+
// Live vertex count of an in-progress polygon draft (slab / ceiling), so the
|
|
348
|
+
// contextual HUD can gate hints on it (e.g. "Finish" only once ≥ 3 points).
|
|
349
|
+
// 0 when not drafting. Not persisted.
|
|
350
|
+
draftVertexCount: number
|
|
351
|
+
setDraftVertexCount: (count: number) => void
|
|
352
|
+
// Painter application scope — how far one paint click spreads (this surface /
|
|
353
|
+
// whole item / all matching / room). One global mode, target-aware in the HUD
|
|
354
|
+
// (see `lib/paint-scope.ts`), defaulting to the narrowest `'single'`. Not
|
|
355
|
+
// persisted: a "paint everything" scope should reset each session.
|
|
356
|
+
paintScope: PaintScope
|
|
357
|
+
setPaintScope: (scope: PaintScope) => void
|
|
358
|
+
// Cycle the scope within the hovered node's available set and return the new
|
|
359
|
+
// value. Bound to Shift while in paint mode.
|
|
360
|
+
cyclePaintScope: () => PaintScope
|
|
361
|
+
// When true, clicking a surface in paint mode clears it back to its
|
|
362
|
+
// default material instead of applying `activePaintMaterial`.
|
|
363
|
+
paintEraser: boolean
|
|
364
|
+
setPaintEraser: (eraser: boolean) => void
|
|
185
365
|
primeMaterialPaintFromSelection: () => MaterialPaintSelectionSnapshot
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
366
|
+
// What the cursor is over in paint mode: the scopes it offers + labels for the
|
|
367
|
+
// HUD chip. `null` when not over a paintable surface (drives the "hover a
|
|
368
|
+
// surface" hint). Set by the selection-manager paint hover; not persisted.
|
|
369
|
+
paintHover: PaintHoverInfo | null
|
|
370
|
+
setPaintHover: (info: PaintHoverInfo | null) => void
|
|
371
|
+
// Embedder capability: true when a host (e.g. community) can locate a selected
|
|
372
|
+
// node in its catalog browser. Gates the node action menu's "Find" button; the
|
|
373
|
+
// editor itself emits `selection:find-node` and lets the host fulfil it. Not
|
|
374
|
+
// persisted — it's a per-mount capability the host registers.
|
|
375
|
+
canFindNode: boolean
|
|
376
|
+
setCanFindNode: (canFind: boolean) => void
|
|
190
377
|
selectedReferenceId: string | null
|
|
191
378
|
setSelectedReferenceId: (id: string | null) => void
|
|
379
|
+
// Guide id with an in-flight reference-scale measurement (line drawing or
|
|
380
|
+
// length dialog). Owned by the floorplan panel; mirrored here so the
|
|
381
|
+
// reference panel can flip its Set Scale button into a Cancel.
|
|
382
|
+
referenceScaleActiveGuideId: string | null
|
|
383
|
+
setReferenceScaleActiveGuideId: (id: string | null) => void
|
|
192
384
|
guideUi: Record<string, GuideUiState>
|
|
193
385
|
setGuideLocked: (guideId: string, locked: boolean) => void
|
|
194
386
|
setGuideScaleReferenceVisible: (guideId: string, visible: boolean) => void
|
|
@@ -196,15 +388,21 @@ type EditorState = {
|
|
|
196
388
|
// Space detection for cutaway mode
|
|
197
389
|
spaces: Record<string, Space>
|
|
198
390
|
setSpaces: (spaces: Record<string, Space>) => void
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
setEditingHole: (hole: { nodeId: string; holeIndex: number } | null) => void
|
|
391
|
+
hoveredHole: SurfaceHoleTarget | null
|
|
392
|
+
setHoveredHole: (hole: SurfaceHoleTarget | null) => void
|
|
202
393
|
// Preview mode (viewer-like experience inside the editor)
|
|
203
394
|
isPreviewMode: boolean
|
|
204
395
|
setPreviewMode: (preview: boolean) => void
|
|
205
|
-
// Capture mode (snapshot toolbar — hides panels for clean framing)
|
|
396
|
+
// Capture mode (snapshot toolbar — hides panels for clean framing).
|
|
397
|
+
// `captureMode` is the canonical discriminated-union state; the boolean
|
|
398
|
+
// `isCaptureMode` is kept synced as a derived convenience for the many
|
|
399
|
+
// existing read sites that just gate chrome visibility on "is capture
|
|
400
|
+
// active". New write sites should pass a `CaptureMode` shape; passing a
|
|
401
|
+
// boolean is accepted as a back-compat shim (`true` → `'standard'`,
|
|
402
|
+
// `false` → `'idle'`).
|
|
403
|
+
captureMode: CaptureMode
|
|
206
404
|
isCaptureMode: boolean
|
|
207
|
-
setCaptureMode: (
|
|
405
|
+
setCaptureMode: (next: boolean | CaptureMode) => void
|
|
208
406
|
// View mode (3D only, 2D only, or split 2D+3D)
|
|
209
407
|
viewMode: ViewMode
|
|
210
408
|
setViewMode: (mode: ViewMode) => void
|
|
@@ -216,10 +414,38 @@ type EditorState = {
|
|
|
216
414
|
toggleFloorplanOpen: () => void
|
|
217
415
|
isFloorplanHovered: boolean
|
|
218
416
|
setFloorplanHovered: (hovered: boolean) => void
|
|
417
|
+
// Toggleable DWV riser-diagram (plumbing isometric) overlay.
|
|
418
|
+
isRiserOpen: boolean
|
|
419
|
+
setRiserOpen: (open: boolean) => void
|
|
420
|
+
toggleRiserOpen: () => void
|
|
421
|
+
navigationSyncPose: NavigationSyncPose | null
|
|
422
|
+
publishNavigationSyncPose: (pose: NavigationSyncPoseInput) => void
|
|
219
423
|
floorplanSelectionTool: FloorplanSelectionTool
|
|
220
424
|
setFloorplanSelectionTool: (tool: FloorplanSelectionTool) => void
|
|
221
425
|
gridSnapStep: GridSnapStep
|
|
222
426
|
setGridSnapStep: (step: GridSnapStep) => void
|
|
427
|
+
// Cycles the grid step through GRID_SNAP_STEPS (0.5 → 0.25 → 0.1 → 0.05 →
|
|
428
|
+
// 0.5) and returns the new value. Bound to the measurement-step shortcut.
|
|
429
|
+
cycleGridSnapStep: () => GridSnapStep
|
|
430
|
+
// Magnetic snapping while drafting — snaps wall endpoints onto existing
|
|
431
|
+
// wall corners / wall bodies (the "magnetic" beacon). Independent of grid
|
|
432
|
+
// snap. On by default; toggled from the Display menu.
|
|
433
|
+
magneticSnap: boolean
|
|
434
|
+
setMagneticSnap: (enabled: boolean) => void
|
|
435
|
+
// Per-context, user-cyclable snapping mode (see `lib/snapping-mode.ts`). Each
|
|
436
|
+
// activity (wall / item / polygon) keeps its own mode + default, because they
|
|
437
|
+
// want different snapping — drawing a wall wants grid + angle, nudging an item
|
|
438
|
+
// wants free movement that only catches alignment lines. Resolved to the live
|
|
439
|
+
// context via `getActiveSnappingMode()`; maps onto `gridSnapStep`/`magneticSnap`
|
|
440
|
+
// via `resolveSnapFlags`. Persisted per context.
|
|
441
|
+
snappingModeByContext: Record<SnapContext, SnappingMode>
|
|
442
|
+
setSnappingMode: (context: SnapContext, mode: SnappingMode) => void
|
|
443
|
+
// Cycle the *active* context's mode within its own set; returns the new value.
|
|
444
|
+
cycleSnappingMode: () => SnappingMode
|
|
445
|
+
continuationByContext: Record<ContinuationContext, ContinuationMode>
|
|
446
|
+
setContinuation: (context: ContinuationContext, mode: ContinuationMode) => void
|
|
447
|
+
cycleContinuation: (context: ContinuationContext) => ContinuationMode
|
|
448
|
+
getContinuation: (context: ContinuationContext) => ContinuationMode
|
|
223
449
|
showReferenceFloor: boolean
|
|
224
450
|
toggleReferenceFloor: () => void
|
|
225
451
|
setShowReferenceFloor: (show: boolean) => void
|
|
@@ -230,13 +456,23 @@ type EditorState = {
|
|
|
230
456
|
// Development-only camera debug flag for inspecting underside geometry
|
|
231
457
|
allowUndergroundCamera: boolean
|
|
232
458
|
setAllowUndergroundCamera: (enabled: boolean) => void
|
|
459
|
+
// Development-only debug overlay: draw each wall's opening-snap hit area
|
|
460
|
+
// (the capsule of points within the snap radius of its centerline). Lets us
|
|
461
|
+
// see why a door/window snaps where it does.
|
|
462
|
+
show2dVoronoi: boolean
|
|
463
|
+
setShow2dVoronoi: (enabled: boolean) => void
|
|
233
464
|
// First-person walkthrough mode (street view)
|
|
234
465
|
isFirstPersonMode: boolean
|
|
235
466
|
_viewModeBeforeFirstPerson: ViewMode | null
|
|
236
467
|
setFirstPersonMode: (enabled: boolean) => void
|
|
468
|
+
// Workspace mode: 'edit' is the full editing surface; 'studio' is the
|
|
469
|
+
// render/snapshot surface (clean canvas, no editing chrome or selection).
|
|
470
|
+
// Entering studio forces a 3D-only view and restores the prior view on exit.
|
|
471
|
+
workspaceMode: WorkspaceMode
|
|
472
|
+
_viewModeBeforeStudio: ViewMode | null
|
|
473
|
+
setWorkspaceMode: (mode: WorkspaceMode) => void
|
|
237
474
|
activeSidebarPanel: string
|
|
238
475
|
setActiveSidebarPanel: (id: string) => void
|
|
239
|
-
setIsCaptureMode: (enabled: boolean) => void
|
|
240
476
|
floorplanPaneRatio: number
|
|
241
477
|
setFloorplanPaneRatio: (ratio: number) => void
|
|
242
478
|
// Mobile-only: pixel height of the secondary panel sheet while open (0 when closed).
|
|
@@ -257,6 +493,10 @@ type PersistedEditorLayoutState = Pick<
|
|
|
257
493
|
| 'splitOrientation'
|
|
258
494
|
| 'floorplanSelectionTool'
|
|
259
495
|
| 'gridSnapStep'
|
|
496
|
+
| 'magneticSnap'
|
|
497
|
+
| 'lastMeasurementKind'
|
|
498
|
+
| 'snappingModeByContext'
|
|
499
|
+
| 'continuationByContext'
|
|
260
500
|
| 'showReferenceFloor'
|
|
261
501
|
| 'referenceFloorOffset'
|
|
262
502
|
| 'referenceFloorOpacity'
|
|
@@ -279,6 +519,19 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
279
519
|
splitOrientation: 'horizontal',
|
|
280
520
|
floorplanSelectionTool: 'click',
|
|
281
521
|
gridSnapStep: 0.5,
|
|
522
|
+
magneticSnap: true,
|
|
523
|
+
lastMeasurementKind: DEFAULT_CREATABLE_MEASUREMENT_KIND,
|
|
524
|
+
snappingModeByContext: {
|
|
525
|
+
wall: defaultSnappingModeFor('wall'),
|
|
526
|
+
item: defaultSnappingModeFor('item'),
|
|
527
|
+
polygon: defaultSnappingModeFor('polygon'),
|
|
528
|
+
},
|
|
529
|
+
continuationByContext: {
|
|
530
|
+
wall: CONTINUATION_PROFILES.wall.default,
|
|
531
|
+
fence: CONTINUATION_PROFILES.fence.default,
|
|
532
|
+
point: CONTINUATION_PROFILES.point.default,
|
|
533
|
+
cabinet: CONTINUATION_PROFILES.cabinet.default,
|
|
534
|
+
},
|
|
282
535
|
showReferenceFloor: false,
|
|
283
536
|
referenceFloorOffset: 1,
|
|
284
537
|
referenceFloorOpacity: 0.35,
|
|
@@ -286,6 +539,10 @@ export const DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE: PersistedEditorLayoutState =
|
|
|
286
539
|
|
|
287
540
|
const GRID_SNAP_STEPS: GridSnapStep[] = [0.5, 0.25, 0.1, 0.05]
|
|
288
541
|
|
|
542
|
+
type SelectDefaultBuildingAndLevelOptions = {
|
|
543
|
+
forceGroundLevel?: boolean
|
|
544
|
+
}
|
|
545
|
+
|
|
289
546
|
function normalizeModeForPhase(phase: Phase, mode: Mode | undefined): Mode {
|
|
290
547
|
if (phase === 'site') {
|
|
291
548
|
return 'select'
|
|
@@ -377,8 +634,51 @@ export function normalizePersistedEditorUiState(
|
|
|
377
634
|
}
|
|
378
635
|
}
|
|
379
636
|
|
|
637
|
+
// Validate a persisted per-context mode against that context's allowed set
|
|
638
|
+
// (so e.g. a stale `angles` for items resets), falling back to its default.
|
|
639
|
+
function migrateSnappingMode(value: unknown, context: SnapContext): SnappingMode {
|
|
640
|
+
return snappingModesFor(context).includes(value as SnappingMode)
|
|
641
|
+
? (value as SnappingMode)
|
|
642
|
+
: defaultSnappingModeFor(context)
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
type LegacyContinuationState = {
|
|
646
|
+
continuationByContext?: Partial<Record<ContinuationContext, unknown>>
|
|
647
|
+
wallChainMode?: unknown
|
|
648
|
+
fenceChainMode?: unknown
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function migrateContinuationMode(
|
|
652
|
+
value: unknown,
|
|
653
|
+
context: ContinuationContext,
|
|
654
|
+
): ContinuationMode | null {
|
|
655
|
+
const profile = CONTINUATION_PROFILES[context]
|
|
656
|
+
return profile.options.includes(value as ContinuationMode) ? (value as ContinuationMode) : null
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
function normalizeContinuationByContext(
|
|
660
|
+
state: LegacyContinuationState | null | undefined,
|
|
661
|
+
): Record<ContinuationContext, ContinuationMode> {
|
|
662
|
+
return {
|
|
663
|
+
wall:
|
|
664
|
+
migrateContinuationMode(state?.continuationByContext?.wall, 'wall') ??
|
|
665
|
+
migrateContinuationMode(state?.wallChainMode, 'wall') ??
|
|
666
|
+
CONTINUATION_PROFILES.wall.default,
|
|
667
|
+
fence:
|
|
668
|
+
migrateContinuationMode(state?.continuationByContext?.fence, 'fence') ??
|
|
669
|
+
migrateContinuationMode(state?.fenceChainMode, 'fence') ??
|
|
670
|
+
CONTINUATION_PROFILES.fence.default,
|
|
671
|
+
point:
|
|
672
|
+
migrateContinuationMode(state?.continuationByContext?.point, 'point') ??
|
|
673
|
+
CONTINUATION_PROFILES.point.default,
|
|
674
|
+
cabinet:
|
|
675
|
+
migrateContinuationMode(state?.continuationByContext?.cabinet, 'cabinet') ??
|
|
676
|
+
CONTINUATION_PROFILES.cabinet.default,
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
380
680
|
function normalizePersistedEditorLayoutState(
|
|
381
|
-
state: Partial<PersistedEditorLayoutState> | null | undefined,
|
|
681
|
+
state: (Partial<PersistedEditorLayoutState> & LegacyContinuationState) | null | undefined,
|
|
382
682
|
): PersistedEditorLayoutState {
|
|
383
683
|
return {
|
|
384
684
|
activeSidebarPanel:
|
|
@@ -391,6 +691,15 @@ function normalizePersistedEditorLayoutState(
|
|
|
391
691
|
gridSnapStep: GRID_SNAP_STEPS.includes(state?.gridSnapStep as GridSnapStep)
|
|
392
692
|
? (state?.gridSnapStep as GridSnapStep)
|
|
393
693
|
: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
694
|
+
// Default on: only an explicit persisted `false` disables it.
|
|
695
|
+
magneticSnap: state?.magneticSnap !== false,
|
|
696
|
+
lastMeasurementKind: normalizeCreatableMeasurementKind(state?.lastMeasurementKind),
|
|
697
|
+
snappingModeByContext: {
|
|
698
|
+
wall: migrateSnappingMode(state?.snappingModeByContext?.wall, 'wall'),
|
|
699
|
+
item: migrateSnappingMode(state?.snappingModeByContext?.item, 'item'),
|
|
700
|
+
polygon: migrateSnappingMode(state?.snappingModeByContext?.polygon, 'polygon'),
|
|
701
|
+
},
|
|
702
|
+
continuationByContext: normalizeContinuationByContext(state),
|
|
394
703
|
showReferenceFloor: state?.showReferenceFloor === true,
|
|
395
704
|
referenceFloorOffset:
|
|
396
705
|
typeof state?.referenceFloorOffset === 'number' && state.referenceFloorOffset >= 1
|
|
@@ -420,46 +729,101 @@ export function hasCustomPersistedEditorUiState(
|
|
|
420
729
|
)
|
|
421
730
|
}
|
|
422
731
|
|
|
732
|
+
function getDefaultLevelId(
|
|
733
|
+
buildingNode: BuildingNode,
|
|
734
|
+
nodes: Record<string, AnyNode>,
|
|
735
|
+
): LevelNode['id'] | null {
|
|
736
|
+
const levels = buildingNode.children
|
|
737
|
+
.map((childId) => nodes[childId as AnyNodeId])
|
|
738
|
+
.filter((node): node is LevelNode => node?.type === 'level')
|
|
739
|
+
|
|
740
|
+
if (levels.length === 0) {
|
|
741
|
+
return null
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const groundLevel = levels.find((level) => level.level === 0)
|
|
745
|
+
if (groundLevel) {
|
|
746
|
+
return groundLevel.id
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const firstLevel = levels[0]
|
|
750
|
+
if (!firstLevel) {
|
|
751
|
+
return null
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
let lowestLevel = firstLevel
|
|
755
|
+
for (const level of levels.slice(1)) {
|
|
756
|
+
if (level.level < lowestLevel.level) {
|
|
757
|
+
lowestLevel = level
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
return lowestLevel.id
|
|
762
|
+
}
|
|
763
|
+
|
|
423
764
|
/**
|
|
424
765
|
* Selects the first building and level 0 in the scene.
|
|
425
766
|
* Safe to call any time — no-ops if already selected or scene is empty.
|
|
426
767
|
*/
|
|
427
|
-
export function selectDefaultBuildingAndLevel() {
|
|
768
|
+
export function selectDefaultBuildingAndLevel(options: SelectDefaultBuildingAndLevelOptions = {}) {
|
|
428
769
|
const viewer = useViewer.getState()
|
|
429
770
|
const scene = useScene.getState()
|
|
430
771
|
|
|
431
|
-
|
|
772
|
+
const selectedBuilding = viewer.selection.buildingId
|
|
773
|
+
? scene.nodes[viewer.selection.buildingId]
|
|
774
|
+
: null
|
|
775
|
+
let buildingNode =
|
|
776
|
+
selectedBuilding?.type === 'building' ? (selectedBuilding as BuildingNode) : null
|
|
432
777
|
|
|
433
778
|
// If no building selected, find the first one from site's children
|
|
434
|
-
if (!
|
|
779
|
+
if (!buildingNode) {
|
|
435
780
|
const siteNode = scene.rootNodeIds[0] ? scene.nodes[scene.rootNodeIds[0]] : null
|
|
436
781
|
if (siteNode?.type === 'site') {
|
|
437
|
-
|
|
438
|
-
.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
buildingId = firstBuilding.id as BuildingNode['id']
|
|
442
|
-
viewer.setSelection({ buildingId })
|
|
443
|
-
}
|
|
782
|
+
buildingNode =
|
|
783
|
+
siteNode.children
|
|
784
|
+
.map((childId) => scene.nodes[childId as AnyNodeId])
|
|
785
|
+
.find((node): node is BuildingNode => node?.type === 'building') ?? null
|
|
444
786
|
}
|
|
445
787
|
}
|
|
446
788
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
789
|
+
if (!buildingNode) {
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
const selectedLevel = viewer.selection.levelId ? scene.nodes[viewer.selection.levelId] : null
|
|
794
|
+
const selectedLevelBelongsToBuilding =
|
|
795
|
+
selectedLevel?.type === 'level' && selectedLevel.parentId === buildingNode.id
|
|
796
|
+
const shouldSelectDefaultLevel = options.forceGroundLevel || !selectedLevelBelongsToBuilding
|
|
797
|
+
const defaultLevelId = shouldSelectDefaultLevel
|
|
798
|
+
? getDefaultLevelId(buildingNode, scene.nodes as Record<string, AnyNode>)
|
|
799
|
+
: null
|
|
800
|
+
|
|
801
|
+
const selectionUpdate: Parameters<typeof viewer.setSelection>[0] = {}
|
|
802
|
+
if (viewer.selection.buildingId !== buildingNode.id) {
|
|
803
|
+
selectionUpdate.buildingId = buildingNode.id
|
|
460
804
|
}
|
|
805
|
+
if (defaultLevelId) {
|
|
806
|
+
selectionUpdate.levelId = defaultLevelId
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
if (Object.keys(selectionUpdate).length > 0) {
|
|
810
|
+
viewer.setSelection(selectionUpdate)
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
export function selectSiteFloorplanContext() {
|
|
815
|
+
selectDefaultBuildingAndLevel({ forceGroundLevel: true })
|
|
816
|
+
useViewer.getState().setSelection({
|
|
817
|
+
selectedIds: [],
|
|
818
|
+
zoneId: null,
|
|
819
|
+
})
|
|
461
820
|
}
|
|
462
821
|
|
|
822
|
+
// Stashes the view mode the user was in before entering capture, so we can
|
|
823
|
+
// restore it on exit. Snapshot capture always frames in 3D — the 2D/split
|
|
824
|
+
// floorplan panes render nothing meaningful for a thumbnail.
|
|
825
|
+
let viewModeBeforeCapture: ViewMode | null = null
|
|
826
|
+
|
|
463
827
|
const useEditor = create<EditorState>()(
|
|
464
828
|
persist(
|
|
465
829
|
(set, get) => ({
|
|
@@ -488,12 +852,9 @@ const useEditor = create<EditorState>()(
|
|
|
488
852
|
set({ mode: 'select', tool: null, catalogCategory: null })
|
|
489
853
|
}
|
|
490
854
|
|
|
491
|
-
const viewer = useViewer.getState()
|
|
492
|
-
|
|
493
855
|
switch (phase) {
|
|
494
856
|
case 'site':
|
|
495
|
-
|
|
496
|
-
viewer.resetSelection()
|
|
857
|
+
selectSiteFloorplanContext()
|
|
497
858
|
break
|
|
498
859
|
|
|
499
860
|
case 'structure':
|
|
@@ -531,9 +892,26 @@ const useEditor = create<EditorState>()(
|
|
|
531
892
|
else if (tool) {
|
|
532
893
|
set({ tool: null })
|
|
533
894
|
}
|
|
895
|
+
|
|
896
|
+
const scope = useInteractionScope.getState()
|
|
897
|
+
if (mode === 'material-paint') scope.begin({ kind: 'painting' })
|
|
898
|
+
else scope.endIf((s) => s.kind === 'painting')
|
|
534
899
|
},
|
|
535
900
|
tool: DEFAULT_PERSISTED_EDITOR_UI_STATE.tool,
|
|
536
901
|
setTool: (tool) => set({ tool }),
|
|
902
|
+
toolDefaults: {},
|
|
903
|
+
setToolDefaults: (tool, defaults) =>
|
|
904
|
+
set((state) => {
|
|
905
|
+
const next = { ...state.toolDefaults }
|
|
906
|
+
if (defaults === null) {
|
|
907
|
+
delete next[tool]
|
|
908
|
+
} else {
|
|
909
|
+
next[tool] = defaults
|
|
910
|
+
}
|
|
911
|
+
return { toolDefaults: next }
|
|
912
|
+
}),
|
|
913
|
+
lastMeasurementKind: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.lastMeasurementKind,
|
|
914
|
+
setLastMeasurementKind: (kind) => set({ lastMeasurementKind: kind }),
|
|
537
915
|
structureLayer: DEFAULT_PERSISTED_EDITOR_UI_STATE.structureLayer,
|
|
538
916
|
setStructureLayer: (layer) => {
|
|
539
917
|
const { mode } = get()
|
|
@@ -555,38 +933,84 @@ const useEditor = create<EditorState>()(
|
|
|
555
933
|
setCatalogCategory: (category) => set({ catalogCategory: category }),
|
|
556
934
|
selectedItem: null,
|
|
557
935
|
setSelectedItem: (item) => set({ selectedItem: item }),
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
936
|
+
placementDragMode: false,
|
|
937
|
+
setPlacementDragMode: (dragMode) => set({ placementDragMode: dragMode }),
|
|
938
|
+
roofHostDragArmedId: null,
|
|
939
|
+
setRoofHostDragArmedId: (nodeId) => set({ roofHostDragArmedId: nodeId }),
|
|
940
|
+
// The node being placed/moved now lives inside the interaction scope
|
|
941
|
+
// (`useMovingNode` / `getMovingNode`), not a `useEditor` flag. This setter
|
|
942
|
+
// remains the single entry point: it drives the scope and still touches
|
|
943
|
+
// `movingNodeOrigin` / `placementDragMode` so cross-store subscribers that
|
|
944
|
+
// watch this store (community placement) keep firing on move start/end.
|
|
945
|
+
setMovingNode: (node) => {
|
|
946
|
+
const scope = useInteractionScope.getState()
|
|
947
|
+
if (node === null) {
|
|
948
|
+
scope.endIf((s) => s.kind === 'placing' || s.kind === 'moving')
|
|
949
|
+
// Preserve `movingNodeOrigin` across the clear so the non-owning
|
|
950
|
+
// side's effect cleanup — which fires after `setMovingNode(null)`
|
|
951
|
+
// propagates — can still read who finalised. The next non-null
|
|
952
|
+
// `setMovingNode` resets it. Always clear the press-drag flag.
|
|
953
|
+
set({ placementDragMode: false })
|
|
954
|
+
return
|
|
955
|
+
}
|
|
956
|
+
const targetNode = resolveMovingNodeTarget(node)
|
|
957
|
+
const isNew = Boolean((targetNode as { metadata?: { isNew?: boolean } }).metadata?.isNew)
|
|
958
|
+
if (isNew) {
|
|
959
|
+
scope.begin({
|
|
960
|
+
kind: 'placing',
|
|
961
|
+
node: targetNode,
|
|
962
|
+
nodeId: targetNode.id,
|
|
963
|
+
nodeType: targetNode.type,
|
|
964
|
+
view: '3d',
|
|
965
|
+
pressDrag: get().placementDragMode,
|
|
966
|
+
})
|
|
967
|
+
} else {
|
|
968
|
+
scope.begin({
|
|
969
|
+
kind: 'moving',
|
|
970
|
+
node: targetNode,
|
|
971
|
+
nodeId: targetNode.id,
|
|
972
|
+
nodeType: targetNode.type,
|
|
973
|
+
view: '3d',
|
|
974
|
+
})
|
|
975
|
+
}
|
|
976
|
+
set({ movingNodeOrigin: null })
|
|
977
|
+
},
|
|
978
|
+
movingNodeOrigin: null as '2d' | '3d' | null,
|
|
979
|
+
setMovingNodeOrigin: (origin) => set({ movingNodeOrigin: origin }),
|
|
980
|
+
rotationAxis: 'y',
|
|
981
|
+
cycleRotationAxis: () => {
|
|
982
|
+
const order = ['y', 'x', 'z'] as const
|
|
983
|
+
const next = order[(order.indexOf(get().rotationAxis as 'y' | 'x' | 'z') + 1) % 3]!
|
|
984
|
+
set({ rotationAxis: next })
|
|
985
|
+
return next
|
|
986
|
+
},
|
|
581
987
|
selectedMaterialTarget: null,
|
|
582
988
|
setSelectedMaterialTarget: (target) => set({ selectedMaterialTarget: target }),
|
|
583
989
|
activePaintMaterial: null,
|
|
584
|
-
|
|
990
|
+
// Picking a material implies paint, not erase — clear the eraser so the
|
|
991
|
+
// next click applies the chosen material.
|
|
992
|
+
setActivePaintMaterial: (material) =>
|
|
993
|
+
set({ activePaintMaterial: material, paintEraser: false }),
|
|
585
994
|
activePaintTarget: 'wall',
|
|
586
995
|
setActivePaintTarget: (target) =>
|
|
587
996
|
set((state) =>
|
|
588
997
|
state.activePaintTarget === target ? state : { activePaintTarget: target },
|
|
589
998
|
),
|
|
999
|
+
draftVertexCount: 0,
|
|
1000
|
+
setDraftVertexCount: (count) =>
|
|
1001
|
+
set((state) => (state.draftVertexCount === count ? state : { draftVertexCount: count })),
|
|
1002
|
+
paintScope: 'single',
|
|
1003
|
+
setPaintScope: (scope) => set({ paintScope: scope }),
|
|
1004
|
+
cyclePaintScope: () => {
|
|
1005
|
+
// Cycle within the hovered node's available scopes (what the click will
|
|
1006
|
+
// actually hit). With nothing paintable hovered there's only `single`.
|
|
1007
|
+
const scopes = get().paintHover?.scopes ?? (['single'] as PaintScope[])
|
|
1008
|
+
const next = cyclePaintScopeValue(get().paintScope, scopes)
|
|
1009
|
+
set({ paintScope: next })
|
|
1010
|
+
return next
|
|
1011
|
+
},
|
|
1012
|
+
paintEraser: false,
|
|
1013
|
+
setPaintEraser: (eraser) => set({ paintEraser: eraser }),
|
|
590
1014
|
primeMaterialPaintFromSelection: () => {
|
|
591
1015
|
const selectedId =
|
|
592
1016
|
useViewer.getState().selection.selectedIds.length === 1
|
|
@@ -614,15 +1038,14 @@ const useEditor = create<EditorState>()(
|
|
|
614
1038
|
activePaintMaterial: activePaintMaterial ?? get().activePaintMaterial,
|
|
615
1039
|
}
|
|
616
1040
|
},
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
),
|
|
622
|
-
isPaintPanelOpen: false,
|
|
623
|
-
setPaintPanelOpen: (open) => set({ isPaintPanelOpen: open }),
|
|
1041
|
+
paintHover: null,
|
|
1042
|
+
setPaintHover: (info) => set({ paintHover: info }),
|
|
1043
|
+
canFindNode: false,
|
|
1044
|
+
setCanFindNode: (canFind) => set({ canFindNode: canFind }),
|
|
624
1045
|
selectedReferenceId: null,
|
|
625
1046
|
setSelectedReferenceId: (id) => set({ selectedReferenceId: id }),
|
|
1047
|
+
referenceScaleActiveGuideId: null,
|
|
1048
|
+
setReferenceScaleActiveGuideId: (id) => set({ referenceScaleActiveGuideId: id }),
|
|
626
1049
|
guideUi: {},
|
|
627
1050
|
setGuideLocked: (guideId, locked) =>
|
|
628
1051
|
set((state) => ({
|
|
@@ -655,8 +1078,14 @@ const useEditor = create<EditorState>()(
|
|
|
655
1078
|
}),
|
|
656
1079
|
spaces: {},
|
|
657
1080
|
setSpaces: (spaces) => set({ spaces }),
|
|
658
|
-
|
|
659
|
-
|
|
1081
|
+
hoveredHole: null,
|
|
1082
|
+
setHoveredHole: (hole) =>
|
|
1083
|
+
set((state) =>
|
|
1084
|
+
state.hoveredHole?.nodeId === hole?.nodeId &&
|
|
1085
|
+
state.hoveredHole?.holeIndex === hole?.holeIndex
|
|
1086
|
+
? state
|
|
1087
|
+
: { hoveredHole: hole },
|
|
1088
|
+
),
|
|
660
1089
|
isPreviewMode: false,
|
|
661
1090
|
setPreviewMode: (preview) => {
|
|
662
1091
|
if (preview) {
|
|
@@ -667,8 +1096,41 @@ const useEditor = create<EditorState>()(
|
|
|
667
1096
|
set({ isPreviewMode: false })
|
|
668
1097
|
}
|
|
669
1098
|
},
|
|
1099
|
+
captureMode: { mode: 'idle' } as CaptureMode,
|
|
670
1100
|
isCaptureMode: false,
|
|
671
|
-
setCaptureMode: (
|
|
1101
|
+
setCaptureMode: (next) => {
|
|
1102
|
+
const resolved: CaptureMode =
|
|
1103
|
+
typeof next === 'boolean' ? { mode: next ? 'standard' : 'idle' } : next
|
|
1104
|
+
const entering = resolved.mode !== 'idle'
|
|
1105
|
+
set((state) => {
|
|
1106
|
+
if (entering) {
|
|
1107
|
+
// Force 3D for the shot. Remember the prior mode only on the first
|
|
1108
|
+
// entry (viewMode is already '3d' on re-entry), so we restore the
|
|
1109
|
+
// user's real choice — not the forced '3d' — when capture ends.
|
|
1110
|
+
if (state.viewMode !== '3d') {
|
|
1111
|
+
viewModeBeforeCapture = state.viewMode
|
|
1112
|
+
return {
|
|
1113
|
+
captureMode: resolved,
|
|
1114
|
+
isCaptureMode: true,
|
|
1115
|
+
viewMode: '3d',
|
|
1116
|
+
isFloorplanOpen: false,
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
return { captureMode: resolved, isCaptureMode: true }
|
|
1120
|
+
}
|
|
1121
|
+
const restore = viewModeBeforeCapture
|
|
1122
|
+
viewModeBeforeCapture = null
|
|
1123
|
+
if (restore && restore !== '3d') {
|
|
1124
|
+
return {
|
|
1125
|
+
captureMode: resolved,
|
|
1126
|
+
isCaptureMode: false,
|
|
1127
|
+
viewMode: restore,
|
|
1128
|
+
isFloorplanOpen: true,
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return { captureMode: resolved, isCaptureMode: false }
|
|
1132
|
+
})
|
|
1133
|
+
},
|
|
672
1134
|
viewMode: DEFAULT_PERSISTED_EDITOR_UI_STATE.viewMode,
|
|
673
1135
|
setViewMode: (mode) => set({ viewMode: mode, isFloorplanOpen: mode !== '3d' }),
|
|
674
1136
|
splitOrientation: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.splitOrientation,
|
|
@@ -682,10 +1144,63 @@ const useEditor = create<EditorState>()(
|
|
|
682
1144
|
}),
|
|
683
1145
|
isFloorplanHovered: false,
|
|
684
1146
|
setFloorplanHovered: (hovered) => set({ isFloorplanHovered: hovered }),
|
|
1147
|
+
isRiserOpen: false,
|
|
1148
|
+
setRiserOpen: (open) => set({ isRiserOpen: open }),
|
|
1149
|
+
toggleRiserOpen: () => set((state) => ({ isRiserOpen: !state.isRiserOpen })),
|
|
1150
|
+
navigationSyncPose: null,
|
|
1151
|
+
publishNavigationSyncPose: (pose) =>
|
|
1152
|
+
set((state) => ({
|
|
1153
|
+
navigationSyncPose: {
|
|
1154
|
+
...pose,
|
|
1155
|
+
revision: (state.navigationSyncPose?.revision ?? 0) + 1,
|
|
1156
|
+
},
|
|
1157
|
+
})),
|
|
685
1158
|
floorplanSelectionTool: 'click' as FloorplanSelectionTool,
|
|
686
1159
|
setFloorplanSelectionTool: (tool) => set({ floorplanSelectionTool: tool }),
|
|
687
1160
|
gridSnapStep: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.gridSnapStep,
|
|
688
1161
|
setGridSnapStep: (step) => set({ gridSnapStep: step }),
|
|
1162
|
+
cycleGridSnapStep: () => {
|
|
1163
|
+
const current = get().gridSnapStep
|
|
1164
|
+
const index = GRID_SNAP_STEPS.indexOf(current)
|
|
1165
|
+
const next = GRID_SNAP_STEPS[(index + 1) % GRID_SNAP_STEPS.length] ?? GRID_SNAP_STEPS[0]!
|
|
1166
|
+
set({ gridSnapStep: next })
|
|
1167
|
+
return next
|
|
1168
|
+
},
|
|
1169
|
+
magneticSnap: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.magneticSnap,
|
|
1170
|
+
setMagneticSnap: (enabled) => set({ magneticSnap: enabled }),
|
|
1171
|
+
snappingModeByContext: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.snappingModeByContext,
|
|
1172
|
+
setSnappingMode: (context, mode) =>
|
|
1173
|
+
set((state) => ({
|
|
1174
|
+
snappingModeByContext: { ...state.snappingModeByContext, [context]: mode },
|
|
1175
|
+
})),
|
|
1176
|
+
cycleSnappingMode: () => {
|
|
1177
|
+
const context = getActiveSnapContext() ?? 'item'
|
|
1178
|
+
const current = get().snappingModeByContext[context]
|
|
1179
|
+
const next = cycleSnappingModeIn(context, current)
|
|
1180
|
+
set((state) => ({
|
|
1181
|
+
snappingModeByContext: { ...state.snappingModeByContext, [context]: next },
|
|
1182
|
+
}))
|
|
1183
|
+
return next
|
|
1184
|
+
},
|
|
1185
|
+
continuationByContext: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.continuationByContext,
|
|
1186
|
+
setContinuation: (context, mode) => {
|
|
1187
|
+
const next =
|
|
1188
|
+
migrateContinuationMode(mode, context) ?? CONTINUATION_PROFILES[context].default
|
|
1189
|
+
set((state) => ({
|
|
1190
|
+
continuationByContext: { ...state.continuationByContext, [context]: next },
|
|
1191
|
+
}))
|
|
1192
|
+
},
|
|
1193
|
+
cycleContinuation: (context) => {
|
|
1194
|
+
const next = nextContinuation(context, get().getContinuation(context))
|
|
1195
|
+
set((state) => ({
|
|
1196
|
+
continuationByContext: { ...state.continuationByContext, [context]: next },
|
|
1197
|
+
}))
|
|
1198
|
+
return next
|
|
1199
|
+
},
|
|
1200
|
+
getContinuation: (context) => {
|
|
1201
|
+
const current = get().continuationByContext[context]
|
|
1202
|
+
return migrateContinuationMode(current, context) ?? CONTINUATION_PROFILES[context].default
|
|
1203
|
+
},
|
|
689
1204
|
showReferenceFloor: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.showReferenceFloor,
|
|
690
1205
|
toggleReferenceFloor: () =>
|
|
691
1206
|
set((state) => ({ showReferenceFloor: !state.showReferenceFloor })),
|
|
@@ -698,6 +1213,8 @@ const useEditor = create<EditorState>()(
|
|
|
698
1213
|
set({ referenceFloorOpacity: Math.min(0.8, Math.max(0.1, opacity)) }),
|
|
699
1214
|
allowUndergroundCamera: false,
|
|
700
1215
|
setAllowUndergroundCamera: (enabled) => set({ allowUndergroundCamera: enabled }),
|
|
1216
|
+
show2dVoronoi: false,
|
|
1217
|
+
setShow2dVoronoi: (enabled) => set({ show2dVoronoi: enabled }),
|
|
701
1218
|
isFirstPersonMode: false,
|
|
702
1219
|
_viewModeBeforeFirstPerson: null as ViewMode | null,
|
|
703
1220
|
setFirstPersonMode: (enabled) => {
|
|
@@ -721,9 +1238,34 @@ const useEditor = create<EditorState>()(
|
|
|
721
1238
|
})
|
|
722
1239
|
}
|
|
723
1240
|
},
|
|
1241
|
+
workspaceMode: 'edit' as WorkspaceMode,
|
|
1242
|
+
_viewModeBeforeStudio: null as ViewMode | null,
|
|
1243
|
+
setWorkspaceMode: (mode) => {
|
|
1244
|
+
if (get().workspaceMode === mode) return
|
|
1245
|
+
if (mode === 'studio') {
|
|
1246
|
+
const currentViewMode = get().viewMode
|
|
1247
|
+
set({
|
|
1248
|
+
workspaceMode: 'studio',
|
|
1249
|
+
_viewModeBeforeStudio: currentViewMode,
|
|
1250
|
+
viewMode: '3d',
|
|
1251
|
+
isFloorplanOpen: false,
|
|
1252
|
+
mode: 'select',
|
|
1253
|
+
tool: null,
|
|
1254
|
+
catalogCategory: null,
|
|
1255
|
+
})
|
|
1256
|
+
// Clear selection so no edit affordances bleed into the clean canvas.
|
|
1257
|
+
useViewer.getState().setSelection({ selectedIds: [], zoneId: null })
|
|
1258
|
+
} else {
|
|
1259
|
+
const prevMode = get()._viewModeBeforeStudio
|
|
1260
|
+
set({
|
|
1261
|
+
workspaceMode: 'edit',
|
|
1262
|
+
_viewModeBeforeStudio: null,
|
|
1263
|
+
...(prevMode ? { viewMode: prevMode, isFloorplanOpen: prevMode !== '3d' } : {}),
|
|
1264
|
+
})
|
|
1265
|
+
}
|
|
1266
|
+
},
|
|
724
1267
|
activeSidebarPanel: DEFAULT_ACTIVE_SIDEBAR_PANEL,
|
|
725
1268
|
setActiveSidebarPanel: (id) => set({ activeSidebarPanel: id }),
|
|
726
|
-
setIsCaptureMode: (enabled) => set({ isCaptureMode: enabled }),
|
|
727
1269
|
floorplanPaneRatio: DEFAULT_PERSISTED_EDITOR_LAYOUT_STATE.floorplanPaneRatio,
|
|
728
1270
|
setFloorplanPaneRatio: (ratio) =>
|
|
729
1271
|
set({ floorplanPaneRatio: normalizeFloorplanPaneRatio(ratio) }),
|
|
@@ -732,11 +1274,28 @@ const useEditor = create<EditorState>()(
|
|
|
732
1274
|
}),
|
|
733
1275
|
{
|
|
734
1276
|
name: 'pascal-editor-ui-preferences',
|
|
735
|
-
merge: (persistedState, currentState) =>
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
1277
|
+
merge: (persistedState, currentState) => {
|
|
1278
|
+
const uiState = normalizePersistedEditorUiState(
|
|
1279
|
+
persistedState as Partial<PersistedEditorState>,
|
|
1280
|
+
)
|
|
1281
|
+
const layoutState = normalizePersistedEditorLayoutState(
|
|
1282
|
+
persistedState as Partial<PersistedEditorState>,
|
|
1283
|
+
)
|
|
1284
|
+
|
|
1285
|
+
return {
|
|
1286
|
+
...currentState,
|
|
1287
|
+
...uiState,
|
|
1288
|
+
...layoutState,
|
|
1289
|
+
...(uiState.mode === 'build' && uiState.tool === 'measurement'
|
|
1290
|
+
? {
|
|
1291
|
+
toolDefaults: {
|
|
1292
|
+
...currentState.toolDefaults,
|
|
1293
|
+
measurement: { kind: layoutState.lastMeasurementKind },
|
|
1294
|
+
},
|
|
1295
|
+
}
|
|
1296
|
+
: {}),
|
|
1297
|
+
}
|
|
1298
|
+
},
|
|
740
1299
|
partialize: (state) => ({
|
|
741
1300
|
phase: state.phase,
|
|
742
1301
|
mode: state.mode,
|
|
@@ -750,12 +1309,106 @@ const useEditor = create<EditorState>()(
|
|
|
750
1309
|
splitOrientation: state.splitOrientation,
|
|
751
1310
|
floorplanSelectionTool: state.floorplanSelectionTool,
|
|
752
1311
|
gridSnapStep: state.gridSnapStep,
|
|
1312
|
+
magneticSnap: state.magneticSnap,
|
|
1313
|
+
lastMeasurementKind: state.lastMeasurementKind,
|
|
1314
|
+
snappingModeByContext: state.snappingModeByContext,
|
|
1315
|
+
continuationByContext: state.continuationByContext,
|
|
753
1316
|
showReferenceFloor: state.showReferenceFloor,
|
|
754
1317
|
referenceFloorOffset: state.referenceFloorOffset,
|
|
755
1318
|
referenceFloorOpacity: state.referenceFloorOpacity,
|
|
756
1319
|
}),
|
|
1320
|
+
skipHydration: true,
|
|
757
1321
|
},
|
|
758
1322
|
),
|
|
759
1323
|
)
|
|
760
1324
|
|
|
1325
|
+
/**
|
|
1326
|
+
* Effective magnetic-snap state: the legacy `magneticSnap` flag AND the active
|
|
1327
|
+
* context's snapping mode. With exclusive modes, magnetic (alignment axes + wall
|
|
1328
|
+
* corner-join) is on only in `'lines'`. Read from the smallest magnetic choke
|
|
1329
|
+
* points so the mode is honoured without retuning any snap math.
|
|
1330
|
+
*/
|
|
1331
|
+
export function isMagneticSnapActive(): boolean {
|
|
1332
|
+
const state = useEditor.getState()
|
|
1333
|
+
return state.magneticSnap && resolveSnapFlags(getActiveSnappingMode()).magnetic
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* Effective angle-lock state: the active context's snapping mode. With exclusive
|
|
1338
|
+
* modes the 15°/45° lock is on only in `'angles'`. Read from the smallest
|
|
1339
|
+
* angle-lock choke points (wall / fence draft call sites).
|
|
1340
|
+
*/
|
|
1341
|
+
export function isAngleSnapActive(): boolean {
|
|
1342
|
+
return resolveSnapFlags(getActiveSnappingMode()).angles
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* Effective grid-lattice state: the active context's snapping mode. With
|
|
1347
|
+
* exclusive modes the grid quantize is on only in `'grid'`.
|
|
1348
|
+
*/
|
|
1349
|
+
export function isGridSnapActive(): boolean {
|
|
1350
|
+
return resolveSnapFlags(getActiveSnappingMode()).grid
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* Whether alignment "lines" should be DISPLAYED for the active context.
|
|
1355
|
+
*
|
|
1356
|
+
* True whenever a snappable context is active — in EVERY snapping mode,
|
|
1357
|
+
* including `'off'`. The guides are passive reference feedback; this is
|
|
1358
|
+
* decoupled from the magnetic *pull*: a producer publishes guides whenever this
|
|
1359
|
+
* is true, but only applies the alignment delta when `isMagneticSnapActive()`
|
|
1360
|
+
* (i.e. `'lines'`). So the user always sees the same alignment lines while
|
|
1361
|
+
* snapping to grid / angles / off, and only snaps to them in `'lines'` mode.
|
|
1362
|
+
*/
|
|
1363
|
+
export function isAlignmentGuideActive(): boolean {
|
|
1364
|
+
return getActiveSnapContext() !== null
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/**
|
|
1368
|
+
* The snapping context for what the user is currently doing (wall / item /
|
|
1369
|
+
* polygon), or null when nothing snappable is active. Derived from the
|
|
1370
|
+
* authoritative interaction scope, falling back to the armed build tool (the
|
|
1371
|
+
* `drafting` scope isn't wired). The single source every snap reader + the HUD
|
|
1372
|
+
* resolve their mode through.
|
|
1373
|
+
*/
|
|
1374
|
+
export function getActiveSnapContext(): SnapContext | null {
|
|
1375
|
+
const editor = useEditor.getState()
|
|
1376
|
+
return snapContextOf({
|
|
1377
|
+
scope: useInteractionScope.getState().scope,
|
|
1378
|
+
mode: editor.mode,
|
|
1379
|
+
tool: editor.tool,
|
|
1380
|
+
profileOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapProfile,
|
|
1381
|
+
draftDirectionalOf: (typeOrTool) => nodeRegistry.get(typeOrTool)?.snapDraftDirectional ?? true,
|
|
1382
|
+
})
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
export function getActiveContinuationContext(): ContinuationContext | null {
|
|
1386
|
+
const scope = useInteractionScope.getState().scope
|
|
1387
|
+
if (scope.kind === 'drafting') return continuationContextOf(scope.tool)
|
|
1388
|
+
if (scope.kind === 'placing') return continuationContextOf(scope.nodeType)
|
|
1389
|
+
if (scope.kind !== 'idle') return null
|
|
1390
|
+
|
|
1391
|
+
const editor = useEditor.getState()
|
|
1392
|
+
if (editor.mode !== 'build' || !editor.tool) return null
|
|
1393
|
+
return continuationContextOf(editor.tool)
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
export function getContinuation(context: ContinuationContext): ContinuationMode {
|
|
1397
|
+
return useEditor.getState().getContinuation(context)
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* The effective snapping mode for the active context. Falls back to `'off'` when
|
|
1402
|
+
* no snappable context is active (select / idle, no armed tool) so grid /
|
|
1403
|
+
* magnetic / angle readers — including the snap-grid overlay — stay inert
|
|
1404
|
+
* outside an interaction. Per-context defaults (item is `'grid'`) only take
|
|
1405
|
+
* effect once a tool is armed or an interaction begins; otherwise the item
|
|
1406
|
+
* default would light up the snap grid at idle.
|
|
1407
|
+
*/
|
|
1408
|
+
export function getActiveSnappingMode(): SnappingMode {
|
|
1409
|
+
const context = getActiveSnapContext()
|
|
1410
|
+
if (!context) return 'off'
|
|
1411
|
+
return useEditor.getState().snappingModeByContext[context]
|
|
1412
|
+
}
|
|
1413
|
+
|
|
761
1414
|
export default useEditor
|