@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,95 @@
|
|
|
1
|
+
// The one place that answers "where is the ground under this pointer ray?".
|
|
2
|
+
//
|
|
3
|
+
// Before terrain there was no question to answer: the ground was `y = 0` and
|
|
4
|
+
// every consumer intersected its own horizontal plane. Terrain makes those
|
|
5
|
+
// planes wrong in a way that is invisible on flat ground and badly wrong on a
|
|
6
|
+
// slope — a ray intersected at the datum instead of at the surface reports an XZ
|
|
7
|
+
// that slides along the ray, further the more oblique the camera. Item placement
|
|
8
|
+
// already repairs this locally (`resolvePointerSupportSurface`); this module
|
|
9
|
+
// exists so the repair is shared instead of copied into column, wall, fence, and
|
|
10
|
+
// the sculpt tool.
|
|
11
|
+
//
|
|
12
|
+
// It lives in `editor` rather than `core` because the decision needs one piece of
|
|
13
|
+
// editor knowledge — which plane a consumer is asking about — while the geometry
|
|
14
|
+
// it delegates to (`raycastTerrain`, `heightAt`) is pure core.
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
heightAt,
|
|
18
|
+
isSiteDatum,
|
|
19
|
+
raycastTerrain,
|
|
20
|
+
type SiteNode,
|
|
21
|
+
type TerrainField,
|
|
22
|
+
type TerrainHit,
|
|
23
|
+
terrainFieldOf,
|
|
24
|
+
useScene,
|
|
25
|
+
} from '@pascal-app/core'
|
|
26
|
+
|
|
27
|
+
/** The active site's terrain field, or null when the scene has no sculpted ground. */
|
|
28
|
+
export function siteTerrainField(): TerrainField | null {
|
|
29
|
+
const { nodes, rootNodeIds } = useScene.getState()
|
|
30
|
+
const siteId = rootNodeIds[0]
|
|
31
|
+
const site = siteId ? nodes[siteId] : null
|
|
32
|
+
if (site?.type !== 'site') return null
|
|
33
|
+
return terrainFieldOf(site as SiteNode)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* True when a horizontal query plane at `planeY` is the *site ground*, as opposed
|
|
38
|
+
* to a built flat surface that happens to be horizontal.
|
|
39
|
+
*
|
|
40
|
+
* This is the whole scoping rule, and getting it wrong in either direction is
|
|
41
|
+
* worse than not having terrain at all:
|
|
42
|
+
*
|
|
43
|
+
* - Treat every horizontal plane as ground and a second-storey wall draft snaps
|
|
44
|
+
* to the hillside under the building.
|
|
45
|
+
* - Treat none of them as ground and every ground tool keeps the perspective skew
|
|
46
|
+
* terrain was supposed to remove.
|
|
47
|
+
*
|
|
48
|
+
* The datum is exactly the right discriminator because terrain *is* the surface at
|
|
49
|
+
* the datum: the site renderer removes the flat ground fill when terrain mounts,
|
|
50
|
+
* so at the datum there is nothing else to hit. Every other horizontal plane —
|
|
51
|
+
* a storey base, a slab top, a shelf — is a real flat surface built by the user,
|
|
52
|
+
* and a plane is the correct model for it.
|
|
53
|
+
*
|
|
54
|
+
* `isSiteDatum` comes from core so this rule and the one the support resolver
|
|
55
|
+
* applies (`terrain-support.ts`) cannot drift into disagreeing about which
|
|
56
|
+
* surfaces are ground.
|
|
57
|
+
*/
|
|
58
|
+
export function isSiteGroundPlane(planeY: number): boolean {
|
|
59
|
+
return isSiteDatum(planeY)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Where a pointer ray meets the sculpted ground, or null when the flat-plane path
|
|
64
|
+
* should be used instead — no terrain, the ray misses the field, or the query
|
|
65
|
+
* plane is a built surface rather than the ground.
|
|
66
|
+
*
|
|
67
|
+
* Returning null rather than a fallback hit is deliberate: the caller already owns
|
|
68
|
+
* a correct plane intersection for its own frame (world for grid events,
|
|
69
|
+
* level-local for support election), and duplicating that here would mean two
|
|
70
|
+
* definitions of the fallback to keep in sync.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveTerrainGroundHit(
|
|
73
|
+
origin: readonly [number, number, number],
|
|
74
|
+
direction: readonly [number, number, number],
|
|
75
|
+
planeY: number,
|
|
76
|
+
): TerrainHit | null {
|
|
77
|
+
if (!isSiteGroundPlane(planeY)) return null
|
|
78
|
+
const field = siteTerrainField()
|
|
79
|
+
if (!field) return null
|
|
80
|
+
return raycastTerrain(field, origin, direction)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Ground height at a world XZ, for consumers with no ray to march.
|
|
85
|
+
*
|
|
86
|
+
* The 2D plan view is the case this exists for: an orthographic top-down camera
|
|
87
|
+
* makes the pointer ray vertical, so the march degenerates to a single sample and
|
|
88
|
+
* there is no XZ skew to correct — only a Y to report.
|
|
89
|
+
*/
|
|
90
|
+
export function groundHeightAt(x: number, z: number, planeY: number): number | null {
|
|
91
|
+
if (!isSiteGroundPlane(planeY)) return null
|
|
92
|
+
const field = siteTerrainField()
|
|
93
|
+
if (!field) return null
|
|
94
|
+
return heightAt(field, x, z)
|
|
95
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
type AnyNode,
|
|
4
|
+
type AnyNodeId,
|
|
5
|
+
BuildingNode,
|
|
6
|
+
clearSceneHistory,
|
|
7
|
+
LevelNode,
|
|
8
|
+
useScene,
|
|
9
|
+
} from '@pascal-app/core'
|
|
10
|
+
import { installHistoryCommandDelegate, runRedo, runUndo } from './history'
|
|
11
|
+
|
|
12
|
+
type RafFn = (cb: (time: number) => void) => number
|
|
13
|
+
;(globalThis as unknown as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (cb) => {
|
|
14
|
+
cb(0)
|
|
15
|
+
return 0
|
|
16
|
+
}
|
|
17
|
+
;(globalThis as unknown as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??=
|
|
18
|
+
() => {}
|
|
19
|
+
|
|
20
|
+
const BUILDING_ID = 'building_history_controller' as AnyNodeId
|
|
21
|
+
const LEVEL_ID = 'level_history_controller' as AnyNodeId
|
|
22
|
+
let disposeController = () => {}
|
|
23
|
+
|
|
24
|
+
function levelNumber(): number {
|
|
25
|
+
return (useScene.getState().nodes[LEVEL_ID] as { level: number }).level
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('editor history controller', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
disposeController()
|
|
31
|
+
disposeController = () => {}
|
|
32
|
+
const level = LevelNode.parse({
|
|
33
|
+
id: LEVEL_ID,
|
|
34
|
+
parentId: BUILDING_ID,
|
|
35
|
+
children: [],
|
|
36
|
+
level: 0,
|
|
37
|
+
})
|
|
38
|
+
const building = BuildingNode.parse({
|
|
39
|
+
id: BUILDING_ID,
|
|
40
|
+
parentId: null,
|
|
41
|
+
children: [LEVEL_ID],
|
|
42
|
+
})
|
|
43
|
+
useScene.setState({
|
|
44
|
+
nodes: { [BUILDING_ID]: building, [LEVEL_ID]: level },
|
|
45
|
+
rootNodeIds: [BUILDING_ID],
|
|
46
|
+
dirtyNodes: new Set<AnyNodeId>(),
|
|
47
|
+
collections: {},
|
|
48
|
+
materials: {},
|
|
49
|
+
readOnly: false,
|
|
50
|
+
} as never)
|
|
51
|
+
clearSceneHistory()
|
|
52
|
+
useScene.getState().updateNode(LEVEL_ID, { level: 1 } as Partial<AnyNode>)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
afterEach(() => {
|
|
56
|
+
disposeController()
|
|
57
|
+
disposeController = () => {}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('delegates undo and redo while a host delegate is installed', () => {
|
|
61
|
+
const undo = mock(() => {})
|
|
62
|
+
const redo = mock(() => {})
|
|
63
|
+
disposeController = installHistoryCommandDelegate({ undo, redo })
|
|
64
|
+
|
|
65
|
+
runUndo()
|
|
66
|
+
runRedo()
|
|
67
|
+
|
|
68
|
+
expect(undo).toHaveBeenCalledTimes(1)
|
|
69
|
+
expect(redo).toHaveBeenCalledTimes(1)
|
|
70
|
+
expect(levelNumber()).toBe(1)
|
|
71
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(1)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('falls back to standalone Zundo undo and redo when no controller is installed', () => {
|
|
75
|
+
runUndo()
|
|
76
|
+
expect(levelNumber()).toBe(0)
|
|
77
|
+
expect(useScene.temporal.getState().futureStates).toHaveLength(1)
|
|
78
|
+
|
|
79
|
+
runRedo()
|
|
80
|
+
expect(levelNumber()).toBe(1)
|
|
81
|
+
expect(useScene.temporal.getState().pastStates).toHaveLength(1)
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('an older cleanup cannot uninstall a newer controller', () => {
|
|
85
|
+
const firstUndo = mock(() => {})
|
|
86
|
+
const stopFirst = installHistoryCommandDelegate({ undo: firstUndo, redo: () => {} })
|
|
87
|
+
const secondUndo = mock(() => {})
|
|
88
|
+
disposeController = installHistoryCommandDelegate({ undo: secondUndo, redo: () => {} })
|
|
89
|
+
|
|
90
|
+
stopFirst()
|
|
91
|
+
runUndo()
|
|
92
|
+
|
|
93
|
+
expect(firstUndo).toHaveBeenCalledTimes(0)
|
|
94
|
+
expect(secondUndo).toHaveBeenCalledTimes(1)
|
|
95
|
+
})
|
|
96
|
+
})
|
package/src/lib/history.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { useLiveNodeOverrides, useLiveTransforms, useScene } from '@pascal-app/core'
|
|
2
2
|
|
|
3
|
+
export type HistoryCommandDelegate = {
|
|
4
|
+
undo: () => void
|
|
5
|
+
redo: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
let historyCommandDelegate: HistoryCommandDelegate | null = null
|
|
9
|
+
|
|
10
|
+
export function installHistoryCommandDelegate(delegate: HistoryCommandDelegate): () => void {
|
|
11
|
+
historyCommandDelegate = delegate
|
|
12
|
+
return () => {
|
|
13
|
+
if (historyCommandDelegate === delegate) historyCommandDelegate = null
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
3
17
|
function refreshSceneAfterHistoryJump() {
|
|
4
18
|
useLiveNodeOverrides.getState().clearAll()
|
|
5
19
|
useLiveTransforms.getState().clearAll()
|
|
@@ -11,11 +25,28 @@ function refreshSceneAfterHistoryJump() {
|
|
|
11
25
|
}
|
|
12
26
|
|
|
13
27
|
export function runUndo() {
|
|
28
|
+
if (historyCommandDelegate) {
|
|
29
|
+
historyCommandDelegate.undo()
|
|
30
|
+
return
|
|
31
|
+
}
|
|
14
32
|
useScene.temporal.getState().undo()
|
|
15
33
|
refreshSceneAfterHistoryJump()
|
|
16
34
|
}
|
|
17
35
|
|
|
18
36
|
export function runRedo() {
|
|
37
|
+
if (historyCommandDelegate) {
|
|
38
|
+
historyCommandDelegate.redo()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
19
41
|
useScene.temporal.getState().redo()
|
|
20
42
|
refreshSceneAfterHistoryJump()
|
|
21
43
|
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* ⌘Z / ⌘⇧Z (undo/redo). Pointer-drag sessions intercept these in the capture
|
|
47
|
+
* phase and cancel the gesture instead — mid-drag, "undo" means "abort what my
|
|
48
|
+
* mouse is doing", never a history jump under a live pointer.
|
|
49
|
+
*/
|
|
50
|
+
export function isHistoryShortcut(e: KeyboardEvent) {
|
|
51
|
+
return (e.metaKey || e.ctrlKey) && (e.key === 'z' || e.key === 'Z')
|
|
52
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
3
|
+
import {
|
|
4
|
+
type AttachClass,
|
|
5
|
+
attachClassOf,
|
|
6
|
+
type HotSetCandidate,
|
|
7
|
+
isCandidateInHotSet,
|
|
8
|
+
isPickableForAttach,
|
|
9
|
+
} from './hot-set'
|
|
10
|
+
|
|
11
|
+
const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknown as AnyNode
|
|
12
|
+
|
|
13
|
+
const floor: HotSetCandidate = {
|
|
14
|
+
type: 'level',
|
|
15
|
+
isFloorLike: true,
|
|
16
|
+
exposesTop: false,
|
|
17
|
+
attachClass: 'surface',
|
|
18
|
+
}
|
|
19
|
+
const wall: HotSetCandidate = {
|
|
20
|
+
type: 'wall',
|
|
21
|
+
isFloorLike: false,
|
|
22
|
+
exposesTop: false,
|
|
23
|
+
attachClass: 'surface',
|
|
24
|
+
}
|
|
25
|
+
const ceiling: HotSetCandidate = {
|
|
26
|
+
type: 'ceiling',
|
|
27
|
+
isFloorLike: false,
|
|
28
|
+
exposesTop: false,
|
|
29
|
+
attachClass: 'surface',
|
|
30
|
+
}
|
|
31
|
+
const table: HotSetCandidate = {
|
|
32
|
+
type: 'item',
|
|
33
|
+
isFloorLike: false,
|
|
34
|
+
exposesTop: true,
|
|
35
|
+
attachClass: 'surface',
|
|
36
|
+
}
|
|
37
|
+
const wallShelf: HotSetCandidate = {
|
|
38
|
+
type: 'shelf',
|
|
39
|
+
isFloorLike: false,
|
|
40
|
+
exposesTop: true,
|
|
41
|
+
attachClass: 'wall',
|
|
42
|
+
}
|
|
43
|
+
const ceilingFan: HotSetCandidate = {
|
|
44
|
+
type: 'item',
|
|
45
|
+
isFloorLike: false,
|
|
46
|
+
exposesTop: true,
|
|
47
|
+
attachClass: 'ceiling',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('attachClassOf', () => {
|
|
51
|
+
test('wall and wall-side collapse to wall', () => {
|
|
52
|
+
expect(attachClassOf('wall')).toBe('wall')
|
|
53
|
+
expect(attachClassOf('wall-side')).toBe('wall')
|
|
54
|
+
})
|
|
55
|
+
test('ceiling maps to ceiling', () => {
|
|
56
|
+
expect(attachClassOf('ceiling')).toBe('ceiling')
|
|
57
|
+
})
|
|
58
|
+
test('undefined/null/unknown is surface-resting', () => {
|
|
59
|
+
expect(attachClassOf(undefined)).toBe('surface')
|
|
60
|
+
expect(attachClassOf(null)).toBe('surface')
|
|
61
|
+
expect(attachClassOf('')).toBe('surface')
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('isPickableForAttach — wall-mounted (window)', () => {
|
|
66
|
+
test('only walls are eligible; floor/ceiling/tops are not', () => {
|
|
67
|
+
expect(isPickableForAttach('wall', wall)).toBe(true)
|
|
68
|
+
expect(isPickableForAttach('wall', floor)).toBe(false)
|
|
69
|
+
expect(isPickableForAttach('wall', ceiling)).toBe(false)
|
|
70
|
+
expect(isPickableForAttach('wall', table)).toBe(false)
|
|
71
|
+
expect(isPickableForAttach('wall', wallShelf)).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe('isPickableForAttach — ceiling-mounted', () => {
|
|
76
|
+
test('only ceilings are eligible', () => {
|
|
77
|
+
expect(isPickableForAttach('ceiling', ceiling)).toBe(true)
|
|
78
|
+
expect(isPickableForAttach('ceiling', wall)).toBe(false)
|
|
79
|
+
expect(isPickableForAttach('ceiling', floor)).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('isPickableForAttach — surface-resting (sofa / cactus)', () => {
|
|
84
|
+
test('floor is always eligible', () => {
|
|
85
|
+
expect(isPickableForAttach('surface', floor)).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
test('host tops (table, wall-shelf top) are eligible', () => {
|
|
88
|
+
expect(isPickableForAttach('surface', table)).toBe(true)
|
|
89
|
+
expect(isPickableForAttach('surface', wallShelf)).toBe(true)
|
|
90
|
+
})
|
|
91
|
+
test('a wall (no top surface) is not eligible', () => {
|
|
92
|
+
expect(isPickableForAttach('surface', wall)).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
test('a ceiling-mounted host (ceiling fan) is never eligible — Track E', () => {
|
|
95
|
+
expect(isPickableForAttach('surface', ceilingFan)).toBe(false)
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe('isCandidateInHotSet — by scope', () => {
|
|
100
|
+
const surfaceClass: AttachClass = 'surface'
|
|
101
|
+
test('idle: everything is in the hot-set (selection filtering lives elsewhere)', () => {
|
|
102
|
+
expect(isCandidateInHotSet({ kind: 'idle' }, null, ceilingFan)).toBe(true)
|
|
103
|
+
})
|
|
104
|
+
test('placing a surface item: derives from attach class', () => {
|
|
105
|
+
const scope = {
|
|
106
|
+
kind: 'placing' as const,
|
|
107
|
+
node: mockNode('i1', 'item'),
|
|
108
|
+
nodeId: 'i1',
|
|
109
|
+
nodeType: 'item',
|
|
110
|
+
view: '3d' as const,
|
|
111
|
+
pressDrag: false,
|
|
112
|
+
}
|
|
113
|
+
expect(isCandidateInHotSet(scope, surfaceClass, floor)).toBe(true)
|
|
114
|
+
expect(isCandidateInHotSet(scope, surfaceClass, ceilingFan)).toBe(false)
|
|
115
|
+
})
|
|
116
|
+
test('moving a wall-mounted item: only walls', () => {
|
|
117
|
+
const scope = {
|
|
118
|
+
kind: 'moving' as const,
|
|
119
|
+
node: mockNode('w1', 'window'),
|
|
120
|
+
nodeId: 'w1',
|
|
121
|
+
nodeType: 'window',
|
|
122
|
+
view: '2d' as const,
|
|
123
|
+
}
|
|
124
|
+
expect(isCandidateInHotSet(scope, 'wall', wall)).toBe(true)
|
|
125
|
+
expect(isCandidateInHotSet(scope, 'wall', table)).toBe(false)
|
|
126
|
+
})
|
|
127
|
+
test('non-placement active scopes target nothing in the scene', () => {
|
|
128
|
+
expect(isCandidateInHotSet({ kind: 'box-select' }, null, floor)).toBe(false)
|
|
129
|
+
expect(
|
|
130
|
+
isCandidateInHotSet({ kind: 'handle-drag', nodeId: 'x', handle: 'h' }, null, floor),
|
|
131
|
+
).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// The hot-set: which scene objects are raycast-eligible during an interaction.
|
|
2
|
+
//
|
|
3
|
+
// It is never hand-authored per interaction. It falls out of the node's
|
|
4
|
+
// `asset.attachTo` plus whether a candidate exposes a top surface. "Floor item"
|
|
5
|
+
// really means surface-resting: it rests on the floor *or* any host's top
|
|
6
|
+
// surface. Walls and ceilings are the special attach modes. Adding a node kind
|
|
7
|
+
// = set `attachTo` (or leave blank); the hot-set follows with zero per-kind
|
|
8
|
+
// wiring.
|
|
9
|
+
|
|
10
|
+
import type { InteractionScope } from './scope'
|
|
11
|
+
|
|
12
|
+
// What a node attaches to, collapsed to the three classes the hot-set cares
|
|
13
|
+
// about. `wall-side` is a wall attachment; everything without an explicit
|
|
14
|
+
// `attachTo` is surface-resting.
|
|
15
|
+
export type AttachClass = 'wall' | 'ceiling' | 'surface'
|
|
16
|
+
|
|
17
|
+
export function attachClassOf(attachTo: string | undefined | null): AttachClass {
|
|
18
|
+
if (attachTo === 'wall' || attachTo === 'wall-side') return 'wall'
|
|
19
|
+
if (attachTo === 'ceiling') return 'ceiling'
|
|
20
|
+
return 'surface'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The metadata the hot-set needs about a candidate host/surface. Derived from
|
|
24
|
+
// the candidate node + its registry definition by the caller, so this module
|
|
25
|
+
// stays pure and unit-testable without the scene or registry.
|
|
26
|
+
export type HotSetCandidate = {
|
|
27
|
+
type: string
|
|
28
|
+
// The level floor plane / ground a surface-resting node can always rest on.
|
|
29
|
+
isFloorLike: boolean
|
|
30
|
+
// The candidate exposes a usable top surface (registry
|
|
31
|
+
// `capabilities.surfaces.top`) — a table, a shelf, a slab.
|
|
32
|
+
exposesTop: boolean
|
|
33
|
+
// The candidate's own attach class. A ceiling fan is `ceiling`: it hangs from
|
|
34
|
+
// the ceiling and must never act as a host top (Track E).
|
|
35
|
+
attachClass: AttachClass
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// For a node whose attach class is `placed`, is `candidate` a valid
|
|
39
|
+
// host/surface to pick during placement or move?
|
|
40
|
+
export function isPickableForAttach(placed: AttachClass, candidate: HotSetCandidate): boolean {
|
|
41
|
+
if (placed === 'wall') return candidate.type === 'wall'
|
|
42
|
+
if (placed === 'ceiling') return candidate.type === 'ceiling'
|
|
43
|
+
// Surface-resting: the floor, or any host that exposes a top surface — but
|
|
44
|
+
// never a ceiling-mounted host (a floor lamp must not land on a ceiling fan).
|
|
45
|
+
if (candidate.isFloorLike) return true
|
|
46
|
+
if (!candidate.exposesTop) return false
|
|
47
|
+
if (candidate.attachClass === 'ceiling') return false
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The hot-set predicate for a whole scope. For placing/moving it derives from
|
|
52
|
+
// the moving node's attach class; for every other active scope nothing in the
|
|
53
|
+
// scene is a placement target, so the body's own raycast owns the pointer.
|
|
54
|
+
// `idle` returns true here — selection/phase filtering stays in the selection
|
|
55
|
+
// manager; this only narrows what an *active* interaction can target.
|
|
56
|
+
export function isCandidateInHotSet(
|
|
57
|
+
scope: InteractionScope,
|
|
58
|
+
placedAttachClass: AttachClass | null,
|
|
59
|
+
candidate: HotSetCandidate,
|
|
60
|
+
): boolean {
|
|
61
|
+
if (scope.kind === 'idle') return true
|
|
62
|
+
if (scope.kind === 'placing' || scope.kind === 'moving') {
|
|
63
|
+
if (placedAttachClass === null) return true
|
|
64
|
+
return isPickableForAttach(placedAttachClass, candidate)
|
|
65
|
+
}
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import type { AnyNode } from '@pascal-app/core'
|
|
3
|
+
import { resolveOverlayPolicy } from './overlay-policy'
|
|
4
|
+
import type { ActiveInteractionScope } from './scope'
|
|
5
|
+
|
|
6
|
+
const mockNode = (id: string, type: string): AnyNode => ({ id, type }) as unknown as AnyNode
|
|
7
|
+
|
|
8
|
+
const ACTIVE_SCOPES: ActiveInteractionScope[] = [
|
|
9
|
+
{
|
|
10
|
+
kind: 'placing',
|
|
11
|
+
node: mockNode('i1', 'item'),
|
|
12
|
+
nodeId: 'i1',
|
|
13
|
+
nodeType: 'item',
|
|
14
|
+
view: '3d',
|
|
15
|
+
pressDrag: false,
|
|
16
|
+
},
|
|
17
|
+
{ kind: 'moving', node: mockNode('i1', 'item'), nodeId: 'i1', nodeType: 'item', view: '2d' },
|
|
18
|
+
{ kind: 'handle-drag', nodeId: 'w1', handle: 'height' },
|
|
19
|
+
{ kind: 'drafting', tool: 'wall' },
|
|
20
|
+
{ kind: 'reshaping', nodeId: 's1', reshape: 'hole', driver: 'tool', holeIndex: 0 },
|
|
21
|
+
{ kind: 'box-select' },
|
|
22
|
+
{ kind: 'painting' },
|
|
23
|
+
{ kind: 'sculpting' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
describe('resolveOverlayPolicy', () => {
|
|
27
|
+
test('idle keeps everything shown and pickable', () => {
|
|
28
|
+
const p = resolveOverlayPolicy({ kind: 'idle' })
|
|
29
|
+
expect(p.zoneLabels).toBe('shown')
|
|
30
|
+
expect(p.contextBadges).toBe('shown')
|
|
31
|
+
expect(p.conflictingControls).toBe('shown')
|
|
32
|
+
expect(p.sceneObjectsPickable).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('every active scope hides zone labels, fades badges, hides conflicting controls', () => {
|
|
36
|
+
for (const scope of ACTIVE_SCOPES) {
|
|
37
|
+
const p = resolveOverlayPolicy(scope)
|
|
38
|
+
expect(p.zoneLabels).toBe('hidden')
|
|
39
|
+
expect(p.contextBadges).toBe('faded')
|
|
40
|
+
expect(p.conflictingControls).toBe('hidden')
|
|
41
|
+
expect(p.sceneObjectsPickable).toBe(false)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('active affordances and the contextual HUD always stay interactive', () => {
|
|
46
|
+
for (const scope of [{ kind: 'idle' } as const, ...ACTIVE_SCOPES]) {
|
|
47
|
+
const p = resolveOverlayPolicy(scope)
|
|
48
|
+
expect(p.activeAffordances).toBe('shown')
|
|
49
|
+
expect(p.contextualHudInteractive).toBe(true)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The overlay scope matrix — the "Sims-light" feel. During any non-idle
|
|
2
|
+
// interaction, two layers behave differently:
|
|
3
|
+
//
|
|
4
|
+
// - 3D scene objects stay VISIBLE but become NON-pickable (the hot-set owns
|
|
5
|
+
// what the active interaction can target). Context is preserved; you just
|
|
6
|
+
// can't grab the wrong thing.
|
|
7
|
+
// - DOM/HUD overlays step back, differentiated by how distracting they are:
|
|
8
|
+
// zone labels -> hidden (not a primary editing concern)
|
|
9
|
+
// context badges -> faded + pointer-events:none (hover name pills)
|
|
10
|
+
// other controls -> hard-hidden (other objects' handles, the floating
|
|
11
|
+
// action menu, conflicting controls)
|
|
12
|
+
//
|
|
13
|
+
// The active interaction's own affordances (ghost, snap guides, dimension
|
|
14
|
+
// labels, the active handle) always stay — "default-off, opt-in for the active
|
|
15
|
+
// action". The contextual control HUD is exempt from the pointer-events
|
|
16
|
+
// step-back because it *is* the active interaction's own controls.
|
|
17
|
+
|
|
18
|
+
import { type InteractionScope, isActive } from './scope'
|
|
19
|
+
|
|
20
|
+
export type OverlayVisibility = 'shown' | 'faded' | 'hidden'
|
|
21
|
+
|
|
22
|
+
export type OverlayPolicy = {
|
|
23
|
+
zoneLabels: OverlayVisibility
|
|
24
|
+
// Hover name pills / context badges.
|
|
25
|
+
contextBadges: OverlayVisibility
|
|
26
|
+
// Other objects' handles + the floating action menu — anything whose action
|
|
27
|
+
// would conflict with the active interaction.
|
|
28
|
+
conflictingControls: OverlayVisibility
|
|
29
|
+
// Non-active scene objects: visible always, pickable only when idle.
|
|
30
|
+
sceneObjectsPickable: boolean
|
|
31
|
+
// The active interaction's own ghost/guides/dimension labels/handle. Always
|
|
32
|
+
// shown; this field exists so consumers can assert the contract.
|
|
33
|
+
activeAffordances: 'shown'
|
|
34
|
+
// The contextual control HUD keeps pointer events even while everything else
|
|
35
|
+
// steps back, because it is the active interaction's own controls.
|
|
36
|
+
contextualHudInteractive: boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const IDLE_POLICY: OverlayPolicy = {
|
|
40
|
+
zoneLabels: 'shown',
|
|
41
|
+
contextBadges: 'shown',
|
|
42
|
+
conflictingControls: 'shown',
|
|
43
|
+
sceneObjectsPickable: true,
|
|
44
|
+
activeAffordances: 'shown',
|
|
45
|
+
contextualHudInteractive: true,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ACTIVE_POLICY: OverlayPolicy = {
|
|
49
|
+
zoneLabels: 'hidden',
|
|
50
|
+
contextBadges: 'faded',
|
|
51
|
+
conflictingControls: 'hidden',
|
|
52
|
+
sceneObjectsPickable: false,
|
|
53
|
+
activeAffordances: 'shown',
|
|
54
|
+
contextualHudInteractive: true,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveOverlayPolicy(scope: InteractionScope): OverlayPolicy {
|
|
58
|
+
return isActive(scope) ? ACTIVE_POLICY : IDLE_POLICY
|
|
59
|
+
}
|