@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,64 @@
|
|
|
1
|
+
export type ContinuationContext = 'wall' | 'fence' | 'point' | 'cabinet'
|
|
2
|
+
export type ContinuationMode = string
|
|
3
|
+
|
|
4
|
+
export const CONTINUATION_PROFILES: Record<
|
|
5
|
+
ContinuationContext,
|
|
6
|
+
{
|
|
7
|
+
options: ContinuationMode[]
|
|
8
|
+
default: ContinuationMode
|
|
9
|
+
labels: Record<string, string>
|
|
10
|
+
icons: Record<string, string>
|
|
11
|
+
}
|
|
12
|
+
> = {
|
|
13
|
+
wall: {
|
|
14
|
+
options: ['room', 'single'],
|
|
15
|
+
default: 'room',
|
|
16
|
+
labels: { room: 'Room (auto-close)', single: 'Single wall' },
|
|
17
|
+
icons: { room: 'lucide:square', single: 'lucide:minus' },
|
|
18
|
+
},
|
|
19
|
+
fence: {
|
|
20
|
+
options: ['single', 'continuous', 'curved'],
|
|
21
|
+
default: 'continuous',
|
|
22
|
+
labels: {
|
|
23
|
+
continuous: 'Continuous',
|
|
24
|
+
single: 'Single fence',
|
|
25
|
+
curved: 'Curved fence',
|
|
26
|
+
},
|
|
27
|
+
icons: {
|
|
28
|
+
continuous: 'lucide:waypoints',
|
|
29
|
+
single: 'lucide:minus',
|
|
30
|
+
curved: 'lucide:spline',
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
point: {
|
|
34
|
+
options: ['once', 'repeat'],
|
|
35
|
+
default: 'once',
|
|
36
|
+
labels: { once: 'Place once', repeat: 'Place multiple' },
|
|
37
|
+
icons: { once: 'lucide:target', repeat: 'lucide:copy-plus' },
|
|
38
|
+
},
|
|
39
|
+
cabinet: {
|
|
40
|
+
options: ['single', 'continuous'],
|
|
41
|
+
default: 'single',
|
|
42
|
+
labels: { single: 'Single cabinet', continuous: 'Continuous run' },
|
|
43
|
+
icons: { single: 'lucide:minus', continuous: 'lucide:waypoints' },
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const POINT_KINDS = new Set(['item', 'door', 'window', 'shelf', 'column'])
|
|
48
|
+
|
|
49
|
+
export function nextContinuation(
|
|
50
|
+
context: ContinuationContext,
|
|
51
|
+
current: ContinuationMode,
|
|
52
|
+
): ContinuationMode {
|
|
53
|
+
const profile = CONTINUATION_PROFILES[context]
|
|
54
|
+
const index = profile.options.indexOf(current)
|
|
55
|
+
if (index === -1) return profile.default
|
|
56
|
+
return profile.options[(index + 1) % profile.options.length] ?? profile.default
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function continuationContextOf(kind: string): ContinuationContext | null {
|
|
60
|
+
if (kind === 'wall') return 'wall'
|
|
61
|
+
if (kind === 'fence') return 'fence'
|
|
62
|
+
if (kind === 'cabinet') return 'cabinet'
|
|
63
|
+
return POINT_KINDS.has(kind) ? 'point' : null
|
|
64
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeDefinition,
|
|
5
|
+
DEFAULT_ANGLE_STEP,
|
|
6
|
+
nodeRegistry,
|
|
7
|
+
registerNode,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { z } from 'zod'
|
|
10
|
+
import {
|
|
11
|
+
canDirectMoveNode,
|
|
12
|
+
resolveDirectManipulationNode,
|
|
13
|
+
resolveDirectRotationDragDelta,
|
|
14
|
+
resolveMoveActionNode,
|
|
15
|
+
snapDirectRotationDelta,
|
|
16
|
+
} from './direct-manipulation'
|
|
17
|
+
|
|
18
|
+
function registerTestDefinition(kind: string, overrides: Partial<AnyNodeDefinition>) {
|
|
19
|
+
if (nodeRegistry.has(kind)) return
|
|
20
|
+
registerNode({
|
|
21
|
+
kind,
|
|
22
|
+
schemaVersion: 1,
|
|
23
|
+
schema: z.object({ type: z.literal(kind) }) as never,
|
|
24
|
+
category: 'structure',
|
|
25
|
+
defaults: () => ({ type: kind }) as never,
|
|
26
|
+
capabilities: {},
|
|
27
|
+
renderer: { kind: 'parametric', module: async () => ({ default: () => null }) },
|
|
28
|
+
...overrides,
|
|
29
|
+
} as AnyNodeDefinition)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('snapDirectRotationDelta', () => {
|
|
33
|
+
test('snaps rotation deltas to the default angle increment', () => {
|
|
34
|
+
expect(snapDirectRotationDelta(DEFAULT_ANGLE_STEP * 0.49, false)).toBe(0)
|
|
35
|
+
expect(snapDirectRotationDelta(DEFAULT_ANGLE_STEP * 0.51, false)).toBeCloseTo(
|
|
36
|
+
DEFAULT_ANGLE_STEP,
|
|
37
|
+
)
|
|
38
|
+
expect(snapDirectRotationDelta(DEFAULT_ANGLE_STEP * -1.49, false)).toBeCloseTo(
|
|
39
|
+
-DEFAULT_ANGLE_STEP,
|
|
40
|
+
)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('keeps the raw rotation delta while free-rotating', () => {
|
|
44
|
+
const rawDelta = DEFAULT_ANGLE_STEP * 0.42
|
|
45
|
+
expect(snapDirectRotationDelta(rawDelta, true)).toBe(rawDelta)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe('resolveDirectRotationDragDelta', () => {
|
|
50
|
+
test('maps horizontal pointer motion to the direct rotation delta direction', () => {
|
|
51
|
+
const radiansPerPixel = DEFAULT_ANGLE_STEP / 12
|
|
52
|
+
|
|
53
|
+
expect(resolveDirectRotationDragDelta(100, 112, radiansPerPixel, false)).toBeCloseTo(
|
|
54
|
+
-DEFAULT_ANGLE_STEP,
|
|
55
|
+
)
|
|
56
|
+
expect(resolveDirectRotationDragDelta(100, 88, radiansPerPixel, false)).toBeCloseTo(
|
|
57
|
+
DEFAULT_ANGLE_STEP,
|
|
58
|
+
)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('keeps unsnapped drag deltas while free-rotating', () => {
|
|
62
|
+
expect(resolveDirectRotationDragDelta(100, 103, 0.1, true)).toBeCloseTo(-0.3)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe('canDirectMoveNode', () => {
|
|
67
|
+
// Accepts kinds with a 3D-mountable move tool (`movable` or
|
|
68
|
+
// `affordanceTools.move`); floorplan-only movers (zone) are excluded.
|
|
69
|
+
test('rejects floorplan-only move targets (no 3D tool mounts)', () => {
|
|
70
|
+
const kind = 'direct-move-floorplan-only-test'
|
|
71
|
+
registerTestDefinition(kind, { floorplanMoveTarget: {} as never })
|
|
72
|
+
|
|
73
|
+
expect(canDirectMoveNode({ id: 'node_1', type: kind } as unknown as AnyNode)).toBe(false)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('rejects MEP kinds that own move through bespoke selection affordances', () => {
|
|
77
|
+
for (const kind of [
|
|
78
|
+
'duct-segment',
|
|
79
|
+
'duct-fitting',
|
|
80
|
+
'pipe-segment',
|
|
81
|
+
'pipe-fitting',
|
|
82
|
+
'lineset',
|
|
83
|
+
'liquid-line',
|
|
84
|
+
]) {
|
|
85
|
+
expect(canDirectMoveNode({ id: 'node_1', type: kind } as unknown as AnyNode)).toBe(false)
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('accepts kinds with a bespoke move tool', () => {
|
|
90
|
+
const kind = 'direct-move-bespoke-tool-test'
|
|
91
|
+
registerTestDefinition(kind, {
|
|
92
|
+
affordanceTools: {
|
|
93
|
+
move: async () => ({ default: () => null }),
|
|
94
|
+
} as never,
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
expect(canDirectMoveNode({ id: 'node_1', type: kind } as unknown as AnyNode)).toBe(true)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('accepts nodes with the generic movable capability', () => {
|
|
101
|
+
const kind = 'direct-move-movable-test'
|
|
102
|
+
registerTestDefinition(kind, {
|
|
103
|
+
capabilities: {
|
|
104
|
+
movable: { axes: ['x', 'z'], gridSnap: true },
|
|
105
|
+
},
|
|
106
|
+
} as Partial<AnyNodeDefinition>)
|
|
107
|
+
|
|
108
|
+
expect(canDirectMoveNode({ id: 'node_1', type: kind } as unknown as AnyNode)).toBe(true)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('rejects kinds with no registered move path', () => {
|
|
112
|
+
const kind = 'direct-move-none-test'
|
|
113
|
+
registerTestDefinition(kind, {})
|
|
114
|
+
|
|
115
|
+
expect(canDirectMoveNode({ id: 'node_1', type: kind } as unknown as AnyNode)).toBe(false)
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
describe('resolveDirectManipulationNode', () => {
|
|
120
|
+
test('routes proxied members to their assembly for direct transforms', () => {
|
|
121
|
+
const group = {
|
|
122
|
+
id: 'direct_manipulation_group',
|
|
123
|
+
type: 'direct-manipulation-group-test',
|
|
124
|
+
} as unknown as AnyNode
|
|
125
|
+
const member = {
|
|
126
|
+
id: 'direct_manipulation_member',
|
|
127
|
+
type: 'direct-manipulation-member-test',
|
|
128
|
+
metadata: { nodeSelectionProxyId: group.id },
|
|
129
|
+
} as unknown as AnyNode
|
|
130
|
+
|
|
131
|
+
expect(
|
|
132
|
+
resolveDirectManipulationNode(member, {
|
|
133
|
+
[group.id]: group,
|
|
134
|
+
[member.id]: member,
|
|
135
|
+
}),
|
|
136
|
+
).toBe(group)
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
test('falls back to the selected node when the proxy target is missing', () => {
|
|
140
|
+
const member = {
|
|
141
|
+
id: 'direct_manipulation_orphan_member',
|
|
142
|
+
type: 'direct-manipulation-member-test',
|
|
143
|
+
metadata: { nodeSelectionProxyId: 'missing_group' },
|
|
144
|
+
} as unknown as AnyNode
|
|
145
|
+
|
|
146
|
+
expect(resolveDirectManipulationNode(member, { [member.id]: member })).toBe(member)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
test('routes parent-frame children to their rotatable parent', () => {
|
|
150
|
+
const parentKind = 'direct-manipulation-parent-frame-parent-test'
|
|
151
|
+
const childKind = 'direct-manipulation-parent-frame-child-test'
|
|
152
|
+
registerTestDefinition(parentKind, {
|
|
153
|
+
capabilities: { rotatable: { axes: ['y'], snapAngles: [Math.PI / 4] } },
|
|
154
|
+
})
|
|
155
|
+
registerTestDefinition(childKind, {
|
|
156
|
+
capabilities: {
|
|
157
|
+
movable: {
|
|
158
|
+
axes: ['x', 'z'],
|
|
159
|
+
gridSnap: true,
|
|
160
|
+
parentFrame: {
|
|
161
|
+
resolveParent: (node: AnyNode, nodes: Readonly<Record<string, AnyNode>>) =>
|
|
162
|
+
(node.parentId ? nodes[node.parentId] : null) ?? null,
|
|
163
|
+
parentRotationY: () => 0,
|
|
164
|
+
localToPlan: (_parent: AnyNode, local: readonly [number, number, number]) => [
|
|
165
|
+
local[0],
|
|
166
|
+
local[1],
|
|
167
|
+
local[2],
|
|
168
|
+
],
|
|
169
|
+
planToLocal: (_parent: AnyNode, planX: number, localY: number, planZ: number) => [
|
|
170
|
+
planX,
|
|
171
|
+
localY,
|
|
172
|
+
planZ,
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const parent = { id: 'direct_manipulation_parent', type: parentKind } as unknown as AnyNode
|
|
180
|
+
const child = {
|
|
181
|
+
id: 'direct_manipulation_child',
|
|
182
|
+
type: childKind,
|
|
183
|
+
parentId: parent.id,
|
|
184
|
+
} as unknown as AnyNode
|
|
185
|
+
|
|
186
|
+
expect(
|
|
187
|
+
resolveDirectManipulationNode(child, {
|
|
188
|
+
[parent.id]: parent,
|
|
189
|
+
[child.id]: child,
|
|
190
|
+
}),
|
|
191
|
+
).toBe(parent)
|
|
192
|
+
})
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
describe('resolveMoveActionNode', () => {
|
|
196
|
+
test('routes a nested same-kind child move to its host', () => {
|
|
197
|
+
const kind = 'move-action-nested-kind-test'
|
|
198
|
+
registerTestDefinition(kind, {
|
|
199
|
+
capabilities: {
|
|
200
|
+
movable: {
|
|
201
|
+
axes: ['x', 'z'],
|
|
202
|
+
parentFrame: {
|
|
203
|
+
resolveParent: (node: AnyNode, nodes: Readonly<Record<string, AnyNode>>) =>
|
|
204
|
+
(node.parentId ? nodes[node.parentId] : null) ?? null,
|
|
205
|
+
parentRotationY: () => 0,
|
|
206
|
+
localToPlan: (_parent: AnyNode, local: readonly [number, number, number]) => [
|
|
207
|
+
local[0],
|
|
208
|
+
local[1],
|
|
209
|
+
local[2],
|
|
210
|
+
],
|
|
211
|
+
planToLocal: (_parent: AnyNode, planX: number, localY: number, planZ: number) => [
|
|
212
|
+
planX,
|
|
213
|
+
localY,
|
|
214
|
+
planZ,
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
})
|
|
220
|
+
const parent = { id: 'move_action_parent', type: kind } as unknown as AnyNode
|
|
221
|
+
const child = {
|
|
222
|
+
id: 'move_action_child',
|
|
223
|
+
type: kind,
|
|
224
|
+
parentId: parent.id,
|
|
225
|
+
} as unknown as AnyNode
|
|
226
|
+
|
|
227
|
+
expect(
|
|
228
|
+
resolveMoveActionNode(child, {
|
|
229
|
+
[parent.id]: parent,
|
|
230
|
+
[child.id]: child,
|
|
231
|
+
}),
|
|
232
|
+
).toBe(parent)
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
test('keeps a child independently movable when its parent is a different kind', () => {
|
|
236
|
+
const parentKind = 'move-action-parent-kind-test'
|
|
237
|
+
const childKind = 'move-action-child-kind-test'
|
|
238
|
+
registerTestDefinition(parentKind, {})
|
|
239
|
+
registerTestDefinition(childKind, {
|
|
240
|
+
capabilities: {
|
|
241
|
+
movable: {
|
|
242
|
+
axes: ['x', 'z'],
|
|
243
|
+
parentFrame: {
|
|
244
|
+
resolveParent: (node: AnyNode, nodes: Readonly<Record<string, AnyNode>>) =>
|
|
245
|
+
(node.parentId ? nodes[node.parentId] : null) ?? null,
|
|
246
|
+
parentRotationY: () => 0,
|
|
247
|
+
localToPlan: (_parent: AnyNode, local: readonly [number, number, number]) => [
|
|
248
|
+
local[0],
|
|
249
|
+
local[1],
|
|
250
|
+
local[2],
|
|
251
|
+
],
|
|
252
|
+
planToLocal: (_parent: AnyNode, planX: number, localY: number, planZ: number) => [
|
|
253
|
+
planX,
|
|
254
|
+
localY,
|
|
255
|
+
planZ,
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
const parent = { id: 'move_action_run', type: parentKind } as unknown as AnyNode
|
|
262
|
+
const child = {
|
|
263
|
+
id: 'move_action_module',
|
|
264
|
+
type: childKind,
|
|
265
|
+
parentId: parent.id,
|
|
266
|
+
} as unknown as AnyNode
|
|
267
|
+
|
|
268
|
+
expect(
|
|
269
|
+
resolveMoveActionNode(child, {
|
|
270
|
+
[parent.id]: parent,
|
|
271
|
+
[child.id]: child,
|
|
272
|
+
}),
|
|
273
|
+
).toBe(child)
|
|
274
|
+
})
|
|
275
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type AnyNode,
|
|
3
|
+
type ArcResizeHandle,
|
|
4
|
+
createSceneApi,
|
|
5
|
+
DEFAULT_ANGLE_STEP,
|
|
6
|
+
type HandleDescriptor,
|
|
7
|
+
hasRegistry3DMoveTool,
|
|
8
|
+
isMovable,
|
|
9
|
+
nodeRegistry,
|
|
10
|
+
resolveSelectionProxyId,
|
|
11
|
+
type SceneApi,
|
|
12
|
+
useScene,
|
|
13
|
+
} from '@pascal-app/core'
|
|
14
|
+
|
|
15
|
+
function resolveHandles(node: AnyNode): HandleDescriptor<AnyNode>[] {
|
|
16
|
+
const handles = nodeRegistry.get(node.type)?.handles
|
|
17
|
+
if (!handles) return []
|
|
18
|
+
return (
|
|
19
|
+
typeof handles === 'function' ? handles(node as never) : handles
|
|
20
|
+
) as HandleDescriptor<AnyNode>[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getDirectRotateHandle(node: AnyNode): ArcResizeHandle<AnyNode> | null {
|
|
24
|
+
for (const handle of resolveHandles(node)) {
|
|
25
|
+
if (handle.kind === 'arc-resize' && handle.shape === 'rotate') {
|
|
26
|
+
return handle as ArcResizeHandle<AnyNode>
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function canDirectRotateNode(node: AnyNode): boolean {
|
|
33
|
+
return (
|
|
34
|
+
getDirectRotateHandle(node) !== null ||
|
|
35
|
+
nodeRegistry.get(node.type)?.capabilities?.rotatable !== undefined
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const BESPOKE_SELECTION_MOVE_KINDS = new Set([
|
|
40
|
+
'duct-segment',
|
|
41
|
+
'duct-fitting',
|
|
42
|
+
'pipe-segment',
|
|
43
|
+
'pipe-fitting',
|
|
44
|
+
'lineset',
|
|
45
|
+
'liquid-line',
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
export function canDirectMoveNode(node: AnyNode): boolean {
|
|
49
|
+
// These MEP kinds own move through bespoke selection rigs (latch cubes,
|
|
50
|
+
// directional arrows, grid-driven previews). Sending body drags/clicks
|
|
51
|
+
// through the generic direct-move handoff conflicts with that path and can
|
|
52
|
+
// leave the editor appearing frozen while their mover waits for the wrong
|
|
53
|
+
// gesture stream.
|
|
54
|
+
if (BESPOKE_SELECTION_MOVE_KINDS.has(node.type)) return false
|
|
55
|
+
// 3D direct move (Ctrl/Meta-drag, the move-cross grip) needs a move tool that
|
|
56
|
+
// mounts in 3D — distinct from `isRegistryMovable`, which also accepts
|
|
57
|
+
// floorplan-only movers (zone) for the 2D plan.
|
|
58
|
+
if (!hasRegistry3DMoveTool(node.type)) return false
|
|
59
|
+
// Bespoke movers (`affordanceTools.move`) own their constraints and often
|
|
60
|
+
// deliberately omit `capabilities.movable` — only gate registry-movable
|
|
61
|
+
// kinds on `isMovable`, so a per-node `movable.override` (e.g. a cabinet
|
|
62
|
+
// run locked behind its selection proxy) can opt out of direct move.
|
|
63
|
+
if (nodeRegistry.get(node.type)?.affordanceTools?.move) return true
|
|
64
|
+
return isMovable(node)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function resolveDirectManipulationNode(
|
|
68
|
+
node: AnyNode,
|
|
69
|
+
nodes: Readonly<Record<string, AnyNode | undefined>>,
|
|
70
|
+
): AnyNode {
|
|
71
|
+
const target = nodes[resolveSelectionProxyId(node, nodes)] ?? node
|
|
72
|
+
const parentFrame = nodeRegistry.get(target.type)?.capabilities?.movable?.parentFrame
|
|
73
|
+
const parent = parentFrame?.resolveParent(target, nodes as Readonly<Record<string, AnyNode>>)
|
|
74
|
+
return parent && canDirectRotateNode(parent) ? parent : target
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function resolveMoveActionNode(
|
|
78
|
+
node: AnyNode,
|
|
79
|
+
nodes: Readonly<Record<string, AnyNode | undefined>>,
|
|
80
|
+
): AnyNode {
|
|
81
|
+
const parentFrame = nodeRegistry.get(node.type)?.capabilities?.movable?.parentFrame
|
|
82
|
+
const parent = parentFrame?.resolveParent(node, nodes as Readonly<Record<string, AnyNode>>)
|
|
83
|
+
return parent?.type === node.type ? parent : node
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function snapDirectRotationDelta(delta: number, free: boolean): number {
|
|
87
|
+
return free ? delta : Math.round(delta / DEFAULT_ANGLE_STEP) * DEFAULT_ANGLE_STEP
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function resolveDirectRotationDragDelta(
|
|
91
|
+
startX: number,
|
|
92
|
+
clientX: number,
|
|
93
|
+
radiansPerPixel: number,
|
|
94
|
+
free: boolean,
|
|
95
|
+
): number {
|
|
96
|
+
return snapDirectRotationDelta((startX - clientX) * radiansPerPixel, free)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resolveDirectRotationPatch(
|
|
100
|
+
node: AnyNode,
|
|
101
|
+
delta: number,
|
|
102
|
+
sceneApi: SceneApi = createSceneApi(useScene),
|
|
103
|
+
): Partial<AnyNode> | null {
|
|
104
|
+
const rotateHandle = getDirectRotateHandle(node)
|
|
105
|
+
if (rotateHandle) {
|
|
106
|
+
return rotateHandle.apply(node, delta, sceneApi) as Partial<AnyNode>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const rotation = (node as { rotation?: unknown }).rotation
|
|
110
|
+
if (typeof rotation === 'number') {
|
|
111
|
+
return { rotation: rotation - delta } as Partial<AnyNode>
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(rotation)) {
|
|
114
|
+
const [rx = 0, ry = 0, rz = 0] = rotation as [number?, number?, number?]
|
|
115
|
+
return { rotation: [rx, ry - delta, rz] } as Partial<AnyNode>
|
|
116
|
+
}
|
|
117
|
+
return null
|
|
118
|
+
}
|
package/src/lib/editor-api.ts
CHANGED
|
@@ -1,33 +1,20 @@
|
|
|
1
|
-
import type { AnyNode, EditorApi
|
|
1
|
+
import type { AnyNode, EditorApi } from '@pascal-app/core'
|
|
2
2
|
import useEditor from '../store/use-editor'
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import useInteractionScope from '../store/use-interaction-scope'
|
|
4
|
+
import {
|
|
5
|
+
controlPointReshapeScope,
|
|
6
|
+
endpointReshapeScope,
|
|
7
|
+
tangentReshapeScope,
|
|
8
|
+
} from './interaction/scope'
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* is a table lookup rather than an `if (type === 'wall')` chain so
|
|
12
|
-
* adding a new endpoint-draggable kind is a one-line entry instead
|
|
13
|
-
* of a new branch. Each entry casts the generic `AnyNode` to its
|
|
14
|
-
* concrete kind — the lookup key already guarantees the type.
|
|
15
|
-
*/
|
|
16
|
-
const endpointEngagers: Record<string, EndpointEngager> = {
|
|
17
|
-
wall: (node, endpoint, editor) =>
|
|
18
|
-
editor.setMovingWallEndpoint({ wall: node as WallNode, endpoint }),
|
|
19
|
-
fence: (node, endpoint, editor) =>
|
|
20
|
-
editor.setMovingFenceEndpoint({ fence: node as FenceNode, endpoint }),
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Concrete {@link EditorApi} backed by `useEditor`. Descriptors call into
|
|
25
|
-
* editor state through this interface; the editor owns the actual setter
|
|
26
|
-
* names so core stays decoupled.
|
|
11
|
+
* Concrete {@link EditorApi} backed by `useEditor` + the interaction scope.
|
|
12
|
+
* Descriptors call into editor state through this interface; the editor owns
|
|
13
|
+
* the actual store wiring so core stays decoupled.
|
|
27
14
|
*
|
|
28
|
-
* `engageMove` clears any in-progress endpoint drag or curve gesture
|
|
29
|
-
*
|
|
30
|
-
*
|
|
15
|
+
* `engageMove` no longer clears any in-progress endpoint drag or curve gesture:
|
|
16
|
+
* `setMovingNode` begins the `moving` scope, and the scope is single-owner, so
|
|
17
|
+
* it atomically replaces any prior reshape — there is no separate flag to reset.
|
|
31
18
|
*/
|
|
32
19
|
export function createEditorApi(): EditorApi {
|
|
33
20
|
return {
|
|
@@ -39,10 +26,6 @@ export function createEditorApi(): EditorApi {
|
|
|
39
26
|
// cast lets registry-driven move kinds through without forcing a
|
|
40
27
|
// schema-level type widening.
|
|
41
28
|
editor.setMovingNode(node as Parameters<typeof editor.setMovingNode>[0])
|
|
42
|
-
editor.setMovingWallEndpoint(null)
|
|
43
|
-
editor.setMovingFenceEndpoint(null)
|
|
44
|
-
editor.setCurvingWall(null)
|
|
45
|
-
editor.setCurvingFence(null)
|
|
46
29
|
},
|
|
47
30
|
engageMoveDrag(node: AnyNode) {
|
|
48
31
|
const editor = useEditor.getState()
|
|
@@ -50,13 +33,18 @@ export function createEditorApi(): EditorApi {
|
|
|
50
33
|
// it at setup and wires its commit-on-release listener.
|
|
51
34
|
editor.setPlacementDragMode(true)
|
|
52
35
|
editor.setMovingNode(node as Parameters<typeof editor.setMovingNode>[0])
|
|
53
|
-
editor.setMovingWallEndpoint(null)
|
|
54
|
-
editor.setMovingFenceEndpoint(null)
|
|
55
|
-
editor.setCurvingWall(null)
|
|
56
|
-
editor.setCurvingFence(null)
|
|
57
36
|
},
|
|
58
37
|
engageEndpointMove(node: AnyNode, endpoint: 'start' | 'end') {
|
|
59
|
-
|
|
38
|
+
// Endpoint reshape is kind-agnostic: the scope carries the node id + which
|
|
39
|
+
// endpoint, and consumers recover the node from the scene. Adding a new
|
|
40
|
+
// endpoint-draggable kind needs no entry here.
|
|
41
|
+
useInteractionScope.getState().begin(endpointReshapeScope(node.id, endpoint))
|
|
42
|
+
},
|
|
43
|
+
engageControlPointMove(node: AnyNode, index: number) {
|
|
44
|
+
useInteractionScope.getState().begin(controlPointReshapeScope(node.id, index))
|
|
45
|
+
},
|
|
46
|
+
engageTangentMove(node: AnyNode, index: number, side: 'in' | 'out') {
|
|
47
|
+
useInteractionScope.getState().begin(tangentReshapeScope(node.id, index, side))
|
|
60
48
|
},
|
|
61
49
|
}
|
|
62
50
|
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { type AnyNode, CeilingNode, LevelNode, SlabNode } from '@pascal-app/core'
|
|
3
|
+
import useElevationGuides from '../store/use-elevation-guides'
|
|
4
|
+
import {
|
|
5
|
+
clearStructuralElevationGuide,
|
|
6
|
+
collectElevationSnapTargets,
|
|
7
|
+
publishStructuralElevationGuide,
|
|
8
|
+
resolveElevationSnapMatch,
|
|
9
|
+
resolveStructuralElevationSnap,
|
|
10
|
+
} from './elevation-guides'
|
|
11
|
+
|
|
12
|
+
function structuralScene() {
|
|
13
|
+
const rawLevel = LevelNode.parse({ level: 0, height: 3 })
|
|
14
|
+
const slab = SlabNode.parse({
|
|
15
|
+
parentId: rawLevel.id,
|
|
16
|
+
polygon: [
|
|
17
|
+
[2, -1],
|
|
18
|
+
[4, -1],
|
|
19
|
+
[4, 1],
|
|
20
|
+
[2, 1],
|
|
21
|
+
],
|
|
22
|
+
elevation: 0.6,
|
|
23
|
+
thickness: 0.2,
|
|
24
|
+
})
|
|
25
|
+
const ceiling = CeilingNode.parse({
|
|
26
|
+
parentId: rawLevel.id,
|
|
27
|
+
polygon: [
|
|
28
|
+
[-4, -1],
|
|
29
|
+
[-2, -1],
|
|
30
|
+
[-2, 1],
|
|
31
|
+
[-4, 1],
|
|
32
|
+
],
|
|
33
|
+
height: 2.4,
|
|
34
|
+
})
|
|
35
|
+
const level = { ...rawLevel, children: [slab.id, ceiling.id] }
|
|
36
|
+
const nodes: Record<string, AnyNode> = {
|
|
37
|
+
[level.id]: level,
|
|
38
|
+
[slab.id]: slab,
|
|
39
|
+
[ceiling.id]: ceiling,
|
|
40
|
+
}
|
|
41
|
+
return { ceiling, level, nodes, slab }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('elevation guides', () => {
|
|
45
|
+
test('collects the level, slab faces, and ceiling plane on the source level', () => {
|
|
46
|
+
const { level, nodes, slab } = structuralScene()
|
|
47
|
+
const targets = collectElevationSnapTargets(
|
|
48
|
+
{ nodeId: 'wall_moving', levelId: level.id, anchor: [0, 0] },
|
|
49
|
+
nodes,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
expect(targets.map((target) => [target.label, target.elevation])).toEqual([
|
|
53
|
+
['Level', 0],
|
|
54
|
+
['Slab top', 0.6],
|
|
55
|
+
['Slab underside', 0.39999999999999997],
|
|
56
|
+
['Ceiling', 2.4],
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
const withoutSelf = collectElevationSnapTargets(
|
|
60
|
+
{ nodeId: slab.id, levelId: level.id, anchor: [0, 0] },
|
|
61
|
+
nodes,
|
|
62
|
+
)
|
|
63
|
+
expect(withoutSelf.some((target) => target.id.startsWith(slab.id))).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('snaps inside the Y tolerance and leaves values outside it alone', () => {
|
|
67
|
+
const { level, nodes } = structuralScene()
|
|
68
|
+
const source = { nodeId: 'wall_moving', levelId: level.id, anchor: [0, 0] } as const
|
|
69
|
+
|
|
70
|
+
expect(resolveStructuralElevationSnap(source, 0.54, nodes)).toBe(0.6)
|
|
71
|
+
expect(resolveStructuralElevationSnap(source, 0.49, nodes)).toBe(0.49)
|
|
72
|
+
expect(resolveStructuralElevationSnap(source, 2.34, nodes)).toBe(2.4)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('uses plan distance to disambiguate datums at the same elevation', () => {
|
|
76
|
+
const match = resolveElevationSnapMatch(
|
|
77
|
+
1,
|
|
78
|
+
[0, 0],
|
|
79
|
+
[
|
|
80
|
+
{ id: 'far', elevation: 1, anchor: [10, 0], label: 'Far' },
|
|
81
|
+
{ id: 'near', elevation: 1, anchor: [2, 0], label: 'Near' },
|
|
82
|
+
],
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
expect(match?.target.id).toBe('near')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('publishes one owner-scoped guide only while exactly aligned', () => {
|
|
89
|
+
const { level, nodes } = structuralScene()
|
|
90
|
+
const source = { nodeId: 'wall_moving', levelId: level.id, anchor: [0, 0] } as const
|
|
91
|
+
useElevationGuides.setState({ guide: null })
|
|
92
|
+
|
|
93
|
+
publishStructuralElevationGuide(source, 0.6, nodes)
|
|
94
|
+
expect(useElevationGuides.getState().guide).toMatchObject({
|
|
95
|
+
ownerId: 'wall_moving',
|
|
96
|
+
elevation: 0.6,
|
|
97
|
+
label: 'Slab top',
|
|
98
|
+
direction: [1, 0],
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
clearStructuralElevationGuide('someone_else')
|
|
102
|
+
expect(useElevationGuides.getState().guide).not.toBeNull()
|
|
103
|
+
|
|
104
|
+
publishStructuralElevationGuide(source, 0.7, nodes)
|
|
105
|
+
expect(useElevationGuides.getState().guide).toBeNull()
|
|
106
|
+
})
|
|
107
|
+
})
|