@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
|
@@ -6,15 +6,15 @@ import {
|
|
|
6
6
|
type ArcResizeHandle,
|
|
7
7
|
type Cursor,
|
|
8
8
|
createSceneApi,
|
|
9
|
+
DEFAULT_ANGLE_STEP,
|
|
9
10
|
type HandleDescriptor,
|
|
10
11
|
type HandlePortal,
|
|
12
|
+
type LatchHandle,
|
|
11
13
|
type LinearResizeHandle,
|
|
12
14
|
nodeRegistry,
|
|
13
15
|
type RadialResizeHandle,
|
|
14
16
|
sceneRegistry,
|
|
15
|
-
snapScalar,
|
|
16
17
|
type TapActionHandle,
|
|
17
|
-
type TranslateHandle,
|
|
18
18
|
useLiveNodeOverrides,
|
|
19
19
|
useScene,
|
|
20
20
|
} from '@pascal-app/core'
|
|
@@ -34,15 +34,25 @@ import {
|
|
|
34
34
|
OrthographicCamera,
|
|
35
35
|
Plane,
|
|
36
36
|
Quaternion,
|
|
37
|
+
Ray,
|
|
37
38
|
RingGeometry,
|
|
38
39
|
Vector3,
|
|
39
40
|
} from 'three'
|
|
40
41
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
42
|
+
|
|
41
43
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
42
44
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
45
|
+
import { RESIZE_HANDLE_DRAG_LABEL, ROTATE_HANDLE_DRAG_LABEL } from '../../lib/contextual-help'
|
|
43
46
|
import { createEditorApi } from '../../lib/editor-api'
|
|
44
47
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
45
|
-
import
|
|
48
|
+
import useDirectManipulationFeedback from '../../store/use-direct-manipulation-feedback'
|
|
49
|
+
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
|
|
50
|
+
import useInteractionScope, {
|
|
51
|
+
useEndpointReshape,
|
|
52
|
+
useIsCurveReshape,
|
|
53
|
+
useMovingNode,
|
|
54
|
+
} from '../../store/use-interaction-scope'
|
|
55
|
+
import useOpeningGuides from '../../store/use-opening-guides'
|
|
46
56
|
import { formatAngleRadians } from '../tools/shared/segment-angle'
|
|
47
57
|
import {
|
|
48
58
|
ARROW_COLOR,
|
|
@@ -52,17 +62,70 @@ import {
|
|
|
52
62
|
HandleArrow,
|
|
53
63
|
NO_RAYCAST,
|
|
54
64
|
} from './handles/handle-arrow'
|
|
65
|
+
import { replacePreviewOverrideIds } from './handles/preview-overrides'
|
|
66
|
+
import { resolveResizeSnapValue } from './handles/resize-snap'
|
|
55
67
|
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
56
68
|
|
|
69
|
+
// Pooled scratch for the handle rig's world-relative pose mapping.
|
|
70
|
+
const _rigRelative = new Matrix4()
|
|
71
|
+
const _rigScratchScale = new Vector3()
|
|
72
|
+
const _resizeAxisW = new Vector3()
|
|
73
|
+
const _resizeScale = new Vector3()
|
|
74
|
+
const _resizeQuaternion = new Quaternion()
|
|
75
|
+
const _resizeOriginW = new Vector3()
|
|
76
|
+
const _resizePositionW = new Vector3()
|
|
77
|
+
const _resizeRay = new Ray()
|
|
78
|
+
const _resizeRayW = new Vector3()
|
|
79
|
+
const MEASUREMENT_SURFACE_EXCLUDE_USER_DATA = { measurementSurface: false }
|
|
80
|
+
|
|
81
|
+
// Tilt that stands a flat XZ-plane move cross up into a node's facing plane
|
|
82
|
+
// (its local XY = a wall face) for `plane: 'node-normal'` handles.
|
|
83
|
+
const NODE_NORMAL_TILT: [number, number, number] = [Math.PI / 2, 0, 0]
|
|
84
|
+
|
|
85
|
+
function axisVector(axis: 'x' | 'y' | 'z', target: Vector3) {
|
|
86
|
+
target.set(0, 0, 0)
|
|
87
|
+
if (axis === 'x') target.x = 1
|
|
88
|
+
else if (axis === 'y') target.y = 1
|
|
89
|
+
else target.z = 1
|
|
90
|
+
return target
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function axisScale(axis: 'x' | 'y' | 'z', scale: Vector3) {
|
|
94
|
+
return axis === 'x' ? scale.x : axis === 'y' ? scale.y : scale.z
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function closestAxisParameterToRay(axisOrigin: Vector3, axisDirection: Vector3, ray: Ray) {
|
|
98
|
+
_resizeRayW.subVectors(axisOrigin, ray.origin)
|
|
99
|
+
const b = axisDirection.dot(ray.direction)
|
|
100
|
+
const d = axisDirection.dot(_resizeRayW)
|
|
101
|
+
const e = ray.direction.dot(_resizeRayW)
|
|
102
|
+
const denominator = 1 - b * b
|
|
103
|
+
if (Math.abs(denominator) < 1e-6) {
|
|
104
|
+
return -d
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const axisParameter = (b * e - d) / denominator
|
|
108
|
+
const rayParameter = e + b * axisParameter
|
|
109
|
+
if (rayParameter < 0) {
|
|
110
|
+
return -d
|
|
111
|
+
}
|
|
112
|
+
return axisParameter
|
|
113
|
+
}
|
|
114
|
+
|
|
57
115
|
export {
|
|
58
116
|
ARROW_COLOR,
|
|
59
117
|
ARROW_HOVER_COLOR,
|
|
60
118
|
ARROW_SCALE,
|
|
119
|
+
createArrowHandleGeometry,
|
|
61
120
|
createArrowHitAreaGeometry,
|
|
62
121
|
createEndpointHitAreaGeometry,
|
|
63
122
|
createMoveCrossHandleGeometry,
|
|
64
123
|
createRotateArrowHandleGeometry,
|
|
65
124
|
createRotateArrowHitAreaGeometry,
|
|
125
|
+
HandleArrow,
|
|
126
|
+
type HandleArrowInputShape,
|
|
127
|
+
type HandleArrowPlacement,
|
|
128
|
+
type HandleArrowProps,
|
|
66
129
|
HIT_AREA_MARGIN,
|
|
67
130
|
InvisibleHandleHitArea,
|
|
68
131
|
NO_RAYCAST,
|
|
@@ -76,6 +139,20 @@ export { swallowNextClick } from './handles/use-handle-drag'
|
|
|
76
139
|
// its end). Upward trackers — wall / chimney height — stop at the cube.
|
|
77
140
|
const TRACKER_THROUGH = 0.12
|
|
78
141
|
|
|
142
|
+
type SceneApiForHandles = ReturnType<typeof createSceneApi>
|
|
143
|
+
type CenteredGuideDecoration = {
|
|
144
|
+
center?: (node: AnyNode, sceneApi: SceneApiForHandles) => readonly [number, number, number]
|
|
145
|
+
radius: (node: AnyNode, sceneApi: SceneApiForHandles) => number
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function guideDecorationCenter(decoration: unknown, node: AnyNode, sceneApi: SceneApiForHandles) {
|
|
149
|
+
return (decoration as CenteredGuideDecoration | undefined)?.center?.(node, sceneApi)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function guideDecorationRadius(decoration: unknown, node: AnyNode, sceneApi: SceneApiForHandles) {
|
|
153
|
+
return (decoration as CenteredGuideDecoration).radius(node, sceneApi)
|
|
154
|
+
}
|
|
155
|
+
|
|
79
156
|
// Mirrors the formatter used by wall / fence measurement labels so all
|
|
80
157
|
// in-world dimension chips read consistently.
|
|
81
158
|
function formatDimension(value: number, unit: 'metric' | 'imperial'): string {
|
|
@@ -122,23 +199,22 @@ function DimensionLabel({
|
|
|
122
199
|
|
|
123
200
|
export function NodeArrowHandles() {
|
|
124
201
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
202
|
+
const activeRotateNodeId = useDirectManipulationFeedback((state) => state.activeRotateNodeId)
|
|
125
203
|
const mode = useEditor((state) => state.mode)
|
|
126
204
|
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
127
|
-
const movingNode =
|
|
128
|
-
const placementDragMode = useEditor((state) => state.placementDragMode)
|
|
205
|
+
const movingNode = useMovingNode()
|
|
129
206
|
// Endpoint / curve drags reshape the selected wall or fence; hide its
|
|
130
207
|
// resize arrows for the duration so they don't clutter (or get blocked
|
|
131
208
|
// by) the drag's own cursor + dimension overlays. Mirrors the same guard
|
|
132
209
|
// on the legacy wall handles (`WallMoveSideHandles`).
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
)
|
|
210
|
+
const endpointReshape = useEndpointReshape()
|
|
211
|
+
const isCurveReshape = useIsCurveReshape()
|
|
212
|
+
|
|
213
|
+
const selectedId = selectedIds.length === 1 ? selectedIds[0] : activeRotateNodeId
|
|
214
|
+
const rawNode = useScene((state) => {
|
|
215
|
+
if (!selectedId) return null
|
|
216
|
+
return state.nodes[selectedId as AnyNodeId] ?? null
|
|
217
|
+
})
|
|
142
218
|
|
|
143
219
|
// Merge any live drag override so the arrows themselves (positions,
|
|
144
220
|
// ring decorations) track the in-flight drag instead of freezing at
|
|
@@ -151,26 +227,37 @@ export function NodeArrowHandles() {
|
|
|
151
227
|
() => (rawNode && liveOverride ? ({ ...rawNode, ...liveOverride } as AnyNode) : rawNode),
|
|
152
228
|
[rawNode, liveOverride],
|
|
153
229
|
)
|
|
154
|
-
const isOwnPressDragMove =
|
|
155
|
-
placementDragMode && movingNode !== null && selectedId !== null && movingNode.id === selectedId
|
|
156
|
-
|
|
157
230
|
const def = node ? nodeRegistry.get(node.type) : null
|
|
231
|
+
const descriptorSceneApi = useMemo(() => createSceneApi(useScene), [])
|
|
158
232
|
const descriptors = useMemo(() => {
|
|
159
233
|
if (!(node && def?.handles)) return null
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
234
|
+
const all =
|
|
235
|
+
typeof def.handles === 'function'
|
|
236
|
+
? def.handles(node as never, descriptorSceneApi)
|
|
237
|
+
: (def.handles as HandleDescriptor[])
|
|
238
|
+
// The whole-node move-cross gizmo is gone: moving is now click-to-move on
|
|
239
|
+
// the selected node body (see selection-manager). Drop both flavours — the
|
|
240
|
+
// `translate` ground cross (column/roof/shelf/spawn) and the `tap-action`
|
|
241
|
+
// `move-cross` (item/door/window/elevator/stair) — keep rotate/resize.
|
|
242
|
+
return all.filter(
|
|
243
|
+
(d) =>
|
|
244
|
+
d.kind !== 'translate' &&
|
|
245
|
+
!('shape' in d && d.shape === 'move-cross') &&
|
|
246
|
+
(d.kind !== 'linear-resize' || d.visible?.(node as never, descriptorSceneApi) !== false),
|
|
247
|
+
)
|
|
248
|
+
}, [node, def, descriptorSceneApi])
|
|
164
249
|
|
|
165
250
|
const shouldRender =
|
|
166
251
|
Boolean(node && descriptors?.length) &&
|
|
167
252
|
!isFloorplanHovered &&
|
|
168
253
|
mode !== 'delete' &&
|
|
169
|
-
(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
!
|
|
254
|
+
// Any whole-node move (placement or press-drag) hides the rig: the item is
|
|
255
|
+
// following the cursor, so its rotate/resize handles would only clutter and
|
|
256
|
+
// draw stray selection rays. The active handle-drag scope (resize/rotate)
|
|
257
|
+
// sets `activeHandleDrag`, not `movingNode`, so those are unaffected.
|
|
258
|
+
!movingNode &&
|
|
259
|
+
!endpointReshape &&
|
|
260
|
+
!isCurveReshape
|
|
174
261
|
|
|
175
262
|
if (!shouldRender || !node || !descriptors) return null
|
|
176
263
|
// Key by the selected node id so switching selection REMOUNTS the rig.
|
|
@@ -275,14 +362,32 @@ function NodeArrowHandlesForNode({
|
|
|
275
362
|
// exclusion the wall arrow also goes without.
|
|
276
363
|
|
|
277
364
|
useFrame(() => {
|
|
365
|
+
if (innerRef.current && innerRide && portalObject) {
|
|
366
|
+
// Grandparent mode: pose the rig by mapping the node's WORLD pose
|
|
367
|
+
// into the portal target's frame. Copying the parent + node
|
|
368
|
+
// registry poses (the previous approach) assumed the node mesh is
|
|
369
|
+
// a DIRECT child of the parent's registered object — roof-hosted
|
|
370
|
+
// openings break that with an intermediate face-frame group, which
|
|
371
|
+
// the world-relative mapping absorbs for free. For wall children
|
|
372
|
+
// the result is identical (portal⁻¹ ∘ node = wall.local ∘ node.local).
|
|
373
|
+
if (outerRef.current) {
|
|
374
|
+
outerRef.current.position.set(0, 0, 0)
|
|
375
|
+
outerRef.current.quaternion.identity()
|
|
376
|
+
}
|
|
377
|
+
portalObject.updateWorldMatrix(true, false)
|
|
378
|
+
innerRide.updateWorldMatrix(true, false)
|
|
379
|
+
_rigRelative.copy(portalObject.matrixWorld).invert().multiply(innerRide.matrixWorld)
|
|
380
|
+
_rigRelative.decompose(
|
|
381
|
+
innerRef.current.position,
|
|
382
|
+
innerRef.current.quaternion,
|
|
383
|
+
_rigScratchScale,
|
|
384
|
+
)
|
|
385
|
+
return
|
|
386
|
+
}
|
|
278
387
|
if (outerRef.current && outerRide) {
|
|
279
388
|
outerRef.current.position.copy(outerRide.position)
|
|
280
389
|
outerRef.current.quaternion.copy(outerRide.quaternion)
|
|
281
390
|
}
|
|
282
|
-
if (innerRef.current && innerRide) {
|
|
283
|
-
innerRef.current.position.copy(innerRide.position)
|
|
284
|
-
innerRef.current.quaternion.copy(innerRide.quaternion)
|
|
285
|
-
}
|
|
286
391
|
})
|
|
287
392
|
|
|
288
393
|
// Active-drag tracking. When a handle starts dragging, it claims its
|
|
@@ -299,6 +404,21 @@ function NodeArrowHandlesForNode({
|
|
|
299
404
|
// hook count between renders and trip React's rules-of-hooks check.
|
|
300
405
|
const [activeIndex, setActiveIndex] = useState<number | null>(null)
|
|
301
406
|
const [preDragNode, setPreDragNode] = useState<AnyNode | null>(null)
|
|
407
|
+
// Latch groups currently toggled open. A `latch` cube descriptor flips its
|
|
408
|
+
// group here on click; arrows tagged with a `latchGroup` only render while
|
|
409
|
+
// their group is in this set. Local to this mount, so it resets on deselect
|
|
410
|
+
// (the rig remounts per selection — see the `key` on NodeArrowHandlesForNode).
|
|
411
|
+
const [openLatchGroups, setOpenLatchGroups] = useState<ReadonlySet<string>>(() => new Set())
|
|
412
|
+
const toggleLatchGroup = useMemo(
|
|
413
|
+
() => (group: string) =>
|
|
414
|
+
setOpenLatchGroups((prev) => {
|
|
415
|
+
const next = new Set(prev)
|
|
416
|
+
if (next.has(group)) next.delete(group)
|
|
417
|
+
else next.add(group)
|
|
418
|
+
return next
|
|
419
|
+
}),
|
|
420
|
+
[],
|
|
421
|
+
)
|
|
302
422
|
const dragControls = useMemo<HandleDragControls>(
|
|
303
423
|
() => ({
|
|
304
424
|
onStart: (index: number, snapshot: AnyNode) => {
|
|
@@ -328,25 +448,48 @@ function NodeArrowHandlesForNode({
|
|
|
328
448
|
// resize that re-centres the mesh) must NOT fire for the non-active arrows
|
|
329
449
|
// here, or they'd lag behind the moving item.
|
|
330
450
|
const activeIsTranslate = activeIndex !== null && descriptors[activeIndex]?.kind === 'translate'
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
451
|
+
// While a rotate gizmo is mid-drag, drop the opposite-side move cross: you
|
|
452
|
+
// can't move and rotate at once, so it only clutters the rotation.
|
|
453
|
+
const activeDescriptor = activeIndex !== null ? descriptors[activeIndex] : undefined
|
|
454
|
+
const activeIsRotate =
|
|
455
|
+
!!activeDescriptor && 'shape' in activeDescriptor && activeDescriptor.shape === 'rotate'
|
|
456
|
+
|
|
457
|
+
const arrows = descriptors.map((descriptor, index) => {
|
|
458
|
+
if (activeIsRotate && 'shape' in descriptor && descriptor.shape === 'move-cross') return null
|
|
459
|
+
// A `latch` cube toggles its group's visibility; render it always.
|
|
460
|
+
if (descriptor.kind === 'latch') {
|
|
461
|
+
return (
|
|
462
|
+
<LatchCube
|
|
463
|
+
descriptor={descriptor}
|
|
464
|
+
key={index}
|
|
465
|
+
node={node}
|
|
466
|
+
onToggle={toggleLatchGroup}
|
|
467
|
+
open={openLatchGroups.has(descriptor.group)}
|
|
468
|
+
/>
|
|
469
|
+
)
|
|
470
|
+
}
|
|
471
|
+
// Arrows tagged with a latch group stay hidden until that group is open.
|
|
472
|
+
const latchGroup = descriptor.kind === 'linear-resize' ? descriptor.latchGroup : undefined
|
|
473
|
+
if (latchGroup && !openLatchGroups.has(latchGroup)) return null
|
|
474
|
+
return (
|
|
475
|
+
<ArrowHandle
|
|
476
|
+
activeIndex={activeIndex}
|
|
477
|
+
descriptor={descriptor}
|
|
478
|
+
dragControls={dragControls}
|
|
479
|
+
handleIndex={index}
|
|
480
|
+
// Descriptors come from a per-node-kind static list, so index is a
|
|
481
|
+
// stable identity within this node's selection cycle.
|
|
482
|
+
key={index}
|
|
483
|
+
liveNode={node}
|
|
484
|
+
preDragNode={preDragNode}
|
|
485
|
+
rideObject={arrowFrame}
|
|
486
|
+
suppressFreeze={activeIsTranslate}
|
|
487
|
+
/>
|
|
488
|
+
)
|
|
489
|
+
})
|
|
347
490
|
|
|
348
491
|
return createPortal(
|
|
349
|
-
<group ref={outerRef}>
|
|
492
|
+
<group ref={outerRef} userData={MEASUREMENT_SURFACE_EXCLUDE_USER_DATA}>
|
|
350
493
|
{innerRideId !== null ? <group ref={innerRef}>{arrows}</group> : arrows}
|
|
351
494
|
</group>,
|
|
352
495
|
portalObject,
|
|
@@ -443,17 +586,6 @@ function ArrowHandle({
|
|
|
443
586
|
/>
|
|
444
587
|
)
|
|
445
588
|
}
|
|
446
|
-
if (descriptor.kind === 'translate') {
|
|
447
|
-
return (
|
|
448
|
-
<TranslateArrow
|
|
449
|
-
descriptor={descriptor}
|
|
450
|
-
dragControls={dragControls}
|
|
451
|
-
handleIndex={handleIndex}
|
|
452
|
-
node={placementNode}
|
|
453
|
-
rideObject={rideObject}
|
|
454
|
-
/>
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
589
|
if (descriptor.kind === 'tap-action') {
|
|
458
590
|
// Tap-action handles (fence side-move arrows, corner pickers) aren't
|
|
459
591
|
// resize handles, so the freeze-at-pre-drag mechanism — which only
|
|
@@ -524,6 +656,9 @@ function LinearArrow({
|
|
|
524
656
|
// floating dimension pill (via `activeHandleDrag`) and its own in-world
|
|
525
657
|
// chip is suppressed — matches the wall height handle.
|
|
526
658
|
const measureLabel = descriptor.kind === 'linear-resize' ? descriptor.measureLabel : undefined
|
|
659
|
+
// Optional per-tick feedback hook (doors/windows publish proximity/sill guides
|
|
660
|
+
// for the edge being resized); cleared when the drag ends.
|
|
661
|
+
const onDrag = descriptor.kind === 'linear-resize' ? descriptor.onDrag : undefined
|
|
527
662
|
const placementSceneApi = useMemo(() => createSceneApi(useScene), [])
|
|
528
663
|
const basePosition = descriptor.placement.position(node, placementSceneApi)
|
|
529
664
|
// `freezeOffset` (in node-local frame) cancels the mesh's `position`
|
|
@@ -555,40 +690,33 @@ function LinearArrow({
|
|
|
555
690
|
rideObject,
|
|
556
691
|
setIsDragging,
|
|
557
692
|
onStart: ({
|
|
558
|
-
camera: dragCamera,
|
|
559
693
|
event,
|
|
694
|
+
getPointerRay,
|
|
560
695
|
initialNode,
|
|
561
|
-
intersectPlane,
|
|
562
696
|
nodeId,
|
|
563
697
|
rideObject: dragRideObject,
|
|
564
698
|
sceneApi,
|
|
565
699
|
}) => {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
const plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal, worldOrigin)
|
|
700
|
+
dragRideObject.matrixWorld.decompose(_resizePositionW, _resizeQuaternion, _resizeScale)
|
|
701
|
+
_resizeOriginW.set(...position).applyMatrix4(dragRideObject.matrixWorld)
|
|
702
|
+
axisVector(descriptor.axis, _resizeAxisW).applyQuaternion(_resizeQuaternion).normalize()
|
|
703
|
+
const localToWorldScale = axisScale(descriptor.axis, _resizeScale)
|
|
704
|
+
if (Math.abs(localToWorldScale) < 1e-6 || _resizeAxisW.lengthSq() === 0) return null
|
|
572
705
|
|
|
573
|
-
const
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
706
|
+
const initialPointer =
|
|
707
|
+
closestAxisParameterToRay(
|
|
708
|
+
_resizeOriginW,
|
|
709
|
+
_resizeAxisW,
|
|
710
|
+
getPointerRay(event.nativeEvent.clientX, event.nativeEvent.clientY, _resizeRay),
|
|
711
|
+
) / localToWorldScale
|
|
578
712
|
|
|
579
713
|
const overrideId =
|
|
580
714
|
(descriptor.kind === 'linear-resize'
|
|
581
715
|
? descriptor.overrideTarget?.(initialNode as never, sceneApi)
|
|
582
716
|
: undefined) ?? nodeId
|
|
583
717
|
const initialValue = descriptor.currentValue(initialNode)
|
|
584
|
-
const initialPointer =
|
|
585
|
-
descriptor.axis === 'x' ? hitLocal.x : descriptor.axis === 'y' ? hitLocal.y : hitLocal.z
|
|
586
718
|
const minBound = resolveBound(descriptor.min, Number.NEGATIVE_INFINITY, initialNode, sceneApi)
|
|
587
719
|
const maxBound = resolveBound(descriptor.max, Number.POSITIVE_INFINITY, initialNode, sceneApi)
|
|
588
|
-
const gridSnapStep =
|
|
589
|
-
descriptor.kind === 'linear-resize' && descriptor.gridSnap
|
|
590
|
-
? useEditor.getState().gridSnapStep
|
|
591
|
-
: null
|
|
592
720
|
const factor =
|
|
593
721
|
descriptor.kind === 'radial-resize'
|
|
594
722
|
? 1
|
|
@@ -598,36 +726,92 @@ function LinearArrow({
|
|
|
598
726
|
? 1
|
|
599
727
|
: -1
|
|
600
728
|
|
|
729
|
+
// Last value an emitted resize tick fired at — a new tick fires only
|
|
730
|
+
// when the (snapped + clamped) value actually changes, so the cue
|
|
731
|
+
// tracks real size steps instead of every sub-pixel pointer jitter.
|
|
732
|
+
let lastTickValue = initialValue
|
|
733
|
+
let previewOverrideIds = new Set<AnyNodeId>()
|
|
734
|
+
|
|
601
735
|
return {
|
|
602
736
|
overrideId,
|
|
737
|
+
commit:
|
|
738
|
+
descriptor.kind === 'linear-resize' && descriptor.commit
|
|
739
|
+
? (patch) => descriptor.commit?.(initialNode, patch, sceneApi)
|
|
740
|
+
: undefined,
|
|
603
741
|
onBegin: () => {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
742
|
+
// Always claim the handle-drag scope so the HUD knows a resize is the
|
|
743
|
+
// active interaction (keeps the idle select hints off-screen). The
|
|
744
|
+
// dimension-pill handles carry their `measureLabel`; plain resize
|
|
745
|
+
// arrows use the generic label.
|
|
746
|
+
useInteractionScope.getState().begin({
|
|
747
|
+
kind: 'handle-drag',
|
|
748
|
+
nodeId,
|
|
749
|
+
handle: measureLabel ?? RESIZE_HANDLE_DRAG_LABEL,
|
|
750
|
+
})
|
|
607
751
|
},
|
|
608
752
|
onEnd: () => {
|
|
609
|
-
|
|
610
|
-
|
|
753
|
+
useInteractionScope.getState().endIf((sc) => sc.kind === 'handle-drag')
|
|
754
|
+
if (descriptor.kind === 'linear-resize') {
|
|
755
|
+
descriptor.onDragEnd?.(initialNode as never, sceneApi)
|
|
611
756
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
return null
|
|
757
|
+
if (onDrag) useOpeningGuides.getState().clear()
|
|
758
|
+
for (const previewId of previewOverrideIds) {
|
|
759
|
+
useLiveNodeOverrides.getState().clear(previewId)
|
|
760
|
+
useScene.getState().markDirty(previewId)
|
|
617
761
|
}
|
|
618
|
-
|
|
762
|
+
},
|
|
763
|
+
move: ({ event: moveEvent, getPointerRay: getMovePointerRay }) => {
|
|
619
764
|
const currentPointer =
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
765
|
+
closestAxisParameterToRay(
|
|
766
|
+
_resizeOriginW,
|
|
767
|
+
_resizeAxisW,
|
|
768
|
+
getMovePointerRay(moveEvent.clientX, moveEvent.clientY, _resizeRay),
|
|
769
|
+
) / localToWorldScale
|
|
625
770
|
const delta = currentPointer - initialPointer
|
|
626
771
|
const rawNext = initialValue + delta * factor
|
|
627
|
-
const
|
|
628
|
-
|
|
772
|
+
const linearDescriptor = descriptor.kind === 'linear-resize' ? descriptor : null
|
|
773
|
+
const snappedNext = resolveResizeSnapValue({
|
|
774
|
+
rawValue: rawNext,
|
|
775
|
+
gridSnapEnabled: linearDescriptor?.gridSnap === true,
|
|
776
|
+
gridSnapActive: isGridSnapActive(),
|
|
777
|
+
gridSnapStep: useEditor.getState().gridSnapStep,
|
|
778
|
+
magneticSnapActive: isMagneticSnapActive(),
|
|
779
|
+
magneticSnap: linearDescriptor?.magneticSnap
|
|
780
|
+
? (value) => linearDescriptor.magneticSnap?.(initialNode, value, sceneApi) ?? value
|
|
781
|
+
: undefined,
|
|
782
|
+
})
|
|
629
783
|
const next = Math.min(maxBound, Math.max(minBound, snappedNext))
|
|
630
|
-
|
|
784
|
+
if (next !== lastTickValue) {
|
|
785
|
+
lastTickValue = next
|
|
786
|
+
sfxEmitter.emit('sfx:resize')
|
|
787
|
+
}
|
|
788
|
+
const patch = descriptor.apply(initialNode as never, next, sceneApi) as Partial<AnyNode>
|
|
789
|
+
if (descriptor.kind === 'linear-resize' && descriptor.previewOverrides) {
|
|
790
|
+
const previewEntries = descriptor.previewOverrides(initialNode as never, next, sceneApi)
|
|
791
|
+
const nextPreviewOverrideIds = replacePreviewOverrideIds(
|
|
792
|
+
previewOverrideIds,
|
|
793
|
+
previewEntries,
|
|
794
|
+
(previewId) => {
|
|
795
|
+
useLiveNodeOverrides.getState().clear(previewId)
|
|
796
|
+
useScene.getState().markDirty(previewId)
|
|
797
|
+
},
|
|
798
|
+
)
|
|
799
|
+
useLiveNodeOverrides
|
|
800
|
+
.getState()
|
|
801
|
+
.setMany(
|
|
802
|
+
previewEntries.map(([id, previewPatch]) => [
|
|
803
|
+
id,
|
|
804
|
+
previewPatch as Record<string, unknown>,
|
|
805
|
+
]),
|
|
806
|
+
)
|
|
807
|
+
for (const [previewId] of previewEntries) {
|
|
808
|
+
useScene.getState().markDirty(previewId)
|
|
809
|
+
}
|
|
810
|
+
previewOverrideIds = nextPreviewOverrideIds
|
|
811
|
+
}
|
|
812
|
+
// Let the kind publish live guides for the edge being resized.
|
|
813
|
+
onDrag?.({ ...(initialNode as object), ...patch } as AnyNode, sceneApi)
|
|
814
|
+
return patch
|
|
631
815
|
},
|
|
632
816
|
}
|
|
633
817
|
},
|
|
@@ -637,10 +821,19 @@ function LinearArrow({
|
|
|
637
821
|
// X+Z rotation chain matching DoorHeightArrowHandle. When the handle
|
|
638
822
|
// sits below the node (placement Y < 0, e.g. window bottom arrow),
|
|
639
823
|
// flip the Z rotation so the chevron points outward (downward).
|
|
824
|
+
//
|
|
825
|
+
// For axis === 'x' with `faceNormal` (wall-mounted opening width arrows),
|
|
826
|
+
// roll the blade 90° about its own pointing (X) axis so it stands up from
|
|
827
|
+
// the horizontal XZ plane into the node's facing plane (XY = the wall
|
|
828
|
+
// face) — otherwise the blade is seen edge-on from the front.
|
|
829
|
+
const faceNormalX =
|
|
830
|
+
descriptor.kind === 'linear-resize' && descriptor.axis === 'x' && descriptor.faceNormal === true
|
|
640
831
|
const innerRotation: [number, number, number] =
|
|
641
832
|
descriptor.axis === 'y'
|
|
642
833
|
? [0, Math.PI / 2, position[1] < 0 ? -Math.PI / 2 : Math.PI / 2]
|
|
643
|
-
:
|
|
834
|
+
: faceNormalX
|
|
835
|
+
? [Math.PI / 2, 0, 0]
|
|
836
|
+
: [0, 0, 0]
|
|
644
837
|
|
|
645
838
|
// Optional guide decoration — linear handles use it for curved-stair
|
|
646
839
|
// width / inner-radius rings; radial handles use it for the column's
|
|
@@ -686,7 +879,8 @@ function LinearArrow({
|
|
|
686
879
|
<>
|
|
687
880
|
{showDecoration && decoration ? (
|
|
688
881
|
<GuideRing
|
|
689
|
-
|
|
882
|
+
center={guideDecorationCenter(decoration, node, placementSceneApi)}
|
|
883
|
+
radius={guideDecorationRadius(decoration, node, placementSceneApi)}
|
|
690
884
|
y={decoration.y?.(node as never) ?? 0}
|
|
691
885
|
/>
|
|
692
886
|
) : null}
|
|
@@ -714,7 +908,8 @@ function LinearArrow({
|
|
|
714
908
|
<>
|
|
715
909
|
{showDecoration && decoration ? (
|
|
716
910
|
<GuideRing
|
|
717
|
-
|
|
911
|
+
center={guideDecorationCenter(decoration, node, placementSceneApi)}
|
|
912
|
+
radius={guideDecorationRadius(decoration, node, placementSceneApi)}
|
|
718
913
|
y={decoration.y?.(node as never) ?? 0}
|
|
719
914
|
/>
|
|
720
915
|
) : null}
|
|
@@ -726,6 +921,7 @@ function LinearArrow({
|
|
|
726
921
|
onPointerDown={activate}
|
|
727
922
|
placement={{ position, rotation: [0, rotationY, 0], baseScale }}
|
|
728
923
|
shape="chevron"
|
|
924
|
+
thin
|
|
729
925
|
>
|
|
730
926
|
{showLabel ? <DimensionLabel position={[0, 0.22, 0]} text={labelText} /> : null}
|
|
731
927
|
</HandleArrow>
|
|
@@ -737,7 +933,15 @@ function LinearArrow({
|
|
|
737
933
|
// e.g. the curved-stair width arrow traces the outer rim, the inner-radius
|
|
738
934
|
// arrow traces the central pillar. Floats at node-local `y`, lies in the
|
|
739
935
|
// XZ plane.
|
|
740
|
-
export function GuideRing({
|
|
936
|
+
export function GuideRing({
|
|
937
|
+
center,
|
|
938
|
+
radius,
|
|
939
|
+
y,
|
|
940
|
+
}: {
|
|
941
|
+
center?: readonly [number, number, number]
|
|
942
|
+
radius: number
|
|
943
|
+
y: number
|
|
944
|
+
}) {
|
|
741
945
|
const safeRadius = Math.max(radius, 0.01)
|
|
742
946
|
const ringGeometry = useMemo(() => {
|
|
743
947
|
const inner = Math.max(safeRadius - 0.015, 0.001)
|
|
@@ -764,7 +968,7 @@ export function GuideRing({ radius, y }: { radius: number; y: number }) {
|
|
|
764
968
|
frustumCulled={false}
|
|
765
969
|
geometry={ringGeometry}
|
|
766
970
|
material={ringMaterial}
|
|
767
|
-
position={[0, y, 0]}
|
|
971
|
+
position={center ? [center[0], y, center[2]] : [0, y, 0]}
|
|
768
972
|
renderOrder={1009}
|
|
769
973
|
rotation={[-Math.PI / 2, 0, 0]}
|
|
770
974
|
/>
|
|
@@ -873,11 +1077,13 @@ function RotationGuideOutline({ geometry }: { geometry: BufferGeometry }) {
|
|
|
873
1077
|
// from the pivot; the fill is pulled inside it so it reads as the handle
|
|
874
1078
|
// swinging around rather than overlapping the icon.
|
|
875
1079
|
function RotationWedge({
|
|
1080
|
+
center,
|
|
876
1081
|
delta,
|
|
877
1082
|
handleAngle,
|
|
878
1083
|
orbitRadius,
|
|
879
1084
|
y,
|
|
880
1085
|
}: {
|
|
1086
|
+
center?: readonly [number, number, number]
|
|
881
1087
|
delta: number
|
|
882
1088
|
handleAngle: number
|
|
883
1089
|
orbitRadius: number
|
|
@@ -917,7 +1123,7 @@ function RotationWedge({
|
|
|
917
1123
|
]
|
|
918
1124
|
|
|
919
1125
|
return (
|
|
920
|
-
<group position={[0, y, 0]}>
|
|
1126
|
+
<group position={center ? [center[0], y, center[2]] : [0, y, 0]}>
|
|
921
1127
|
<mesh
|
|
922
1128
|
frustumCulled={false}
|
|
923
1129
|
geometry={fill}
|
|
@@ -971,6 +1177,8 @@ function ArcArrow({
|
|
|
971
1177
|
// corner) render a two-headed curved arrow; everything else (stair
|
|
972
1178
|
// sweep, etc.) keeps the chevron.
|
|
973
1179
|
const isRotateShape = descriptor.shape === 'rotate'
|
|
1180
|
+
const activeRotateNodeId = useDirectManipulationFeedback((state) => state.activeRotateNodeId)
|
|
1181
|
+
const isDirectRotating = isRotateShape && activeRotateNodeId === liveNode.id
|
|
974
1182
|
// 'node-normal' spins the node about its local +Z (a wall item flat against
|
|
975
1183
|
// its wall) instead of yaw about world-Y. The drag plane and the icon both
|
|
976
1184
|
// tilt into that plane, and the horizontal-only wedge/ring readout is
|
|
@@ -1010,7 +1218,8 @@ function ArcArrow({
|
|
|
1010
1218
|
// arrow is hovered or dragging. Same recipe as the linear / radial
|
|
1011
1219
|
// decoration path.
|
|
1012
1220
|
const decoration = descriptor.decoration
|
|
1013
|
-
const
|
|
1221
|
+
const decorationCenter = guideDecorationCenter(decoration, node, placementSceneApi)
|
|
1222
|
+
const showDecoration = Boolean(decoration) && (isHovered || isDragging || isDirectRotating)
|
|
1014
1223
|
|
|
1015
1224
|
const activate = useHandleDrag({
|
|
1016
1225
|
kind: 'drag',
|
|
@@ -1062,8 +1271,23 @@ function ArcArrow({
|
|
|
1062
1271
|
}
|
|
1063
1272
|
const initialAngle = angleOf(hitWorld)
|
|
1064
1273
|
|
|
1274
|
+
// Advertise the rotate interaction so the contextual HUD can surface the
|
|
1275
|
+
// Shift = free-rotation toggle (the angle-step bypass below). Resize
|
|
1276
|
+
// handles route a measurement label here; rotate gets a sentinel label so
|
|
1277
|
+
// the HUD shows the rotate hint, not a dimension pill.
|
|
1278
|
+
if (isRotateShape) {
|
|
1279
|
+
useInteractionScope
|
|
1280
|
+
.getState()
|
|
1281
|
+
.begin({ kind: 'handle-drag', nodeId: node.id, handle: ROTATE_HANDLE_DRAG_LABEL })
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1065
1284
|
return {
|
|
1066
|
-
onEnd: () =>
|
|
1285
|
+
onEnd: () => {
|
|
1286
|
+
setRotationDelta(null)
|
|
1287
|
+
if (isRotateShape) {
|
|
1288
|
+
useInteractionScope.getState().endIf((sc) => sc.kind === 'handle-drag')
|
|
1289
|
+
}
|
|
1290
|
+
},
|
|
1067
1291
|
move: ({ event: moveEvent, intersectPlane: intersectMovePlane }) => {
|
|
1068
1292
|
const hit = new Vector3()
|
|
1069
1293
|
if (!intersectMovePlane(moveEvent.clientX, moveEvent.clientY, plane, hit)) return null
|
|
@@ -1072,9 +1296,8 @@ function ArcArrow({
|
|
|
1072
1296
|
while (delta > Math.PI) delta -= 2 * Math.PI
|
|
1073
1297
|
while (delta < -Math.PI) delta += 2 * Math.PI
|
|
1074
1298
|
|
|
1075
|
-
if (moveEvent.shiftKey && descriptor.shape === 'rotate') {
|
|
1076
|
-
|
|
1077
|
-
delta = Math.round(delta / step) * step
|
|
1299
|
+
if (!moveEvent.shiftKey && descriptor.shape === 'rotate') {
|
|
1300
|
+
delta = Math.round(delta / DEFAULT_ANGLE_STEP) * DEFAULT_ANGLE_STEP
|
|
1078
1301
|
}
|
|
1079
1302
|
|
|
1080
1303
|
if (isRotateShape && !isNodeNormalRot) {
|
|
@@ -1086,18 +1309,12 @@ function ArcArrow({
|
|
|
1086
1309
|
},
|
|
1087
1310
|
})
|
|
1088
1311
|
|
|
1089
|
-
// Suppress "declared but unused" for `liveNode` — ArcArrow's apply
|
|
1090
|
-
// operates entirely on `initialNode` (snapshot taken inside activate)
|
|
1091
|
-
// and `delta` (live cursor angle), so the live store node doesn't
|
|
1092
|
-
// appear in the rotation pipeline. The prop is still required because
|
|
1093
|
-
// ArrowHandle passes it uniformly to every variant.
|
|
1094
|
-
void liveNode
|
|
1095
|
-
|
|
1096
1312
|
return (
|
|
1097
1313
|
<>
|
|
1098
1314
|
{showDecoration && decoration ? (
|
|
1099
1315
|
<GuideRing
|
|
1100
|
-
|
|
1316
|
+
center={guideDecorationCenter(decoration, node, placementSceneApi)}
|
|
1317
|
+
radius={guideDecorationRadius(decoration, node, placementSceneApi)}
|
|
1101
1318
|
y={decoration.y?.(node as never) ?? 0}
|
|
1102
1319
|
/>
|
|
1103
1320
|
) : null}
|
|
@@ -1107,16 +1324,23 @@ function ArcArrow({
|
|
|
1107
1324
|
ring on any surface — flat ground or a pitched roof. */}
|
|
1108
1325
|
{rotationDelta !== null ? (
|
|
1109
1326
|
<RotationWedge
|
|
1327
|
+
center={decorationCenter}
|
|
1110
1328
|
delta={rotationDelta}
|
|
1111
|
-
handleAngle={Math.atan2(
|
|
1112
|
-
|
|
1329
|
+
handleAngle={Math.atan2(
|
|
1330
|
+
position[2] - (decorationCenter?.[2] ?? 0),
|
|
1331
|
+
position[0] - (decorationCenter?.[0] ?? 0),
|
|
1332
|
+
)}
|
|
1333
|
+
orbitRadius={Math.hypot(
|
|
1334
|
+
position[0] - (decorationCenter?.[0] ?? 0),
|
|
1335
|
+
position[2] - (decorationCenter?.[2] ?? 0),
|
|
1336
|
+
)}
|
|
1113
1337
|
y={decoration?.y?.(node as never) ?? 0}
|
|
1114
1338
|
/>
|
|
1115
1339
|
) : null}
|
|
1116
1340
|
<HandleArrow
|
|
1117
1341
|
activeCursor={dragCursor}
|
|
1118
1342
|
cursor={hoverCursor}
|
|
1119
|
-
hover={isHovered}
|
|
1343
|
+
hover={isHovered || isDirectRotating}
|
|
1120
1344
|
onHoverChange={setIsHovered}
|
|
1121
1345
|
onPointerDown={activate}
|
|
1122
1346
|
placement={{
|
|
@@ -1127,69 +1351,12 @@ function ArcArrow({
|
|
|
1127
1351
|
baseScale,
|
|
1128
1352
|
}}
|
|
1129
1353
|
shape={isRotateShape ? 'curved-arrow' : 'chevron'}
|
|
1354
|
+
thin
|
|
1130
1355
|
/>
|
|
1131
1356
|
</>
|
|
1132
1357
|
)
|
|
1133
1358
|
}
|
|
1134
1359
|
|
|
1135
|
-
// Free ground-plane move gizmo (the 4-way cross). Press-drag-release: raycast
|
|
1136
|
-
// the horizontal plane at the node's base, convert the hit into the node's
|
|
1137
|
-
// parent-local frame, add the delta to the node's drag-start position, grid-
|
|
1138
|
-
// snap via the descriptor's `snapExtents`, and publish to `useLiveNodeOverrides`
|
|
1139
|
-
// each move — committing one write to the store on release. The override stays
|
|
1140
|
-
// at base Y; `<FloorElevationSystem>` reads that effective node and owns the
|
|
1141
|
-
// presentation-only slab lift so the handle path shares the menu-move stacking
|
|
1142
|
-
// contract without storing lifted positions.
|
|
1143
|
-
function TranslateArrow({
|
|
1144
|
-
descriptor,
|
|
1145
|
-
node,
|
|
1146
|
-
}: {
|
|
1147
|
-
descriptor: TranslateHandle<AnyNode>
|
|
1148
|
-
node: AnyNode
|
|
1149
|
-
handleIndex: number
|
|
1150
|
-
dragControls: HandleDragControls
|
|
1151
|
-
rideObject: Object3D
|
|
1152
|
-
}) {
|
|
1153
|
-
const [isHovered, setIsHovered] = useState(false)
|
|
1154
|
-
const { camera } = useThree()
|
|
1155
|
-
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
1156
|
-
const baseScale = zoom * ARROW_SCALE
|
|
1157
|
-
|
|
1158
|
-
const placementSceneApi = useMemo(() => createSceneApi(useScene), [])
|
|
1159
|
-
const position = descriptor.placement.position(node, placementSceneApi)
|
|
1160
|
-
const cursor: Cursor = 'move'
|
|
1161
|
-
// 'node-normal' constrains the drag to the wall face (plane ⟂ the node's
|
|
1162
|
-
// local +Z). Its cross icon stands up into that plane (tilt about X).
|
|
1163
|
-
const isWallPlane = descriptor.plane === 'node-normal'
|
|
1164
|
-
|
|
1165
|
-
// Same function as the floating action menu's Move button
|
|
1166
|
-
// (`floating-action-menu.tsx` → `handleMove`): arm the registry move tool,
|
|
1167
|
-
// which owns the cursor follow, grid + alignment snap, green guide overlay,
|
|
1168
|
-
// and click-to-commit. Routes both entry points through one path so the
|
|
1169
|
-
// 3D translate gizmo and the floating Move button behave identically.
|
|
1170
|
-
const activate = (event: ThreeEvent<PointerEvent>) => {
|
|
1171
|
-
event.stopPropagation()
|
|
1172
|
-
sfxEmitter.emit('sfx:item-pick')
|
|
1173
|
-
useEditor.getState().setMovingNode(node as never)
|
|
1174
|
-
useViewer.getState().setSelection({ selectedIds: [] })
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// The cross is built flat in the XZ plane. On a wall, tilt it up about X so
|
|
1178
|
-
// it lies in the item-local XY plane (= the wall face).
|
|
1179
|
-
const iconRotation: [number, number, number] = isWallPlane ? [Math.PI / 2, 0, 0] : [0, 0, 0]
|
|
1180
|
-
|
|
1181
|
-
return (
|
|
1182
|
-
<HandleArrow
|
|
1183
|
-
cursor={cursor}
|
|
1184
|
-
hover={isHovered}
|
|
1185
|
-
onHoverChange={setIsHovered}
|
|
1186
|
-
onPointerDown={activate}
|
|
1187
|
-
placement={{ position, rotation: iconRotation, baseScale }}
|
|
1188
|
-
shape="cross"
|
|
1189
|
-
/>
|
|
1190
|
-
)
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
1360
|
// Click-to-engage affordance — no drag plumbing, just a click target. The
|
|
1194
1361
|
// descriptor's `onActivate` receives sceneApi + editorApi so it can engage
|
|
1195
1362
|
// move tools, endpoint drags, or any other editor-state transition without
|
|
@@ -1210,6 +1377,7 @@ function TapActionArrow({
|
|
|
1210
1377
|
const rotationY = descriptor.placement.rotationY?.(node, placementSceneApi) ?? 0
|
|
1211
1378
|
const shape = descriptor.shape ?? 'arrow'
|
|
1212
1379
|
const cursor: Cursor = descriptor.cursor ?? (shape === 'corner-picker' ? 'move' : 'ew-resize')
|
|
1380
|
+
const round = descriptor.round ?? false
|
|
1213
1381
|
|
|
1214
1382
|
const onActivate = useHandleDrag({
|
|
1215
1383
|
kind: 'tap',
|
|
@@ -1230,20 +1398,75 @@ function TapActionArrow({
|
|
|
1230
1398
|
onHoverChange={setIsHovered}
|
|
1231
1399
|
onPointerDown={onActivate}
|
|
1232
1400
|
position={position}
|
|
1401
|
+
round={round}
|
|
1233
1402
|
/>
|
|
1234
1403
|
)
|
|
1235
1404
|
}
|
|
1236
1405
|
|
|
1237
|
-
// Default 'arrow' shape — the standard chevron.
|
|
1238
1406
|
const baseScale = zoom * ARROW_SCALE
|
|
1407
|
+
// A `move-cross` with `plane: 'node-normal'` stands up into the node's facing
|
|
1408
|
+
// plane (a wall face) like the door / window / wall-item move grips; other
|
|
1409
|
+
// tap-actions keep their in-plane `rotationY`.
|
|
1410
|
+
const rotation: [number, number, number] =
|
|
1411
|
+
descriptor.plane === 'node-normal' ? NODE_NORMAL_TILT : [0, rotationY, 0]
|
|
1239
1412
|
return (
|
|
1240
1413
|
<HandleArrow
|
|
1241
1414
|
cursor={cursor}
|
|
1242
1415
|
hover={isHovered}
|
|
1243
1416
|
onHoverChange={setIsHovered}
|
|
1244
1417
|
onPointerDown={onActivate}
|
|
1418
|
+
placement={{ position, rotation, baseScale }}
|
|
1419
|
+
shape={shape === 'move-cross' ? 'move-cross' : 'chevron'}
|
|
1420
|
+
thin
|
|
1421
|
+
/>
|
|
1422
|
+
)
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Click-to-latch cube. A persistent grip (the `tracker` cube) that toggles
|
|
1426
|
+
// the visibility of every arrow tagged with its `latchGroup` on click. Sized
|
|
1427
|
+
// to match the duct selection cube (`baseScale = zoom`, full TRACKER_CUBE_SIZE)
|
|
1428
|
+
// so every latch grip reads the same across the app. Stays highlighted while
|
|
1429
|
+
// its group is open so the user can tell it's engaged.
|
|
1430
|
+
function LatchCube({
|
|
1431
|
+
descriptor,
|
|
1432
|
+
node,
|
|
1433
|
+
open,
|
|
1434
|
+
onToggle,
|
|
1435
|
+
}: {
|
|
1436
|
+
descriptor: LatchHandle<AnyNode>
|
|
1437
|
+
node: AnyNode
|
|
1438
|
+
open: boolean
|
|
1439
|
+
onToggle: (group: string) => void
|
|
1440
|
+
}) {
|
|
1441
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
1442
|
+
const { camera } = useThree()
|
|
1443
|
+
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
1444
|
+
const baseScale = zoom
|
|
1445
|
+
|
|
1446
|
+
const placementSceneApi = useMemo(() => createSceneApi(useScene), [])
|
|
1447
|
+
const position = descriptor.placement.position(node, placementSceneApi)
|
|
1448
|
+
const rotationY = descriptor.placement.rotationY?.(node, placementSceneApi) ?? 0
|
|
1449
|
+
|
|
1450
|
+
// Route through the shared tap path so the cube click is swallowed before it
|
|
1451
|
+
// reaches the select tool — stops R3F propagation, suppresses box-select, and
|
|
1452
|
+
// eats the trailing DOM click that would otherwise select the host node.
|
|
1453
|
+
const onPointerDown = useHandleDrag({
|
|
1454
|
+
kind: 'tap',
|
|
1455
|
+
onTap: () => {
|
|
1456
|
+
setIsHovered(false)
|
|
1457
|
+
onToggle(descriptor.group)
|
|
1458
|
+
},
|
|
1459
|
+
})
|
|
1460
|
+
|
|
1461
|
+
return (
|
|
1462
|
+
<HandleArrow
|
|
1463
|
+
cursor="grab"
|
|
1464
|
+
hover={isHovered || open}
|
|
1465
|
+
hoverScale={1.15}
|
|
1466
|
+
onHoverChange={setIsHovered}
|
|
1467
|
+
onPointerDown={onPointerDown}
|
|
1245
1468
|
placement={{ position, rotation: [0, rotationY, 0], baseScale }}
|
|
1246
|
-
shape="
|
|
1469
|
+
shape="tracker"
|
|
1247
1470
|
/>
|
|
1248
1471
|
)
|
|
1249
1472
|
}
|
|
@@ -1281,6 +1504,7 @@ function CornerPickerShape({
|
|
|
1281
1504
|
hover,
|
|
1282
1505
|
onHoverChange,
|
|
1283
1506
|
onPointerDown,
|
|
1507
|
+
round = false,
|
|
1284
1508
|
}: {
|
|
1285
1509
|
position: readonly [number, number, number]
|
|
1286
1510
|
height: number
|
|
@@ -1289,6 +1513,7 @@ function CornerPickerShape({
|
|
|
1289
1513
|
hover: boolean
|
|
1290
1514
|
onHoverChange: (hovered: boolean) => void
|
|
1291
1515
|
onPointerDown: (event: ThreeEvent<PointerEvent>) => void
|
|
1516
|
+
round?: boolean
|
|
1292
1517
|
}) {
|
|
1293
1518
|
const dashedGeometry = useMemo(() => buildDashedVerticalGeometry(height), [height])
|
|
1294
1519
|
useEffect(() => () => dashedGeometry.dispose(), [dashedGeometry])
|
|
@@ -1358,7 +1583,10 @@ function CornerPickerShape({
|
|
|
1358
1583
|
position={position}
|
|
1359
1584
|
renderOrder={1001}
|
|
1360
1585
|
/>
|
|
1361
|
-
<group
|
|
1586
|
+
<group
|
|
1587
|
+
position={[position[0], position[1] + CORNER_FLOOR_OFFSET, position[2]]}
|
|
1588
|
+
ref={billboardRef}
|
|
1589
|
+
>
|
|
1362
1590
|
<HandleArrow
|
|
1363
1591
|
cursor={cursor}
|
|
1364
1592
|
hover={hover}
|
|
@@ -1366,10 +1594,11 @@ function CornerPickerShape({
|
|
|
1366
1594
|
onHoverChange={onHoverChange}
|
|
1367
1595
|
onPointerDown={onPointerDown}
|
|
1368
1596
|
placement={{ position: [0, 0, 0], baseScale }}
|
|
1597
|
+
round={round}
|
|
1369
1598
|
shape="corner-picker"
|
|
1370
1599
|
/>
|
|
1371
1600
|
<mesh material={ringMaterial} renderOrder={1002} scale={scale}>
|
|
1372
|
-
<ringGeometry args={[CORNER_HEX_RADIUS, CORNER_HEX_RADIUS * 1.18, 6]} />
|
|
1601
|
+
<ringGeometry args={[CORNER_HEX_RADIUS, CORNER_HEX_RADIUS * 1.18, round ? 32 : 6]} />
|
|
1373
1602
|
</mesh>
|
|
1374
1603
|
</group>
|
|
1375
1604
|
</>
|