@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,206 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
3
|
+
import {
|
|
4
|
+
type ActiveInteractionScope,
|
|
5
|
+
curveReshapeScope,
|
|
6
|
+
editingHoleInfo,
|
|
7
|
+
handleDragInfo,
|
|
8
|
+
isActive,
|
|
9
|
+
isIdle,
|
|
10
|
+
isToolDrivenReshape,
|
|
11
|
+
scopeNodeId,
|
|
12
|
+
selectionEnabled,
|
|
13
|
+
} from '../lib/interaction/scope'
|
|
14
|
+
import useInteractionScope from './use-interaction-scope'
|
|
15
|
+
|
|
16
|
+
// A placing/moving scope carries the node inline. Tests only assert on id/type,
|
|
17
|
+
// so a structural stand-in is enough.
|
|
18
|
+
const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknown as AnyNode
|
|
19
|
+
|
|
20
|
+
function reset() {
|
|
21
|
+
useInteractionScope.getState().end()
|
|
22
|
+
}
|
|
23
|
+
afterEach(reset)
|
|
24
|
+
|
|
25
|
+
describe('use-interaction-scope state machine', () => {
|
|
26
|
+
test('starts idle', () => {
|
|
27
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
28
|
+
expect(isIdle(useInteractionScope.getState().scope)).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('begin enters an interaction; end returns to idle atomically', () => {
|
|
32
|
+
const s = useInteractionScope.getState()
|
|
33
|
+
s.begin({
|
|
34
|
+
kind: 'moving',
|
|
35
|
+
node: mockNode('item_1', 'item'),
|
|
36
|
+
nodeId: 'item_1',
|
|
37
|
+
nodeType: 'item',
|
|
38
|
+
view: '3d',
|
|
39
|
+
})
|
|
40
|
+
expect(useInteractionScope.getState().scope).toEqual({
|
|
41
|
+
kind: 'moving',
|
|
42
|
+
node: mockNode('item_1', 'item'),
|
|
43
|
+
nodeId: 'item_1',
|
|
44
|
+
nodeType: 'item',
|
|
45
|
+
view: '3d',
|
|
46
|
+
})
|
|
47
|
+
s.end()
|
|
48
|
+
// No interaction payload leaks past end — the scope is plain idle, so a
|
|
49
|
+
// stale nodeId/handle is unrepresentable.
|
|
50
|
+
expect(useInteractionScope.getState().scope).toEqual({ kind: 'idle' })
|
|
51
|
+
expect(scopeNodeId(useInteractionScope.getState().scope)).toBeNull()
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
test('begin is single-owner: a new interaction replaces the prior one', () => {
|
|
55
|
+
const s = useInteractionScope.getState()
|
|
56
|
+
s.begin({ kind: 'drafting', tool: 'wall' })
|
|
57
|
+
s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
|
|
58
|
+
const scope = useInteractionScope.getState().scope
|
|
59
|
+
expect(scope.kind).toBe('handle-drag')
|
|
60
|
+
// The prior drafting payload is gone — illegal "drafting + handle-drag"
|
|
61
|
+
// combination is unrepresentable.
|
|
62
|
+
expect(scopeNodeId(scope)).toBe('wall_1')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('update patches the live payload of the active scope', () => {
|
|
66
|
+
const s = useInteractionScope.getState()
|
|
67
|
+
s.begin({
|
|
68
|
+
kind: 'placing',
|
|
69
|
+
node: mockNode('i1', 'item'),
|
|
70
|
+
nodeId: 'i1',
|
|
71
|
+
nodeType: 'item',
|
|
72
|
+
view: '3d',
|
|
73
|
+
pressDrag: false,
|
|
74
|
+
})
|
|
75
|
+
s.update({ pressDrag: true })
|
|
76
|
+
const scope = useInteractionScope.getState().scope
|
|
77
|
+
expect(scope.kind === 'placing' && scope.pressDrag).toBe(true)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('update is a no-op when idle', () => {
|
|
81
|
+
useInteractionScope.getState().update({
|
|
82
|
+
kind: 'moving',
|
|
83
|
+
node: mockNode('x', 'item'),
|
|
84
|
+
nodeId: 'x',
|
|
85
|
+
nodeType: 'item',
|
|
86
|
+
view: '3d',
|
|
87
|
+
})
|
|
88
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('update cannot change which interaction is running', () => {
|
|
92
|
+
const s = useInteractionScope.getState()
|
|
93
|
+
s.begin({
|
|
94
|
+
kind: 'moving',
|
|
95
|
+
node: mockNode('i1', 'item'),
|
|
96
|
+
nodeId: 'i1',
|
|
97
|
+
nodeType: 'item',
|
|
98
|
+
view: '3d',
|
|
99
|
+
})
|
|
100
|
+
s.update({
|
|
101
|
+
kind: 'placing',
|
|
102
|
+
node: mockNode('i1', 'item'),
|
|
103
|
+
nodeId: 'i1',
|
|
104
|
+
nodeType: 'item',
|
|
105
|
+
view: '3d',
|
|
106
|
+
pressDrag: true,
|
|
107
|
+
})
|
|
108
|
+
expect(useInteractionScope.getState().scope.kind).toBe('moving')
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('selectionEnabled only while idle', () => {
|
|
112
|
+
const s = useInteractionScope.getState()
|
|
113
|
+
expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(true)
|
|
114
|
+
s.begin({ kind: 'box-select' })
|
|
115
|
+
expect(selectionEnabled(useInteractionScope.getState().scope)).toBe(false)
|
|
116
|
+
expect(isActive(useInteractionScope.getState().scope)).toBe(true)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('end is idempotent', () => {
|
|
120
|
+
const s = useInteractionScope.getState()
|
|
121
|
+
s.end()
|
|
122
|
+
s.end()
|
|
123
|
+
expect(useInteractionScope.getState().scope.kind).toBe('idle')
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('derived flag views are leak-free (no parallel flags)', () => {
|
|
128
|
+
const scope = () => useInteractionScope.getState().scope
|
|
129
|
+
|
|
130
|
+
test('handleDragInfo mirrors handle-drag and clears on end', () => {
|
|
131
|
+
const s = useInteractionScope.getState()
|
|
132
|
+
s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
|
|
133
|
+
expect(handleDragInfo(scope())).toEqual({ nodeId: 'wall_1', label: 'height' })
|
|
134
|
+
s.end()
|
|
135
|
+
// After end the derived view is null — a stale activeHandleDrag is
|
|
136
|
+
// unrepresentable because it is a pure function of the single scope.
|
|
137
|
+
expect(handleDragInfo(scope())).toBeNull()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('editingHoleInfo mirrors a hole reshape and clears on end', () => {
|
|
141
|
+
const s = useInteractionScope.getState()
|
|
142
|
+
s.begin({
|
|
143
|
+
kind: 'reshaping',
|
|
144
|
+
nodeId: 'slab_1',
|
|
145
|
+
reshape: 'hole',
|
|
146
|
+
driver: 'tool',
|
|
147
|
+
holeIndex: 2,
|
|
148
|
+
})
|
|
149
|
+
expect(editingHoleInfo(scope())).toEqual({ nodeId: 'slab_1', holeIndex: 2 })
|
|
150
|
+
s.end()
|
|
151
|
+
expect(editingHoleInfo(scope())).toBeNull()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('a non-hole reshape never reads as an editing hole', () => {
|
|
155
|
+
const s = useInteractionScope.getState()
|
|
156
|
+
s.begin({ kind: 'reshaping', nodeId: 'wall_1', reshape: 'curve', driver: 'tool' })
|
|
157
|
+
expect(editingHoleInfo(scope())).toBeNull()
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('a floorplan-owned curve drag does not activate the registry reshape tool', () => {
|
|
161
|
+
expect(isToolDrivenReshape(curveReshapeScope('wall_1', 'floorplan'))).toBe(false)
|
|
162
|
+
expect(isToolDrivenReshape(curveReshapeScope('wall_1', 'tool'))).toBe(true)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
test('switching interactions never leaks the prior derived view', () => {
|
|
166
|
+
const s = useInteractionScope.getState()
|
|
167
|
+
s.begin({ kind: 'handle-drag', nodeId: 'wall_1', handle: 'height' })
|
|
168
|
+
// Single-owner replacement: the handle-drag view must vanish the instant a
|
|
169
|
+
// different interaction begins — the two cannot be simultaneously active.
|
|
170
|
+
s.begin({
|
|
171
|
+
kind: 'reshaping',
|
|
172
|
+
nodeId: 'slab_1',
|
|
173
|
+
reshape: 'hole',
|
|
174
|
+
driver: 'tool',
|
|
175
|
+
holeIndex: 0,
|
|
176
|
+
})
|
|
177
|
+
expect(handleDragInfo(scope())).toBeNull()
|
|
178
|
+
expect(editingHoleInfo(scope())).toEqual({ nodeId: 'slab_1', holeIndex: 0 })
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
test('every active scope kind leaves at most the views it owns', () => {
|
|
182
|
+
const s = useInteractionScope.getState()
|
|
183
|
+
const kinds: ActiveInteractionScope[] = [
|
|
184
|
+
{
|
|
185
|
+
kind: 'placing',
|
|
186
|
+
node: mockNode('i', 'item'),
|
|
187
|
+
nodeId: 'i',
|
|
188
|
+
nodeType: 'item',
|
|
189
|
+
view: '3d',
|
|
190
|
+
pressDrag: false,
|
|
191
|
+
},
|
|
192
|
+
{ kind: 'moving', node: mockNode('i', 'item'), nodeId: 'i', nodeType: 'item', view: '3d' },
|
|
193
|
+
{ kind: 'drafting', tool: 'wall' },
|
|
194
|
+
{ kind: 'box-select' },
|
|
195
|
+
{ kind: 'painting' },
|
|
196
|
+
{ kind: 'sculpting' },
|
|
197
|
+
]
|
|
198
|
+
for (const k of kinds) {
|
|
199
|
+
s.begin(k)
|
|
200
|
+
// None of these own a handle-drag or hole view.
|
|
201
|
+
expect(handleDragInfo(scope())).toBeNull()
|
|
202
|
+
expect(editingHoleInfo(scope())).toBeNull()
|
|
203
|
+
}
|
|
204
|
+
s.end()
|
|
205
|
+
})
|
|
206
|
+
})
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNode, type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useRef } from 'react'
|
|
5
|
+
import { create } from 'zustand'
|
|
6
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
7
|
+
import {
|
|
8
|
+
type ActiveInteractionScope,
|
|
9
|
+
controlPointReshapeInfo,
|
|
10
|
+
editingHoleInfo,
|
|
11
|
+
endpointReshapeInfo,
|
|
12
|
+
handleDragInfo,
|
|
13
|
+
IDLE_SCOPE,
|
|
14
|
+
type InteractionScope,
|
|
15
|
+
isCurveReshape,
|
|
16
|
+
isFloorplanDrivenReshape,
|
|
17
|
+
isToolDrivenReshape,
|
|
18
|
+
movingNodeOf,
|
|
19
|
+
reshapingNodeId,
|
|
20
|
+
tangentReshapeInfo,
|
|
21
|
+
} from '../lib/interaction/scope'
|
|
22
|
+
|
|
23
|
+
// The authoritative interaction state machine. A single owner holds exactly one
|
|
24
|
+
// scope at a time. `begin` enters an interaction (atomically replacing any prior
|
|
25
|
+
// one — a single owner, no producer races), `update` narrows the live payload,
|
|
26
|
+
// and `end` returns to idle atomically so no interaction payload can leak past
|
|
27
|
+
// the end of its interaction. There is no setter that can leave the store in an
|
|
28
|
+
// illegal half-state: the only writable shape is `InteractionScope`.
|
|
29
|
+
|
|
30
|
+
export type InteractionScopeState = {
|
|
31
|
+
scope: InteractionScope
|
|
32
|
+
// Enter an interaction. If one is already active it is ended first, so the
|
|
33
|
+
// store is always single-owner.
|
|
34
|
+
begin: (scope: ActiveInteractionScope) => void
|
|
35
|
+
// Patch the current scope's payload. Ignored when idle, or when the patch's
|
|
36
|
+
// implied kind differs from the active kind — payload updates must not change
|
|
37
|
+
// which interaction is running (use `begin` for that).
|
|
38
|
+
update: (patch: Partial<ActiveInteractionScope>) => void
|
|
39
|
+
// Return to idle atomically. Both commit and cancel paths call this; the
|
|
40
|
+
// distinction (write vs revert) lives in the interaction body, not here.
|
|
41
|
+
end: () => void
|
|
42
|
+
// Return to idle only if the active scope matches `match`. Used when scope is
|
|
43
|
+
// driven from independent legacy flag clears, so clearing one flag (e.g. a
|
|
44
|
+
// fence curve) cannot stomp an unrelated active scope (e.g. a wall move).
|
|
45
|
+
endIf: (match: (scope: ActiveInteractionScope) => boolean) => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const useInteractionScope = create<InteractionScopeState>((set, get) => ({
|
|
49
|
+
scope: IDLE_SCOPE,
|
|
50
|
+
begin: (scope) => set({ scope }),
|
|
51
|
+
update: (patch) =>
|
|
52
|
+
set((state) => {
|
|
53
|
+
if (state.scope.kind === 'idle') return state
|
|
54
|
+
if ('kind' in patch && patch.kind !== state.scope.kind) return state
|
|
55
|
+
return { scope: { ...state.scope, ...patch } as InteractionScope }
|
|
56
|
+
}),
|
|
57
|
+
end: () => {
|
|
58
|
+
if (get().scope.kind === 'idle') return
|
|
59
|
+
set({ scope: IDLE_SCOPE })
|
|
60
|
+
},
|
|
61
|
+
endIf: (match) => {
|
|
62
|
+
const scope = get().scope
|
|
63
|
+
if (scope.kind === 'idle') return
|
|
64
|
+
if (match(scope)) set({ scope: IDLE_SCOPE })
|
|
65
|
+
},
|
|
66
|
+
}))
|
|
67
|
+
|
|
68
|
+
// Derived, reference-stable views of the active scope, replacing the legacy
|
|
69
|
+
// `useEditor.activeHandleDrag` / `useEditor.editingHole` flags. `useShallow`
|
|
70
|
+
// keeps the result reference-stable across unrelated scope changes, so hot-path
|
|
71
|
+
// subscribers (camera controls, floating menu) don't re-render on every update.
|
|
72
|
+
export const useActiveHandleDrag = (): { nodeId: string; label: string } | null =>
|
|
73
|
+
useInteractionScope(useShallow((s) => handleDragInfo(s.scope)))
|
|
74
|
+
|
|
75
|
+
export const useEditingHole = (): { nodeId: string; holeIndex: number } | null =>
|
|
76
|
+
useInteractionScope(useShallow((s) => editingHoleInfo(s.scope)))
|
|
77
|
+
|
|
78
|
+
// Imperative (non-React) reads for event handlers / effects.
|
|
79
|
+
export const getEditingHole = (): { nodeId: string; holeIndex: number } | null =>
|
|
80
|
+
editingHoleInfo(useInteractionScope.getState().scope)
|
|
81
|
+
|
|
82
|
+
export const getIsCurveReshape = (): boolean => isCurveReshape(useInteractionScope.getState().scope)
|
|
83
|
+
|
|
84
|
+
// Replaces the legacy `curvingWall` / `curvingFence` existence flags. The
|
|
85
|
+
// wall-vs-fence distinction (both now map to one `reshaping/'curve'` scope) is
|
|
86
|
+
// recovered by reading the reshaped node's type from `useReshapingNode`.
|
|
87
|
+
export const useIsCurveReshape = (): boolean => useInteractionScope((s) => isCurveReshape(s.scope))
|
|
88
|
+
|
|
89
|
+
export const useIsToolDrivenReshape = (): boolean =>
|
|
90
|
+
useInteractionScope((s) => isToolDrivenReshape(s.scope))
|
|
91
|
+
|
|
92
|
+
export const useIsFloorplanDrivenReshape = (): boolean =>
|
|
93
|
+
useInteractionScope((s) => isFloorplanDrivenReshape(s.scope))
|
|
94
|
+
|
|
95
|
+
// Replaces the legacy `movingWallEndpoint` / `movingFenceEndpoint` payloads,
|
|
96
|
+
// minus the node (fetch it from `useReshapingNode`).
|
|
97
|
+
export const useEndpointReshape = (): { nodeId: string; endpoint: 'start' | 'end' } | null =>
|
|
98
|
+
useInteractionScope(useShallow((s) => endpointReshapeInfo(s.scope)))
|
|
99
|
+
|
|
100
|
+
export const useControlPointReshape = (): { nodeId: string; index: number } | null =>
|
|
101
|
+
useInteractionScope(useShallow((s) => controlPointReshapeInfo(s.scope)))
|
|
102
|
+
|
|
103
|
+
export const useTangentReshape = (): { nodeId: string; index: number; side: 'in' | 'out' } | null =>
|
|
104
|
+
useInteractionScope(useShallow((s) => tangentReshapeInfo(s.scope)))
|
|
105
|
+
|
|
106
|
+
// The node currently being reshaped (curve / endpoint / hole), looked up live
|
|
107
|
+
// from the scene by the scope's `nodeId`. During a reshape the scene node holds
|
|
108
|
+
// the same data the legacy `curvingWall` / `movingWallEndpoint.wall` carried, so
|
|
109
|
+
// consumers that need the full node (affordance-tool mounts, wall-vs-fence type
|
|
110
|
+
// checks) read it here instead of from a parallel flag.
|
|
111
|
+
export const useReshapingNode = (): AnyNode | null => {
|
|
112
|
+
const nodeId = useInteractionScope((s) => reshapingNodeId(s.scope))
|
|
113
|
+
// Snapshot the node ONCE when the reshape begins (keyed on nodeId), like the
|
|
114
|
+
// legacy `curvingWall` / `movingWallEndpoint.wall` flags did. The affordance
|
|
115
|
+
// tools write the node live during the drag; subscribing to the live scene
|
|
116
|
+
// node would feed those writes straight back into the tool — the curve resets
|
|
117
|
+
// on pointer-stop, the endpoint drag loops and freezes. nodeId is stable for
|
|
118
|
+
// the whole gesture, so a ref snapshot stays frozen until the next reshape.
|
|
119
|
+
const snapshot = useRef<{ id: string | null; node: AnyNode | null }>({ id: null, node: null })
|
|
120
|
+
if (snapshot.current.id !== nodeId) {
|
|
121
|
+
snapshot.current = {
|
|
122
|
+
id: nodeId,
|
|
123
|
+
node: nodeId ? (useScene.getState().nodes[nodeId as AnyNodeId] ?? null) : null,
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return snapshot.current.node
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The node currently being placed or moved. Replaces the legacy
|
|
130
|
+
// `useEditor.movingNode` flag. Unlike `useReshapingNode`, no `useRef` snapshot is
|
|
131
|
+
// needed: the node is carried inline in the scope and set once at `begin`, so it
|
|
132
|
+
// is already a stable reference for the whole gesture (nothing calls `begin` mid
|
|
133
|
+
// drag). Returns null whenever no placing/moving interaction is active.
|
|
134
|
+
export const useMovingNode = (): AnyNode | null => useInteractionScope((s) => movingNodeOf(s.scope))
|
|
135
|
+
|
|
136
|
+
// Imperative (non-React) read for event handlers / effects.
|
|
137
|
+
export const getMovingNode = (): AnyNode | null =>
|
|
138
|
+
movingNodeOf(useInteractionScope.getState().scope)
|
|
139
|
+
|
|
140
|
+
export default useInteractionScope
|