@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,136 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import useFenceCurveDraft from './use-fence-curve-draft'
|
|
3
|
+
import { useFloorplanDraftPreview } from './use-floorplan-draft-preview'
|
|
4
|
+
import { usePathDraftPreview } from './use-path-draft-preview'
|
|
5
|
+
import { useStairBuildPreview } from './use-stair-build-preview'
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
useFenceCurveDraft.getState().reset()
|
|
9
|
+
useFloorplanDraftPreview.getState().reset()
|
|
10
|
+
usePathDraftPreview.getState().reset()
|
|
11
|
+
useStairBuildPreview.getState().reset()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('live draft preview stores', () => {
|
|
15
|
+
test('dedupes polygon snapshots and owns an immutable point copy', () => {
|
|
16
|
+
let changes = 0
|
|
17
|
+
const unsubscribe = useFloorplanDraftPreview.subscribe(() => {
|
|
18
|
+
changes += 1
|
|
19
|
+
})
|
|
20
|
+
const points: Array<[number, number]> = [
|
|
21
|
+
[0, 0],
|
|
22
|
+
[2, 0],
|
|
23
|
+
]
|
|
24
|
+
useFloorplanDraftPreview.getState().setPolygonDraft('slab', points)
|
|
25
|
+
useFloorplanDraftPreview.getState().setPolygonDraft('slab', points)
|
|
26
|
+
points[0]![0] = 9
|
|
27
|
+
|
|
28
|
+
expect(changes).toBe(1)
|
|
29
|
+
expect(useFloorplanDraftPreview.getState().polygonDraftPoints).toEqual([
|
|
30
|
+
[0, 0],
|
|
31
|
+
[2, 0],
|
|
32
|
+
])
|
|
33
|
+
unsubscribe()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('publishes stair point and rotation atomically', () => {
|
|
37
|
+
let changes = 0
|
|
38
|
+
const unsubscribe = useStairBuildPreview.subscribe(() => {
|
|
39
|
+
changes += 1
|
|
40
|
+
})
|
|
41
|
+
useStairBuildPreview.getState().setPreview([3, 4], Math.PI / 2)
|
|
42
|
+
useStairBuildPreview.getState().setPreview([3, 4], Math.PI / 2)
|
|
43
|
+
|
|
44
|
+
expect(changes).toBe(1)
|
|
45
|
+
expect(useStairBuildPreview.getState()).toMatchObject({
|
|
46
|
+
point: [3, 4],
|
|
47
|
+
rotation: Math.PI / 2,
|
|
48
|
+
})
|
|
49
|
+
unsubscribe()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('dedupes curved-fence control points and cursor', () => {
|
|
53
|
+
let changes = 0
|
|
54
|
+
const unsubscribe = useFenceCurveDraft.subscribe(() => {
|
|
55
|
+
changes += 1
|
|
56
|
+
})
|
|
57
|
+
const points: Array<[number, number]> = [
|
|
58
|
+
[0, 0],
|
|
59
|
+
[1, 1],
|
|
60
|
+
]
|
|
61
|
+
useFenceCurveDraft.getState().setDraft(points, [2, 0])
|
|
62
|
+
useFenceCurveDraft.getState().setDraft(points, [2, 0])
|
|
63
|
+
points[0]![0] = 5
|
|
64
|
+
|
|
65
|
+
expect(changes).toBe(1)
|
|
66
|
+
expect(useFenceCurveDraft.getState()).toMatchObject({
|
|
67
|
+
cursor: [2, 0],
|
|
68
|
+
pointCount: 2,
|
|
69
|
+
points: [
|
|
70
|
+
[0, 0],
|
|
71
|
+
[1, 1],
|
|
72
|
+
],
|
|
73
|
+
})
|
|
74
|
+
unsubscribe()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
test('dedupes path drafts and owns immutable point, parameter, and related-node copies', () => {
|
|
78
|
+
let changes = 0
|
|
79
|
+
const unsubscribe = usePathDraftPreview.subscribe(() => {
|
|
80
|
+
changes += 1
|
|
81
|
+
})
|
|
82
|
+
const points: Array<[number, number, number]> = [[0, 1, 2]]
|
|
83
|
+
const parameters = { diameter: 6, shape: 'round' }
|
|
84
|
+
const relatedNodes = [
|
|
85
|
+
{
|
|
86
|
+
angle: 90,
|
|
87
|
+
branchAngle: 90,
|
|
88
|
+
diameter: 6,
|
|
89
|
+
diameter2: 6,
|
|
90
|
+
ductMaterial: 'sheet-metal' as const,
|
|
91
|
+
fittingType: 'elbow' as const,
|
|
92
|
+
height: 8,
|
|
93
|
+
height2: 8,
|
|
94
|
+
id: 'duct-fitting_live-draft-0' as const,
|
|
95
|
+
metadata: {},
|
|
96
|
+
object: 'node' as const,
|
|
97
|
+
parentId: 'level_1' as const,
|
|
98
|
+
position: [1, 2, 3] as [number, number, number],
|
|
99
|
+
rotation: [0, 0, 0] as [number, number, number],
|
|
100
|
+
shape: 'round' as const,
|
|
101
|
+
shape2: 'round' as const,
|
|
102
|
+
slots: undefined,
|
|
103
|
+
system: 'supply' as const,
|
|
104
|
+
type: 'duct-fitting' as const,
|
|
105
|
+
visible: true,
|
|
106
|
+
width: 14,
|
|
107
|
+
width2: 14,
|
|
108
|
+
},
|
|
109
|
+
]
|
|
110
|
+
usePathDraftPreview
|
|
111
|
+
.getState()
|
|
112
|
+
.setDraft('duct-segment', points, [3, 4, 5], parameters, relatedNodes)
|
|
113
|
+
usePathDraftPreview
|
|
114
|
+
.getState()
|
|
115
|
+
.setDraft('duct-segment', points, [3, 4, 5], parameters, relatedNodes)
|
|
116
|
+
points[0]![0] = 9
|
|
117
|
+
parameters.diameter = 12
|
|
118
|
+
relatedNodes[0]!.position[0] = 9
|
|
119
|
+
|
|
120
|
+
expect(changes).toBe(1)
|
|
121
|
+
expect(usePathDraftPreview.getState()).toMatchObject({
|
|
122
|
+
cursor: [3, 4, 5],
|
|
123
|
+
kind: 'duct-segment',
|
|
124
|
+
parameters: { diameter: 6, shape: 'round' },
|
|
125
|
+
points: [[0, 1, 2]],
|
|
126
|
+
relatedNodes: [
|
|
127
|
+
expect.objectContaining({
|
|
128
|
+
id: 'duct-fitting_live-draft-0',
|
|
129
|
+
position: [1, 2, 3],
|
|
130
|
+
type: 'duct-fitting',
|
|
131
|
+
}),
|
|
132
|
+
],
|
|
133
|
+
})
|
|
134
|
+
unsubscribe()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
navigationSyncPoseStore,
|
|
4
|
+
publishNavigationSyncPoseToStore,
|
|
5
|
+
subscribeNavigationSyncPose,
|
|
6
|
+
} from './navigation-sync-pose-store'
|
|
7
|
+
import type { NavigationSyncPose } from './use-editor'
|
|
8
|
+
|
|
9
|
+
const pose = {
|
|
10
|
+
source: '2d' as const,
|
|
11
|
+
revision: 1,
|
|
12
|
+
target: [1, 2, 3] as [number, number, number],
|
|
13
|
+
azimuth: 0.5,
|
|
14
|
+
viewWidth: 12,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
navigationSyncPoseStore.setState({ pose: null })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
describe('navigation sync pose store', () => {
|
|
22
|
+
test('replays the latest pose and streams only later pose writes', () => {
|
|
23
|
+
publishNavigationSyncPoseToStore(pose)
|
|
24
|
+
const received: NavigationSyncPose[] = []
|
|
25
|
+
const unsubscribe = subscribeNavigationSyncPose((nextPose) => received.push(nextPose))
|
|
26
|
+
const nextPose = { ...pose, revision: 2, viewWidth: 8 }
|
|
27
|
+
|
|
28
|
+
publishNavigationSyncPoseToStore(nextPose)
|
|
29
|
+
unsubscribe()
|
|
30
|
+
publishNavigationSyncPoseToStore({ ...pose, revision: 3 })
|
|
31
|
+
|
|
32
|
+
expect(received).toEqual([pose, nextPose])
|
|
33
|
+
})
|
|
34
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { subscribeWithSelector } from 'zustand/middleware'
|
|
2
|
+
import { createStore } from 'zustand/vanilla'
|
|
3
|
+
import type { NavigationSyncPose } from './use-editor'
|
|
4
|
+
|
|
5
|
+
type NavigationSyncPoseState = {
|
|
6
|
+
pose: NavigationSyncPose | null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const navigationSyncPoseStore = createStore<NavigationSyncPoseState>()(
|
|
10
|
+
subscribeWithSelector<NavigationSyncPoseState>(() => ({ pose: null })),
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
export function publishNavigationSyncPoseToStore(pose: NavigationSyncPose) {
|
|
14
|
+
navigationSyncPoseStore.setState({ pose })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function subscribeNavigationSyncPose(listener: (pose: NavigationSyncPose) => void) {
|
|
18
|
+
const currentPose = navigationSyncPoseStore.getState().pose
|
|
19
|
+
if (currentPose) listener(currentPose)
|
|
20
|
+
|
|
21
|
+
return navigationSyncPoseStore.subscribe(
|
|
22
|
+
(state) => state.pose,
|
|
23
|
+
(pose) => {
|
|
24
|
+
if (pose) listener(pose)
|
|
25
|
+
},
|
|
26
|
+
)
|
|
27
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import useEditor, { isBrushMode, type Mode, normalizePersistedEditorUiState } from './use-editor'
|
|
3
|
+
import useInteractionScope from './use-interaction-scope'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The mode-lifecycle contract for terrain sculpting.
|
|
7
|
+
*
|
|
8
|
+
* These are not tests of the brush — that lives in `core`'s terrain-brush tests.
|
|
9
|
+
* They cover the part the user experiences as "does the tool fight the rest of
|
|
10
|
+
* the editor": entering sculpt mode must not leave conflicting affordances
|
|
11
|
+
* armed, and leaving it must not leak sculpt-only state into select mode.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function reset() {
|
|
15
|
+
// These come first: each one forces `mode: 'select'` on entry, so leaving one
|
|
16
|
+
// latched would make the next test's `setMode('terrain-sculpt')` a no-op.
|
|
17
|
+
useEditor.getState().setPreviewMode(false)
|
|
18
|
+
useEditor.getState().setFirstPersonMode(false)
|
|
19
|
+
useEditor.getState().setWorkspaceMode('edit')
|
|
20
|
+
// Before the mode: `setViewMode('2d')` disarms sculpt, and leaving a stale view
|
|
21
|
+
// behind would silently change what the next test's `setMode` does.
|
|
22
|
+
useEditor.getState().setViewMode('3d')
|
|
23
|
+
useEditor.getState().setMode('select')
|
|
24
|
+
useEditor.getState().setTerrainVerb('flatten')
|
|
25
|
+
useEditor.getState().setTerrainSampling(false)
|
|
26
|
+
useInteractionScope.getState().end()
|
|
27
|
+
}
|
|
28
|
+
afterEach(reset)
|
|
29
|
+
|
|
30
|
+
describe('terrain-sculpt mode lifecycle', () => {
|
|
31
|
+
test('entering claims the sculpting scope for the whole mode', () => {
|
|
32
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
33
|
+
// Held by the mode, not by a pointer gesture: an active scope is what makes
|
|
34
|
+
// every other node's handles and the floating action menu step back.
|
|
35
|
+
expect(useInteractionScope.getState().scope.kind).toBe('sculpting')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('leaving releases it, so selection works again', () => {
|
|
39
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
40
|
+
useEditor.getState().setMode('select')
|
|
41
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
test('arming sculpt from another phase moves to the site phase', () => {
|
|
45
|
+
useEditor.getState().setPhase('structure')
|
|
46
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
47
|
+
// Otherwise `normalizeModeForPhase` rejects the mode on the next rehydrate
|
|
48
|
+
// and the UI shows a mode the store does not hold.
|
|
49
|
+
expect(useEditor.getState().phase).toBe('site')
|
|
50
|
+
expect(useEditor.getState().mode).toBe('terrain-sculpt')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
test('a phase switch out of site drops the scope', () => {
|
|
54
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
55
|
+
// This path rewrites `mode` without going through `setMode`, which is
|
|
56
|
+
// exactly why the scope sync is its own function.
|
|
57
|
+
useEditor.getState().setPhase('structure')
|
|
58
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
describe('the eyedropper arm is not allowed to outlive sculpt mode', () => {
|
|
63
|
+
test('leaving via setMode disarms it', () => {
|
|
64
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
65
|
+
useEditor.getState().setTerrainSampling(true)
|
|
66
|
+
useEditor.getState().setMode('select')
|
|
67
|
+
// A one-shot arm that survived would swallow the user's first click back in
|
|
68
|
+
// select mode, with the panel that showed it as armed already unmounted.
|
|
69
|
+
expect(useEditor.getState().terrainSampling).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('leaving via a phase switch disarms it too', () => {
|
|
73
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
74
|
+
useEditor.getState().setTerrainSampling(true)
|
|
75
|
+
useEditor.getState().setPhase('furnish')
|
|
76
|
+
expect(useEditor.getState().terrainSampling).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('switching away from flatten disarms it', () => {
|
|
80
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
81
|
+
useEditor.getState().setTerrainSampling(true)
|
|
82
|
+
useEditor.getState().setTerrainVerb('raise')
|
|
83
|
+
expect(useEditor.getState().terrainSampling).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('staying on flatten keeps it armed', () => {
|
|
87
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
88
|
+
useEditor.getState().setTerrainSampling(true)
|
|
89
|
+
useEditor.getState().setTerrainVerb('flatten')
|
|
90
|
+
expect(useEditor.getState().terrainSampling).toBe(true)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('picking a target consumes the arm', () => {
|
|
94
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
95
|
+
useEditor.getState().setTerrainSampling(true)
|
|
96
|
+
useEditor.getState().setTerrainFlattenTarget(2.5)
|
|
97
|
+
expect(useEditor.getState().terrainFlattenTarget).toBe(2.5)
|
|
98
|
+
expect(useEditor.getState().terrainSampling).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe('the brush and the 3D canvas travel together', () => {
|
|
103
|
+
// In `2d` the 3D pane is still mounted but `display: none`, so nothing about an
|
|
104
|
+
// armed brush is observable and no pointer event can reach it. Every path that
|
|
105
|
+
// could pair the two has to resolve it, and they resolve it in opposite
|
|
106
|
+
// directions on purpose: arming the mode asks for the ground (so the view
|
|
107
|
+
// moves), while choosing the floorplan asks for the floorplan (so the mode
|
|
108
|
+
// yields).
|
|
109
|
+
test('arming from the 2D floorplan opens a 3D pane beside it', () => {
|
|
110
|
+
useEditor.getState().setViewMode('2d')
|
|
111
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
112
|
+
// `split`, not `3d`: the floorplan was a deliberate choice, so keep it.
|
|
113
|
+
expect(useEditor.getState().viewMode).toBe('split')
|
|
114
|
+
expect(useEditor.getState().isFloorplanOpen).toBe(true)
|
|
115
|
+
expect(useEditor.getState().mode).toBe('terrain-sculpt')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
test('switching to the 2D floorplan disarms the brush', () => {
|
|
119
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
120
|
+
useEditor.getState().setViewMode('2d')
|
|
121
|
+
expect(useEditor.getState().mode).toBe('select')
|
|
122
|
+
// And the scope goes with it — otherwise selection stays suppressed in the
|
|
123
|
+
// floorplan the user just asked for, with no visible way out.
|
|
124
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
test('split keeps it armed — the canvas is right there', () => {
|
|
128
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
129
|
+
useEditor.getState().setViewMode('split')
|
|
130
|
+
expect(useEditor.getState().mode).toBe('terrain-sculpt')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('rehydrating the pair resolves it to select', () => {
|
|
134
|
+
// Mode and view persist independently and rehydrate runs neither setter, so
|
|
135
|
+
// this is the one reconciliation the setters cannot cover.
|
|
136
|
+
const state = normalizePersistedEditorUiState({
|
|
137
|
+
phase: 'site',
|
|
138
|
+
mode: 'terrain-sculpt',
|
|
139
|
+
viewMode: '2d',
|
|
140
|
+
})
|
|
141
|
+
expect(state.mode).toBe('select')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
test('rehydrating sculpt beside a visible canvas restores the mode', () => {
|
|
145
|
+
const state = normalizePersistedEditorUiState({
|
|
146
|
+
phase: 'site',
|
|
147
|
+
mode: 'terrain-sculpt',
|
|
148
|
+
viewMode: 'split',
|
|
149
|
+
})
|
|
150
|
+
expect(state.mode).toBe('terrain-sculpt')
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('the modes that hide the editing canvas release the brush', () => {
|
|
155
|
+
// Preview / walkthrough / studio all unmount `ToolManager` (via `noEditing`),
|
|
156
|
+
// so the sculpt tool that would release the scope on unmount is gone. Each one
|
|
157
|
+
// resets `mode` with a raw `set` rather than `setMode`, which is exactly the
|
|
158
|
+
// bug class `setPhase` had: a `sculpting` scope leaked here disables selection
|
|
159
|
+
// across the whole editor with nothing left mounted to clear it.
|
|
160
|
+
test('entering preview releases it', () => {
|
|
161
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
162
|
+
useEditor.getState().setPreviewMode(true)
|
|
163
|
+
expect(useEditor.getState().mode).toBe('select')
|
|
164
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
test('entering the first-person walkthrough releases it', () => {
|
|
168
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
169
|
+
useEditor.getState().setFirstPersonMode(true)
|
|
170
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test('entering the studio workspace releases it', () => {
|
|
174
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
175
|
+
useEditor.getState().setWorkspaceMode('studio')
|
|
176
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
test('a structure-layer switch releases it', () => {
|
|
180
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
181
|
+
// Reachable from the layer toggle while sculpt is armed, and it takes the
|
|
182
|
+
// same raw-`set` path — the mode resets, so the scope has to follow.
|
|
183
|
+
useEditor.getState().setStructureLayer('zones')
|
|
184
|
+
expect(useEditor.getState().mode).toBe('select')
|
|
185
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('entering sculpt mode stands other affordances down', () => {
|
|
190
|
+
test('the armed build tool is cleared', () => {
|
|
191
|
+
useEditor.getState().setPhase('structure')
|
|
192
|
+
useEditor.getState().setMode('build')
|
|
193
|
+
useEditor.getState().setTool('wall')
|
|
194
|
+
useEditor.getState().setMode('terrain-sculpt')
|
|
195
|
+
// A ghost still armed under the brush would place a wall on the first dab.
|
|
196
|
+
expect(useEditor.getState().tool).toBeNull()
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
describe('isBrushMode', () => {
|
|
201
|
+
/**
|
|
202
|
+
* The set of modes that hold their interaction scope for the whole mode rather
|
|
203
|
+
* than for a pointer gesture. It matters outside the store because a scope is
|
|
204
|
+
* single-owner: anything calling `begin()` while a brush mode holds its scope
|
|
205
|
+
* evicts it silently, leaving the brush armed and painting while the editor
|
|
206
|
+
* believes a drag is running. Nearly every producer needs a selection — which
|
|
207
|
+
* entering a brush mode clears — but the clipboard paths read the clipboard, so
|
|
208
|
+
* `pasteSelectionAndPickUp` asks this before picking clones up.
|
|
209
|
+
*/
|
|
210
|
+
test('is exactly the two modes that hold a scope for their duration', () => {
|
|
211
|
+
const modes: Mode[] = ['select', 'edit', 'delete', 'build', 'material-paint', 'terrain-sculpt']
|
|
212
|
+
const held: Mode[] = []
|
|
213
|
+
for (const mode of modes) {
|
|
214
|
+
useEditor.getState().setMode(mode)
|
|
215
|
+
const kind = useInteractionScope.getState().scope.kind
|
|
216
|
+
if (kind === 'painting' || kind === 'sculpting') held.push(mode)
|
|
217
|
+
}
|
|
218
|
+
// Derived from observed scope behaviour rather than restating the literal, so
|
|
219
|
+
// a third brush mode that forgets `isBrushMode` fails here.
|
|
220
|
+
expect(held.filter(isBrushMode)).toEqual(held)
|
|
221
|
+
expect(modes.filter(isBrushMode)).toEqual(held)
|
|
222
|
+
})
|
|
223
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { create } from 'zustand'
|
|
2
|
+
|
|
3
|
+
type DeleteConfirmationRequest = {
|
|
4
|
+
count: number
|
|
5
|
+
onConfirm: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
type DeleteConfirmationStore = {
|
|
9
|
+
request: DeleteConfirmationRequest | null
|
|
10
|
+
cancel: () => void
|
|
11
|
+
confirm: () => void
|
|
12
|
+
requestConfirmation: (request: DeleteConfirmationRequest) => void
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const useDeleteConfirmation = create<DeleteConfirmationStore>((set, get) => ({
|
|
16
|
+
request: null,
|
|
17
|
+
cancel: () => set({ request: null }),
|
|
18
|
+
confirm: () => {
|
|
19
|
+
const request = get().request
|
|
20
|
+
if (!request) return
|
|
21
|
+
set({ request: null })
|
|
22
|
+
request.onConfirm()
|
|
23
|
+
},
|
|
24
|
+
requestConfirmation: (request) => set({ request }),
|
|
25
|
+
}))
|
|
26
|
+
|
|
27
|
+
export default useDeleteConfirmation
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AnyNodeId } from '@pascal-app/core'
|
|
2
|
+
import { create } from 'zustand'
|
|
3
|
+
|
|
4
|
+
type DirectManipulationFeedbackState = {
|
|
5
|
+
activeRotateNodeId: AnyNodeId | null
|
|
6
|
+
setActiveRotateNodeId(nodeId: AnyNodeId | null): void
|
|
7
|
+
clearActiveRotateNodeId(nodeId?: AnyNodeId): void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const useDirectManipulationFeedback = create<DirectManipulationFeedbackState>((set) => ({
|
|
11
|
+
activeRotateNodeId: null,
|
|
12
|
+
setActiveRotateNodeId: (activeRotateNodeId) => set({ activeRotateNodeId }),
|
|
13
|
+
clearActiveRotateNodeId: (nodeId) =>
|
|
14
|
+
set((state) => {
|
|
15
|
+
if (nodeId !== undefined && state.activeRotateNodeId !== nodeId) return {}
|
|
16
|
+
return { activeRotateNodeId: null }
|
|
17
|
+
}),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
export default useDirectManipulationFeedback
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import useDrawingView, { normalizeAnnotationLayoutOverrides } from './use-drawing-view'
|
|
3
|
+
|
|
4
|
+
describe('drawing type', () => {
|
|
5
|
+
test('keeps the workspace on the floor plan', () => {
|
|
6
|
+
expect(useDrawingView.getState().drawingType).toBe('floor-plan')
|
|
7
|
+
expect('setDrawingType' in useDrawingView.getState()).toBe(false)
|
|
8
|
+
})
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
describe('normalizeAnnotationLayoutOverrides', () => {
|
|
12
|
+
test('keeps finite pinned drawing-view annotation offsets', () => {
|
|
13
|
+
expect(
|
|
14
|
+
normalizeAnnotationLayoutOverrides({
|
|
15
|
+
a: { dx: 1.25, dy: -0.5, pinned: true },
|
|
16
|
+
stale: { dx: Number.NaN, dy: 0, pinned: true },
|
|
17
|
+
unpinned: { dx: 1, dy: 2, pinned: false },
|
|
18
|
+
}),
|
|
19
|
+
).toEqual({
|
|
20
|
+
a: { dx: 1.25, dy: -0.5, pinned: true },
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
})
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import type { ConstructionDrawingType } from '@pascal-app/core'
|
|
4
|
+
import { create } from 'zustand'
|
|
5
|
+
import { persist } from 'zustand/middleware'
|
|
6
|
+
|
|
7
|
+
export const DRAWING_TYPE_OPTIONS = [
|
|
8
|
+
{ id: 'floor-plan', label: 'Floor plan' },
|
|
9
|
+
{ id: 'foundation-plan', label: 'Foundation plan' },
|
|
10
|
+
{ id: 'reflected-ceiling-plan', label: 'Reflected ceiling plan' },
|
|
11
|
+
{ id: 'roof-plan', label: 'Roof plan' },
|
|
12
|
+
{ id: 'site-plan', label: 'Site plan' },
|
|
13
|
+
] as const satisfies readonly { id: ConstructionDrawingType; label: string }[]
|
|
14
|
+
|
|
15
|
+
export type DrawingAnnotationLayoutOverride = {
|
|
16
|
+
dx: number
|
|
17
|
+
dy: number
|
|
18
|
+
pinned: true
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type DrawingAnnotationLayoutOverrides = Record<string, DrawingAnnotationLayoutOverride>
|
|
22
|
+
|
|
23
|
+
type DrawingViewState = {
|
|
24
|
+
drawingType: Extract<ConstructionDrawingType, 'floor-plan'>
|
|
25
|
+
annotationLayoutOverrides: DrawingAnnotationLayoutOverrides
|
|
26
|
+
setAnnotationLayoutOverride: (
|
|
27
|
+
id: string,
|
|
28
|
+
override: DrawingAnnotationLayoutOverride | null,
|
|
29
|
+
) => void
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeAnnotationLayoutOverrides(
|
|
33
|
+
value: unknown,
|
|
34
|
+
): DrawingAnnotationLayoutOverrides {
|
|
35
|
+
if (!value || typeof value !== 'object') return {}
|
|
36
|
+
const out: DrawingAnnotationLayoutOverrides = {}
|
|
37
|
+
for (const [id, raw] of Object.entries(value)) {
|
|
38
|
+
if (!id || !raw || typeof raw !== 'object') continue
|
|
39
|
+
const dx = (raw as { dx?: unknown }).dx
|
|
40
|
+
const dy = (raw as { dy?: unknown }).dy
|
|
41
|
+
const pinned = (raw as { pinned?: unknown }).pinned
|
|
42
|
+
if (
|
|
43
|
+
typeof dx === 'number' &&
|
|
44
|
+
Number.isFinite(dx) &&
|
|
45
|
+
typeof dy === 'number' &&
|
|
46
|
+
Number.isFinite(dy) &&
|
|
47
|
+
pinned === true
|
|
48
|
+
) {
|
|
49
|
+
out[id] = { dx, dy, pinned: true }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const useDrawingView = create<DrawingViewState>()(
|
|
56
|
+
persist(
|
|
57
|
+
(set) => ({
|
|
58
|
+
drawingType: 'floor-plan',
|
|
59
|
+
annotationLayoutOverrides: {},
|
|
60
|
+
setAnnotationLayoutOverride: (id, override) =>
|
|
61
|
+
set((state) => {
|
|
62
|
+
const next = { ...state.annotationLayoutOverrides }
|
|
63
|
+
if (override) next[id] = override
|
|
64
|
+
else delete next[id]
|
|
65
|
+
return { annotationLayoutOverrides: next }
|
|
66
|
+
}),
|
|
67
|
+
}),
|
|
68
|
+
{
|
|
69
|
+
name: 'pascal-floorplan-drawing-view',
|
|
70
|
+
merge: (persistedState, currentState) => ({
|
|
71
|
+
...currentState,
|
|
72
|
+
annotationLayoutOverrides: normalizeAnnotationLayoutOverrides(
|
|
73
|
+
(persistedState as { annotationLayoutOverrides?: unknown } | undefined)
|
|
74
|
+
?.annotationLayoutOverrides,
|
|
75
|
+
),
|
|
76
|
+
}),
|
|
77
|
+
partialize: (state) => ({
|
|
78
|
+
annotationLayoutOverrides: state.annotationLayoutOverrides,
|
|
79
|
+
}),
|
|
80
|
+
},
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
export default useDrawingView
|