@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,38 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type AnyNodeId, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { type ComponentType, Suspense, useMemo } from 'react'
|
|
6
|
+
import { getRegistryAffordanceTool } from '../tools/shared/affordance-dispatch'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Editor-mounted dispatcher for a kind's selection-time editing UI.
|
|
10
|
+
*
|
|
11
|
+
* Some kinds expose drag-to-edit affordances that should appear only
|
|
12
|
+
* while a single node of that kind is selected — duct / pipe / lineset
|
|
13
|
+
* path-point handles, fitting Alt-axis-cycling listeners. These read
|
|
14
|
+
* `useEditor` (grid snap step, rotation axis) and render the editor's
|
|
15
|
+
* `DimensionPill`, so they must NOT ride in `def.system` (which the
|
|
16
|
+
* viewer package mounts for the read-only route). The kind declares the
|
|
17
|
+
* component under `def.affordanceTools.selection` and this manager —
|
|
18
|
+
* mounted inside the editor only — loads it for the selected kind.
|
|
19
|
+
*/
|
|
20
|
+
export function SelectionAffordanceManager() {
|
|
21
|
+
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
22
|
+
const selectedKind = useScene((s) => {
|
|
23
|
+
if (selectedIds.length !== 1) return null
|
|
24
|
+
return s.nodes[selectedIds[0] as AnyNodeId]?.type ?? null
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const Component = useMemo<ComponentType | null>(() => {
|
|
28
|
+
if (!selectedKind) return null
|
|
29
|
+
return getRegistryAffordanceTool(selectedKind, 'selection')
|
|
30
|
+
}, [selectedKind])
|
|
31
|
+
|
|
32
|
+
if (!Component) return null
|
|
33
|
+
return (
|
|
34
|
+
<Suspense fallback={null}>
|
|
35
|
+
<Component />
|
|
36
|
+
</Suspense>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -6,8 +6,10 @@ import { Check, Pencil } from 'lucide-react'
|
|
|
6
6
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
7
7
|
import { createPortal } from 'react-dom'
|
|
8
8
|
import { useShallow } from 'zustand/react/shallow'
|
|
9
|
+
import { resolveOverlayPolicy } from '../../../lib/interaction/overlay-policy'
|
|
9
10
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
10
11
|
import useEditor from '../../../store/use-editor'
|
|
12
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
11
13
|
|
|
12
14
|
// ─── Per-zone label editor ────────────────────────────────────────────────────
|
|
13
15
|
|
|
@@ -19,6 +21,10 @@ function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
|
|
|
19
21
|
const selectedZoneId = useViewer((s) => s.selection.zoneId)
|
|
20
22
|
const hoveredId = useViewer((s) => s.hoveredId)
|
|
21
23
|
const mode = useEditor((s) => s.mode)
|
|
24
|
+
// During an active interaction the zone label is a context badge that steps
|
|
25
|
+
// back: faded + non-interactive so it can't be hovered/clicked mid-action.
|
|
26
|
+
const scope = useInteractionScope((s) => s.scope)
|
|
27
|
+
const labelStepBack = resolveOverlayPolicy(scope).contextBadges === 'faded'
|
|
22
28
|
const isSelected = selectedZoneId === zoneId
|
|
23
29
|
const isDeleteHovered = mode === 'delete' && hoveredId === zoneId
|
|
24
30
|
const [editing, setEditing] = useState(false)
|
|
@@ -149,7 +155,8 @@ function ZoneLabelEditor({ zoneId }: { zoneId: ZoneNode['id'] }) {
|
|
|
149
155
|
fontSize: 14,
|
|
150
156
|
fontFamily: 'sans-serif',
|
|
151
157
|
userSelect: 'none',
|
|
152
|
-
pointerEvents: 'auto',
|
|
158
|
+
pointerEvents: labelStepBack ? 'none' : 'auto',
|
|
159
|
+
opacity: labelStepBack ? 0.4 : undefined,
|
|
153
160
|
display: 'inline-flex',
|
|
154
161
|
alignItems: 'center',
|
|
155
162
|
gap: 4,
|
|
@@ -309,6 +316,12 @@ export function ZoneLabelEditorSystem() {
|
|
|
309
316
|
.map((n) => n.id as ZoneNode['id']),
|
|
310
317
|
),
|
|
311
318
|
)
|
|
319
|
+
// The zone renderer unmounts its <Html> label when zones are hidden —
|
|
320
|
+
// unmount the portals with it, or each editor would hold (and rAF-poll for)
|
|
321
|
+
// a detached label element.
|
|
322
|
+
const showZones = useViewer((s) => s.showZones)
|
|
323
|
+
|
|
324
|
+
if (!showZones) return null
|
|
312
325
|
|
|
313
326
|
return (
|
|
314
327
|
<>
|
|
@@ -1,21 +1,46 @@
|
|
|
1
1
|
import { sceneRegistry, useScene, type ZoneNode } from '@pascal-app/core'
|
|
2
2
|
import { useViewer } from '@pascal-app/viewer'
|
|
3
3
|
import { useFrame } from '@react-three/fiber'
|
|
4
|
+
import { useEffect } from 'react'
|
|
4
5
|
import { type Group, MathUtils, type Mesh } from 'three'
|
|
5
6
|
import type { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
7
|
+
import { resolveOverlayPolicy } from '../../../lib/interaction/overlay-policy'
|
|
6
8
|
import useEditor from '../../../store/use-editor'
|
|
9
|
+
import useInteractionScope from '../../../store/use-interaction-scope'
|
|
7
10
|
|
|
8
11
|
// Disable raycasting on zone geometry so clicks pass through to items underneath.
|
|
9
12
|
// Zone selection in the editor is handled exclusively via the HTML label overlay.
|
|
10
13
|
const noopRaycast = () => {}
|
|
11
14
|
|
|
12
15
|
export const ZoneSystem = () => {
|
|
16
|
+
// Outside the zones layer (or during snapshot capture) zones unmount
|
|
17
|
+
// entirely — meshes AND drei <Html> labels, which cost per-frame matrix work
|
|
18
|
+
// + live DOM even at opacity 0. The renderer reads this viewer flag; the
|
|
19
|
+
// unmount cleanup restores the default so preview / first-person surfaces
|
|
20
|
+
// (which swap this system for ViewerZoneSystem) keep their labels.
|
|
21
|
+
const structureLayerState = useEditor((s) => s.structureLayer)
|
|
22
|
+
const isCaptureModeState = useEditor((s) => s.isCaptureMode)
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
useViewer.getState().setShowZones(structureLayerState === 'zones' && !isCaptureModeState)
|
|
25
|
+
return () => useViewer.getState().setShowZones(true)
|
|
26
|
+
}, [structureLayerState, isCaptureModeState])
|
|
27
|
+
|
|
13
28
|
useFrame((_, delta) => {
|
|
29
|
+
if (!useViewer.getState().showZones) return
|
|
30
|
+
|
|
14
31
|
const structureLayer = useEditor.getState().structureLayer
|
|
15
32
|
const editorMode = useEditor.getState().mode
|
|
16
33
|
const selectedLevelId = useViewer.getState().selection.levelId
|
|
17
34
|
const selectedZoneId = useViewer.getState().selection.zoneId
|
|
18
35
|
const hoveredId = useViewer.getState().hoveredId
|
|
36
|
+
// Snapshot capture is a clean, camera-only surface — never show zone
|
|
37
|
+
// geometry or the HTML zone tags in the framed shot.
|
|
38
|
+
const isCaptureMode = useEditor.getState().isCaptureMode
|
|
39
|
+
|
|
40
|
+
// During any active interaction zone labels step back entirely — they are
|
|
41
|
+
// not a primary editing concern and would distract / invite misclicks.
|
|
42
|
+
const zoneLabelsHidden =
|
|
43
|
+
resolveOverlayPolicy(useInteractionScope.getState().scope).zoneLabels === 'hidden'
|
|
19
44
|
|
|
20
45
|
const zoneGeometryVisible = structureLayer === 'zones'
|
|
21
46
|
const zones = sceneRegistry.byType.zone || new Set()
|
|
@@ -35,12 +60,22 @@ export const ZoneSystem = () => {
|
|
|
35
60
|
// Keep group visible (so <Html> labels stay active), hide/show meshes only.
|
|
36
61
|
// Show meshes when: in zone mode, selected, or delete-hovered.
|
|
37
62
|
if (!obj.visible) obj.visible = true
|
|
38
|
-
const meshVisible = zoneGeometryVisible || isSelected || isDeleteHovered
|
|
39
|
-
const targetOpacity =
|
|
63
|
+
const meshVisible = !isCaptureMode && (zoneGeometryVisible || isSelected || isDeleteHovered)
|
|
64
|
+
const targetOpacity = isCaptureMode
|
|
65
|
+
? 0
|
|
66
|
+
: isSelected || isDeleteHovered
|
|
67
|
+
? 1
|
|
68
|
+
: zoneGeometryVisible
|
|
69
|
+
? 1
|
|
70
|
+
: 0
|
|
40
71
|
|
|
72
|
+
// Raycast is re-disabled per frame (not once per group): the meshes
|
|
73
|
+
// remount whenever the zones layer toggles, so a one-shot flag on the
|
|
74
|
+
// persistent group would leave fresh meshes clickable.
|
|
41
75
|
const walls = (obj as Group).getObjectByName('walls') as Mesh | undefined
|
|
42
76
|
if (walls) {
|
|
43
77
|
walls.visible = meshVisible
|
|
78
|
+
walls.raycast = noopRaycast
|
|
44
79
|
const material = walls.material as MeshBasicNodeMaterial
|
|
45
80
|
if (material?.userData?.uOpacity) {
|
|
46
81
|
material.userData.uOpacity.value = MathUtils.lerp(
|
|
@@ -54,6 +89,7 @@ export const ZoneSystem = () => {
|
|
|
54
89
|
const floor = (obj as Group).getObjectByName('floor') as Mesh | undefined
|
|
55
90
|
if (floor) {
|
|
56
91
|
floor.visible = meshVisible
|
|
92
|
+
floor.raycast = noopRaycast
|
|
57
93
|
const material = floor.material as MeshBasicNodeMaterial
|
|
58
94
|
if (material?.userData?.uOpacity) {
|
|
59
95
|
material.userData.uOpacity.value = MathUtils.lerp(
|
|
@@ -64,17 +100,10 @@ export const ZoneSystem = () => {
|
|
|
64
100
|
}
|
|
65
101
|
}
|
|
66
102
|
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
child.raycast = noopRaycast
|
|
72
|
-
})
|
|
73
|
-
obj.userData.__raycastDisabled = true
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Labels: always visible on the current level (regardless of mode)
|
|
77
|
-
const showLabel = !!selectedLevelId && isOnSelectedLevel
|
|
103
|
+
// Labels: visible on the current level (regardless of mode), but never
|
|
104
|
+
// during snapshot capture.
|
|
105
|
+
const showLabel =
|
|
106
|
+
!isCaptureMode && !zoneLabelsHidden && !!selectedLevelId && isOnSelectedLevel
|
|
78
107
|
const labelOpacity = showLabel ? '1' : '0'
|
|
79
108
|
const labelEl = document.getElementById(`${zoneId}-label`)
|
|
80
109
|
if (labelEl && labelEl.style.opacity !== labelOpacity) {
|
|
@@ -9,11 +9,18 @@ import {
|
|
|
9
9
|
resolveAlignment,
|
|
10
10
|
useScene,
|
|
11
11
|
} from '@pascal-app/core'
|
|
12
|
-
import { useAlignmentGuides } from '@pascal-app/editor'
|
|
13
12
|
import { useEffect, useMemo, useRef } from 'react'
|
|
14
13
|
import * as THREE from 'three'
|
|
15
14
|
import { resolveCurrentBuildingId, resolveElevatorSupportY } from '../../../lib/elevator-support'
|
|
16
15
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
16
|
+
|
|
17
|
+
import useAlignmentGuides from '../../../store/use-alignment-guides'
|
|
18
|
+
import useEditor, {
|
|
19
|
+
isAlignmentGuideActive,
|
|
20
|
+
isGridSnapActive,
|
|
21
|
+
isMagneticSnapActive,
|
|
22
|
+
} from '../../../store/use-editor'
|
|
23
|
+
|
|
17
24
|
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
18
25
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
19
26
|
import {
|
|
@@ -163,13 +170,16 @@ export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId,
|
|
|
163
170
|
// point: resolving against the grid point would only ever catch anchors
|
|
164
171
|
// that happen to sit on a grid line, so off-grid items (furniture, angled
|
|
165
172
|
// walls) would never surface a guide. The matched axis locks exactly to the
|
|
166
|
-
// candidate's coordinate; the other axis keeps its grid snap.
|
|
173
|
+
// candidate's coordinate; the other axis keeps its grid snap. Guides are
|
|
174
|
+
// published in every snapping mode (including Off); the magnetic pull toward
|
|
175
|
+
// them (applySnap) applies only in 'lines' mode.
|
|
167
176
|
const alignPoint = (
|
|
168
177
|
gridX: number,
|
|
169
178
|
gridZ: number,
|
|
170
179
|
rawX: number,
|
|
171
180
|
rawZ: number,
|
|
172
181
|
bypass: boolean,
|
|
182
|
+
applySnap: boolean,
|
|
173
183
|
): [number, number] => {
|
|
174
184
|
if (bypass || alignmentCandidates.length === 0) {
|
|
175
185
|
useAlignmentGuides.getState().clear()
|
|
@@ -185,6 +195,7 @@ export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId,
|
|
|
185
195
|
return [gridX, gridZ]
|
|
186
196
|
}
|
|
187
197
|
useAlignmentGuides.getState().set(ar.guides)
|
|
198
|
+
if (!applySnap) return [gridX, gridZ]
|
|
188
199
|
let x = gridX
|
|
189
200
|
let z = gridZ
|
|
190
201
|
for (const guide of ar.guides) {
|
|
@@ -195,12 +206,20 @@ export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId,
|
|
|
195
206
|
}
|
|
196
207
|
|
|
197
208
|
const onGridMove = (event: GridEvent) => {
|
|
209
|
+
// Grid snap follows the global mode (live step so the HUD chip is
|
|
210
|
+
// honest); Off keeps the raw cursor. Shift cycles the mode centrally.
|
|
211
|
+
const step = useEditor.getState().gridSnapStep
|
|
198
212
|
const [gridX, gridZ] = alignPoint(
|
|
199
|
-
|
|
200
|
-
|
|
213
|
+
isGridSnapActive()
|
|
214
|
+
? Math.round(event.localPosition[0] / step) * step
|
|
215
|
+
: event.localPosition[0],
|
|
216
|
+
isGridSnapActive()
|
|
217
|
+
? Math.round(event.localPosition[2] / step) * step
|
|
218
|
+
: event.localPosition[2],
|
|
201
219
|
event.localPosition[0],
|
|
202
220
|
event.localPosition[2],
|
|
203
|
-
|
|
221
|
+
!isAlignmentGuideActive(),
|
|
222
|
+
isMagneticSnapActive(),
|
|
204
223
|
)
|
|
205
224
|
const supportY = resolveElevatorSupportY({
|
|
206
225
|
buildingId: currentBuildingId,
|
|
@@ -220,6 +239,7 @@ export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId,
|
|
|
220
239
|
})
|
|
221
240
|
|
|
222
241
|
if (
|
|
242
|
+
(isGridSnapActive() || isMagneticSnapActive()) &&
|
|
223
243
|
previousGridPosRef.current &&
|
|
224
244
|
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
225
245
|
) {
|
|
@@ -237,12 +257,18 @@ export const ElevatorTool: React.FC<ElevatorToolProps> = ({ buildingId, levelId,
|
|
|
237
257
|
})
|
|
238
258
|
if (!latestBuildingId) return
|
|
239
259
|
|
|
260
|
+
const step = useEditor.getState().gridSnapStep
|
|
240
261
|
const [gridX, gridZ] = alignPoint(
|
|
241
|
-
|
|
242
|
-
|
|
262
|
+
isGridSnapActive()
|
|
263
|
+
? Math.round(event.localPosition[0] / step) * step
|
|
264
|
+
: event.localPosition[0],
|
|
265
|
+
isGridSnapActive()
|
|
266
|
+
? Math.round(event.localPosition[2] / step) * step
|
|
267
|
+
: event.localPosition[2],
|
|
243
268
|
event.localPosition[0],
|
|
244
269
|
event.localPosition[2],
|
|
245
|
-
|
|
270
|
+
!isAlignmentGuideActive(),
|
|
271
|
+
isMagneticSnapActive(),
|
|
246
272
|
)
|
|
247
273
|
commitElevatorPlacement(
|
|
248
274
|
latestBuildingId,
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
16
16
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
17
17
|
import { resolveElevatorSupportY } from '../../../lib/elevator-support'
|
|
18
|
+
import { consumePlacementDragRelease } from '../../../lib/placement-drag-release'
|
|
18
19
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
19
20
|
import useEditor from '../../../store/use-editor'
|
|
20
21
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
@@ -131,8 +132,9 @@ export function MoveElevatorTool({
|
|
|
131
132
|
}
|
|
132
133
|
|
|
133
134
|
const onGridMove = (event: GridEvent) => {
|
|
134
|
-
const
|
|
135
|
-
const
|
|
135
|
+
const bypassSnap = event.nativeEvent?.shiftKey === true
|
|
136
|
+
const rawX = bypassSnap ? event.localPosition[0] : Math.round(event.localPosition[0] * 2) / 2
|
|
137
|
+
const rawZ = bypassSnap ? event.localPosition[2] : Math.round(event.localPosition[2] * 2) / 2
|
|
136
138
|
const anchor = dragAnchorRef.current ?? [rawX, rawZ]
|
|
137
139
|
dragAnchorRef.current = anchor
|
|
138
140
|
const gridX = movingNode.position[0] + (rawX - anchor[0])
|
|
@@ -145,6 +147,7 @@ export function MoveElevatorTool({
|
|
|
145
147
|
})
|
|
146
148
|
|
|
147
149
|
if (
|
|
150
|
+
!bypassSnap &&
|
|
148
151
|
previousGridPosRef.current &&
|
|
149
152
|
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
150
153
|
) {
|
|
@@ -158,6 +161,7 @@ export function MoveElevatorTool({
|
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
const onGridClick = (event: GridEvent) => {
|
|
164
|
+
if (wasCommitted) return
|
|
161
165
|
const nextPosition: ElevatorNode['position'] = [...previewPositionRef.current]
|
|
162
166
|
|
|
163
167
|
wasCommitted = true
|
|
@@ -187,6 +191,11 @@ export function MoveElevatorTool({
|
|
|
187
191
|
event.nativeEvent?.stopPropagation?.()
|
|
188
192
|
}
|
|
189
193
|
|
|
194
|
+
const onPlacementDragPointerUp = (event: PointerEvent) => {
|
|
195
|
+
if (!consumePlacementDragRelease(event)) return
|
|
196
|
+
onGridClick({ nativeEvent: event } as unknown as GridEvent)
|
|
197
|
+
}
|
|
198
|
+
|
|
190
199
|
const onCancel = () => {
|
|
191
200
|
wasCancelled = true
|
|
192
201
|
clearPreview()
|
|
@@ -229,6 +238,7 @@ export function MoveElevatorTool({
|
|
|
229
238
|
emitter.on('grid:click', onGridClick)
|
|
230
239
|
emitter.on('tool:cancel', onCancel)
|
|
231
240
|
window.addEventListener('keydown', onKeyDown)
|
|
241
|
+
window.addEventListener('pointerup', onPlacementDragPointerUp)
|
|
232
242
|
|
|
233
243
|
return () => {
|
|
234
244
|
clearPreview()
|
|
@@ -245,6 +255,7 @@ export function MoveElevatorTool({
|
|
|
245
255
|
emitter.off('grid:click', onGridClick)
|
|
246
256
|
emitter.off('tool:cancel', onCancel)
|
|
247
257
|
window.removeEventListener('keydown', onKeyDown)
|
|
258
|
+
window.removeEventListener('pointerup', onPlacementDragPointerUp)
|
|
248
259
|
}
|
|
249
260
|
}, [movingNode, exitMoveMode])
|
|
250
261
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
+
DEFAULT_ANGLE_STEP,
|
|
2
3
|
FenceNode,
|
|
4
|
+
getTwoPointFenceCurveTangents,
|
|
3
5
|
getWallCurveFrameAt,
|
|
4
6
|
getWallCurveLength,
|
|
5
7
|
isCurvedWall,
|
|
8
|
+
resolveFenceSupportSlabPatch,
|
|
9
|
+
snapPointAlongAngleRay,
|
|
6
10
|
useScene,
|
|
7
11
|
type WallNode,
|
|
8
12
|
} from '@pascal-app/core'
|
|
@@ -12,9 +16,7 @@ import useEditor from '../../../store/use-editor'
|
|
|
12
16
|
import {
|
|
13
17
|
findWallSnapTarget,
|
|
14
18
|
getSegmentGridStep,
|
|
15
|
-
getWallAngleSnapStep,
|
|
16
19
|
isSegmentLongEnough,
|
|
17
|
-
snapPointTo45Degrees,
|
|
18
20
|
snapPointToGrid,
|
|
19
21
|
type WallPlanPoint,
|
|
20
22
|
} from '../wall/wall-drafting'
|
|
@@ -132,7 +134,9 @@ export function snapFenceDraftPoint(args: {
|
|
|
132
134
|
start?: FencePlanPoint
|
|
133
135
|
angleSnap?: boolean
|
|
134
136
|
ignoreFenceIds?: string[]
|
|
135
|
-
|
|
137
|
+
bypassSnap?: boolean
|
|
138
|
+
magnetic?: boolean
|
|
139
|
+
/** Override the grid step. */
|
|
136
140
|
step?: number
|
|
137
141
|
/**
|
|
138
142
|
* Optional grid-snap function. When provided, replaces the default
|
|
@@ -142,23 +146,64 @@ export function snapFenceDraftPoint(args: {
|
|
|
142
146
|
*/
|
|
143
147
|
gridSnap?: (point: FencePlanPoint) => FencePlanPoint
|
|
144
148
|
}): FencePlanPoint {
|
|
145
|
-
const {
|
|
149
|
+
const {
|
|
150
|
+
point,
|
|
151
|
+
walls,
|
|
152
|
+
fences,
|
|
153
|
+
start,
|
|
154
|
+
angleSnap = false,
|
|
155
|
+
ignoreFenceIds,
|
|
156
|
+
bypassSnap = false,
|
|
157
|
+
magnetic = true,
|
|
158
|
+
step,
|
|
159
|
+
gridSnap,
|
|
160
|
+
} = args
|
|
161
|
+
if (bypassSnap) return point
|
|
162
|
+
|
|
146
163
|
const gridStep = step ?? getSegmentGridStep()
|
|
147
|
-
|
|
148
|
-
|
|
164
|
+
|
|
165
|
+
// Magnetic endpoint snap must beat the angle lock, and the lock can pull
|
|
166
|
+
// the cursor far enough off an endpoint that probing the locked point
|
|
167
|
+
// would never engage — so under the lock, probe from the RAW cursor
|
|
168
|
+
// first (mirrors `snapWallDraftPointDetailed`'s special-point pre-pass).
|
|
169
|
+
if (start && angleSnap) {
|
|
170
|
+
const rawTarget =
|
|
171
|
+
magnetic &&
|
|
172
|
+
(findFenceSnapTarget(point, fences, ignoreFenceIds) ?? findWallSnapTarget(point, walls))
|
|
173
|
+
if (rawTarget) return rawTarget
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// The angle path snaps the distance ALONG the 15° ray — a scalar, the
|
|
177
|
+
// same in world and local frames — so the `gridSnap` world-grid override
|
|
178
|
+
// only applies when the angle lock is off.
|
|
179
|
+
const basePoint: FencePlanPoint =
|
|
149
180
|
start && angleSnap
|
|
150
|
-
?
|
|
181
|
+
? [...snapPointAlongAngleRay(start, point, DEFAULT_ANGLE_STEP, gridStep)]
|
|
151
182
|
: gridSnap
|
|
152
183
|
? gridSnap(point)
|
|
153
184
|
: snapPointToGrid(point, gridStep)
|
|
154
|
-
|
|
185
|
+
if (!magnetic) return basePoint
|
|
155
186
|
|
|
187
|
+
const fenceSnapTarget = findFenceSnapTarget(basePoint, fences, ignoreFenceIds)
|
|
156
188
|
return fenceSnapTarget ?? findWallSnapTarget(basePoint, walls) ?? basePoint
|
|
157
189
|
}
|
|
158
190
|
|
|
191
|
+
export type FenceCommitOptions = {
|
|
192
|
+
/**
|
|
193
|
+
* Pointer-decided support cap (level-local Y) from
|
|
194
|
+
* `resolvePointerSupportSurface` — the 3D tool passes the elevation of
|
|
195
|
+
* the surface the commit click actually aimed at, so a fence drawn on a
|
|
196
|
+
* deck top persists the deck as its lift host while one drawn at the
|
|
197
|
+
* floor underneath stays grounded. Omitted by 2D floor-plan commits (no
|
|
198
|
+
* camera ray): those keep the uncapped max election.
|
|
199
|
+
*/
|
|
200
|
+
supportCap?: number | null
|
|
201
|
+
}
|
|
202
|
+
|
|
159
203
|
export function createFenceOnCurrentLevel(
|
|
160
204
|
start: FencePlanPoint,
|
|
161
205
|
end: FencePlanPoint,
|
|
206
|
+
options?: FenceCommitOptions,
|
|
162
207
|
): FenceNode | null {
|
|
163
208
|
const currentLevelId = useViewer.getState().selection.levelId
|
|
164
209
|
const { createNode, nodes } = useScene.getState()
|
|
@@ -178,6 +223,60 @@ export function createFenceOnCurrentLevel(
|
|
|
178
223
|
start,
|
|
179
224
|
end,
|
|
180
225
|
})
|
|
226
|
+
// Fences run no per-frame support election — the persisted host IS the
|
|
227
|
+
// lift (absent = level floor), so elect it at commit, pointer-capped.
|
|
228
|
+
fence.supportSlabId = resolveFenceSupportSlabPatch(
|
|
229
|
+
{ ...fence, parentId: currentLevelId },
|
|
230
|
+
nodes,
|
|
231
|
+
{ maxElevation: options?.supportCap ?? null },
|
|
232
|
+
).supportSlabId
|
|
233
|
+
|
|
234
|
+
createNode(fence, currentLevelId)
|
|
235
|
+
sfxEmitter.emit('sfx:structure-build')
|
|
236
|
+
|
|
237
|
+
return fence
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Commit a smooth spline fence from a list of drawn control points. The
|
|
242
|
+
* centerline becomes a Catmull-Rom curve through `path`; `start`/`end` are
|
|
243
|
+
* pinned to the first/last point so endpoint handles, bbox, and miter
|
|
244
|
+
* references stay valid. Requires >= 2 points spanning a usable distance.
|
|
245
|
+
*/
|
|
246
|
+
export function createSplineFenceOnCurrentLevel(
|
|
247
|
+
path: FencePlanPoint[],
|
|
248
|
+
tangents = getTwoPointFenceCurveTangents(path),
|
|
249
|
+
options?: FenceCommitOptions,
|
|
250
|
+
): FenceNode | null {
|
|
251
|
+
const currentLevelId = useViewer.getState().selection.levelId
|
|
252
|
+
const { createNode, nodes } = useScene.getState()
|
|
253
|
+
|
|
254
|
+
if (!currentLevelId || path.length < 2) {
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
257
|
+
const start = path[0]!
|
|
258
|
+
const end = path[path.length - 1]!
|
|
259
|
+
// A degenerate single-point-ish path (all clicks on one spot) is rejected
|
|
260
|
+
// the same way a too-short straight segment is.
|
|
261
|
+
if (!isSegmentLongEnough(start, end) && path.length < 3) {
|
|
262
|
+
return null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const fenceCount = Object.values(nodes).filter((node) => node.type === 'fence').length
|
|
266
|
+
const defaults = useEditor.getState().toolDefaults.fence ?? {}
|
|
267
|
+
const fence = FenceNode.parse({
|
|
268
|
+
...defaults,
|
|
269
|
+
name: `Fence ${fenceCount + 1}`,
|
|
270
|
+
start,
|
|
271
|
+
end,
|
|
272
|
+
path,
|
|
273
|
+
tangents,
|
|
274
|
+
})
|
|
275
|
+
fence.supportSlabId = resolveFenceSupportSlabPatch(
|
|
276
|
+
{ ...fence, parentId: currentLevelId },
|
|
277
|
+
nodes,
|
|
278
|
+
{ maxElevation: options?.supportCap ?? null },
|
|
279
|
+
).supportSlabId
|
|
181
280
|
|
|
182
281
|
createNode(fence, currentLevelId)
|
|
183
282
|
sfxEmitter.emit('sfx:structure-build')
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AnyNodeId, ElevatorNode, SpawnNode } from '@pascal-app/core'
|
|
2
2
|
import { nodeRegistry } from '@pascal-app/core'
|
|
3
3
|
import { Suspense } from 'react'
|
|
4
|
-
import
|
|
4
|
+
import { useMovingNode } from '../../../store/use-interaction-scope'
|
|
5
5
|
import { MoveElevatorTool } from '../elevator/move-elevator-tool'
|
|
6
6
|
import { MoveRegistryNodeTool } from '../registry/move-registry-node-tool'
|
|
7
7
|
import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
|
|
@@ -9,14 +9,17 @@ import { getRegistryAffordanceTool } from '../shared/affordance-dispatch'
|
|
|
9
9
|
/**
|
|
10
10
|
* MoveTool dispatcher. Routes to (in order):
|
|
11
11
|
*
|
|
12
|
-
* 1. `
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* (
|
|
18
|
-
*
|
|
19
|
-
*
|
|
12
|
+
* 1. `def.affordanceTools.move` — kind-owned move component, lazy-loaded
|
|
13
|
+
* via `getRegistryAffordanceTool`. Covers generic movers
|
|
14
|
+
* (slab / ceiling / wall / fence / column / item / door / window), the
|
|
15
|
+
* bespoke roof / roof-segment / stair / stair-segment / building
|
|
16
|
+
* movers, and the polyline / fitting ghost-placement movers
|
|
17
|
+
* (duct-segment / duct-fitting). A kind that ships its own mover wins
|
|
18
|
+
* even if it also declares `capabilities.movable` (duct-fitting keeps
|
|
19
|
+
* `movable` for the inspector / hint readers but places via its ghost).
|
|
20
|
+
* 2. `MoveRegistryNodeTool` — generic translate-on-XZ for kinds that only
|
|
21
|
+
* declare `capabilities.movable` (shelf, spawn, duct-terminal,
|
|
22
|
+
* hvac-equipment, …).
|
|
20
23
|
* 3. `elevator` is the lone remaining legacy arm — its bespoke cab/shaft
|
|
21
24
|
* mover hasn't been ported to a kind-owned affordance yet.
|
|
22
25
|
*/
|
|
@@ -24,14 +27,11 @@ export const MoveTool: React.FC<{
|
|
|
24
27
|
onNodeMoved?: (nodeId: AnyNodeId) => void
|
|
25
28
|
onSpawnMoved?: (nodeId: SpawnNode['id']) => void
|
|
26
29
|
}> = ({ onNodeMoved }) => {
|
|
27
|
-
const movingNode =
|
|
30
|
+
const movingNode = useMovingNode()
|
|
28
31
|
|
|
29
32
|
if (!movingNode) return null
|
|
30
33
|
|
|
31
34
|
const def = nodeRegistry.get(movingNode.type)
|
|
32
|
-
if (def?.capabilities?.movable) {
|
|
33
|
-
return <MoveRegistryNodeTool node={movingNode} />
|
|
34
|
-
}
|
|
35
35
|
|
|
36
36
|
const RegistryMove = getRegistryAffordanceTool(movingNode.type, 'move')
|
|
37
37
|
if (RegistryMove) {
|
|
@@ -42,6 +42,10 @@ export const MoveTool: React.FC<{
|
|
|
42
42
|
)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
if (def?.capabilities?.movable) {
|
|
46
|
+
return <MoveRegistryNodeTool node={movingNode} />
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
if (movingNode.type === 'elevator')
|
|
46
50
|
return <MoveElevatorTool node={movingNode as ElevatorNode} onCommitted={onNodeMoved} />
|
|
47
51
|
return null
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { stripTransient } from './placement-math'
|
|
2
|
+
import { getDetachedAttachmentPreviewLift, stripTransient } from './placement-math'
|
|
3
3
|
|
|
4
4
|
describe('stripTransient', () => {
|
|
5
5
|
test('removes placement-only metadata flags before commit', () => {
|
|
@@ -8,3 +8,15 @@ describe('stripTransient', () => {
|
|
|
8
8
|
})
|
|
9
9
|
})
|
|
10
10
|
})
|
|
11
|
+
|
|
12
|
+
describe('getDetachedAttachmentPreviewLift', () => {
|
|
13
|
+
test('raises attach-only item previews while they are detached from their host', () => {
|
|
14
|
+
expect(getDetachedAttachmentPreviewLift('wall')).toBeGreaterThan(0)
|
|
15
|
+
expect(getDetachedAttachmentPreviewLift('wall-side')).toBeGreaterThan(0)
|
|
16
|
+
expect(getDetachedAttachmentPreviewLift('ceiling')).toBeGreaterThan(0)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
test('keeps floor item previews on the floor', () => {
|
|
20
|
+
expect(getDetachedAttachmentPreviewLift(undefined)).toBe(0)
|
|
21
|
+
})
|
|
22
|
+
})
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import { type AssetInput, isObject } from '@pascal-app/core'
|
|
2
2
|
import { Euler, Matrix3, type Matrix4, Quaternion, Vector3 } from 'three'
|
|
3
|
-
import
|
|
3
|
+
import { resolveSnapFlags } from '../../../lib/snapping-mode'
|
|
4
|
+
import useEditor, { getActiveSnappingMode } from '../../../store/use-editor'
|
|
4
5
|
|
|
6
|
+
// Sentinel returned when the active context's snapping mode disables grid snap.
|
|
7
|
+
// The snap helpers below treat any `step <= 0` as "no grid snap" and pass the
|
|
8
|
+
// raw value through. For items the default mode is now `lines` (grid off), so
|
|
9
|
+
// item placement/move is free + line-snap unless the user opts into `grid`.
|
|
5
10
|
function getGridSnapStep(): number {
|
|
6
|
-
return useEditor.getState().gridSnapStep
|
|
11
|
+
return resolveSnapFlags(getActiveSnappingMode()).grid ? useEditor.getState().gridSnapStep : 0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const ROTATION_QUANTUM = Math.PI / 4
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* R/T rotation: round the current angle to the nearest 45° then step ONE
|
|
18
|
+
* increment in `direction` (+1 / -1), so the node always lands on a clean 45°
|
|
19
|
+
* multiple regardless of its starting angle (12° → 45°, 40° → 90°) rather than a
|
|
20
|
+
* blind ±45° from an arbitrary angle.
|
|
21
|
+
*/
|
|
22
|
+
export function steppedRotation(current: number, direction: 1 | -1): number {
|
|
23
|
+
return (Math.round(current / ROTATION_QUANTUM) + direction) * ROTATION_QUANTUM
|
|
7
24
|
}
|
|
8
25
|
|
|
9
26
|
function positiveModulo(value: number, divisor: number): number {
|
|
@@ -14,6 +31,7 @@ function positiveModulo(value: number, divisor: number): number {
|
|
|
14
31
|
* Snaps a position to the active grid step, aligning item edges to grid lines.
|
|
15
32
|
*/
|
|
16
33
|
export function snapToGrid(position: number, dimension: number, step = getGridSnapStep()): number {
|
|
34
|
+
if (step <= 0) return position
|
|
17
35
|
const halfDim = dimension / 2
|
|
18
36
|
const offset = positiveModulo(halfDim, step)
|
|
19
37
|
return Math.round((position - offset) / step) * step + offset
|
|
@@ -23,6 +41,7 @@ export function snapToGrid(position: number, dimension: number, step = getGridSn
|
|
|
23
41
|
* Snap a value to the active grid step (used for wall-local positions).
|
|
24
42
|
*/
|
|
25
43
|
export function snapToHalf(value: number, step = getGridSnapStep()): number {
|
|
44
|
+
if (step <= 0) return value
|
|
26
45
|
return Math.round(value / step) * step
|
|
27
46
|
}
|
|
28
47
|
|
|
@@ -30,6 +49,7 @@ export function snapToHalf(value: number, step = getGridSnapStep()): number {
|
|
|
30
49
|
* Round a value up to the next multiple of `step`, with a minimum of `step`.
|
|
31
50
|
*/
|
|
32
51
|
export function snapUpToGridStep(value: number, step = getGridSnapStep()): number {
|
|
52
|
+
if (step <= 0) return value
|
|
33
53
|
return Math.max(step, Math.ceil(value / step) * step)
|
|
34
54
|
}
|
|
35
55
|
|
|
@@ -55,6 +75,12 @@ export function getGridAlignedDimensions(
|
|
|
55
75
|
return [snapUpToGridStep(w, step), h, snapUpToGridStep(d, step)]
|
|
56
76
|
}
|
|
57
77
|
|
|
78
|
+
export function getDetachedAttachmentPreviewLift(
|
|
79
|
+
attachTo: AssetInput['attachTo'] | null | undefined,
|
|
80
|
+
): number {
|
|
81
|
+
return attachTo ? 0.45 : 0
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
/**
|
|
59
85
|
* Calculate cursor rotation in WORLD space from wall normal and orientation.
|
|
60
86
|
*/
|