@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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
export type ElevationGuide3D = {
|
|
4
|
+
ownerId: string
|
|
5
|
+
levelId: string
|
|
6
|
+
center: readonly [number, number]
|
|
7
|
+
direction: readonly [number, number]
|
|
8
|
+
elevation: number
|
|
9
|
+
label: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type ElevationGuidesState = {
|
|
13
|
+
guide: ElevationGuide3D | null
|
|
14
|
+
publish(guide: ElevationGuide3D): void
|
|
15
|
+
clear(ownerId: string): void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const useElevationGuides = create<ElevationGuidesState>((set) => ({
|
|
19
|
+
guide: null,
|
|
20
|
+
publish: (guide) =>
|
|
21
|
+
set((state) => {
|
|
22
|
+
const current = state.guide
|
|
23
|
+
if (
|
|
24
|
+
current?.ownerId === guide.ownerId &&
|
|
25
|
+
current.levelId === guide.levelId &&
|
|
26
|
+
current.elevation === guide.elevation &&
|
|
27
|
+
current.label === guide.label &&
|
|
28
|
+
current.center[0] === guide.center[0] &&
|
|
29
|
+
current.center[1] === guide.center[1] &&
|
|
30
|
+
current.direction[0] === guide.direction[0] &&
|
|
31
|
+
current.direction[1] === guide.direction[1]
|
|
32
|
+
) {
|
|
33
|
+
return state
|
|
34
|
+
}
|
|
35
|
+
return { guide }
|
|
36
|
+
}),
|
|
37
|
+
clear: (ownerId) => set((state) => (state.guide?.ownerId === ownerId ? { guide: null } : state)),
|
|
38
|
+
}))
|
|
39
|
+
|
|
40
|
+
export default useElevationGuides
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Ephemeral store for the forward-facing floor triangle shown while placing or
|
|
2
|
+
// moving a node. A single editor-side overlay (`<FacingPoseIndicator>`)
|
|
3
|
+
// subscribes and renders the triangle; every placement/move path publishes its
|
|
4
|
+
// ghost pose here instead of drawing its own triangle. This is deliberately the
|
|
5
|
+
// one renderer for the facing indicator: rendering it from inside a tool's own
|
|
6
|
+
// cursor ghost (especially tools living in `@pascal-app/nodes`) left it
|
|
7
|
+
// invisible, while the editor-side overlay renders reliably. Producers clear on
|
|
8
|
+
// commit, cancel, and unmount.
|
|
9
|
+
//
|
|
10
|
+
// Poses are in the same building-local frame the tools already work in (the
|
|
11
|
+
// overlay is mounted inside ToolManager's building-local group).
|
|
12
|
+
|
|
13
|
+
import { create } from 'zustand'
|
|
14
|
+
|
|
15
|
+
export type FacingPose = {
|
|
16
|
+
/** Ghost origin in building-local space. */
|
|
17
|
+
position: [number, number, number]
|
|
18
|
+
/** Ghost yaw (radians). The triangle inherits this so it points where the
|
|
19
|
+
* node faces. */
|
|
20
|
+
rotationY: number
|
|
21
|
+
/** Footprint depth along the ghost's local +Z; the triangle sits just past
|
|
22
|
+
* `center[1] + depth / 2`. */
|
|
23
|
+
depth: number
|
|
24
|
+
/** Footprint centre offset `[x, z]` in the ghost's local frame. Defaults to
|
|
25
|
+
* the origin. Kinds whose forward edge isn't centred on the origin (e.g. a
|
|
26
|
+
* stair, whose run starts at the entry) shift the triangle via this. */
|
|
27
|
+
center?: [number, number]
|
|
28
|
+
/** Point along local -Z (the front is the -Z side) instead of +Z. */
|
|
29
|
+
reversed?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type FacingPoseState = {
|
|
33
|
+
pose: FacingPose | null
|
|
34
|
+
set(pose: FacingPose): void
|
|
35
|
+
clear(): void
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const useFacingPose = create<FacingPoseState>((set) => ({
|
|
39
|
+
pose: null,
|
|
40
|
+
set: (pose) => set({ pose }),
|
|
41
|
+
clear: () => set({ pose: null }),
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
export default useFacingPose
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Ephemeral store for the in-progress curved-fence draft. Written by the 3D
|
|
2
|
+
// spline tool and read by the contextual helper plus out-of-tree consumers.
|
|
3
|
+
// Reset on commit, cancel, and unmount — never persisted or recorded in undo
|
|
4
|
+
// history.
|
|
5
|
+
|
|
6
|
+
import { create } from 'zustand'
|
|
7
|
+
|
|
8
|
+
export type FenceCurveDraftPoint = [number, number]
|
|
9
|
+
|
|
10
|
+
type FenceCurveDraftState = {
|
|
11
|
+
pointCount: number
|
|
12
|
+
points: FenceCurveDraftPoint[]
|
|
13
|
+
cursor: FenceCurveDraftPoint | null
|
|
14
|
+
setDraft(points: readonly FenceCurveDraftPoint[], cursor: FenceCurveDraftPoint | null): void
|
|
15
|
+
reset(): void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function pointsEqual(a: readonly FenceCurveDraftPoint[], b: readonly FenceCurveDraftPoint[]) {
|
|
19
|
+
return (
|
|
20
|
+
a.length === b.length &&
|
|
21
|
+
a.every((point, index) => point[0] === b[index]?.[0] && point[1] === b[index]?.[1])
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const useFenceCurveDraft = create<FenceCurveDraftState>((set) => ({
|
|
26
|
+
pointCount: 0,
|
|
27
|
+
points: [],
|
|
28
|
+
cursor: null,
|
|
29
|
+
setDraft: (points, cursor) =>
|
|
30
|
+
set((state) => {
|
|
31
|
+
const sameCursor =
|
|
32
|
+
(!cursor && !state.cursor) ||
|
|
33
|
+
Boolean(
|
|
34
|
+
cursor && state.cursor && state.cursor[0] === cursor[0] && state.cursor[1] === cursor[1],
|
|
35
|
+
)
|
|
36
|
+
if (sameCursor && pointsEqual(state.points, points)) return state
|
|
37
|
+
return {
|
|
38
|
+
pointCount: points.length,
|
|
39
|
+
points: points.map(([x, z]) => [x, z] as FenceCurveDraftPoint),
|
|
40
|
+
cursor: cursor ? [cursor[0], cursor[1]] : null,
|
|
41
|
+
}
|
|
42
|
+
}),
|
|
43
|
+
reset: () =>
|
|
44
|
+
set((state) =>
|
|
45
|
+
state.pointCount === 0 && state.points.length === 0 && state.cursor === null
|
|
46
|
+
? state
|
|
47
|
+
: { pointCount: 0, points: [], cursor: null },
|
|
48
|
+
),
|
|
49
|
+
}))
|
|
50
|
+
|
|
51
|
+
export default useFenceCurveDraft
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
export type WalkthroughInteract = { label: string; verb: string } | null
|
|
4
|
+
|
|
5
|
+
export type FirstPersonHudState = {
|
|
6
|
+
floorLabel: string | null
|
|
7
|
+
zoneLabel: string | null
|
|
8
|
+
interact: WalkthroughInteract
|
|
9
|
+
setHud: (hud: Partial<Pick<FirstPersonHudState, 'floorLabel' | 'zoneLabel' | 'interact'>>) => void
|
|
10
|
+
reset: () => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useFirstPersonHud = create<FirstPersonHudState>((set) => ({
|
|
14
|
+
floorLabel: null,
|
|
15
|
+
zoneLabel: null,
|
|
16
|
+
interact: null,
|
|
17
|
+
setHud: (hud) =>
|
|
18
|
+
set((state) => {
|
|
19
|
+
const floorLabel = hud.floorLabel === undefined ? state.floorLabel : hud.floorLabel
|
|
20
|
+
const zoneLabel = hud.zoneLabel === undefined ? state.zoneLabel : hud.zoneLabel
|
|
21
|
+
const interact = hud.interact === undefined ? state.interact : hud.interact
|
|
22
|
+
|
|
23
|
+
if (
|
|
24
|
+
floorLabel === state.floorLabel &&
|
|
25
|
+
zoneLabel === state.zoneLabel &&
|
|
26
|
+
interact?.label === state.interact?.label &&
|
|
27
|
+
interact?.verb === state.interact?.verb
|
|
28
|
+
) {
|
|
29
|
+
return state
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { floorLabel, zoneLabel, interact }
|
|
33
|
+
}),
|
|
34
|
+
reset: () =>
|
|
35
|
+
set((state) =>
|
|
36
|
+
state.floorLabel === null && state.zoneLabel === null && state.interact === null
|
|
37
|
+
? state
|
|
38
|
+
: { floorLabel: null, zoneLabel: null, interact: null },
|
|
39
|
+
),
|
|
40
|
+
}))
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand'
|
|
4
|
+
import { persist } from 'zustand/middleware'
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY,
|
|
7
|
+
type FloorplanAnnotationCategory,
|
|
8
|
+
type FloorplanAnnotationVisibility,
|
|
9
|
+
normalizeFloorplanAnnotationVisibility,
|
|
10
|
+
} from '../lib/floorplan/annotation-visibility'
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
|
|
13
|
+
type FloorplanWallDimensionReference,
|
|
14
|
+
normalizeFloorplanWallDimensionReference,
|
|
15
|
+
} from '../lib/floorplan/floorplan-extension'
|
|
16
|
+
|
|
17
|
+
type FloorplanAnnotationVisibilityState = {
|
|
18
|
+
visibility: FloorplanAnnotationVisibility
|
|
19
|
+
wallDimensionReference: FloorplanWallDimensionReference
|
|
20
|
+
setCategory: (category: FloorplanAnnotationCategory, visible: boolean) => void
|
|
21
|
+
setWallDimensionReference: (reference: FloorplanWallDimensionReference) => void
|
|
22
|
+
reset: () => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const useFloorplanAnnotationVisibility = create<FloorplanAnnotationVisibilityState>()(
|
|
26
|
+
persist(
|
|
27
|
+
(set) => ({
|
|
28
|
+
visibility: { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY },
|
|
29
|
+
wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
|
|
30
|
+
setCategory: (category, visible) =>
|
|
31
|
+
set((state) => ({ visibility: { ...state.visibility, [category]: visible } })),
|
|
32
|
+
setWallDimensionReference: (wallDimensionReference) => set({ wallDimensionReference }),
|
|
33
|
+
reset: () =>
|
|
34
|
+
set({
|
|
35
|
+
visibility: { ...DEFAULT_FLOORPLAN_ANNOTATION_VISIBILITY },
|
|
36
|
+
wallDimensionReference: DEFAULT_FLOORPLAN_WALL_DIMENSION_REFERENCE,
|
|
37
|
+
}),
|
|
38
|
+
}),
|
|
39
|
+
{
|
|
40
|
+
name: 'pascal-floorplan-annotation-visibility',
|
|
41
|
+
merge: (persistedState, currentState) => ({
|
|
42
|
+
...currentState,
|
|
43
|
+
visibility: normalizeFloorplanAnnotationVisibility(
|
|
44
|
+
(persistedState as { visibility?: unknown } | undefined)?.visibility,
|
|
45
|
+
),
|
|
46
|
+
wallDimensionReference: normalizeFloorplanWallDimensionReference(
|
|
47
|
+
(persistedState as { wallDimensionReference?: unknown } | undefined)
|
|
48
|
+
?.wallDimensionReference,
|
|
49
|
+
),
|
|
50
|
+
}),
|
|
51
|
+
partialize: (state) =>
|
|
52
|
+
({
|
|
53
|
+
visibility: state.visibility,
|
|
54
|
+
wallDimensionReference: state.wallDimensionReference,
|
|
55
|
+
}) as FloorplanAnnotationVisibilityState,
|
|
56
|
+
},
|
|
57
|
+
),
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
export default useFloorplanAnnotationVisibility
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Ephemeral store for the 2D floor-plan's in-flight DRAFT preview state — the
|
|
2
|
+
// hot, per-pointer-move values every build/edit tool republishes on `grid:move`
|
|
3
|
+
// (the snapped cursor point today; wall/fence/roof draft endpoints as later
|
|
4
|
+
// slices land here). It exists so those per-move updates DON'T live in
|
|
5
|
+
// `FloorplanPanel`'s own `useState`: the panel is a ~10k-line component whose
|
|
6
|
+
// render costs ~120-220ms, so a `setState` per move made every 2D draft tool
|
|
7
|
+
// feel laggy. Producers write via `getState().setX(...)` (no panel re-render);
|
|
8
|
+
// the small overlay leaves subscribe and re-render alone. Same pattern that
|
|
9
|
+
// keeps stair / column / elevator placement smooth (`useStairBuildPreview`,
|
|
10
|
+
// `usePlacementPreview`).
|
|
11
|
+
//
|
|
12
|
+
// Editor-only. Producers clear on tool-inactive, commit, and unmount.
|
|
13
|
+
|
|
14
|
+
import type { WallPlanPoint } from '@pascal-app/core'
|
|
15
|
+
import { create } from 'zustand'
|
|
16
|
+
|
|
17
|
+
/** Screen-space (SVG-local px) cursor point — drives the coordinate badge. */
|
|
18
|
+
type SvgPoint = { x: number; y: number }
|
|
19
|
+
|
|
20
|
+
export type FloorplanPolygonDraftType = 'ceiling' | 'slab' | 'zone'
|
|
21
|
+
|
|
22
|
+
type FloorplanDraftPreviewState = {
|
|
23
|
+
/** Snapped plan-XZ point under the cursor; drives the crosshair + the
|
|
24
|
+
* cursor-following polygon-draft preview. `null` when idle. */
|
|
25
|
+
cursorPoint: WallPlanPoint | null
|
|
26
|
+
/** Screen-space cursor point driving the coordinate-indicator badge. Set on
|
|
27
|
+
* every SVG `pointermove` while a build/select tool is active, so it's the
|
|
28
|
+
* single hottest 2D update — keeping it out of panel state is what stops the
|
|
29
|
+
* panel re-rendering per move. `null` when idle. */
|
|
30
|
+
cursorPosition: SvgPoint | null
|
|
31
|
+
/** Live END point of the open wall / fence / roof draft segment — the per-move
|
|
32
|
+
* endpoint that drives the 2D draft polygon + measurement. Each is `null`
|
|
33
|
+
* unless that tool's draft is open. The START points are mirrored here (set
|
|
34
|
+
* per click / per 3D draft move) so out-of-tree consumers — e.g. the hosted
|
|
35
|
+
* host-owned preview publishers — can observe the whole open
|
|
36
|
+
* segment; panel state remains the 2D interaction source of truth. */
|
|
37
|
+
wallDraftEnd: WallPlanPoint | null
|
|
38
|
+
fenceDraftEnd: WallPlanPoint | null
|
|
39
|
+
roofDraftEnd: WallPlanPoint | null
|
|
40
|
+
wallDraftStart: WallPlanPoint | null
|
|
41
|
+
fenceDraftStart: WallPlanPoint | null
|
|
42
|
+
roofDraftStart: WallPlanPoint | null
|
|
43
|
+
polygonDraftType: FloorplanPolygonDraftType | null
|
|
44
|
+
polygonDraftPoints: WallPlanPoint[]
|
|
45
|
+
/** Set the snapped cursor point. No-ops (skips the store update, so
|
|
46
|
+
* subscribers don't re-render) when unchanged — `grid:move` fires far more
|
|
47
|
+
* often than the snapped cell actually changes. */
|
|
48
|
+
setCursorPoint(point: WallPlanPoint | null): void
|
|
49
|
+
/** Set the screen-space cursor point (deduped on x/y). */
|
|
50
|
+
setCursorPosition(point: SvgPoint | null): void
|
|
51
|
+
setWallDraftEnd(point: WallPlanPoint | null): void
|
|
52
|
+
setFenceDraftEnd(point: WallPlanPoint | null): void
|
|
53
|
+
setRoofDraftEnd(point: WallPlanPoint | null): void
|
|
54
|
+
setWallDraftStart(point: WallPlanPoint | null): void
|
|
55
|
+
setFenceDraftStart(point: WallPlanPoint | null): void
|
|
56
|
+
setRoofDraftStart(point: WallPlanPoint | null): void
|
|
57
|
+
setPolygonDraft(type: FloorplanPolygonDraftType | null, points: readonly WallPlanPoint[]): void
|
|
58
|
+
reset(): void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function planPointsEqual(a: readonly WallPlanPoint[], b: readonly WallPlanPoint[]) {
|
|
62
|
+
return (
|
|
63
|
+
a.length === b.length &&
|
|
64
|
+
a.every((point, index) => point[0] === b[index]?.[0] && point[1] === b[index]?.[1])
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function setPlanPointField(
|
|
69
|
+
field:
|
|
70
|
+
| 'fenceDraftEnd'
|
|
71
|
+
| 'fenceDraftStart'
|
|
72
|
+
| 'roofDraftEnd'
|
|
73
|
+
| 'roofDraftStart'
|
|
74
|
+
| 'wallDraftEnd'
|
|
75
|
+
| 'wallDraftStart',
|
|
76
|
+
point: WallPlanPoint | null,
|
|
77
|
+
) {
|
|
78
|
+
return (
|
|
79
|
+
state: FloorplanDraftPreviewState,
|
|
80
|
+
): Partial<FloorplanDraftPreviewState> | typeof state => {
|
|
81
|
+
const prev = state[field]
|
|
82
|
+
if (!point && !prev) return state
|
|
83
|
+
if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
|
|
84
|
+
return { [field]: point }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const useFloorplanDraftPreview = create<FloorplanDraftPreviewState>((set) => ({
|
|
89
|
+
cursorPoint: null,
|
|
90
|
+
cursorPosition: null,
|
|
91
|
+
wallDraftEnd: null,
|
|
92
|
+
fenceDraftEnd: null,
|
|
93
|
+
roofDraftEnd: null,
|
|
94
|
+
wallDraftStart: null,
|
|
95
|
+
fenceDraftStart: null,
|
|
96
|
+
roofDraftStart: null,
|
|
97
|
+
polygonDraftType: null,
|
|
98
|
+
polygonDraftPoints: [],
|
|
99
|
+
setCursorPoint: (point) =>
|
|
100
|
+
set((state) => {
|
|
101
|
+
const prev = state.cursorPoint
|
|
102
|
+
if (!point && !prev) return state
|
|
103
|
+
if (point && prev && prev[0] === point[0] && prev[1] === point[1]) return state
|
|
104
|
+
return { cursorPoint: point }
|
|
105
|
+
}),
|
|
106
|
+
setCursorPosition: (point) =>
|
|
107
|
+
set((state) => {
|
|
108
|
+
const prev = state.cursorPosition
|
|
109
|
+
if (!point && !prev) return state
|
|
110
|
+
if (point && prev && prev.x === point.x && prev.y === point.y) return state
|
|
111
|
+
return { cursorPosition: point }
|
|
112
|
+
}),
|
|
113
|
+
setWallDraftEnd: (point) => set(setPlanPointField('wallDraftEnd', point)),
|
|
114
|
+
setFenceDraftEnd: (point) => set(setPlanPointField('fenceDraftEnd', point)),
|
|
115
|
+
setRoofDraftEnd: (point) => set(setPlanPointField('roofDraftEnd', point)),
|
|
116
|
+
setWallDraftStart: (point) => set(setPlanPointField('wallDraftStart', point)),
|
|
117
|
+
setFenceDraftStart: (point) => set(setPlanPointField('fenceDraftStart', point)),
|
|
118
|
+
setRoofDraftStart: (point) => set(setPlanPointField('roofDraftStart', point)),
|
|
119
|
+
setPolygonDraft: (type, points) =>
|
|
120
|
+
set((state) =>
|
|
121
|
+
state.polygonDraftType === type && planPointsEqual(state.polygonDraftPoints, points)
|
|
122
|
+
? state
|
|
123
|
+
: { polygonDraftType: type, polygonDraftPoints: points.map(([x, z]) => [x, z]) },
|
|
124
|
+
),
|
|
125
|
+
reset: () =>
|
|
126
|
+
set((state) =>
|
|
127
|
+
state.cursorPoint === null &&
|
|
128
|
+
state.cursorPosition === null &&
|
|
129
|
+
state.wallDraftEnd === null &&
|
|
130
|
+
state.fenceDraftEnd === null &&
|
|
131
|
+
state.roofDraftEnd === null &&
|
|
132
|
+
state.wallDraftStart === null &&
|
|
133
|
+
state.fenceDraftStart === null &&
|
|
134
|
+
state.roofDraftStart === null &&
|
|
135
|
+
state.polygonDraftType === null &&
|
|
136
|
+
state.polygonDraftPoints.length === 0
|
|
137
|
+
? state
|
|
138
|
+
: {
|
|
139
|
+
cursorPoint: null,
|
|
140
|
+
cursorPosition: null,
|
|
141
|
+
wallDraftEnd: null,
|
|
142
|
+
fenceDraftEnd: null,
|
|
143
|
+
roofDraftEnd: null,
|
|
144
|
+
wallDraftStart: null,
|
|
145
|
+
fenceDraftStart: null,
|
|
146
|
+
roofDraftStart: null,
|
|
147
|
+
polygonDraftType: null,
|
|
148
|
+
polygonDraftPoints: [],
|
|
149
|
+
},
|
|
150
|
+
),
|
|
151
|
+
}))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Ephemeral store for the 2D floor-plan marquee (box-select) drag — the hot,
|
|
2
|
+
// per-pointer-move rectangle the select tool republishes on every move. It
|
|
3
|
+
// lives here, not in `FloorplanPanel`'s `useState`, for the same reason as
|
|
4
|
+
// `useFloorplanDraftPreview`: the panel is a ~10k-line component whose render
|
|
5
|
+
// costs ~120-220ms, so a `setState` per move made dragging a selection box
|
|
6
|
+
// re-render the whole panel. Producers write via `getState()` (no panel
|
|
7
|
+
// re-render); the small marquee overlay leaf subscribes and re-renders alone.
|
|
8
|
+
//
|
|
9
|
+
// The whole drag struct lives here (not just the moving corner) so the
|
|
10
|
+
// pointer-move / -up handlers read it non-reactively via `getState()` and the
|
|
11
|
+
// panel never subscribes. Editor-only; reset on pointer-up / cancel /
|
|
12
|
+
// tool-inactive.
|
|
13
|
+
|
|
14
|
+
import type { WallPlanPoint } from '@pascal-app/core'
|
|
15
|
+
import { create } from 'zustand'
|
|
16
|
+
|
|
17
|
+
export type FloorplanMarqueeDrag = {
|
|
18
|
+
pointerId: number
|
|
19
|
+
startClientX: number
|
|
20
|
+
startClientY: number
|
|
21
|
+
startPlanPoint: WallPlanPoint
|
|
22
|
+
/** Moving corner under the cursor — the only field that changes per move. */
|
|
23
|
+
currentPlanPoint: WallPlanPoint
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type FloorplanMarqueeState = {
|
|
27
|
+
drag: FloorplanMarqueeDrag | null
|
|
28
|
+
begin(drag: FloorplanMarqueeDrag): void
|
|
29
|
+
/** Advance the moving corner. No-ops (skips the store update, so the overlay
|
|
30
|
+
* doesn't re-render) when the snapped point is unchanged or no drag is open. */
|
|
31
|
+
setCurrent(point: WallPlanPoint): void
|
|
32
|
+
reset(): void
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const useFloorplanMarquee = create<FloorplanMarqueeState>((set) => ({
|
|
36
|
+
drag: null,
|
|
37
|
+
begin: (drag) => set({ drag }),
|
|
38
|
+
setCurrent: (point) =>
|
|
39
|
+
set((state) => {
|
|
40
|
+
const prev = state.drag
|
|
41
|
+
if (!prev) return state
|
|
42
|
+
if (prev.currentPlanPoint[0] === point[0] && prev.currentPlanPoint[1] === point[1]) {
|
|
43
|
+
return state
|
|
44
|
+
}
|
|
45
|
+
return { drag: { ...prev, currentPlanPoint: point } }
|
|
46
|
+
}),
|
|
47
|
+
reset: () => set((state) => (state.drag === null ? state : { drag: null })),
|
|
48
|
+
}))
|
|
49
|
+
|
|
50
|
+
export default useFloorplanMarquee
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { create } from 'zustand'
|
|
4
|
+
import { persist } from 'zustand/middleware'
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_FLOORPLAN_MODE,
|
|
7
|
+
type FloorplanMode,
|
|
8
|
+
normalizeFloorplanModesByProject,
|
|
9
|
+
} from '../lib/floorplan/floorplan-mode'
|
|
10
|
+
|
|
11
|
+
export type FloorplanModeNotice = {
|
|
12
|
+
id: number
|
|
13
|
+
kind: 'info' | 'switch-to-expert'
|
|
14
|
+
message: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type FloorplanModeState = {
|
|
18
|
+
mode: FloorplanMode
|
|
19
|
+
projectId: string | null
|
|
20
|
+
modesByProject: Record<string, FloorplanMode>
|
|
21
|
+
hasShownDefaultReassurance: boolean
|
|
22
|
+
notice: FloorplanModeNotice | null
|
|
23
|
+
dismissNotice: () => void
|
|
24
|
+
setMode: (mode: FloorplanMode) => void
|
|
25
|
+
setProjectId: (projectId: string | null) => void
|
|
26
|
+
showExpertModeNotice: (toolLabel: string) => void
|
|
27
|
+
showNotice: (message: string) => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let nextNoticeId = 0
|
|
31
|
+
|
|
32
|
+
const useFloorplanMode = create<FloorplanModeState>()(
|
|
33
|
+
persist(
|
|
34
|
+
(set) => ({
|
|
35
|
+
mode: DEFAULT_FLOORPLAN_MODE,
|
|
36
|
+
projectId: null,
|
|
37
|
+
modesByProject: {},
|
|
38
|
+
hasShownDefaultReassurance: false,
|
|
39
|
+
notice: null,
|
|
40
|
+
dismissNotice: () => set({ notice: null }),
|
|
41
|
+
setMode: (mode) =>
|
|
42
|
+
set((state) => {
|
|
43
|
+
const shouldReassure =
|
|
44
|
+
state.mode === 'expert' && mode === 'default' && !state.hasShownDefaultReassurance
|
|
45
|
+
return {
|
|
46
|
+
mode,
|
|
47
|
+
modesByProject: state.projectId
|
|
48
|
+
? { ...state.modesByProject, [state.projectId]: mode }
|
|
49
|
+
: state.modesByProject,
|
|
50
|
+
hasShownDefaultReassurance: state.hasShownDefaultReassurance || shouldReassure,
|
|
51
|
+
notice: shouldReassure
|
|
52
|
+
? {
|
|
53
|
+
id: ++nextNoticeId,
|
|
54
|
+
kind: 'info',
|
|
55
|
+
message:
|
|
56
|
+
'Default mode hides Expert annotations. Your saved work was not deleted.',
|
|
57
|
+
}
|
|
58
|
+
: state.notice,
|
|
59
|
+
}
|
|
60
|
+
}),
|
|
61
|
+
setProjectId: (projectId) =>
|
|
62
|
+
set((state) => ({
|
|
63
|
+
projectId,
|
|
64
|
+
mode: projectId
|
|
65
|
+
? (state.modesByProject[projectId] ?? DEFAULT_FLOORPLAN_MODE)
|
|
66
|
+
: DEFAULT_FLOORPLAN_MODE,
|
|
67
|
+
notice: null,
|
|
68
|
+
})),
|
|
69
|
+
showExpertModeNotice: (toolLabel) =>
|
|
70
|
+
set({
|
|
71
|
+
notice: {
|
|
72
|
+
id: ++nextNoticeId,
|
|
73
|
+
kind: 'switch-to-expert',
|
|
74
|
+
message: `${toolLabel} is available in Expert mode.`,
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
showNotice: (message) =>
|
|
78
|
+
set({
|
|
79
|
+
notice: {
|
|
80
|
+
id: ++nextNoticeId,
|
|
81
|
+
kind: 'info',
|
|
82
|
+
message,
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
}),
|
|
86
|
+
{
|
|
87
|
+
name: 'pascal-floorplan-mode-by-project',
|
|
88
|
+
merge: (persistedState, currentState) => {
|
|
89
|
+
const persisted = persistedState as
|
|
90
|
+
| { hasShownDefaultReassurance?: unknown; modesByProject?: unknown }
|
|
91
|
+
| undefined
|
|
92
|
+
const modesByProject = normalizeFloorplanModesByProject(persisted?.modesByProject)
|
|
93
|
+
return {
|
|
94
|
+
...currentState,
|
|
95
|
+
modesByProject,
|
|
96
|
+
hasShownDefaultReassurance: persisted?.hasShownDefaultReassurance === true,
|
|
97
|
+
mode: currentState.projectId
|
|
98
|
+
? (modesByProject[currentState.projectId] ?? DEFAULT_FLOORPLAN_MODE)
|
|
99
|
+
: DEFAULT_FLOORPLAN_MODE,
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
partialize: (state) => ({
|
|
103
|
+
hasShownDefaultReassurance: state.hasShownDefaultReassurance,
|
|
104
|
+
modesByProject: state.modesByProject,
|
|
105
|
+
}),
|
|
106
|
+
},
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
export default useFloorplanMode
|