@pascal-app/editor 0.9.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +188 -0
- package/src/lib/measurements.ts +210 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { type RefObject, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
6
|
+
import {
|
|
7
|
+
createQuickMeasurementPointerScheduler,
|
|
8
|
+
resolveQuickMeasurementReport,
|
|
9
|
+
} from '../../lib/quick-measurement'
|
|
10
|
+
import {
|
|
11
|
+
activateQuickMeasurementHudSource,
|
|
12
|
+
clearQuickMeasurementHudSource,
|
|
13
|
+
publishQuickMeasurementHudSource,
|
|
14
|
+
} from '../../store/use-quick-measurement-hud'
|
|
15
|
+
import { useFloorplanRender } from './floorplan-render-context'
|
|
16
|
+
|
|
17
|
+
type FloorplanQuickMeasureHit = {
|
|
18
|
+
nodeId: string
|
|
19
|
+
point: { x: number; y: number }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nodeIdFromElement(element: Element | null): string | null {
|
|
23
|
+
const entry = element?.closest<SVGGElement>('.floorplan-registry-entry[data-node-id]')
|
|
24
|
+
return entry?.dataset.nodeId ?? null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function nodeIdAtPointer(svg: SVGSVGElement, event: PointerEvent): string | null {
|
|
28
|
+
const direct = event.target instanceof Element ? nodeIdFromElement(event.target) : null
|
|
29
|
+
if (direct) return direct
|
|
30
|
+
for (const element of document.elementsFromPoint(event.clientX, event.clientY)) {
|
|
31
|
+
if (!svg.contains(element)) continue
|
|
32
|
+
const nodeId = nodeIdFromElement(element)
|
|
33
|
+
if (nodeId) return nodeId
|
|
34
|
+
}
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function pointAtPointer(
|
|
39
|
+
group: SVGGElement,
|
|
40
|
+
event: PointerEvent,
|
|
41
|
+
): FloorplanQuickMeasureHit['point'] | null {
|
|
42
|
+
const matrix = group.getScreenCTM()
|
|
43
|
+
if (!matrix) return null
|
|
44
|
+
const point = new DOMPoint(event.clientX, event.clientY).matrixTransform(matrix.inverse())
|
|
45
|
+
return { x: point.x, y: point.y }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function FloorplanSmartMarker({
|
|
49
|
+
hit,
|
|
50
|
+
pinned,
|
|
51
|
+
unitsPerPixel,
|
|
52
|
+
markerRef,
|
|
53
|
+
}: {
|
|
54
|
+
hit?: FloorplanQuickMeasureHit
|
|
55
|
+
pinned: boolean
|
|
56
|
+
unitsPerPixel: number
|
|
57
|
+
markerRef?: RefObject<SVGGElement | null>
|
|
58
|
+
}) {
|
|
59
|
+
const localRef = useRef<SVGGElement>(null)
|
|
60
|
+
const ref = markerRef ?? localRef
|
|
61
|
+
|
|
62
|
+
useLayoutEffect(() => {
|
|
63
|
+
if (!hit) ref.current?.setAttribute('display', 'none')
|
|
64
|
+
}, [hit, ref])
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<g ref={ref} transform={hit ? `translate(${hit.point.x} ${hit.point.y})` : undefined}>
|
|
68
|
+
<circle
|
|
69
|
+
cx={0}
|
|
70
|
+
cy={0}
|
|
71
|
+
fill="none"
|
|
72
|
+
r={(pinned ? 11 : 8) * unitsPerPixel}
|
|
73
|
+
stroke={pinned ? '#0e7490' : '#0891b2'}
|
|
74
|
+
strokeWidth={pinned ? 2.5 : 2}
|
|
75
|
+
vectorEffect="non-scaling-stroke"
|
|
76
|
+
/>
|
|
77
|
+
{pinned ? <circle cx={0} cy={0} fill="#0e7490" r={2.75 * unitsPerPixel} /> : null}
|
|
78
|
+
</g>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function showFloorplanSmartMarker(
|
|
83
|
+
marker: SVGGElement | null,
|
|
84
|
+
point: FloorplanQuickMeasureHit['point'],
|
|
85
|
+
) {
|
|
86
|
+
if (!marker) return
|
|
87
|
+
marker.setAttribute('transform', `translate(${point.x} ${point.y})`)
|
|
88
|
+
marker.removeAttribute('display')
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hideFloorplanSmartMarker(marker: SVGGElement | null) {
|
|
92
|
+
marker?.setAttribute('display', 'none')
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function FloorplanQuickMeasureLayer() {
|
|
96
|
+
const groupRef = useRef<SVGGElement>(null)
|
|
97
|
+
const hoverRef = useRef<FloorplanQuickMeasureHit | null>(null)
|
|
98
|
+
const hoverNodeIdRef = useRef<string | null>(null)
|
|
99
|
+
const candidateNodeIdRef = useRef<string | null | undefined>(undefined)
|
|
100
|
+
const candidateHasReportRef = useRef(false)
|
|
101
|
+
const hoverMarkerRef = useRef<SVGGElement>(null)
|
|
102
|
+
const [hoverNodeId, setHoverNodeId] = useState<string | null>(null)
|
|
103
|
+
const [pinned, setPinned] = useState<FloorplanQuickMeasureHit | null>(null)
|
|
104
|
+
const nodes = useScene((state) => state.nodes)
|
|
105
|
+
const candidateNodesRef = useRef(nodes)
|
|
106
|
+
const levelId = useViewer((state) => state.selection.levelId)
|
|
107
|
+
const levelRef = useRef(levelId)
|
|
108
|
+
const renderContext = useFloorplanRender()
|
|
109
|
+
const hoverReport = useMemo(
|
|
110
|
+
() => resolveQuickMeasurementReport(hoverNodeId, nodes),
|
|
111
|
+
[hoverNodeId, nodes],
|
|
112
|
+
)
|
|
113
|
+
const pinnedReport = useMemo(
|
|
114
|
+
() => resolveQuickMeasurementReport(pinned?.nodeId ?? null, nodes),
|
|
115
|
+
[pinned?.nodeId, nodes],
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (levelRef.current === levelId) return
|
|
120
|
+
levelRef.current = levelId
|
|
121
|
+
hoverRef.current = null
|
|
122
|
+
hoverNodeIdRef.current = null
|
|
123
|
+
candidateNodeIdRef.current = undefined
|
|
124
|
+
candidateHasReportRef.current = false
|
|
125
|
+
hideFloorplanSmartMarker(hoverMarkerRef.current)
|
|
126
|
+
setHoverNodeId(null)
|
|
127
|
+
setPinned(null)
|
|
128
|
+
}, [levelId])
|
|
129
|
+
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
const group = groupRef.current
|
|
132
|
+
const svg = groupRef.current?.ownerSVGElement
|
|
133
|
+
if (!(group && svg)) return
|
|
134
|
+
const updateHover = (next: FloorplanQuickMeasureHit | null) => {
|
|
135
|
+
hoverRef.current = next
|
|
136
|
+
if (next) showFloorplanSmartMarker(hoverMarkerRef.current, next.point)
|
|
137
|
+
else hideFloorplanSmartMarker(hoverMarkerRef.current)
|
|
138
|
+
const nextNodeId = next?.nodeId ?? null
|
|
139
|
+
if (nextNodeId === hoverNodeIdRef.current) return
|
|
140
|
+
hoverNodeIdRef.current = nextNodeId
|
|
141
|
+
setHoverNodeId(nextNodeId)
|
|
142
|
+
}
|
|
143
|
+
const processPointerMove = (event: PointerEvent) => {
|
|
144
|
+
activateQuickMeasurementHudSource('2d')
|
|
145
|
+
const candidateNodeId = nodeIdAtPointer(svg, event)
|
|
146
|
+
const sceneNodes = useScene.getState().nodes
|
|
147
|
+
if (candidateNodesRef.current !== sceneNodes) {
|
|
148
|
+
candidateNodesRef.current = sceneNodes
|
|
149
|
+
candidateNodeIdRef.current = undefined
|
|
150
|
+
}
|
|
151
|
+
if (candidateNodeId !== candidateNodeIdRef.current) {
|
|
152
|
+
candidateNodeIdRef.current = candidateNodeId
|
|
153
|
+
candidateHasReportRef.current = Boolean(
|
|
154
|
+
resolveQuickMeasurementReport(candidateNodeId, sceneNodes),
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
const point = candidateHasReportRef.current ? pointAtPointer(group, event) : null
|
|
158
|
+
updateHover(candidateNodeId && point ? { nodeId: candidateNodeId, point } : null)
|
|
159
|
+
}
|
|
160
|
+
const pointerScheduler = createQuickMeasurementPointerScheduler(processPointerMove)
|
|
161
|
+
const onPointerMove = (event: PointerEvent) => pointerScheduler.enqueue(event)
|
|
162
|
+
const clear = () => {
|
|
163
|
+
pointerScheduler.clear()
|
|
164
|
+
updateHover(null)
|
|
165
|
+
}
|
|
166
|
+
const onClick = (event: MouseEvent) => {
|
|
167
|
+
const next = hoverRef.current
|
|
168
|
+
if (!(next && event.button === 0)) return
|
|
169
|
+
event.preventDefault()
|
|
170
|
+
event.stopImmediatePropagation()
|
|
171
|
+
activateQuickMeasurementHudSource('2d')
|
|
172
|
+
setPinned(next)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
svg.addEventListener('pointermove', onPointerMove, true)
|
|
176
|
+
svg.addEventListener('pointerleave', clear)
|
|
177
|
+
svg.addEventListener('click', onClick, true)
|
|
178
|
+
return () => {
|
|
179
|
+
svg.removeEventListener('pointermove', onPointerMove, true)
|
|
180
|
+
svg.removeEventListener('pointerleave', clear)
|
|
181
|
+
svg.removeEventListener('click', onClick, true)
|
|
182
|
+
pointerScheduler.clear()
|
|
183
|
+
}
|
|
184
|
+
}, [])
|
|
185
|
+
|
|
186
|
+
const unitsPerPixel = Math.max(renderContext?.unitsPerPixel ?? 0.01, 1e-6)
|
|
187
|
+
const activeHit = hoverReport ? hoverRef.current : pinnedReport ? pinned : null
|
|
188
|
+
const report = hoverReport ?? pinnedReport
|
|
189
|
+
const lensState =
|
|
190
|
+
pinnedReport && activeHit?.nodeId === pinned?.nodeId ? ('pinned' as const) : ('live' as const)
|
|
191
|
+
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
publishQuickMeasurementHudSource('2d', report ? { lensState, report } : null)
|
|
194
|
+
}, [lensState, report])
|
|
195
|
+
|
|
196
|
+
useEffect(() => () => clearQuickMeasurementHudSource('2d'), [])
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<g pointerEvents="none" ref={groupRef}>
|
|
200
|
+
{pinned && pinnedReport ? (
|
|
201
|
+
<FloorplanSmartMarker hit={pinned} pinned unitsPerPixel={unitsPerPixel} />
|
|
202
|
+
) : null}
|
|
203
|
+
<FloorplanSmartMarker
|
|
204
|
+
markerRef={hoverMarkerRef}
|
|
205
|
+
pinned={false}
|
|
206
|
+
unitsPerPixel={unitsPerPixel}
|
|
207
|
+
/>
|
|
208
|
+
</g>
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export default FloorplanQuickMeasureLayer
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { createSceneApi, nodeRegistry, useScene } from '@pascal-app/core'
|
|
4
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
5
|
+
import { type ComponentType, lazy, Suspense, useCallback, useMemo } from 'react'
|
|
6
|
+
import {
|
|
7
|
+
type FloorplanToolContext,
|
|
8
|
+
getFloorplanNodeExtension,
|
|
9
|
+
} from '../../lib/floorplan/floorplan-extension'
|
|
10
|
+
import {
|
|
11
|
+
type FloorplanMode,
|
|
12
|
+
isFloorplanToolAvailableInMode,
|
|
13
|
+
} from '../../lib/floorplan/floorplan-mode'
|
|
14
|
+
import useEditor from '../../store/use-editor'
|
|
15
|
+
import useFloorplanMode from '../../store/use-floorplan-mode'
|
|
16
|
+
|
|
17
|
+
const lazyToolCache = new WeakMap<() => Promise<unknown>, ComponentType<FloorplanToolContext>>()
|
|
18
|
+
|
|
19
|
+
function registeredFloorplanTool(
|
|
20
|
+
tool: string | null,
|
|
21
|
+
mode: FloorplanMode,
|
|
22
|
+
): ComponentType<FloorplanToolContext> | null {
|
|
23
|
+
if (!tool) return null
|
|
24
|
+
const extension = getFloorplanNodeExtension(nodeRegistry.get(tool))
|
|
25
|
+
if (!isFloorplanToolAvailableInMode(extension?.availableModes, mode)) return null
|
|
26
|
+
const loader = extension?.tool
|
|
27
|
+
if (!loader) return null
|
|
28
|
+
const cached = lazyToolCache.get(loader)
|
|
29
|
+
if (cached) return cached
|
|
30
|
+
const component = lazy(loader)
|
|
31
|
+
lazyToolCache.set(loader, component)
|
|
32
|
+
return component
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function FloorplanRegisteredToolLayer() {
|
|
36
|
+
const mode = useEditor((state) => state.mode)
|
|
37
|
+
const tool = useEditor((state) => state.tool)
|
|
38
|
+
const floorplanMode = useFloorplanMode((state) => state.mode)
|
|
39
|
+
const gridSnapStep = useEditor((state) => state.gridSnapStep)
|
|
40
|
+
const toolDefaults = useEditor((state) =>
|
|
41
|
+
state.tool ? (state.toolDefaults[state.tool] ?? null) : null,
|
|
42
|
+
)
|
|
43
|
+
const activeLevelId = useViewer((state) => state.selection.levelId)
|
|
44
|
+
const unit = useViewer((state) => state.unit)
|
|
45
|
+
const metricNotation = useViewer((state) => state.metricNotation)
|
|
46
|
+
const sceneApi = useMemo(() => createSceneApi(useScene), [])
|
|
47
|
+
const selectNode = useCallback(
|
|
48
|
+
(id: Parameters<FloorplanToolContext['selectNode']>[0]) =>
|
|
49
|
+
useViewer.getState().setSelection({ selectedIds: [id] }),
|
|
50
|
+
[],
|
|
51
|
+
)
|
|
52
|
+
const finishTool = useCallback(() => {
|
|
53
|
+
useEditor.getState().setTool(null)
|
|
54
|
+
useEditor.getState().setMode('select')
|
|
55
|
+
}, [])
|
|
56
|
+
if (mode !== 'build') return null
|
|
57
|
+
const Tool = registeredFloorplanTool(tool, floorplanMode)
|
|
58
|
+
return Tool ? (
|
|
59
|
+
<Suspense fallback={null}>
|
|
60
|
+
<Tool
|
|
61
|
+
activeLevelId={activeLevelId}
|
|
62
|
+
finishTool={finishTool}
|
|
63
|
+
gridSnapStep={gridSnapStep}
|
|
64
|
+
metricNotation={metricNotation}
|
|
65
|
+
sceneApi={sceneApi}
|
|
66
|
+
selectNode={selectNode}
|
|
67
|
+
toolDefaults={toolDefaults}
|
|
68
|
+
unit={unit}
|
|
69
|
+
/>
|
|
70
|
+
</Suspense>
|
|
71
|
+
) : null
|
|
72
|
+
}
|
|
@@ -4,19 +4,97 @@ import {
|
|
|
4
4
|
type AnyNode,
|
|
5
5
|
type AnyNodeId,
|
|
6
6
|
type CeilingNode,
|
|
7
|
+
createSceneApi,
|
|
7
8
|
getWallMidpointHandlePoint,
|
|
9
|
+
type NodeQuickAction,
|
|
8
10
|
nodeRegistry,
|
|
11
|
+
runAsSingleSceneHistoryStep,
|
|
9
12
|
type SlabNode,
|
|
10
13
|
useLiveNodeOverrides,
|
|
11
14
|
useScene,
|
|
12
15
|
type WallNode,
|
|
13
16
|
} from '@pascal-app/core'
|
|
14
17
|
import { useViewer } from '@pascal-app/viewer'
|
|
15
|
-
import { useEffect, useState } from 'react'
|
|
18
|
+
import { type MouseEvent, useEffect, useState } from 'react'
|
|
16
19
|
import { createPortal } from 'react-dom'
|
|
20
|
+
import { useShallow } from 'zustand/react/shallow'
|
|
21
|
+
import { useReducedMotion } from '../../hooks/use-reduced-motion'
|
|
22
|
+
import { resolveMoveActionNode } from '../../lib/direct-manipulation'
|
|
23
|
+
import { getFloorplanNodeExtension } from '../../lib/floorplan/floorplan-extension'
|
|
24
|
+
import {
|
|
25
|
+
createFreshPlacementSubtree,
|
|
26
|
+
duplicatesAsFreshSubtree,
|
|
27
|
+
prepareFreshPlacementRootDuplicate,
|
|
28
|
+
} from '../../lib/fresh-planar-placement'
|
|
29
|
+
import { curveReshapeScope } from '../../lib/interaction/scope'
|
|
30
|
+
import { playBlockedQuickActionFeedback } from '../../lib/quick-action-feedback'
|
|
31
|
+
import { collectQuickActionNodeScope } from '../../lib/quick-action-nodes'
|
|
17
32
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
33
|
+
import { cn } from '../../lib/utils'
|
|
18
34
|
import useEditor from '../../store/use-editor'
|
|
35
|
+
import useInteractionScope, {
|
|
36
|
+
useIsCurveReshape,
|
|
37
|
+
useMovingNode,
|
|
38
|
+
} from '../../store/use-interaction-scope'
|
|
19
39
|
import { NodeActionMenu } from '../editor/node-action-menu'
|
|
40
|
+
import { IconRefGlyph } from '../ui/icon-ref'
|
|
41
|
+
|
|
42
|
+
function SideAddGlyph({ direction }: { direction: 'left' | 'right' }) {
|
|
43
|
+
return (
|
|
44
|
+
<svg
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
className="h-3.5 w-3.5"
|
|
47
|
+
fill="none"
|
|
48
|
+
stroke="currentColor"
|
|
49
|
+
strokeLinecap="round"
|
|
50
|
+
strokeLinejoin="round"
|
|
51
|
+
strokeWidth="1.7"
|
|
52
|
+
viewBox="0 0 24 24"
|
|
53
|
+
>
|
|
54
|
+
<rect x="8" y="7" width="9" height="10" rx="1.75" />
|
|
55
|
+
<path d="M12.5 7v10" />
|
|
56
|
+
{direction === 'left' ? (
|
|
57
|
+
<>
|
|
58
|
+
<path d="M6.5 12H2.75" />
|
|
59
|
+
<path d="m5.5 9.75-2.75 2.25 2.75 2.25" />
|
|
60
|
+
</>
|
|
61
|
+
) : (
|
|
62
|
+
<>
|
|
63
|
+
<path d="M17.5 12h3.75" />
|
|
64
|
+
<path d="m18.5 9.75 2.75 2.25-2.75 2.25" />
|
|
65
|
+
</>
|
|
66
|
+
)}
|
|
67
|
+
</svg>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Builtin string tokens map to the generic glyphs above; kind-owned marks
|
|
72
|
+
// arrive as IconRef objects and render through the shared IconRefGlyph.
|
|
73
|
+
// Mirrors the 3D floating-action-menu (2D ↔ 3D parity).
|
|
74
|
+
function QuickActionIcon({ action }: { action: NodeQuickAction }) {
|
|
75
|
+
const icon = action.icon
|
|
76
|
+
if (!icon) return null
|
|
77
|
+
if (typeof icon === 'object') return <IconRefGlyph icon={icon} size={14} />
|
|
78
|
+
switch (icon) {
|
|
79
|
+
case 'add-left':
|
|
80
|
+
return <SideAddGlyph direction="left" />
|
|
81
|
+
case 'add-right':
|
|
82
|
+
return <SideAddGlyph direction="right" />
|
|
83
|
+
default:
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function collectQuickActionNodes(
|
|
89
|
+
nodes: Record<AnyNodeId, AnyNode>,
|
|
90
|
+
selectedId: string | null,
|
|
91
|
+
): Record<AnyNodeId, AnyNode> | null {
|
|
92
|
+
if (!selectedId) return null
|
|
93
|
+
const selected = nodes[selectedId as AnyNodeId]
|
|
94
|
+
const def = selected ? nodeRegistry.get(selected.type) : undefined
|
|
95
|
+
if (!def?.quickActions) return null
|
|
96
|
+
return collectQuickActionNodeScope(nodes, selectedId, def.quickActionNodeScope)
|
|
97
|
+
}
|
|
20
98
|
|
|
21
99
|
/**
|
|
22
100
|
* Floating Move / Duplicate / Delete buttons that appear above the
|
|
@@ -34,30 +112,68 @@ import { NodeActionMenu } from '../editor/node-action-menu'
|
|
|
34
112
|
* `<FloorplanRegistryMoveOverlay>` / dispatcher picks the right path.
|
|
35
113
|
* Walls are excluded — their move is reached via the side-arrow
|
|
36
114
|
* handles emitted from `def.floorplan`, not via a menu button.
|
|
115
|
+
* - Curve (wall only): enters curve reshape mode. The selected wall's
|
|
116
|
+
* midpoint curve handle remains visible so it can be dragged in plan.
|
|
37
117
|
* - Add hole (slab + ceiling only): inserts a small default-square
|
|
38
118
|
* hole at the polygon centroid via `updateNode`. Mirrors the legacy
|
|
39
119
|
* `handleAddHole` in `floating-action-menu.tsx`.
|
|
40
|
-
* - Duplicate:
|
|
41
|
-
*
|
|
120
|
+
* - Duplicate: creates a fresh subtree when the kind opts in, otherwise a
|
|
121
|
+
* root-only copy, then hands that real draft to the placement cursor.
|
|
42
122
|
* - Delete: calls `deleteNode(id)`. Cascade is handled by the registry's
|
|
43
123
|
* `relations.cascadeDelete` if declared on the def.
|
|
44
124
|
*
|
|
45
|
-
* Hidden while
|
|
125
|
+
* Hidden while moving or curving so the menu does not compete with the active affordance.
|
|
46
126
|
*/
|
|
47
127
|
export function FloorplanRegistryActionMenu() {
|
|
48
|
-
const
|
|
49
|
-
|
|
128
|
+
const reducedMotion = useReducedMotion()
|
|
129
|
+
// Sole selection only — a multi-selection gets the group menu
|
|
130
|
+
// (`FloorplanGroupActionMenu`), whose actions target the whole selection.
|
|
131
|
+
const selectedId = useViewer((s) =>
|
|
132
|
+
s.selection.selectedIds.length === 1 ? s.selection.selectedIds[0] : undefined,
|
|
133
|
+
) as AnyNodeId | undefined
|
|
134
|
+
const movingNode = useMovingNode()
|
|
135
|
+
const isCurveReshape = useIsCurveReshape()
|
|
50
136
|
const setMovingNode = useEditor((s) => s.setMovingNode)
|
|
137
|
+
const setMovingNodeOrigin = useEditor((s) => s.setMovingNodeOrigin)
|
|
138
|
+
// Gate on floorplan hover so this 2D menu never coexists with the 3D
|
|
139
|
+
// FloatingActionMenu in split view — that menu hides while the floorplan
|
|
140
|
+
// is hovered, so this one must only show then. Mirrors the legacy
|
|
141
|
+
// FloorplanActionMenuLayer guard. Without it a registry kind (e.g. a
|
|
142
|
+
// duct) shows two Duplicate buttons whenever the pointer is outside the
|
|
143
|
+
// 2D panel.
|
|
144
|
+
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
|
51
145
|
|
|
52
146
|
const [position, setPosition] = useState<{ left: number; top: number } | null>(null)
|
|
53
147
|
|
|
54
148
|
// Only show for registered kinds (skip legacy kinds — they have their
|
|
55
149
|
// own FloorplanActionMenuLayer entries).
|
|
56
150
|
const selectedKind = useScene((s) => (selectedId ? (s.nodes[selectedId]?.type ?? null) : null))
|
|
151
|
+
const canCurve = useScene((s) => {
|
|
152
|
+
if (!selectedId) return false
|
|
153
|
+
const selectedNode = s.nodes[selectedId]
|
|
154
|
+
if (!selectedNode) return false
|
|
155
|
+
const definition = nodeRegistry.get(selectedNode.type)
|
|
156
|
+
const canCurveNode = getFloorplanNodeExtension(definition)?.actionMenu?.canCurve
|
|
157
|
+
return (
|
|
158
|
+
!!definition?.floorplanAffordances?.curve &&
|
|
159
|
+
!!canCurveNode?.({
|
|
160
|
+
node: selectedNode as never,
|
|
161
|
+
nodes: s.nodes,
|
|
162
|
+
})
|
|
163
|
+
)
|
|
164
|
+
})
|
|
57
165
|
const def = selectedKind ? nodeRegistry.get(selectedKind) : null
|
|
58
166
|
const isRegistryKind = !!def
|
|
59
|
-
const isVisible =
|
|
167
|
+
const isVisible =
|
|
168
|
+
isRegistryKind &&
|
|
169
|
+
def?.presentation?.actionMenu !== false &&
|
|
170
|
+
!movingNode &&
|
|
171
|
+
!isCurveReshape &&
|
|
172
|
+
isFloorplanHovered
|
|
60
173
|
const isWall = selectedKind === 'wall'
|
|
174
|
+
const quickActionNodes = useScene(
|
|
175
|
+
useShallow((s) => collectQuickActionNodes(s.nodes, selectedId ?? null)),
|
|
176
|
+
)
|
|
61
177
|
|
|
62
178
|
useEffect(() => {
|
|
63
179
|
if (!(isVisible && selectedId)) {
|
|
@@ -117,6 +233,14 @@ export function FloorplanRegistryActionMenu() {
|
|
|
117
233
|
const node = useScene.getState().nodes[selectedId]
|
|
118
234
|
if (!node) return null
|
|
119
235
|
|
|
236
|
+
const quickActions =
|
|
237
|
+
quickActionNodes && nodeRegistry.get(node.type)?.quickActions
|
|
238
|
+
? (nodeRegistry.get(node.type)?.quickActions?.({
|
|
239
|
+
node: node as never,
|
|
240
|
+
nodes: quickActionNodes,
|
|
241
|
+
}) ?? [])
|
|
242
|
+
: []
|
|
243
|
+
|
|
120
244
|
// Move button is enabled when any of:
|
|
121
245
|
// - `capabilities.movable` (generic translate-on-XZ — shelf / spawn / fence)
|
|
122
246
|
// - `def.floorplanMoveTarget` (anchor-aware 2D — door / window / item)
|
|
@@ -133,7 +257,13 @@ export function FloorplanRegistryActionMenu() {
|
|
|
133
257
|
|
|
134
258
|
const handleMove = () => {
|
|
135
259
|
sfxEmitter.emit('sfx:item-pick')
|
|
136
|
-
|
|
260
|
+
const sceneNodes = useScene.getState().nodes
|
|
261
|
+
setMovingNode(resolveMoveActionNode(node, sceneNodes) as never)
|
|
262
|
+
// 2D-owned move: `FloorplanRegistryMoveOverlay` runs the whole gesture.
|
|
263
|
+
// Mark the origin (after `setMovingNode`, which resets it to null) so
|
|
264
|
+
// `ToolManager` keeps the 3D affordance mover from also adopting the node
|
|
265
|
+
// and reverting it on unmount. Mirrors the orange move-dot path.
|
|
266
|
+
setMovingNodeOrigin('2d')
|
|
137
267
|
// Match the legacy 3D `floating-action-menu`: clear selection so
|
|
138
268
|
// selection-gated affordances unmount during the drag. Specifically
|
|
139
269
|
// the slab / ceiling boundary editor (`ToolManager` shows it when
|
|
@@ -181,21 +311,40 @@ export function FloorplanRegistryActionMenu() {
|
|
|
181
311
|
)
|
|
182
312
|
}
|
|
183
313
|
|
|
314
|
+
const handleCurve = () => {
|
|
315
|
+
if (!canCurve) return
|
|
316
|
+
sfxEmitter.emit('sfx:item-pick')
|
|
317
|
+
useInteractionScope.getState().begin(curveReshapeScope(node.id))
|
|
318
|
+
}
|
|
319
|
+
|
|
184
320
|
const handleDuplicate = () => {
|
|
185
321
|
if (!node.parentId) return
|
|
186
322
|
sfxEmitter.emit('sfx:item-pick')
|
|
187
323
|
useScene.temporal.getState().pause()
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
? (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
324
|
+
let draftId: AnyNodeId | null = null
|
|
325
|
+
try {
|
|
326
|
+
if (duplicatesAsFreshSubtree(node as AnyNode)) {
|
|
327
|
+
draftId = createFreshPlacementSubtree(node.id as AnyNodeId)
|
|
328
|
+
const draft = draftId ? useScene.getState().nodes[draftId] : null
|
|
329
|
+
if (!draft) return
|
|
330
|
+
setMovingNode(draft as never)
|
|
331
|
+
} else {
|
|
332
|
+
const cloned = prepareFreshPlacementRootDuplicate(node as AnyNode)
|
|
333
|
+
const parsed = def.schema.parse(cloned) as AnyNode
|
|
334
|
+
draftId = parsed.id as AnyNodeId
|
|
335
|
+
useScene.getState().createNode(parsed, node.parentId as AnyNodeId)
|
|
336
|
+
setMovingNode(parsed as never)
|
|
337
|
+
}
|
|
338
|
+
setMovingNodeOrigin('2d')
|
|
339
|
+
useViewer.getState().setSelection({ selectedIds: [] })
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (draftId && useScene.getState().nodes[draftId]) {
|
|
342
|
+
useScene.getState().deleteNode(draftId)
|
|
343
|
+
}
|
|
344
|
+
console.error('Failed to duplicate node', error)
|
|
345
|
+
} finally {
|
|
346
|
+
useScene.temporal.getState().resume()
|
|
347
|
+
}
|
|
199
348
|
}
|
|
200
349
|
|
|
201
350
|
const handleDelete = () => {
|
|
@@ -204,9 +353,25 @@ export function FloorplanRegistryActionMenu() {
|
|
|
204
353
|
useViewer.getState().setSelection({ selectedIds: [] })
|
|
205
354
|
}
|
|
206
355
|
|
|
356
|
+
const handleQuickAction = (action: NodeQuickAction, event: MouseEvent<HTMLButtonElement>) => {
|
|
357
|
+
if (action.disabled) {
|
|
358
|
+
if (action.blockedFeedback) {
|
|
359
|
+
playBlockedQuickActionFeedback(event.currentTarget, reducedMotion)
|
|
360
|
+
}
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
const run = () => action.run({ node, sceneApi: createSceneApi(useScene) })
|
|
364
|
+
const result = action.history === 'single' ? runAsSingleSceneHistoryStep(useScene, run) : run()
|
|
365
|
+
if (result?.selectedIds) useViewer.getState().setSelection({ selectedIds: result.selectedIds })
|
|
366
|
+
if (result?.selectedIds) {
|
|
367
|
+
const selectedDifferentNode = result.selectedIds.some((id) => id !== node.id)
|
|
368
|
+
sfxEmitter.emit(selectedDifferentNode ? 'sfx:item-place' : 'sfx:item-pick')
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
207
372
|
return createPortal(
|
|
208
373
|
<div
|
|
209
|
-
className="pointer-events-none fixed z-30"
|
|
374
|
+
className="pointer-events-none fixed z-30 flex w-max flex-col items-center"
|
|
210
375
|
style={{
|
|
211
376
|
left: position.left,
|
|
212
377
|
top: position.top,
|
|
@@ -215,12 +380,46 @@ export function FloorplanRegistryActionMenu() {
|
|
|
215
380
|
>
|
|
216
381
|
<NodeActionMenu
|
|
217
382
|
onAddHole={canAddHole ? handleAddHole : undefined}
|
|
383
|
+
onCurve={canCurve ? handleCurve : undefined}
|
|
218
384
|
onDelete={canDelete ? handleDelete : undefined}
|
|
219
385
|
onDuplicate={canDuplicate ? handleDuplicate : undefined}
|
|
220
386
|
onMove={canMove ? handleMove : undefined}
|
|
221
387
|
onPointerDown={(event) => event.stopPropagation()}
|
|
222
388
|
onPointerUp={(event) => event.stopPropagation()}
|
|
223
389
|
/>
|
|
390
|
+
{quickActions.length > 0 ? (
|
|
391
|
+
<div
|
|
392
|
+
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"
|
|
393
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
394
|
+
onPointerUp={(event) => event.stopPropagation()}
|
|
395
|
+
>
|
|
396
|
+
{quickActions.map((action) => (
|
|
397
|
+
<button
|
|
398
|
+
aria-disabled={action.disabled || undefined}
|
|
399
|
+
aria-label={action.title ?? action.label}
|
|
400
|
+
className={cn(
|
|
401
|
+
'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',
|
|
402
|
+
action.disabled &&
|
|
403
|
+
'cursor-not-allowed opacity-40 hover:bg-transparent hover:text-muted-foreground',
|
|
404
|
+
)}
|
|
405
|
+
disabled={action.disabled && !action.blockedFeedback}
|
|
406
|
+
key={action.id}
|
|
407
|
+
onClick={(event) => handleQuickAction(action, event)}
|
|
408
|
+
title={action.title ?? action.label}
|
|
409
|
+
type="button"
|
|
410
|
+
>
|
|
411
|
+
<span className="flex items-center gap-1.5" data-quick-action-feedback>
|
|
412
|
+
<span className="flex h-3.5 w-3.5 shrink-0 items-center justify-center text-current">
|
|
413
|
+
<QuickActionIcon action={action} />
|
|
414
|
+
</span>
|
|
415
|
+
<span className="whitespace-nowrap leading-none" data-quick-action-label>
|
|
416
|
+
{action.label}
|
|
417
|
+
</span>
|
|
418
|
+
</span>
|
|
419
|
+
</button>
|
|
420
|
+
))}
|
|
421
|
+
</div>
|
|
422
|
+
) : null}
|
|
224
423
|
</div>,
|
|
225
424
|
document.body,
|
|
226
425
|
)
|