@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
|
@@ -5,23 +5,31 @@ import {
|
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type CeilingNode,
|
|
7
7
|
ColumnNode,
|
|
8
|
-
|
|
8
|
+
createSceneApi,
|
|
9
9
|
DoorNode,
|
|
10
10
|
ElevatorNode,
|
|
11
|
+
emitter,
|
|
11
12
|
FenceNode,
|
|
12
13
|
generateId,
|
|
14
|
+
getActiveRoofHeight,
|
|
15
|
+
getEffectiveNode,
|
|
13
16
|
getWallCurveLength,
|
|
17
|
+
getWallEffectiveHeightForNodes,
|
|
14
18
|
getWallThickness,
|
|
15
19
|
ItemNode,
|
|
20
|
+
isCurvedWall,
|
|
16
21
|
isRegistryMovable,
|
|
17
22
|
isRegistrySelectable,
|
|
23
|
+
isSplineFence,
|
|
24
|
+
type NodeQuickAction,
|
|
18
25
|
nodeRegistry,
|
|
19
26
|
RoofSegmentNode,
|
|
27
|
+
runAsSingleSceneHistoryStep,
|
|
20
28
|
type SlabNode,
|
|
21
29
|
SpawnNode,
|
|
22
|
-
StairNode,
|
|
23
30
|
StairSegmentNode,
|
|
24
31
|
sceneRegistry,
|
|
32
|
+
summarizeSystemFor,
|
|
25
33
|
useLiveNodeOverrides,
|
|
26
34
|
useScene,
|
|
27
35
|
WallNode,
|
|
@@ -30,15 +38,48 @@ import {
|
|
|
30
38
|
import { useViewer } from '@pascal-app/viewer'
|
|
31
39
|
import { Html } from '@react-three/drei'
|
|
32
40
|
import { useFrame } from '@react-three/fiber'
|
|
33
|
-
import { useCallback, useRef } from 'react'
|
|
41
|
+
import { useCallback, useMemo, useRef } from 'react'
|
|
34
42
|
import * as THREE from 'three'
|
|
43
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
44
|
+
import { useReducedMotion } from '../../hooks/use-reduced-motion'
|
|
45
|
+
import { resolveMoveActionNode } from '../../lib/direct-manipulation'
|
|
46
|
+
import {
|
|
47
|
+
createFreshPlacementSubtree,
|
|
48
|
+
duplicatesAsFreshSubtree,
|
|
49
|
+
prepareFreshPlacementRootDuplicate,
|
|
50
|
+
} from '../../lib/fresh-planar-placement'
|
|
51
|
+
import { resolveOverlayPolicy } from '../../lib/interaction/overlay-policy'
|
|
52
|
+
import { curveReshapeScope, holeEditScope } from '../../lib/interaction/scope'
|
|
53
|
+
import { playBlockedQuickActionFeedback } from '../../lib/quick-action-feedback'
|
|
54
|
+
import { collectQuickActionNodeScope } from '../../lib/quick-action-nodes'
|
|
35
55
|
import { duplicateRoofSubtree } from '../../lib/roof-duplication'
|
|
36
56
|
import { emitDeleteSFX, sfxEmitter } from '../../lib/sfx-bus'
|
|
37
|
-
import {
|
|
57
|
+
import { cn } from '../../lib/utils'
|
|
38
58
|
import useEditor from '../../store/use-editor'
|
|
59
|
+
import useInteractionScope, {
|
|
60
|
+
useActiveHandleDrag,
|
|
61
|
+
useEndpointReshape,
|
|
62
|
+
useIsCurveReshape,
|
|
63
|
+
} from '../../store/use-interaction-scope'
|
|
64
|
+
import { IconRefGlyph } from '../ui/icon-ref'
|
|
39
65
|
import { formatMeasurement, MeasurementPill } from './measurement-pill'
|
|
40
66
|
import { NodeActionMenu } from './node-action-menu'
|
|
41
67
|
|
|
68
|
+
/**
|
|
69
|
+
* A kind shows the system pill when it exposes typed ports — `def.ports`
|
|
70
|
+
* is exactly what makes a node participate in the supply/return graph the
|
|
71
|
+
* pill summarizes. Keeps the menu off a hand-maintained kind list.
|
|
72
|
+
*/
|
|
73
|
+
const hasPorts = (type: string) => nodeRegistry.get(type)?.ports != null
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A kind shows the rotation-axis pill when its R/T keyboard rotation
|
|
77
|
+
* turns around a user-cyclable axis (`keyboardActions.axisCycling`) —
|
|
78
|
+
* duct / pipe fittings with full 3D orientation.
|
|
79
|
+
*/
|
|
80
|
+
const hasAxisCycling = (type: string) =>
|
|
81
|
+
nodeRegistry.get(type)?.keyboardActions?.axisCycling === true
|
|
82
|
+
|
|
42
83
|
const ALLOWED_TYPES = [
|
|
43
84
|
'item',
|
|
44
85
|
'door',
|
|
@@ -111,6 +152,110 @@ function getMenuYOffset(node: AnyNode | null): number {
|
|
|
111
152
|
return (MENU_Y_OFFSETS[node.type] ?? MENU_Y_OFFSET_DEFAULT) + EXTRA_MENU_LIFT
|
|
112
153
|
}
|
|
113
154
|
|
|
155
|
+
function getAttributeVersion(
|
|
156
|
+
attribute: THREE.BufferAttribute | THREE.InterleavedBufferAttribute | null | undefined,
|
|
157
|
+
): number {
|
|
158
|
+
return attribute && 'version' in attribute && typeof attribute.version === 'number'
|
|
159
|
+
? attribute.version
|
|
160
|
+
: 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function SideAddGlyph({ direction }: { direction: 'left' | 'right' }) {
|
|
164
|
+
return (
|
|
165
|
+
<svg
|
|
166
|
+
aria-hidden="true"
|
|
167
|
+
className="h-3.5 w-3.5"
|
|
168
|
+
fill="none"
|
|
169
|
+
stroke="currentColor"
|
|
170
|
+
strokeLinecap="round"
|
|
171
|
+
strokeLinejoin="round"
|
|
172
|
+
strokeWidth="1.7"
|
|
173
|
+
viewBox="0 0 24 24"
|
|
174
|
+
>
|
|
175
|
+
<rect x="8" y="7" width="9" height="10" rx="1.75" />
|
|
176
|
+
<path d="M12.5 7v10" />
|
|
177
|
+
{direction === 'left' ? (
|
|
178
|
+
<>
|
|
179
|
+
<path d="M6.5 12H2.75" />
|
|
180
|
+
<path d="m5.5 9.75-2.75 2.25 2.75 2.25" />
|
|
181
|
+
</>
|
|
182
|
+
) : (
|
|
183
|
+
<>
|
|
184
|
+
<path d="M17.5 12h3.75" />
|
|
185
|
+
<path d="m18.5 9.75 2.75 2.25-2.75 2.25" />
|
|
186
|
+
</>
|
|
187
|
+
)}
|
|
188
|
+
</svg>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Builtin string tokens map to the generic glyphs above; kind-owned marks
|
|
193
|
+
// arrive as IconRef objects and render through the shared IconRefGlyph.
|
|
194
|
+
function QuickActionIcon({ action }: { action: NodeQuickAction }) {
|
|
195
|
+
const icon = action.icon
|
|
196
|
+
if (!icon) return null
|
|
197
|
+
if (typeof icon === 'object') return <IconRefGlyph icon={icon} size={14} />
|
|
198
|
+
switch (icon) {
|
|
199
|
+
case 'add-left':
|
|
200
|
+
return <SideAddGlyph direction="left" />
|
|
201
|
+
case 'add-right':
|
|
202
|
+
return <SideAddGlyph direction="right" />
|
|
203
|
+
default:
|
|
204
|
+
return null
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function collectQuickActionNodes(
|
|
209
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
210
|
+
selectedId: string | null,
|
|
211
|
+
): Record<AnyNodeId, AnyNode> | null {
|
|
212
|
+
if (!selectedId) return null
|
|
213
|
+
const selected = nodes[selectedId as AnyNodeId]
|
|
214
|
+
const def = selected ? nodeRegistry.get(selected.type) : undefined
|
|
215
|
+
if (!def?.quickActions) return null
|
|
216
|
+
return collectQuickActionNodeScope(nodes, selectedId, def.quickActionNodeScope)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Pooled scratch for the per-frame anchor recompute (see useFrame below) so a
|
|
220
|
+
// dragged node doesn't allocate a fresh Box3 + Vector3 every frame.
|
|
221
|
+
const _anchorBox = new THREE.Box3()
|
|
222
|
+
const _anchorCenter = new THREE.Vector3()
|
|
223
|
+
|
|
224
|
+
function getObjectGeometryKey(object: THREE.Object3D): string {
|
|
225
|
+
const parts: string[] = []
|
|
226
|
+
object.traverse((child) => {
|
|
227
|
+
const geometry = (child as Partial<THREE.Mesh>).geometry
|
|
228
|
+
if (!geometry) return
|
|
229
|
+
|
|
230
|
+
parts.push(
|
|
231
|
+
[
|
|
232
|
+
geometry.id,
|
|
233
|
+
getAttributeVersion(geometry.getAttribute('position')),
|
|
234
|
+
getAttributeVersion(geometry.getIndex()),
|
|
235
|
+
].join(':'),
|
|
236
|
+
)
|
|
237
|
+
})
|
|
238
|
+
return parts.join('|')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function setNodeDerivedMenuAnchor(
|
|
242
|
+
node: AnyNode,
|
|
243
|
+
object: THREE.Object3D,
|
|
244
|
+
target: THREE.Vector3,
|
|
245
|
+
): boolean {
|
|
246
|
+
if (node.type !== 'roof-segment') return false
|
|
247
|
+
|
|
248
|
+
const visualTop =
|
|
249
|
+
node.wallHeight +
|
|
250
|
+
getActiveRoofHeight(node) +
|
|
251
|
+
Math.max(0, node.deckThickness ?? 0) +
|
|
252
|
+
Math.max(0, node.shingleThickness ?? 0)
|
|
253
|
+
|
|
254
|
+
target.set(0, visualTop, 0).applyMatrix4(object.matrixWorld)
|
|
255
|
+
target.y += getMenuYOffset(node)
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
|
|
114
259
|
// Fence schema defaults — mirror packages/nodes/src/fence/definition.ts so the
|
|
115
260
|
// pill reads sensibly before an explicit height / thickness is set.
|
|
116
261
|
const FENCE_DEFAULT_HEIGHT = 1.8
|
|
@@ -125,7 +270,7 @@ function getHeightPillDimensions(node: WallNode | FenceNode): {
|
|
|
125
270
|
} {
|
|
126
271
|
if (node.type === 'wall') {
|
|
127
272
|
return {
|
|
128
|
-
height: node.
|
|
273
|
+
height: getWallEffectiveHeightForNodes(node, useScene.getState().nodes),
|
|
129
274
|
length: getWallCurveLength(node),
|
|
130
275
|
thickness: getWallThickness(node),
|
|
131
276
|
}
|
|
@@ -138,23 +283,27 @@ function getHeightPillDimensions(node: WallNode | FenceNode): {
|
|
|
138
283
|
}
|
|
139
284
|
|
|
140
285
|
export function FloatingActionMenu() {
|
|
286
|
+
const reducedMotion = useReducedMotion()
|
|
141
287
|
const selectedIds = useViewer((s) => s.selection.selectedIds)
|
|
142
288
|
const updateNode = useScene((s) => s.updateNode)
|
|
143
289
|
const mode = useEditor((s) => s.mode)
|
|
144
290
|
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
291
|
+
const canFindNode = useEditor((s) => s.canFindNode)
|
|
292
|
+
const endpointReshape = useEndpointReshape()
|
|
293
|
+
const isCurveReshape = useIsCurveReshape()
|
|
148
294
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
149
|
-
const setCurvingWall = useEditor((s) => s.setCurvingWall)
|
|
150
|
-
const setCurvingFence = useEditor((s) => s.setCurvingFence)
|
|
151
295
|
const setSelection = useViewer((s) => s.setSelection)
|
|
152
|
-
const setEditingHole = useEditor((s) => s.setEditingHole)
|
|
153
296
|
const unit = useViewer((s) => s.unit)
|
|
154
297
|
// Drives the height-drag dimension pill below the menu. `activeHandleDrag`
|
|
155
298
|
// flips only at drag start / end, so subscribing here is cheap — the live
|
|
156
299
|
// height value is written imperatively in the useFrame below.
|
|
157
|
-
const activeHandleDrag =
|
|
300
|
+
const activeHandleDrag = useActiveHandleDrag()
|
|
301
|
+
// R/T rotation axis for kinds with full 3D orientation (duct fittings).
|
|
302
|
+
const rotationAxis = useEditor((s) => s.rotationAxis)
|
|
303
|
+
// The floating action menu is an action-conflicting control: hard-hidden
|
|
304
|
+
// during any active interaction so it never competes with the live action.
|
|
305
|
+
const scope = useInteractionScope((s) => s.scope)
|
|
306
|
+
const menuStepBack = resolveOverlayPolicy(scope).conflictingControls === 'hidden'
|
|
158
307
|
|
|
159
308
|
const groupRef = useRef<THREE.Group>(null)
|
|
160
309
|
const menuScaleRef = useRef<HTMLDivElement>(null)
|
|
@@ -171,22 +320,39 @@ export function FloatingActionMenu() {
|
|
|
171
320
|
const anchorRef = useRef(new THREE.Vector3())
|
|
172
321
|
const hasAnchorRef = useRef(false)
|
|
173
322
|
const lastMatrixRef = useRef(new THREE.Matrix4())
|
|
174
|
-
const lastAnchorKeyRef = useRef<{
|
|
323
|
+
const lastAnchorKeyRef = useRef<{
|
|
324
|
+
id: string | null
|
|
325
|
+
node: AnyNode | null
|
|
326
|
+
geometryKey: string | null
|
|
327
|
+
}>({
|
|
175
328
|
id: null,
|
|
176
329
|
node: null,
|
|
330
|
+
geometryKey: null,
|
|
177
331
|
})
|
|
178
332
|
|
|
179
333
|
// Only show for single selection of specific types
|
|
180
|
-
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
334
|
+
const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null
|
|
181
335
|
|
|
182
336
|
// Subscribe just to the selected node so unrelated scene updates do not
|
|
183
337
|
// re-render this menu.
|
|
184
338
|
const node = useScene((s) => (selectedId ? (s.nodes[selectedId as AnyNodeId] ?? null) : null))
|
|
339
|
+
const quickActionNodes = useScene(useShallow((s) => collectQuickActionNodes(s.nodes, selectedId)))
|
|
340
|
+
const quickActions = useMemo<NodeQuickAction[]>(
|
|
341
|
+
() =>
|
|
342
|
+
node && quickActionNodes
|
|
343
|
+
? (nodeRegistry.get(node.type)?.quickActions?.({
|
|
344
|
+
node: node as never,
|
|
345
|
+
nodes: quickActionNodes,
|
|
346
|
+
}) ?? [])
|
|
347
|
+
: [],
|
|
348
|
+
[node, quickActionNodes],
|
|
349
|
+
)
|
|
185
350
|
// ALLOWED_TYPES is the hardcoded set; registry-driven kinds (any
|
|
186
351
|
// NodeDefinition with `capabilities.selectable`) get the floating menu
|
|
187
352
|
// by default too. Phase 4 collapses these into a single registry check.
|
|
188
353
|
const isValidType = node
|
|
189
|
-
?
|
|
354
|
+
? nodeRegistry.get(node.type)?.presentation?.actionMenu !== false &&
|
|
355
|
+
(ALLOWED_TYPES.includes(node.type) || isRegistrySelectable(node.type))
|
|
190
356
|
: false
|
|
191
357
|
|
|
192
358
|
// Height-drag pill: shown just above the menu only while the selected
|
|
@@ -218,7 +384,7 @@ export function FloatingActionMenu() {
|
|
|
218
384
|
})
|
|
219
385
|
|
|
220
386
|
useFrame((state) => {
|
|
221
|
-
if (!(selectedId && isValidType && groupRef.current)) return
|
|
387
|
+
if (!(selectedId && node && isValidType && groupRef.current)) return
|
|
222
388
|
|
|
223
389
|
// Scale the HTML menu with camera zoom (ortho) or inverse distance
|
|
224
390
|
// (perspective) so it feels anchored to the world, clamped on both ends
|
|
@@ -246,36 +412,69 @@ export function FloatingActionMenu() {
|
|
|
246
412
|
const override = useLiveNodeOverrides.getState().overrides.get(selectedId) as
|
|
247
413
|
| { height?: number }
|
|
248
414
|
| undefined
|
|
249
|
-
const fallbackHeight =
|
|
415
|
+
const fallbackHeight =
|
|
416
|
+
node.type === 'wall'
|
|
417
|
+
? getWallEffectiveHeightForNodes(node, useScene.getState().nodes)
|
|
418
|
+
: FENCE_DEFAULT_HEIGHT
|
|
250
419
|
const liveHeight = override?.height ?? node.height ?? fallbackHeight
|
|
251
420
|
pillHeightRef.current.textContent = `H ${formatMeasurement(liveHeight, unit)}`
|
|
252
421
|
}
|
|
253
422
|
|
|
254
423
|
const obj = sceneRegistry.nodes.get(selectedId)
|
|
255
424
|
if (obj) {
|
|
425
|
+
obj.updateWorldMatrix(true, false)
|
|
426
|
+
|
|
256
427
|
// Recompute the anchor only when the object genuinely changes —
|
|
257
428
|
// reselected, moved (its own world matrix changed), or resized
|
|
258
429
|
// (a fresh store node on commit, or a live override / handle drag
|
|
259
430
|
// mid-resize). A spinning child changes the head's matrix, not the
|
|
260
431
|
// registered group's, so it never triggers a recompute → the menu
|
|
261
432
|
// holds still.
|
|
433
|
+
// Cheapest guards first: a selection swap, the object's own world
|
|
434
|
+
// transform changing (true every frame during a drag), a live override,
|
|
435
|
+
// or an active handle drag all force a recompute on their own — so skip
|
|
436
|
+
// the geometry traversal (`getObjectGeometryKey` walks the whole subtree
|
|
437
|
+
// reading attribute versions) until none of them fired and a
|
|
438
|
+
// geometry-only change is the only thing left that could move the anchor.
|
|
262
439
|
const overrideActive = useLiveNodeOverrides.getState().overrides.get(selectedId) != null
|
|
263
440
|
const dragActive = activeHandleDrag?.nodeId === selectedId
|
|
264
441
|
const selectionChanged =
|
|
265
442
|
lastAnchorKeyRef.current.id !== selectedId || lastAnchorKeyRef.current.node !== node
|
|
266
443
|
const matrixChanged = !lastMatrixRef.current.equals(obj.matrixWorld)
|
|
267
444
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
445
|
+
let geometryKey = lastAnchorKeyRef.current.geometryKey
|
|
446
|
+
let needsRecompute = selectionChanged || matrixChanged || overrideActive || dragActive
|
|
447
|
+
// Only when nothing cheaper fired do we pay for the subtree traversal —
|
|
448
|
+
// a geometry-only change is the lone remaining trigger. When a cheaper
|
|
449
|
+
// guard already forced a recompute the stored key is reused; the matrix
|
|
450
|
+
// (or override/drag) keeps recomputing the anchor every frame, so a
|
|
451
|
+
// geometry edit mid-drag is absorbed, and the next idle frame refreshes
|
|
452
|
+
// the key against the live geometry.
|
|
453
|
+
if (!needsRecompute) {
|
|
454
|
+
geometryKey = getObjectGeometryKey(obj)
|
|
455
|
+
if (geometryKey !== lastAnchorKeyRef.current.geometryKey) needsRecompute = true
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (needsRecompute) {
|
|
459
|
+
const effectiveNode = getEffectiveNode(node)
|
|
460
|
+
if (!setNodeDerivedMenuAnchor(effectiveNode, obj, anchorRef.current)) {
|
|
461
|
+
_anchorBox.setFromObject(obj)
|
|
462
|
+
if (!_anchorBox.isEmpty()) {
|
|
463
|
+
_anchorBox.getCenter(_anchorCenter)
|
|
464
|
+
// Position above the object. Per-type offsets clear each kind's
|
|
465
|
+
// in-world chrome (height-resize arrows, measurement labels).
|
|
466
|
+
anchorRef.current.set(
|
|
467
|
+
_anchorCenter.x,
|
|
468
|
+
_anchorBox.max.y + getMenuYOffset(effectiveNode),
|
|
469
|
+
_anchorCenter.z,
|
|
470
|
+
)
|
|
471
|
+
hasAnchorRef.current = true
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
275
474
|
hasAnchorRef.current = true
|
|
276
475
|
}
|
|
277
476
|
lastMatrixRef.current.copy(obj.matrixWorld)
|
|
278
|
-
lastAnchorKeyRef.current = { id: selectedId, node }
|
|
477
|
+
lastAnchorKeyRef.current = { id: selectedId, node, geometryKey }
|
|
279
478
|
}
|
|
280
479
|
|
|
281
480
|
if (hasAnchorRef.current) {
|
|
@@ -291,22 +490,23 @@ export function FloatingActionMenu() {
|
|
|
291
490
|
sfxEmitter.emit('sfx:item-pick')
|
|
292
491
|
if (node.type === 'wall') {
|
|
293
492
|
if (!canCurveSelectedWall) return
|
|
294
|
-
|
|
493
|
+
useInteractionScope.getState().begin(curveReshapeScope(node.id))
|
|
295
494
|
} else if (node.type === 'fence') {
|
|
296
|
-
|
|
495
|
+
useInteractionScope.getState().begin(curveReshapeScope(node.id))
|
|
297
496
|
} else {
|
|
298
497
|
return
|
|
299
498
|
}
|
|
300
499
|
setSelection({ selectedIds: [] })
|
|
301
500
|
},
|
|
302
|
-
[canCurveSelectedWall, node,
|
|
501
|
+
[canCurveSelectedWall, node, setSelection],
|
|
303
502
|
)
|
|
304
503
|
const handleMove = useCallback(
|
|
305
504
|
(e: React.MouseEvent) => {
|
|
306
505
|
e.stopPropagation()
|
|
307
506
|
if (!node) return
|
|
308
507
|
sfxEmitter.emit('sfx:item-pick')
|
|
309
|
-
|
|
508
|
+
const sceneNodes = useScene.getState().nodes
|
|
509
|
+
setMovingNode(resolveMoveActionNode(node, sceneNodes) as any)
|
|
310
510
|
setSelection({ selectedIds: [] })
|
|
311
511
|
},
|
|
312
512
|
[node, setMovingNode, setSelection],
|
|
@@ -328,9 +528,27 @@ export function FloatingActionMenu() {
|
|
|
328
528
|
|
|
329
529
|
useScene.temporal.getState().pause()
|
|
330
530
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
531
|
+
if (duplicatesAsFreshSubtree(node as AnyNode)) {
|
|
532
|
+
let draftId: AnyNodeId | null = null
|
|
533
|
+
try {
|
|
534
|
+
draftId = createFreshPlacementSubtree(node.id as AnyNodeId)
|
|
535
|
+
const draft = draftId ? useScene.getState().nodes[draftId] : null
|
|
536
|
+
if (draft) {
|
|
537
|
+
setMovingNode(draft as any)
|
|
538
|
+
setSelection({ selectedIds: [] })
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if (draftId && useScene.getState().nodes[draftId]) {
|
|
543
|
+
useScene.getState().deleteNode(draftId)
|
|
544
|
+
}
|
|
545
|
+
console.error('Failed to duplicate node subtree', error)
|
|
546
|
+
}
|
|
547
|
+
useScene.temporal.getState().resume()
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const duplicateInfo = prepareFreshPlacementRootDuplicate(node as AnyNode) as any
|
|
334
552
|
|
|
335
553
|
let duplicate: AnyNode | null = null
|
|
336
554
|
try {
|
|
@@ -353,11 +571,6 @@ export function FloatingActionMenu() {
|
|
|
353
571
|
} else if (node.type === 'roof-segment') {
|
|
354
572
|
duplicateInfo.id = generateId('rseg')
|
|
355
573
|
duplicate = RoofSegmentNode.parse(duplicateInfo)
|
|
356
|
-
} else if (node.type === 'stair') {
|
|
357
|
-
duplicateInfo.children = []
|
|
358
|
-
duplicateInfo.metadata = { ...duplicateInfo.metadata }
|
|
359
|
-
delete duplicateInfo.metadata?.isNew
|
|
360
|
-
duplicate = StairNode.parse(duplicateInfo)
|
|
361
574
|
} else if (node.type === 'stair-segment') {
|
|
362
575
|
duplicate = StairSegmentNode.parse(duplicateInfo)
|
|
363
576
|
} else if (node.type === 'spawn') {
|
|
@@ -395,11 +608,7 @@ export function FloatingActionMenu() {
|
|
|
395
608
|
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
396
609
|
} else if (duplicate.type === 'fence') {
|
|
397
610
|
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
398
|
-
} else if (
|
|
399
|
-
duplicate.type === 'roof-segment' ||
|
|
400
|
-
duplicate.type === 'stair' ||
|
|
401
|
-
duplicate.type === 'stair-segment'
|
|
402
|
-
) {
|
|
611
|
+
} else if (duplicate.type === 'roof-segment' || duplicate.type === 'stair-segment') {
|
|
403
612
|
// Add small offset to make it visible
|
|
404
613
|
if ('position' in duplicate) {
|
|
405
614
|
duplicate.position = [
|
|
@@ -408,13 +617,7 @@ export function FloatingActionMenu() {
|
|
|
408
617
|
duplicate.position[2] + 1,
|
|
409
618
|
]
|
|
410
619
|
}
|
|
411
|
-
|
|
412
|
-
duplicateStairSubtree(node.id as AnyNodeId, { mode: 'move' })
|
|
413
|
-
} else {
|
|
414
|
-
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// Duplicate children for stair nodes
|
|
620
|
+
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
418
621
|
} else if (
|
|
419
622
|
duplicate.type === 'item' ||
|
|
420
623
|
duplicate.type === 'chimney' ||
|
|
@@ -431,10 +634,26 @@ export function FloatingActionMenu() {
|
|
|
431
634
|
// item without clicking" bug. (Item has its own
|
|
432
635
|
// draft-committing move tool, so it must skip the generic
|
|
433
636
|
// registry auto-create branch below.)
|
|
637
|
+
} else if (
|
|
638
|
+
duplicate.type === 'duct-segment' ||
|
|
639
|
+
duplicate.type === 'duct-fitting' ||
|
|
640
|
+
duplicate.type === 'pipe-segment' ||
|
|
641
|
+
duplicate.type === 'lineset' ||
|
|
642
|
+
duplicate.type === 'liquid-line'
|
|
643
|
+
) {
|
|
644
|
+
// Duct runs & fittings, DWV pipe runs, and refrigerant linesets use
|
|
645
|
+
// pure drag-to-place: NO node is inserted into the scene until the
|
|
646
|
+
// commit click. `setMovingNode` below hands the clone (with
|
|
647
|
+
// `metadata.isNew`) to its ghost tool (`MoveDuctSegmentTool` /
|
|
648
|
+
// `MoveDuctFittingTool` / `MovePipeSegmentTool` / `MoveLinesetTool`),
|
|
649
|
+
// which previews a translucent copy inside a footprint bounding box
|
|
650
|
+
// on the cursor and calls `createNode` on the drop click.
|
|
651
|
+
// Pre-creating here would drop a copy before any click — the
|
|
652
|
+
// "auto-places it" bug.
|
|
434
653
|
} else if (nodeRegistry.has(duplicate.type)) {
|
|
435
|
-
// Registry-driven kinds: offset
|
|
436
|
-
//
|
|
437
|
-
//
|
|
654
|
+
// Registry-driven kinds: offset slightly so the duplicate doesn't
|
|
655
|
+
// overlap exactly, then create + hand to the move tool. Mirrors the
|
|
656
|
+
// roof-segment / stair-segment behavior.
|
|
438
657
|
if ('position' in duplicate && Array.isArray((duplicate as any).position)) {
|
|
439
658
|
const pos = (duplicate as { position: [number, number, number] }).position
|
|
440
659
|
;(duplicate as { position: [number, number, number] }).position = [
|
|
@@ -442,6 +661,12 @@ export function FloatingActionMenu() {
|
|
|
442
661
|
pos[1],
|
|
443
662
|
pos[2] + 1,
|
|
444
663
|
]
|
|
664
|
+
} else if ('path' in duplicate && Array.isArray((duplicate as any).path)) {
|
|
665
|
+
// Other polyline kinds (pipe / lineset) carry a `path`, not a
|
|
666
|
+
// `position`. Create the copy HIDDEN so nothing is auto-placed:
|
|
667
|
+
// their shared path mover reveals it as a cursor-following
|
|
668
|
+
// preview on the first mouse move and commits on the next click.
|
|
669
|
+
;(duplicate as { visible?: boolean }).visible = false
|
|
445
670
|
}
|
|
446
671
|
useScene.getState().createNode(duplicate, duplicate.parentId as AnyNodeId)
|
|
447
672
|
}
|
|
@@ -461,12 +686,8 @@ export function FloatingActionMenu() {
|
|
|
461
686
|
nodeRegistry.has(duplicate.type)
|
|
462
687
|
) {
|
|
463
688
|
setMovingNode(duplicate as any)
|
|
464
|
-
} else if (duplicate.type === 'stair') {
|
|
465
|
-
setSelection({ selectedIds: [duplicate.id as AnyNodeId] })
|
|
466
|
-
}
|
|
467
|
-
if (duplicate.type !== 'stair') {
|
|
468
|
-
setSelection({ selectedIds: [] })
|
|
469
689
|
}
|
|
690
|
+
setSelection({ selectedIds: [] })
|
|
470
691
|
}
|
|
471
692
|
},
|
|
472
693
|
[node, setMovingNode, setSelection],
|
|
@@ -503,11 +724,13 @@ export function FloatingActionMenu() {
|
|
|
503
724
|
holes: [...currentHoles, newHole],
|
|
504
725
|
holeMetadata: [...currentMetadata, { source: 'manual' }],
|
|
505
726
|
})
|
|
506
|
-
|
|
727
|
+
useInteractionScope
|
|
728
|
+
.getState()
|
|
729
|
+
.begin(holeEditScope({ nodeId: selectedId, holeIndex: currentHoles.length }))
|
|
507
730
|
// Re-assert selection so the node stays selected
|
|
508
731
|
setSelection({ selectedIds: [selectedId] })
|
|
509
732
|
},
|
|
510
|
-
[node, selectedId, updateNode,
|
|
733
|
+
[node, selectedId, updateNode, setSelection],
|
|
511
734
|
)
|
|
512
735
|
|
|
513
736
|
const handleDelete = useCallback(
|
|
@@ -521,11 +744,43 @@ export function FloatingActionMenu() {
|
|
|
521
744
|
[node?.type, selectedId, setSelection],
|
|
522
745
|
)
|
|
523
746
|
|
|
747
|
+
// "Find in catalog": the editor only signals intent — the host (community)
|
|
748
|
+
// listens for `selection:find-node` and reveals the node in its browser.
|
|
749
|
+
const handleFind = useCallback(
|
|
750
|
+
(e: React.MouseEvent) => {
|
|
751
|
+
e.stopPropagation()
|
|
752
|
+
if (node) emitter.emit('selection:find-node', node)
|
|
753
|
+
},
|
|
754
|
+
[node],
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
const handleQuickAction = useCallback(
|
|
758
|
+
(action: NodeQuickAction) => (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
759
|
+
e.stopPropagation()
|
|
760
|
+
if (!node) return
|
|
761
|
+
if (action.disabled) {
|
|
762
|
+
if (action.blockedFeedback) {
|
|
763
|
+
playBlockedQuickActionFeedback(e.currentTarget, reducedMotion)
|
|
764
|
+
}
|
|
765
|
+
return
|
|
766
|
+
}
|
|
767
|
+
const run = () => action.run({ node, sceneApi: createSceneApi(useScene) })
|
|
768
|
+
const result =
|
|
769
|
+
action.history === 'single' ? runAsSingleSceneHistoryStep(useScene, run) : run()
|
|
770
|
+
if (result?.selectedIds) setSelection({ selectedIds: result.selectedIds })
|
|
771
|
+
if (result?.selectedIds) {
|
|
772
|
+
const selectedDifferentNode = result.selectedIds.some((id) => id !== node.id)
|
|
773
|
+
sfxEmitter.emit(selectedDifferentNode ? 'sfx:item-place' : 'sfx:item-pick')
|
|
774
|
+
}
|
|
775
|
+
},
|
|
776
|
+
[node, reducedMotion, setSelection],
|
|
777
|
+
)
|
|
778
|
+
|
|
524
779
|
if (
|
|
525
780
|
!(selectedId && node && isValidType && !isFloorplanHovered && mode !== 'delete') ||
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
781
|
+
endpointReshape ||
|
|
782
|
+
isCurveReshape ||
|
|
783
|
+
menuStepBack
|
|
529
784
|
)
|
|
530
785
|
return null
|
|
531
786
|
|
|
@@ -540,11 +795,17 @@ export function FloatingActionMenu() {
|
|
|
540
795
|
}}
|
|
541
796
|
zIndexRange={[25, 0]}
|
|
542
797
|
>
|
|
543
|
-
<div
|
|
798
|
+
<div
|
|
799
|
+
className="relative flex flex-col items-center"
|
|
800
|
+
ref={menuScaleRef}
|
|
801
|
+
style={{ transformOrigin: 'center center' }}
|
|
802
|
+
>
|
|
544
803
|
<NodeActionMenu
|
|
804
|
+
onFind={node && canFindNode ? handleFind : undefined}
|
|
545
805
|
onAddHole={node && HOLE_TYPES.includes(node.type) ? handleAddHole : undefined}
|
|
546
806
|
onCurve={
|
|
547
|
-
node?.type === 'fence'
|
|
807
|
+
(node?.type === 'fence' && !isSplineFence(node) && !isCurvedWall(node)) ||
|
|
808
|
+
(node?.type === 'wall' && canCurveSelectedWall)
|
|
548
809
|
? handleCurve
|
|
549
810
|
: undefined
|
|
550
811
|
}
|
|
@@ -567,6 +828,39 @@ export function FloatingActionMenu() {
|
|
|
567
828
|
onPointerDown={(e) => e.stopPropagation()}
|
|
568
829
|
onPointerUp={(e) => e.stopPropagation()}
|
|
569
830
|
/>
|
|
831
|
+
{quickActions.length > 0 ? (
|
|
832
|
+
<div
|
|
833
|
+
className="pointer-events-auto mt-1 inline-flex w-max items-center justify-center gap-0.5 rounded-lg border border-border/50 bg-background/90 px-1.5 py-1 shadow-md backdrop-blur-md"
|
|
834
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
835
|
+
onPointerUp={(e) => e.stopPropagation()}
|
|
836
|
+
>
|
|
837
|
+
{quickActions.map((action) => (
|
|
838
|
+
<button
|
|
839
|
+
aria-disabled={action.disabled || undefined}
|
|
840
|
+
aria-label={action.title ?? action.label}
|
|
841
|
+
className={cn(
|
|
842
|
+
'tooltip-trigger flex items-center rounded-md px-2 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-accent/60 hover:text-foreground',
|
|
843
|
+
action.disabled &&
|
|
844
|
+
'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground',
|
|
845
|
+
)}
|
|
846
|
+
disabled={action.disabled && !action.blockedFeedback}
|
|
847
|
+
key={action.id}
|
|
848
|
+
onClick={handleQuickAction(action)}
|
|
849
|
+
title={action.title ?? action.label}
|
|
850
|
+
type="button"
|
|
851
|
+
>
|
|
852
|
+
<span className="flex items-center gap-1.5" data-quick-action-feedback>
|
|
853
|
+
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center text-current">
|
|
854
|
+
<QuickActionIcon action={action} />
|
|
855
|
+
</span>
|
|
856
|
+
<span className="whitespace-nowrap leading-none" data-quick-action-label>
|
|
857
|
+
{action.label}
|
|
858
|
+
</span>
|
|
859
|
+
</span>
|
|
860
|
+
</button>
|
|
861
|
+
))}
|
|
862
|
+
</div>
|
|
863
|
+
) : null}
|
|
570
864
|
{/* Height-drag dimension pill. Absolutely positioned just above
|
|
571
865
|
the menu (away from the height arrow below it) so it rides the
|
|
572
866
|
same scale transform + anchor, never overlaps the menu, and
|
|
@@ -584,9 +878,86 @@ export function FloatingActionMenu() {
|
|
|
584
878
|
/>
|
|
585
879
|
</div>
|
|
586
880
|
) : null}
|
|
881
|
+
{/* HVAC chrome above the menu — same slot as the wall height
|
|
882
|
+
pill. System pill (which tree, run length, equipment reach)
|
|
883
|
+
for every distribution kind; the rotation-axis pill stacks
|
|
884
|
+
under it for duct fittings. */}
|
|
885
|
+
{node && hasPorts(node.type) ? (
|
|
886
|
+
<div className="-translate-x-1/2 pointer-events-none absolute bottom-full left-1/2 mb-2 flex flex-col items-center gap-1">
|
|
887
|
+
<SystemSummaryPill nodeId={node.id} unit={unit} />
|
|
888
|
+
{hasAxisCycling(node.type) ? (
|
|
889
|
+
<div className="flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur">
|
|
890
|
+
<span className="font-medium text-foreground">
|
|
891
|
+
Axis {rotationAxis.toUpperCase()}
|
|
892
|
+
</span>
|
|
893
|
+
<span aria-hidden className="text-muted-foreground">
|
|
894
|
+
·
|
|
895
|
+
</span>
|
|
896
|
+
<span className="text-muted-foreground">R/T rotate</span>
|
|
897
|
+
<span aria-hidden className="text-muted-foreground">
|
|
898
|
+
·
|
|
899
|
+
</span>
|
|
900
|
+
<span className="text-muted-foreground">⌥ axis</span>
|
|
901
|
+
</div>
|
|
902
|
+
) : null}
|
|
903
|
+
</div>
|
|
904
|
+
) : null}
|
|
587
905
|
</div>
|
|
588
906
|
</Html>
|
|
589
907
|
</group>
|
|
590
908
|
</group>
|
|
591
909
|
)
|
|
592
910
|
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* System summary pill for a selected distribution kind (HVAC duct / DWV
|
|
914
|
+
* pipe / refrigerant lineset): which supply/return tree it belongs to, its
|
|
915
|
+
* run length, and whether it actually reaches a piece of equipment.
|
|
916
|
+
*
|
|
917
|
+
* Mounted only while an HVAC node is selected, so the full-`nodes`
|
|
918
|
+
* subscription it needs (connectivity changes when ANY joint moves) doesn't
|
|
919
|
+
* re-render the always-mounted parent menu on every unrelated scene tick.
|
|
920
|
+
*/
|
|
921
|
+
function SystemSummaryPill({ nodeId, unit }: { nodeId: AnyNodeId; unit: 'metric' | 'imperial' }) {
|
|
922
|
+
const allNodes = useScene((s) => s.nodes)
|
|
923
|
+
const summary = useMemo(() => summarizeSystemFor(nodeId, allNodes), [nodeId, allNodes])
|
|
924
|
+
if (!summary) return null
|
|
925
|
+
return (
|
|
926
|
+
<div className="flex items-center gap-2 whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-4 py-1.5 text-xs tabular-nums shadow-sm backdrop-blur">
|
|
927
|
+
<span className="font-medium text-foreground">
|
|
928
|
+
{summary.systems.length > 0
|
|
929
|
+
? summary.systems.map((sys) => sys[0]!.toUpperCase() + sys.slice(1)).join(' + ')
|
|
930
|
+
: 'System'}
|
|
931
|
+
</span>
|
|
932
|
+
{summary.runCount > 0 ? (
|
|
933
|
+
<>
|
|
934
|
+
<span aria-hidden className="text-muted-foreground">
|
|
935
|
+
·
|
|
936
|
+
</span>
|
|
937
|
+
<span className="text-muted-foreground">
|
|
938
|
+
{formatMeasurement(summary.runLengthM, unit)} · {summary.runCount}{' '}
|
|
939
|
+
{summary.runCount === 1 ? 'run' : 'runs'}
|
|
940
|
+
</span>
|
|
941
|
+
</>
|
|
942
|
+
) : null}
|
|
943
|
+
{summary.terminalCount > 0 ? (
|
|
944
|
+
<>
|
|
945
|
+
<span aria-hidden className="text-muted-foreground">
|
|
946
|
+
·
|
|
947
|
+
</span>
|
|
948
|
+
<span className="text-muted-foreground">
|
|
949
|
+
{summary.terminalCount} {summary.terminalCount === 1 ? 'register' : 'registers'}
|
|
950
|
+
</span>
|
|
951
|
+
</>
|
|
952
|
+
) : null}
|
|
953
|
+
{summary.connectedToEquipment ? null : (
|
|
954
|
+
<>
|
|
955
|
+
<span aria-hidden className="text-muted-foreground">
|
|
956
|
+
·
|
|
957
|
+
</span>
|
|
958
|
+
<span className="font-medium text-amber-500">⚠ no equipment</span>
|
|
959
|
+
</>
|
|
960
|
+
)}
|
|
961
|
+
</div>
|
|
962
|
+
)
|
|
963
|
+
}
|