@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,237 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
normalizeCameraPose,
|
|
4
|
+
planCameraPoseApplication,
|
|
5
|
+
publishInitialCameraPose,
|
|
6
|
+
releaseCameraPoseEventSuppression,
|
|
7
|
+
stepCameraPoseInterpolation,
|
|
8
|
+
withCameraPoseDistance,
|
|
9
|
+
} from './camera-pose'
|
|
10
|
+
|
|
11
|
+
describe('camera pose', () => {
|
|
12
|
+
test('normalizes a finite pose without retaining input tuples', () => {
|
|
13
|
+
const position: [number, number, number] = [1, 2, 3]
|
|
14
|
+
const target: [number, number, number] = [4, 5, 6]
|
|
15
|
+
const pose = normalizeCameraPose({
|
|
16
|
+
position,
|
|
17
|
+
target,
|
|
18
|
+
projection: 'perspective',
|
|
19
|
+
fov: 50,
|
|
20
|
+
viewWidth: 12,
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
expect(pose).toEqual({
|
|
24
|
+
fov: 50,
|
|
25
|
+
position,
|
|
26
|
+
projection: 'perspective',
|
|
27
|
+
target,
|
|
28
|
+
viewWidth: 12,
|
|
29
|
+
})
|
|
30
|
+
expect(pose?.position).not.toBe(position)
|
|
31
|
+
expect(pose?.target).not.toBe(target)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('rejects non-finite, malformed, and unknown camera poses', () => {
|
|
35
|
+
expect(normalizeCameraPose(null)).toBeNull()
|
|
36
|
+
expect(
|
|
37
|
+
normalizeCameraPose({
|
|
38
|
+
position: [1, 2],
|
|
39
|
+
target: [4, 5, 6],
|
|
40
|
+
projection: 'perspective',
|
|
41
|
+
}),
|
|
42
|
+
).toBeNull()
|
|
43
|
+
expect(
|
|
44
|
+
normalizeCameraPose({
|
|
45
|
+
position: [1, Number.NaN, 3],
|
|
46
|
+
target: [4, 5, 6],
|
|
47
|
+
projection: 'perspective',
|
|
48
|
+
}),
|
|
49
|
+
).toBeNull()
|
|
50
|
+
expect(
|
|
51
|
+
normalizeCameraPose({
|
|
52
|
+
position: [1, 2, 3],
|
|
53
|
+
target: [4, 5, Number.POSITIVE_INFINITY],
|
|
54
|
+
projection: 'perspective',
|
|
55
|
+
}),
|
|
56
|
+
).toBeNull()
|
|
57
|
+
expect(
|
|
58
|
+
normalizeCameraPose({
|
|
59
|
+
position: [1, 2, 3],
|
|
60
|
+
target: [4, 5, 6],
|
|
61
|
+
projection: 'panoramic',
|
|
62
|
+
}),
|
|
63
|
+
).toBeNull()
|
|
64
|
+
expect(
|
|
65
|
+
normalizeCameraPose({
|
|
66
|
+
position: [1, 2, 3],
|
|
67
|
+
target: [4, 5, 6],
|
|
68
|
+
projection: 'perspective',
|
|
69
|
+
fov: Number.NaN,
|
|
70
|
+
}),
|
|
71
|
+
).toBeNull()
|
|
72
|
+
expect(
|
|
73
|
+
normalizeCameraPose({
|
|
74
|
+
position: [1, 2, 3],
|
|
75
|
+
target: [4, 5, 6],
|
|
76
|
+
projection: 'orthographic',
|
|
77
|
+
viewWidth: 0,
|
|
78
|
+
}),
|
|
79
|
+
).toBeNull()
|
|
80
|
+
expect(
|
|
81
|
+
normalizeCameraPose({
|
|
82
|
+
position: [1, 2, 3],
|
|
83
|
+
target: [4, 5, 6],
|
|
84
|
+
projection: 'orthographic',
|
|
85
|
+
viewWidth: Number.POSITIVE_INFINITY,
|
|
86
|
+
}),
|
|
87
|
+
).toBeNull()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('plans only perspective fov application and clamps it to a safe range', () => {
|
|
91
|
+
expect(
|
|
92
|
+
planCameraPoseApplication({
|
|
93
|
+
position: [1, 2, 3],
|
|
94
|
+
target: [4, 5, 6],
|
|
95
|
+
projection: 'perspective',
|
|
96
|
+
fov: 0,
|
|
97
|
+
})?.perspectiveFov,
|
|
98
|
+
).toBe(1)
|
|
99
|
+
expect(
|
|
100
|
+
planCameraPoseApplication({
|
|
101
|
+
position: [1, 2, 3],
|
|
102
|
+
target: [4, 5, 6],
|
|
103
|
+
projection: 'perspective',
|
|
104
|
+
fov: 200,
|
|
105
|
+
})?.perspectiveFov,
|
|
106
|
+
).toBe(179)
|
|
107
|
+
expect(
|
|
108
|
+
planCameraPoseApplication({
|
|
109
|
+
position: [1, 2, 3],
|
|
110
|
+
target: [4, 5, 6],
|
|
111
|
+
projection: 'orthographic',
|
|
112
|
+
fov: 70,
|
|
113
|
+
})?.perspectiveFov,
|
|
114
|
+
).toBeNull()
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('interpolates toward the latest pose and converges exactly', () => {
|
|
118
|
+
const destination = {
|
|
119
|
+
position: [12, 6, -4] as [number, number, number],
|
|
120
|
+
target: [2, 1, 3] as [number, number, number],
|
|
121
|
+
projection: 'perspective' as const,
|
|
122
|
+
}
|
|
123
|
+
const first = stepCameraPoseInterpolation([0, 0, 0], [0, 0, 0], destination, 0.016)
|
|
124
|
+
expect(first.settled).toBe(false)
|
|
125
|
+
expect(first.position[0]).toBeGreaterThan(0)
|
|
126
|
+
expect(first.position[0]).toBeLessThan(destination.position[0])
|
|
127
|
+
|
|
128
|
+
let position = first.position
|
|
129
|
+
let target = first.target
|
|
130
|
+
let settled = first.settled
|
|
131
|
+
for (let frame = 0; frame < 240 && !settled; frame += 1) {
|
|
132
|
+
const step = stepCameraPoseInterpolation(position, target, destination, 0.016)
|
|
133
|
+
position = step.position
|
|
134
|
+
target = step.target
|
|
135
|
+
settled = step.settled
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
expect(settled).toBe(true)
|
|
139
|
+
expect(position).toEqual(destination.position)
|
|
140
|
+
expect(target).toEqual(destination.target)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
test('settles an already-current pose without advancing on an invalid delta', () => {
|
|
144
|
+
const destination = {
|
|
145
|
+
position: [1, 2, 3] as [number, number, number],
|
|
146
|
+
target: [4, 5, 6] as [number, number, number],
|
|
147
|
+
projection: 'orthographic' as const,
|
|
148
|
+
}
|
|
149
|
+
expect(
|
|
150
|
+
stepCameraPoseInterpolation(
|
|
151
|
+
destination.position,
|
|
152
|
+
destination.target,
|
|
153
|
+
destination,
|
|
154
|
+
Number.NaN,
|
|
155
|
+
),
|
|
156
|
+
).toEqual({
|
|
157
|
+
position: destination.position,
|
|
158
|
+
settled: true,
|
|
159
|
+
target: destination.target,
|
|
160
|
+
})
|
|
161
|
+
expect(stepCameraPoseInterpolation([0, 0, 0], [0, 0, 0], destination, Number.NaN)).toEqual({
|
|
162
|
+
position: [0, 0, 0],
|
|
163
|
+
settled: false,
|
|
164
|
+
target: [0, 0, 0],
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('publishes the initial pose before any camera control update event', () => {
|
|
169
|
+
let publishCount = 0
|
|
170
|
+
|
|
171
|
+
publishInitialCameraPose(() => {
|
|
172
|
+
publishCount += 1
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
expect(publishCount).toBe(1)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
test('publishes the settled pose when programmatic interpolation releases suppression', () => {
|
|
179
|
+
const suppression = { current: true }
|
|
180
|
+
let publishCount = 0
|
|
181
|
+
|
|
182
|
+
releaseCameraPoseEventSuppression(suppression, () => {
|
|
183
|
+
publishCount += 1
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
expect(suppression.current).toBe(false)
|
|
187
|
+
expect(publishCount).toBe(1)
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('retargets the bounded interpolation owner to the latest pose', () => {
|
|
191
|
+
const first = stepCameraPoseInterpolation(
|
|
192
|
+
[0, 0, 0],
|
|
193
|
+
[0, 0, 0],
|
|
194
|
+
{
|
|
195
|
+
position: [10, 0, 0],
|
|
196
|
+
target: [5, 0, 0],
|
|
197
|
+
projection: 'perspective',
|
|
198
|
+
},
|
|
199
|
+
0.016,
|
|
200
|
+
)
|
|
201
|
+
const retargeted = stepCameraPoseInterpolation(
|
|
202
|
+
first.position,
|
|
203
|
+
first.target,
|
|
204
|
+
{
|
|
205
|
+
position: [-10, 0, 0],
|
|
206
|
+
target: [-5, 0, 0],
|
|
207
|
+
projection: 'perspective',
|
|
208
|
+
},
|
|
209
|
+
0.1,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
expect(retargeted.position[0]).toBeLessThan(first.position[0])
|
|
213
|
+
expect(retargeted.target[0]).toBeLessThan(first.target[0])
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test('preserves view direction while adapting distance to the local viewport', () => {
|
|
217
|
+
const pose = {
|
|
218
|
+
position: [4, 6, 12] as [number, number, number],
|
|
219
|
+
target: [1, 2, 3] as [number, number, number],
|
|
220
|
+
projection: 'perspective' as const,
|
|
221
|
+
viewWidth: 20,
|
|
222
|
+
}
|
|
223
|
+
const adapted = withCameraPoseDistance(pose, 5)
|
|
224
|
+
const sourceDirection = pose.position.map((value, index) => value - pose.target[index]!)
|
|
225
|
+
const adaptedDirection = adapted.position.map((value, index) => value - adapted.target[index]!)
|
|
226
|
+
const sourceLength = Math.hypot(...sourceDirection)
|
|
227
|
+
|
|
228
|
+
expect(Math.hypot(...adaptedDirection)).toBeCloseTo(5)
|
|
229
|
+
const expectedDirection = sourceDirection.map((component) => component * (5 / sourceLength))
|
|
230
|
+
expect(adaptedDirection[0]).toBeCloseTo(expectedDirection[0]!)
|
|
231
|
+
expect(adaptedDirection[1]).toBeCloseTo(expectedDirection[1]!)
|
|
232
|
+
expect(adaptedDirection[2]).toBeCloseTo(expectedDirection[2]!)
|
|
233
|
+
expect(adapted.target).toEqual(pose.target)
|
|
234
|
+
expect(adapted.target).not.toBe(pose.target)
|
|
235
|
+
expect(pose.position).toEqual([4, 6, 12])
|
|
236
|
+
})
|
|
237
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { CameraPose } from '@pascal-app/core'
|
|
2
|
+
|
|
3
|
+
const MIN_PERSPECTIVE_FOV = 1
|
|
4
|
+
const MAX_PERSPECTIVE_FOV = 179
|
|
5
|
+
const INTERPOLATION_TIME_CONSTANT_SECONDS = 0.08
|
|
6
|
+
const MAX_INTERPOLATION_DELTA_SECONDS = 0.1
|
|
7
|
+
const INTERPOLATION_SETTLE_DISTANCE = 0.001
|
|
8
|
+
|
|
9
|
+
export type CameraPoseApplicationPlan = {
|
|
10
|
+
pose: CameraPose
|
|
11
|
+
perspectiveFov: number | null
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type CameraPoseInterpolationStep = {
|
|
15
|
+
position: [number, number, number]
|
|
16
|
+
settled: boolean
|
|
17
|
+
target: [number, number, number]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function publishInitialCameraPose(publishCurrentPose: () => void): void {
|
|
21
|
+
publishCurrentPose()
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function releaseCameraPoseEventSuppression(
|
|
25
|
+
suppression: { current: boolean },
|
|
26
|
+
publishCurrentPose: () => void,
|
|
27
|
+
): void {
|
|
28
|
+
suppression.current = false
|
|
29
|
+
publishCurrentPose()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function finiteVectorTuple(value: unknown): [number, number, number] | null {
|
|
33
|
+
if (
|
|
34
|
+
!(
|
|
35
|
+
Array.isArray(value) &&
|
|
36
|
+
value.length === 3 &&
|
|
37
|
+
value.every((component) => typeof component === 'number' && Number.isFinite(component))
|
|
38
|
+
)
|
|
39
|
+
) {
|
|
40
|
+
return null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return [value[0], value[1], value[2]]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function normalizeCameraPose(value: unknown): CameraPose | null {
|
|
47
|
+
if (!(typeof value === 'object' && value !== null)) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const candidate = value as Partial<CameraPose>
|
|
52
|
+
const position = finiteVectorTuple(candidate.position)
|
|
53
|
+
const target = finiteVectorTuple(candidate.target)
|
|
54
|
+
if (
|
|
55
|
+
!(
|
|
56
|
+
position &&
|
|
57
|
+
target &&
|
|
58
|
+
(candidate.projection === 'perspective' || candidate.projection === 'orthographic')
|
|
59
|
+
)
|
|
60
|
+
) {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
candidate.fov !== undefined &&
|
|
66
|
+
!(typeof candidate.fov === 'number' && Number.isFinite(candidate.fov))
|
|
67
|
+
) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (
|
|
72
|
+
candidate.viewWidth !== undefined &&
|
|
73
|
+
!(
|
|
74
|
+
typeof candidate.viewWidth === 'number' &&
|
|
75
|
+
Number.isFinite(candidate.viewWidth) &&
|
|
76
|
+
candidate.viewWidth > 0
|
|
77
|
+
)
|
|
78
|
+
) {
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...(candidate.fov === undefined ? {} : { fov: candidate.fov }),
|
|
84
|
+
position,
|
|
85
|
+
projection: candidate.projection,
|
|
86
|
+
target,
|
|
87
|
+
...(candidate.viewWidth === undefined ? {} : { viewWidth: candidate.viewWidth }),
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function planCameraPoseApplication(value: unknown): CameraPoseApplicationPlan | null {
|
|
92
|
+
const pose = normalizeCameraPose(value)
|
|
93
|
+
if (!pose) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
pose,
|
|
99
|
+
perspectiveFov:
|
|
100
|
+
pose.projection === 'perspective' && pose.fov !== undefined
|
|
101
|
+
? Math.min(Math.max(pose.fov, MIN_PERSPECTIVE_FOV), MAX_PERSPECTIVE_FOV)
|
|
102
|
+
: null,
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function interpolateVectorTuple(
|
|
107
|
+
current: [number, number, number],
|
|
108
|
+
destination: [number, number, number],
|
|
109
|
+
alpha: number,
|
|
110
|
+
): { settled: boolean; value: [number, number, number] } {
|
|
111
|
+
const dx = destination[0] - current[0]
|
|
112
|
+
const dy = destination[1] - current[1]
|
|
113
|
+
const dz = destination[2] - current[2]
|
|
114
|
+
const settleDistanceSquared = INTERPOLATION_SETTLE_DISTANCE ** 2
|
|
115
|
+
if (dx * dx + dy * dy + dz * dz <= settleDistanceSquared) {
|
|
116
|
+
return { settled: true, value: [...destination] }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const value: [number, number, number] = [
|
|
120
|
+
current[0] + dx * alpha,
|
|
121
|
+
current[1] + dy * alpha,
|
|
122
|
+
current[2] + dz * alpha,
|
|
123
|
+
]
|
|
124
|
+
const remainingX = destination[0] - value[0]
|
|
125
|
+
const remainingY = destination[1] - value[1]
|
|
126
|
+
const remainingZ = destination[2] - value[2]
|
|
127
|
+
const settled =
|
|
128
|
+
remainingX * remainingX + remainingY * remainingY + remainingZ * remainingZ <=
|
|
129
|
+
settleDistanceSquared
|
|
130
|
+
|
|
131
|
+
return { settled, value: settled ? [...destination] : value }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function withCameraPoseDistance(pose: CameraPose, distance: number): CameraPose {
|
|
135
|
+
const dx = pose.position[0] - pose.target[0]
|
|
136
|
+
const dy = pose.position[1] - pose.target[1]
|
|
137
|
+
const dz = pose.position[2] - pose.target[2]
|
|
138
|
+
const currentDistance = Math.hypot(dx, dy, dz)
|
|
139
|
+
if (!(Number.isFinite(distance) && distance > 0 && currentDistance > 0)) {
|
|
140
|
+
return { ...pose, position: [...pose.position], target: [...pose.target] }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const scale = distance / currentDistance
|
|
144
|
+
return {
|
|
145
|
+
...pose,
|
|
146
|
+
position: [
|
|
147
|
+
pose.target[0] + dx * scale,
|
|
148
|
+
pose.target[1] + dy * scale,
|
|
149
|
+
pose.target[2] + dz * scale,
|
|
150
|
+
],
|
|
151
|
+
target: [...pose.target],
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function stepCameraPoseInterpolation(
|
|
156
|
+
currentPosition: [number, number, number],
|
|
157
|
+
currentTarget: [number, number, number],
|
|
158
|
+
destination: CameraPose,
|
|
159
|
+
deltaSeconds: number,
|
|
160
|
+
): CameraPoseInterpolationStep {
|
|
161
|
+
const finiteDelta = Number.isFinite(deltaSeconds) ? deltaSeconds : 0
|
|
162
|
+
const elapsed = Math.min(Math.max(finiteDelta, 0), MAX_INTERPOLATION_DELTA_SECONDS)
|
|
163
|
+
const alpha = 1 - Math.exp(-elapsed / INTERPOLATION_TIME_CONSTANT_SECONDS)
|
|
164
|
+
const position = interpolateVectorTuple(currentPosition, destination.position, alpha)
|
|
165
|
+
const target = interpolateVectorTuple(currentTarget, destination.target, alpha)
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
position: position.value,
|
|
169
|
+
settled: position.settled && target.settled,
|
|
170
|
+
target: target.value,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import {
|
|
2
|
+
clearSurfacePlanSnapFeedback,
|
|
3
|
+
resolveSurfacePlanPointSnap,
|
|
4
|
+
SURFACE_ALIGNMENT_THRESHOLD_M,
|
|
5
|
+
type SurfacePlanSnapInput,
|
|
6
|
+
type SurfacePlanSnapResult,
|
|
7
|
+
} from './surface-plan-snap'
|
|
8
|
+
|
|
9
|
+
const CEILING_SNAP_MOVING_ID = '__ceiling_snap__'
|
|
10
|
+
|
|
11
|
+
export const CEILING_ALIGNMENT_THRESHOLD_M = SURFACE_ALIGNMENT_THRESHOLD_M
|
|
12
|
+
export type CeilingPlanSnapInput = SurfacePlanSnapInput
|
|
13
|
+
export type CeilingPlanSnapResult = SurfacePlanSnapResult
|
|
14
|
+
|
|
15
|
+
export function clearCeilingSnapFeedback() {
|
|
16
|
+
clearSurfacePlanSnapFeedback()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveCeilingPlanPointSnap(input: CeilingPlanSnapInput): CeilingPlanSnapResult {
|
|
20
|
+
return resolveSurfacePlanPointSnap({
|
|
21
|
+
...input,
|
|
22
|
+
movingId: input.movingId ?? CEILING_SNAP_MOVING_ID,
|
|
23
|
+
})
|
|
24
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { resolveSelectModeHelpHints } from './contextual-help'
|
|
3
|
+
|
|
4
|
+
describe('resolveSelectModeHelpHints', () => {
|
|
5
|
+
test('stays hidden in idle select mode with no selection', () => {
|
|
6
|
+
expect(
|
|
7
|
+
resolveSelectModeHelpHints({
|
|
8
|
+
selectedCount: 0,
|
|
9
|
+
hasMovableSelection: false,
|
|
10
|
+
hasRotatableSelection: false,
|
|
11
|
+
commandPressed: false,
|
|
12
|
+
shiftPressed: false,
|
|
13
|
+
}),
|
|
14
|
+
).toEqual([])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('shows multi-select guidance when a modifier is held without selection', () => {
|
|
18
|
+
expect(
|
|
19
|
+
resolveSelectModeHelpHints({
|
|
20
|
+
selectedCount: 0,
|
|
21
|
+
hasMovableSelection: false,
|
|
22
|
+
hasRotatableSelection: false,
|
|
23
|
+
commandPressed: true,
|
|
24
|
+
shiftPressed: false,
|
|
25
|
+
}),
|
|
26
|
+
).toEqual([
|
|
27
|
+
{
|
|
28
|
+
keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
|
|
29
|
+
label: 'Add or remove objects from the selection',
|
|
30
|
+
active: true,
|
|
31
|
+
},
|
|
32
|
+
])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('shows direct manipulation tips for selected movable and rotatable nodes', () => {
|
|
36
|
+
const hints = resolveSelectModeHelpHints({
|
|
37
|
+
selectedCount: 1,
|
|
38
|
+
hasMovableSelection: true,
|
|
39
|
+
hasRotatableSelection: true,
|
|
40
|
+
commandPressed: false,
|
|
41
|
+
shiftPressed: false,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(hints).toContainEqual({
|
|
45
|
+
keys: ['Left click'],
|
|
46
|
+
label: 'Drag selected movable object',
|
|
47
|
+
})
|
|
48
|
+
expect(hints).toContainEqual({
|
|
49
|
+
keys: ['Cmd/Ctrl', 'Right click'],
|
|
50
|
+
label: 'Drag left or right to rotate selected object',
|
|
51
|
+
})
|
|
52
|
+
// Cmd/Ctrl and Shift click both toggle selection membership (3D selection
|
|
53
|
+
// manager and 2D floorplan alike) — advertised as a single or-group row.
|
|
54
|
+
expect(hints).toContainEqual({
|
|
55
|
+
keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
|
|
56
|
+
label: 'Add or remove objects from the selection',
|
|
57
|
+
active: false,
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('multi-selection advertises the group move + rotate gestures', () => {
|
|
62
|
+
const hints = resolveSelectModeHelpHints({
|
|
63
|
+
selectedCount: 3,
|
|
64
|
+
hasMovableSelection: true,
|
|
65
|
+
hasRotatableSelection: true,
|
|
66
|
+
commandPressed: false,
|
|
67
|
+
shiftPressed: false,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(hints).toEqual([
|
|
71
|
+
{
|
|
72
|
+
keys: ['Left click'],
|
|
73
|
+
label: 'Click or drag the selection to move it as one',
|
|
74
|
+
},
|
|
75
|
+
{ keys: ['R / T'], label: 'Rotate the selection ±45°' },
|
|
76
|
+
{
|
|
77
|
+
keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
|
|
78
|
+
label: 'Add or remove objects from the selection',
|
|
79
|
+
active: false,
|
|
80
|
+
},
|
|
81
|
+
{ keys: ['Esc'], label: 'Clear the selection (or click outside)' },
|
|
82
|
+
])
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('holding a modifier keeps the same rows and only lights the selection one', () => {
|
|
86
|
+
// Guides/snapping are governed by the snapping mode (Shift toggles it),
|
|
87
|
+
// so no modifier-specific "freely / with guides / bypass" variants exist.
|
|
88
|
+
const hints = resolveSelectModeHelpHints({
|
|
89
|
+
selectedCount: 1,
|
|
90
|
+
hasMovableSelection: true,
|
|
91
|
+
hasRotatableSelection: true,
|
|
92
|
+
commandPressed: true,
|
|
93
|
+
shiftPressed: true,
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
expect(hints).toEqual([
|
|
97
|
+
{
|
|
98
|
+
keys: ['Left click'],
|
|
99
|
+
label: 'Drag selected movable object',
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
keys: ['Cmd/Ctrl', 'Right click'],
|
|
103
|
+
label: 'Drag left or right to rotate selected object',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
keys: [['Cmd/Ctrl', 'Shift'], 'Left click'],
|
|
107
|
+
label: 'Add or remove objects from the selection',
|
|
108
|
+
active: true,
|
|
109
|
+
},
|
|
110
|
+
])
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
export type ContextualShortcutHint = {
|
|
2
|
+
// A combo of keys pressed together (rendered joined by "+"). An entry may
|
|
3
|
+
// itself be an array of alternatives (rendered joined by "/"), e.g.
|
|
4
|
+
// [['Cmd/Ctrl', 'Shift'], 'Left click'] → "⌘ / ⇧ + click".
|
|
5
|
+
keys: Array<string | string[]>
|
|
6
|
+
label: string
|
|
7
|
+
// Optional secondary line under the label for a terser qualifier
|
|
8
|
+
// (e.g. "disable 15° snap"). The HUD wraps both lines rather than truncating.
|
|
9
|
+
subtitle?: string
|
|
10
|
+
active?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// `activeHandleDrag.label` value a rotate gizmo sets while dragging, so the
|
|
14
|
+
// contextual HUD can surface the Shift = free-rotation toggle for the duration
|
|
15
|
+
// (mirrors how wall drafting advertises Shift). Distinct from resize handles,
|
|
16
|
+
// which route their own measurement label here.
|
|
17
|
+
export const ROTATE_HANDLE_DRAG_LABEL = 'rotate-handle'
|
|
18
|
+
|
|
19
|
+
// `activeHandleDrag.label` a plain resize / radial-resize arrow sets while
|
|
20
|
+
// dragging (when it carries no dimension `measureLabel`). It exists only so the
|
|
21
|
+
// interaction scope is non-idle during a resize, which keeps the idle
|
|
22
|
+
// select-mode hints off-screen — a resize is its own action, not a selection.
|
|
23
|
+
export const RESIZE_HANDLE_DRAG_LABEL = 'resize-handle'
|
|
24
|
+
|
|
25
|
+
// `activeHandleDrag.label`s for the multi-selection group drags: the body
|
|
26
|
+
// drag sessions (2D floorplan + 3D ground plane) and the rotate gizmo. Kept
|
|
27
|
+
// here (not in the session/gizmo modules) so `snapping-mode.ts` can resolve
|
|
28
|
+
// the group move to the 'item' snap context without importing component code.
|
|
29
|
+
export const GROUP_MOVE_DRAG_LABEL = 'group-move-handle'
|
|
30
|
+
export const GROUP_ROTATE_DRAG_LABEL = 'group-rotate-handle'
|
|
31
|
+
|
|
32
|
+
// Hints shown while a rotate gizmo is mid-drag: Shift bypasses the angle step
|
|
33
|
+
// (free rotation), the same toggle wall drafting exposes. `active` lights the
|
|
34
|
+
// pill while Shift is held.
|
|
35
|
+
export function resolveRotateHandleHelpHints(shiftPressed: boolean): ContextualShortcutHint[] {
|
|
36
|
+
return [
|
|
37
|
+
{
|
|
38
|
+
keys: [SHIFT_KEY],
|
|
39
|
+
label: shiftPressed ? 'Rotating freely (no angle step)' : 'Hold to rotate freely',
|
|
40
|
+
active: shiftPressed,
|
|
41
|
+
},
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type SelectModeHelpContext = {
|
|
46
|
+
selectedCount: number
|
|
47
|
+
hasMovableSelection: boolean
|
|
48
|
+
hasRotatableSelection: boolean
|
|
49
|
+
commandPressed: boolean
|
|
50
|
+
shiftPressed: boolean
|
|
51
|
+
// When a single MEP node is selected its in-world handle rig (click a dot to
|
|
52
|
+
// reveal move arrows) is the real editing path, so the panel leads with the
|
|
53
|
+
// handle-specific hints instead of just the generic Cmd-drag tips.
|
|
54
|
+
mepSelection?: 'run' | 'fitting' | null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const COMMAND_KEY = 'Cmd/Ctrl'
|
|
58
|
+
const LEFT_CLICK = 'Left click'
|
|
59
|
+
const RIGHT_CLICK = 'Right click'
|
|
60
|
+
const SHIFT_KEY = 'Shift'
|
|
61
|
+
const CLICK = 'Click'
|
|
62
|
+
const ALT_KEY = 'Alt'
|
|
63
|
+
const ROTATE_KEYS = 'R / T'
|
|
64
|
+
const ESC_KEY = 'Esc'
|
|
65
|
+
|
|
66
|
+
export function resolveSelectModeHelpHints({
|
|
67
|
+
selectedCount,
|
|
68
|
+
hasMovableSelection,
|
|
69
|
+
hasRotatableSelection,
|
|
70
|
+
commandPressed,
|
|
71
|
+
shiftPressed,
|
|
72
|
+
mepSelection = null,
|
|
73
|
+
}: SelectModeHelpContext): ContextualShortcutHint[] {
|
|
74
|
+
const hints: ContextualShortcutHint[] = []
|
|
75
|
+
|
|
76
|
+
if (selectedCount === 0) {
|
|
77
|
+
if (!commandPressed && !shiftPressed) return hints
|
|
78
|
+
|
|
79
|
+
hints.push({
|
|
80
|
+
keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
|
|
81
|
+
label: 'Add or remove objects from the selection',
|
|
82
|
+
active: true,
|
|
83
|
+
})
|
|
84
|
+
return hints
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Multi-selection — advertise the group gestures: drag any member to slide
|
|
88
|
+
// the whole selection (2D floor plan body drag + the 3D move gizmo), R / T
|
|
89
|
+
// to step-rotate the group around its bounding-box center.
|
|
90
|
+
if (selectedCount > 1) {
|
|
91
|
+
hints.push({
|
|
92
|
+
keys: [LEFT_CLICK],
|
|
93
|
+
label: 'Click or drag the selection to move it as one',
|
|
94
|
+
})
|
|
95
|
+
hints.push({ keys: [ROTATE_KEYS], label: 'Rotate the selection ±45°' })
|
|
96
|
+
hints.push({
|
|
97
|
+
keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
|
|
98
|
+
label: 'Add or remove objects from the selection',
|
|
99
|
+
active: commandPressed || shiftPressed,
|
|
100
|
+
})
|
|
101
|
+
hints.push({ keys: [ESC_KEY], label: 'Clear the selection (or click outside)' })
|
|
102
|
+
return hints
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// MEP handle workflow — duct/pipe runs and fittings are edited through the
|
|
106
|
+
// in-world arrow rig that a click on the handle dot reveals, so surface those
|
|
107
|
+
// hints first. A run endpoint's side / up-down arrows swing the run and Alt
|
|
108
|
+
// detaches the joint mid-drag; a fitting's cluster adds rotate arcs, with
|
|
109
|
+
// R / T (and Alt to switch axis) for keyboard rotation.
|
|
110
|
+
if (mepSelection === 'run') {
|
|
111
|
+
hints.push({ keys: [CLICK], label: 'Click a handle dot to show move arrows' })
|
|
112
|
+
hints.push({ keys: [ALT_KEY], label: 'Detach the joint while dragging an arrow' })
|
|
113
|
+
} else if (mepSelection === 'fitting') {
|
|
114
|
+
hints.push({ keys: [CLICK], label: 'Click the handle dot to show move + rotate handles' })
|
|
115
|
+
hints.push({ keys: [ROTATE_KEYS], label: 'Rotate ±45°' })
|
|
116
|
+
hints.push({ keys: [ALT_KEY], label: 'Switch the rotation axis (Y → X → Z)' })
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The rows are the same whatever modifier is held — guides/snapping are
|
|
120
|
+
// governed by the snapping mode (Shift toggles it), not by the modifier of
|
|
121
|
+
// this gesture, so there are no modifier-specific variants to advertise.
|
|
122
|
+
// Holding Cmd/Ctrl or Shift just lights the selection row.
|
|
123
|
+
if (hasMovableSelection) {
|
|
124
|
+
hints.push({
|
|
125
|
+
keys: [LEFT_CLICK],
|
|
126
|
+
label: 'Drag selected movable object',
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (hasRotatableSelection) {
|
|
131
|
+
hints.push({
|
|
132
|
+
keys: [COMMAND_KEY, RIGHT_CLICK],
|
|
133
|
+
label: 'Drag left or right to rotate selected object',
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
hints.push({
|
|
138
|
+
keys: [[COMMAND_KEY, SHIFT_KEY], LEFT_CLICK],
|
|
139
|
+
label: 'Add or remove objects from the selection',
|
|
140
|
+
active: commandPressed || shiftPressed,
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
return hints
|
|
144
|
+
}
|