@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
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type AnyNode,
|
|
5
|
+
type AnyNodeId,
|
|
6
|
+
getWallCurveFrameAt,
|
|
7
|
+
getWallCurveLength,
|
|
8
|
+
getWallPlaneTop,
|
|
9
|
+
getWallThickness,
|
|
10
|
+
isCurvedWall,
|
|
11
|
+
resolveLevelId,
|
|
12
|
+
resolveWallTop,
|
|
13
|
+
sceneRegistry,
|
|
14
|
+
spatialGridManager,
|
|
15
|
+
useScene,
|
|
16
|
+
type WallNode,
|
|
17
|
+
} from '@pascal-app/core'
|
|
4
18
|
import { useWallSnapIndicator, type WallSnapKind } from '@pascal-app/editor'
|
|
5
19
|
import { useViewer } from '@pascal-app/viewer'
|
|
6
20
|
import { useFrame } from '@react-three/fiber'
|
|
7
|
-
import { memo, useRef } from 'react'
|
|
21
|
+
import { memo, useMemo, useRef } from 'react'
|
|
8
22
|
import { BoxGeometry, CircleGeometry, CylinderGeometry, type Group } from 'three'
|
|
9
23
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
10
24
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
@@ -20,8 +34,8 @@ import { EDITOR_LAYER } from '../../lib/constants'
|
|
|
20
34
|
* intersection → ✕ cross wall body (edge) → circle
|
|
21
35
|
*
|
|
22
36
|
* Subscribes to the shared `useWallSnapIndicator` store (published by the wall
|
|
23
|
-
* draft + endpoint-move tools).
|
|
24
|
-
*
|
|
37
|
+
* draft + endpoint-move tools). The vertical mouse pillar and the corner
|
|
38
|
+
* (endpoint) square are green; the other floor glyphs are indigo.
|
|
25
39
|
*
|
|
26
40
|
* The point carries only XZ (building-local plan coords); like the alignment
|
|
27
41
|
* guides it's lifted to the active level's building-local Y each frame so it
|
|
@@ -30,10 +44,19 @@ import { EDITOR_LAYER } from '../../lib/constants'
|
|
|
30
44
|
*/
|
|
31
45
|
|
|
32
46
|
const BEACON_COLOR = 0x81_8c_f8 // indigo-400 — matches the alignment guide accent
|
|
47
|
+
const MARKER_GREEN = 0x22_c5_5e // green-500 — vertical mouse marker + corner (endpoint) glyph
|
|
33
48
|
const BEACON_HEIGHT = 2.5 // world-meter height of the pillar
|
|
34
49
|
const BEACON_RADIUS = 0.018 // world-meter radius of the pillar
|
|
35
50
|
const MARKER = 0.13 // world-meter base size of the floor glyph
|
|
36
51
|
const FLOOR_LIFT = 0.012 // tiny lift so the marker reads above the floor grid
|
|
52
|
+
const WALL_TOP_HIGHLIGHT_LIFT = 0.035
|
|
53
|
+
const WALL_TOP_HIGHLIGHT_HEIGHT = 0.018
|
|
54
|
+
const WALL_TOP_HIGHLIGHT_OVERHANG = 0.14
|
|
55
|
+
const WALL_TOP_GLOW_HEIGHT = 0.026
|
|
56
|
+
const WALL_TOP_GLOW_OVERHANG = 0.36
|
|
57
|
+
const WALL_TOP_END_OVERHANG = 0.08
|
|
58
|
+
const CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH = 0.45
|
|
59
|
+
const NO_RAYCAST = () => null
|
|
37
60
|
|
|
38
61
|
// Shared resources — one material + unit geometries, so snap churn during a
|
|
39
62
|
// drag doesn't rebuild GPU buffers (mirrors the alignment guide layer).
|
|
@@ -45,17 +68,49 @@ const beaconMaterial = new MeshBasicNodeMaterial({
|
|
|
45
68
|
transparent: true,
|
|
46
69
|
opacity: 0.9,
|
|
47
70
|
})
|
|
71
|
+
const greenMarkerMaterial = new MeshBasicNodeMaterial({
|
|
72
|
+
color: MARKER_GREEN,
|
|
73
|
+
depthTest: false,
|
|
74
|
+
depthWrite: false,
|
|
75
|
+
toneMapped: false,
|
|
76
|
+
transparent: true,
|
|
77
|
+
opacity: 0.9,
|
|
78
|
+
})
|
|
79
|
+
const wallTopHighlightMaterial = new MeshBasicNodeMaterial({
|
|
80
|
+
color: BEACON_COLOR,
|
|
81
|
+
depthTest: false,
|
|
82
|
+
depthWrite: false,
|
|
83
|
+
toneMapped: false,
|
|
84
|
+
transparent: true,
|
|
85
|
+
opacity: 0.88,
|
|
86
|
+
})
|
|
87
|
+
const wallTopHighlightGlowMaterial = new MeshBasicNodeMaterial({
|
|
88
|
+
color: BEACON_COLOR,
|
|
89
|
+
depthTest: false,
|
|
90
|
+
depthWrite: false,
|
|
91
|
+
toneMapped: false,
|
|
92
|
+
transparent: true,
|
|
93
|
+
opacity: 0.26,
|
|
94
|
+
})
|
|
48
95
|
const PILLAR_GEOMETRY = new CylinderGeometry(BEACON_RADIUS, BEACON_RADIUS, BEACON_HEIGHT, 8)
|
|
49
96
|
// Flat unit geometries scaled per marker. Boxes are 0.002 tall so they read as
|
|
50
97
|
// a flat plate; circles/triangles lie flat via an X rotation at the mesh.
|
|
51
98
|
const FLAT_BOX_GEOMETRY = new BoxGeometry(1, 0.002, 1)
|
|
99
|
+
const WALL_TOP_HIGHLIGHT_GEOMETRY = new BoxGeometry(1, 1, 1)
|
|
52
100
|
const TRIANGLE_GEOMETRY = new CircleGeometry(1, 3)
|
|
53
101
|
const CIRCLE_GEOMETRY = new CircleGeometry(1, 28)
|
|
54
102
|
|
|
55
103
|
export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
|
|
56
104
|
const point = useWallSnapIndicator((s) => s.point)
|
|
57
105
|
const levelId = useViewer((s) => s.selection.levelId)
|
|
106
|
+
const nodes = useScene((s) => s.nodes)
|
|
58
107
|
const groupRef = useRef<Group>(null)
|
|
108
|
+
const highlightedWalls = useMemo(() => {
|
|
109
|
+
if (!point?.wallIds?.length) return []
|
|
110
|
+
return point.wallIds
|
|
111
|
+
.map((wallId) => nodes[wallId as AnyNodeId])
|
|
112
|
+
.filter((node): node is WallNode => node?.type === 'wall' && node.visible !== false)
|
|
113
|
+
}, [nodes, point?.wallIds])
|
|
59
114
|
|
|
60
115
|
// Track the active level's building-local Y each frame so the beacon stands
|
|
61
116
|
// on the floor being edited, not the building base — same source the
|
|
@@ -70,10 +125,13 @@ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
|
|
|
70
125
|
if (!point) return null
|
|
71
126
|
return (
|
|
72
127
|
<group ref={groupRef}>
|
|
128
|
+
{highlightedWalls.map((wall) => (
|
|
129
|
+
<WallTopHighlight key={wall.id} nodes={nodes} wall={wall} />
|
|
130
|
+
))}
|
|
73
131
|
<mesh
|
|
74
132
|
geometry={PILLAR_GEOMETRY}
|
|
75
133
|
layers={EDITOR_LAYER}
|
|
76
|
-
material={
|
|
134
|
+
material={greenMarkerMaterial}
|
|
77
135
|
position={[point.x, BEACON_HEIGHT / 2, point.z]}
|
|
78
136
|
renderOrder={1002}
|
|
79
137
|
/>
|
|
@@ -82,6 +140,104 @@ export const WallSnapBeaconLayer = memo(function WallSnapBeaconLayer() {
|
|
|
82
140
|
)
|
|
83
141
|
})
|
|
84
142
|
|
|
143
|
+
type WallTopHighlightSegment = {
|
|
144
|
+
angle: number
|
|
145
|
+
center: [number, number]
|
|
146
|
+
length: number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getWallTopY(wall: WallNode, nodes: Readonly<Record<string, AnyNode>>) {
|
|
150
|
+
const levelId = resolveLevelId(wall, nodes as Record<string, AnyNode>)
|
|
151
|
+
const support = spatialGridManager.getSlabSupportForWall(
|
|
152
|
+
levelId,
|
|
153
|
+
wall.start,
|
|
154
|
+
wall.end,
|
|
155
|
+
wall.curveOffset ?? 0,
|
|
156
|
+
wall.thickness,
|
|
157
|
+
wall.supportSlabId,
|
|
158
|
+
)
|
|
159
|
+
const planeTop = getWallPlaneTop(wall, levelId, nodes as Record<string, AnyNode>)
|
|
160
|
+
return resolveWallTop(wall, planeTop, support.elevation) + WALL_TOP_HIGHLIGHT_LIFT
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildHighlightSegment(start: [number, number], end: [number, number]) {
|
|
164
|
+
const dx = end[0] - start[0]
|
|
165
|
+
const dz = end[1] - start[1]
|
|
166
|
+
const length = Math.hypot(dx, dz)
|
|
167
|
+
if (length < 1e-6) return null
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
angle: -Math.atan2(dz, dx),
|
|
171
|
+
center: [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2] as [number, number],
|
|
172
|
+
length,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function buildWallTopHighlightSegments(wall: WallNode): WallTopHighlightSegment[] {
|
|
177
|
+
if (!isCurvedWall(wall)) {
|
|
178
|
+
const segment = buildHighlightSegment(wall.start, wall.end)
|
|
179
|
+
return segment ? [segment] : []
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const sampleCount = Math.max(
|
|
183
|
+
8,
|
|
184
|
+
Math.ceil(getWallCurveLength(wall) / CURVED_WALL_HIGHLIGHT_SEGMENT_LENGTH),
|
|
185
|
+
)
|
|
186
|
+
const segments: WallTopHighlightSegment[] = []
|
|
187
|
+
let previous = getWallCurveFrameAt(wall, 0).point
|
|
188
|
+
for (let index = 1; index <= sampleCount; index += 1) {
|
|
189
|
+
const current = getWallCurveFrameAt(wall, index / sampleCount).point
|
|
190
|
+
const segment = buildHighlightSegment([previous.x, previous.y], [current.x, current.y])
|
|
191
|
+
if (segment) segments.push(segment)
|
|
192
|
+
previous = current
|
|
193
|
+
}
|
|
194
|
+
return segments
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function WallTopHighlight({
|
|
198
|
+
nodes,
|
|
199
|
+
wall,
|
|
200
|
+
}: {
|
|
201
|
+
nodes: Readonly<Record<string, AnyNode>>
|
|
202
|
+
wall: WallNode
|
|
203
|
+
}) {
|
|
204
|
+
const segments = useMemo(() => buildWallTopHighlightSegments(wall), [wall])
|
|
205
|
+
const y = getWallTopY(wall, nodes)
|
|
206
|
+
const width = Math.max(getWallThickness(wall) + WALL_TOP_HIGHLIGHT_OVERHANG, 0.24)
|
|
207
|
+
const glowWidth = Math.max(getWallThickness(wall) + WALL_TOP_GLOW_OVERHANG, 0.42)
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<>
|
|
211
|
+
{segments.map((segment, index) => (
|
|
212
|
+
<group key={`${wall.id}:${index}`}>
|
|
213
|
+
<mesh
|
|
214
|
+
frustumCulled={false}
|
|
215
|
+
geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
|
|
216
|
+
layers={EDITOR_LAYER}
|
|
217
|
+
material={wallTopHighlightGlowMaterial}
|
|
218
|
+
position={[segment.center[0], y - 0.003, segment.center[1]]}
|
|
219
|
+
raycast={NO_RAYCAST}
|
|
220
|
+
renderOrder={1003}
|
|
221
|
+
rotation={[0, segment.angle, 0]}
|
|
222
|
+
scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_GLOW_HEIGHT, glowWidth]}
|
|
223
|
+
/>
|
|
224
|
+
<mesh
|
|
225
|
+
frustumCulled={false}
|
|
226
|
+
geometry={WALL_TOP_HIGHLIGHT_GEOMETRY}
|
|
227
|
+
layers={EDITOR_LAYER}
|
|
228
|
+
material={wallTopHighlightMaterial}
|
|
229
|
+
position={[segment.center[0], y + 0.002, segment.center[1]]}
|
|
230
|
+
raycast={NO_RAYCAST}
|
|
231
|
+
renderOrder={1004}
|
|
232
|
+
rotation={[0, segment.angle, 0]}
|
|
233
|
+
scale={[segment.length + WALL_TOP_END_OVERHANG, WALL_TOP_HIGHLIGHT_HEIGHT, width]}
|
|
234
|
+
/>
|
|
235
|
+
</group>
|
|
236
|
+
))}
|
|
237
|
+
</>
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
85
241
|
/** Floor glyph whose shape encodes which kind of geometry the point snapped to. */
|
|
86
242
|
function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }) {
|
|
87
243
|
const y = FLOOR_LIFT
|
|
@@ -90,7 +246,7 @@ function SnapMarker({ kind, x, z }: { kind: WallSnapKind; x: number; z: number }
|
|
|
90
246
|
<mesh
|
|
91
247
|
geometry={FLAT_BOX_GEOMETRY}
|
|
92
248
|
layers={EDITOR_LAYER}
|
|
93
|
-
material={
|
|
249
|
+
material={greenMarkerMaterial}
|
|
94
250
|
position={[x, y, z]}
|
|
95
251
|
renderOrder={1001}
|
|
96
252
|
scale={[MARKER * 2, 1, MARKER * 2]}
|
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { memo, type MouseEvent as ReactMouseEvent } from 'react'
|
|
4
4
|
import useEditor from '../../store/use-editor'
|
|
5
|
+
import {
|
|
6
|
+
useEndpointReshape,
|
|
7
|
+
useIsCurveReshape,
|
|
8
|
+
useMovingNode,
|
|
9
|
+
} from '../../store/use-interaction-scope'
|
|
5
10
|
import { NodeActionMenu } from '../editor/node-action-menu'
|
|
6
11
|
|
|
7
12
|
type SvgPoint = {
|
|
@@ -48,12 +53,11 @@ export const FloorplanActionMenuLayer = memo(function FloorplanActionMenuLayer({
|
|
|
48
53
|
offsetY = 10,
|
|
49
54
|
}: FloorplanActionMenuLayerProps) {
|
|
50
55
|
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
51
|
-
const movingNode =
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const curvingFence = useEditor((state) => state.curvingFence)
|
|
56
|
+
const movingNode = useMovingNode()
|
|
57
|
+
const endpointReshape = useEndpointReshape()
|
|
58
|
+
const isCurveReshape = useIsCurveReshape()
|
|
55
59
|
|
|
56
|
-
if (!isFloorplanHovered || movingNode ||
|
|
60
|
+
if (!isFloorplanHovered || movingNode || endpointReshape || isCurveReshape) {
|
|
57
61
|
return null
|
|
58
62
|
}
|
|
59
63
|
|
|
@@ -28,6 +28,7 @@ import { useFloorplanRender } from './floorplan-render-context'
|
|
|
28
28
|
export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuideLayer() {
|
|
29
29
|
const guides = useAlignmentGuides((s) => s.guides)
|
|
30
30
|
const unit = useViewer((s) => s.unit)
|
|
31
|
+
const metricNotation = useViewer((s) => s.metricNotation)
|
|
31
32
|
const ctx = useFloorplanRender()
|
|
32
33
|
|
|
33
34
|
if (guides.length === 0) return null
|
|
@@ -61,7 +62,7 @@ export const FloorplanAlignmentGuideLayer = memo(function FloorplanAlignmentGuid
|
|
|
61
62
|
// offset along X.
|
|
62
63
|
const pillX = axis === 'x' ? midX + pillOffset : midX
|
|
63
64
|
const pillZ = axis === 'z' ? midZ + pillOffset : midZ
|
|
64
|
-
const distLabel = formatMeasurement(distMeters, unit)
|
|
65
|
+
const distLabel = formatMeasurement(distMeters, unit, metricNotation)
|
|
65
66
|
const charWidth = pillFontSize * 0.55
|
|
66
67
|
const pillWidth = distLabel.length * charWidth + pillPadX * 2
|
|
67
68
|
const pillHeight = pillFontSize + pillPadY * 2
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { Icon } from '@iconify/react'
|
|
4
|
-
import { memo, useMemo } from 'react'
|
|
4
|
+
import { memo, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
5
5
|
import useEditor, { type FloorplanSelectionTool } from '../../store/use-editor'
|
|
6
|
+
import { useFloorplanDraftPreview } from '../../store/use-floorplan-draft-preview'
|
|
6
7
|
import { furnishTools } from '../ui/action-menu/furnish-tools'
|
|
7
8
|
import { tools as structureTools } from '../ui/action-menu/structure-tools'
|
|
8
|
-
|
|
9
|
-
type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
9
|
+
import {
|
|
10
|
+
type FloorplanCursorPoint,
|
|
11
|
+
projectFloorplanCursorPoint,
|
|
12
|
+
resolveFloorplanCursorIndicatorPosition,
|
|
13
|
+
} from './floorplan-cursor-indicator-position'
|
|
13
14
|
|
|
14
15
|
type FloorplanCursorIndicator =
|
|
15
16
|
| {
|
|
@@ -22,7 +23,7 @@ type FloorplanCursorIndicator =
|
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
type FloorplanCursorIndicatorOverlayProps = {
|
|
25
|
-
cursorPosition:
|
|
26
|
+
cursorPosition: FloorplanCursorPoint | null
|
|
26
27
|
floorplanSelectionTool: FloorplanSelectionTool
|
|
27
28
|
movingOpeningType: 'door' | 'window' | null
|
|
28
29
|
isPanning: boolean
|
|
@@ -46,6 +47,10 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
46
47
|
const tool = useEditor((state) => state.tool)
|
|
47
48
|
const structureLayer = useEditor((state) => state.structureLayer)
|
|
48
49
|
const catalogCategory = useEditor((state) => state.catalogCategory)
|
|
50
|
+
const cursorPoint = useFloorplanDraftPreview((state) => state.cursorPoint)
|
|
51
|
+
const anchorRef = useRef<HTMLDivElement>(null)
|
|
52
|
+
const [projectedCursorPosition, setProjectedCursorPosition] =
|
|
53
|
+
useState<FloorplanCursorPoint | null>(null)
|
|
49
54
|
|
|
50
55
|
const activeFloorplanToolConfig = useMemo(() => {
|
|
51
56
|
if (movingOpeningType) {
|
|
@@ -77,13 +82,38 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
if (mode === 'material-paint') {
|
|
80
|
-
return { kind: 'asset', iconSrc: '/icons/paint.
|
|
85
|
+
return { kind: 'asset', iconSrc: '/icons/paint.webp' }
|
|
81
86
|
}
|
|
82
87
|
|
|
83
88
|
return null
|
|
84
89
|
}, [activeFloorplanToolConfig, floorplanSelectionTool, mode, structureLayer])
|
|
85
90
|
|
|
86
|
-
|
|
91
|
+
useLayoutEffect(() => {
|
|
92
|
+
const anchor = anchorRef.current
|
|
93
|
+
const overlayHost = anchor?.parentElement
|
|
94
|
+
const scene = overlayHost?.querySelector<SVGGElement>('[data-floorplan-scene]')
|
|
95
|
+
const sceneToViewport = scene?.getScreenCTM()
|
|
96
|
+
|
|
97
|
+
if (!(cursorPoint && cursorPosition && overlayHost && sceneToViewport)) {
|
|
98
|
+
setProjectedCursorPosition(null)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const overlayRect = overlayHost.getBoundingClientRect()
|
|
103
|
+
const nextPosition = projectFloorplanCursorPoint(cursorPoint, sceneToViewport, {
|
|
104
|
+
x: overlayRect.left,
|
|
105
|
+
y: overlayRect.top,
|
|
106
|
+
})
|
|
107
|
+
setProjectedCursorPosition((currentPosition) =>
|
|
108
|
+
currentPosition &&
|
|
109
|
+
currentPosition.x === nextPosition.x &&
|
|
110
|
+
currentPosition.y === nextPosition.y
|
|
111
|
+
? currentPosition
|
|
112
|
+
: nextPosition,
|
|
113
|
+
)
|
|
114
|
+
}, [cursorPoint, cursorPosition])
|
|
115
|
+
|
|
116
|
+
const position = resolveFloorplanCursorIndicatorPosition(cursorPosition, projectedCursorPosition)
|
|
87
117
|
|
|
88
118
|
if (!(indicator && position) || isPanning) {
|
|
89
119
|
return null
|
|
@@ -93,6 +123,7 @@ export const FloorplanCursorIndicatorOverlay = memo(function FloorplanCursorIndi
|
|
|
93
123
|
<div
|
|
94
124
|
aria-hidden="true"
|
|
95
125
|
className="pointer-events-none absolute z-20"
|
|
126
|
+
ref={anchorRef}
|
|
96
127
|
style={{ left: position.x, top: position.y }}
|
|
97
128
|
>
|
|
98
129
|
{mode === 'delete' ? (
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
projectFloorplanCursorPoint,
|
|
4
|
+
resolveFloorplanCursorIndicatorPosition,
|
|
5
|
+
} from './floorplan-cursor-indicator-position'
|
|
6
|
+
|
|
7
|
+
describe('projectFloorplanCursorPoint', () => {
|
|
8
|
+
test('projects a snapped plan point into overlay-local screen coordinates', () => {
|
|
9
|
+
expect(
|
|
10
|
+
projectFloorplanCursorPoint(
|
|
11
|
+
[3, 4],
|
|
12
|
+
{
|
|
13
|
+
a: 0,
|
|
14
|
+
b: 10,
|
|
15
|
+
c: -10,
|
|
16
|
+
d: 0,
|
|
17
|
+
e: 128,
|
|
18
|
+
f: 40,
|
|
19
|
+
},
|
|
20
|
+
{ x: 30, y: 20 },
|
|
21
|
+
),
|
|
22
|
+
).toEqual({ x: 58, y: 50 })
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('anchors the placement pin to the projected snap point instead of the raw pointer', () => {
|
|
26
|
+
expect(resolveFloorplanCursorIndicatorPosition({ x: 88, y: 97 }, { x: 58, y: 70 })).toEqual({
|
|
27
|
+
x: 58,
|
|
28
|
+
y: 70,
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export type FloorplanCursorPoint = {
|
|
2
|
+
x: number
|
|
3
|
+
y: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type Matrix2D = {
|
|
7
|
+
a: number
|
|
8
|
+
b: number
|
|
9
|
+
c: number
|
|
10
|
+
d: number
|
|
11
|
+
e: number
|
|
12
|
+
f: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function projectFloorplanCursorPoint(
|
|
16
|
+
point: readonly [number, number],
|
|
17
|
+
sceneToViewport: Matrix2D,
|
|
18
|
+
overlayOrigin: FloorplanCursorPoint,
|
|
19
|
+
): FloorplanCursorPoint {
|
|
20
|
+
return {
|
|
21
|
+
x:
|
|
22
|
+
sceneToViewport.a * point[0] +
|
|
23
|
+
sceneToViewport.c * point[1] +
|
|
24
|
+
sceneToViewport.e -
|
|
25
|
+
overlayOrigin.x,
|
|
26
|
+
y:
|
|
27
|
+
sceneToViewport.b * point[0] +
|
|
28
|
+
sceneToViewport.d * point[1] +
|
|
29
|
+
sceneToViewport.f -
|
|
30
|
+
overlayOrigin.y,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function resolveFloorplanCursorIndicatorPosition(
|
|
35
|
+
cursorPosition: FloorplanCursorPoint | null,
|
|
36
|
+
projectedCursorPosition: FloorplanCursorPoint | null,
|
|
37
|
+
): FloorplanCursorPoint | null {
|
|
38
|
+
return projectedCursorPosition ?? cursorPosition
|
|
39
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useViewer } from '@pascal-app/viewer'
|
|
4
|
+
import { useEffect, useState } from 'react'
|
|
5
|
+
import { createPortal } from 'react-dom'
|
|
6
|
+
import { isActive } from '../../lib/interaction/scope'
|
|
7
|
+
import useEditor from '../../store/use-editor'
|
|
8
|
+
import useInteractionScope, { useMovingNode } from '../../store/use-interaction-scope'
|
|
9
|
+
import {
|
|
10
|
+
deleteSelection,
|
|
11
|
+
duplicateSelectionAndPickUp,
|
|
12
|
+
startGroupPickUp,
|
|
13
|
+
} from '../editor/group-actions'
|
|
14
|
+
import { NodeActionMenu } from '../editor/node-action-menu'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Floating Move / Duplicate / Delete pill for a MULTI-selection in the 2D
|
|
18
|
+
* floor plan — the group sibling of `FloorplanRegistryActionMenu` (which is
|
|
19
|
+
* sole-selection only). Anchored above the dashed group selection box; every
|
|
20
|
+
* action targets the whole selection: Move picks the group up (it rides the
|
|
21
|
+
* cursor until a click places it), Duplicate clones the selection and picks
|
|
22
|
+
* the clones up, Delete removes everything selected.
|
|
23
|
+
*
|
|
24
|
+
* Gated on floorplan hover so it never coexists with the 3D group menu in
|
|
25
|
+
* split view (that one hides while the floor plan is hovered), and hidden
|
|
26
|
+
* during any active interaction so it never competes with a live drag.
|
|
27
|
+
*/
|
|
28
|
+
export function FloorplanGroupActionMenu() {
|
|
29
|
+
const isMultiSelect = useViewer((s) => s.selection.selectedIds.length > 1)
|
|
30
|
+
const movingNode = useMovingNode()
|
|
31
|
+
const isFloorplanHovered = useEditor((s) => s.isFloorplanHovered)
|
|
32
|
+
const scopeActive = useInteractionScope((s) => isActive(s.scope))
|
|
33
|
+
|
|
34
|
+
const [position, setPosition] = useState<{ left: number; top: number } | null>(null)
|
|
35
|
+
|
|
36
|
+
const isVisible = isMultiSelect && !movingNode && isFloorplanHovered && !scopeActive
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!isVisible) {
|
|
40
|
+
setPosition(null)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
let raf = 0
|
|
44
|
+
const tick = () => {
|
|
45
|
+
raf = requestAnimationFrame(tick)
|
|
46
|
+
// The dashed group box exists exactly while the multi-selection has
|
|
47
|
+
// transformable participants — anchor to its top edge. Only publish
|
|
48
|
+
// actual changes so the idle poll doesn't re-render every frame.
|
|
49
|
+
const box = document.querySelector('[data-group-selection-box]') as SVGGElement | null
|
|
50
|
+
if (!box) {
|
|
51
|
+
setPosition((prev) => (prev === null ? prev : null))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
const rect = box.getBoundingClientRect()
|
|
55
|
+
const next = { left: rect.left + rect.width / 2, top: rect.top }
|
|
56
|
+
setPosition((prev) =>
|
|
57
|
+
prev && Math.abs(prev.left - next.left) < 0.5 && Math.abs(prev.top - next.top) < 0.5
|
|
58
|
+
? prev
|
|
59
|
+
: next,
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
raf = requestAnimationFrame(tick)
|
|
63
|
+
return () => cancelAnimationFrame(raf)
|
|
64
|
+
}, [isVisible])
|
|
65
|
+
|
|
66
|
+
if (!(isVisible && position)) return null
|
|
67
|
+
|
|
68
|
+
return createPortal(
|
|
69
|
+
<div
|
|
70
|
+
className="pointer-events-none fixed z-30"
|
|
71
|
+
style={{
|
|
72
|
+
left: position.left,
|
|
73
|
+
top: position.top,
|
|
74
|
+
transform: 'translate(-50%, calc(-100% - 12px))',
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
<NodeActionMenu
|
|
78
|
+
onDelete={() => deleteSelection()}
|
|
79
|
+
onDuplicate={() => duplicateSelectionAndPickUp()}
|
|
80
|
+
onMove={() => startGroupPickUp()}
|
|
81
|
+
onPointerDown={(event) => event.stopPropagation()}
|
|
82
|
+
onPointerUp={(event) => event.stopPropagation()}
|
|
83
|
+
/>
|
|
84
|
+
</div>,
|
|
85
|
+
document.body,
|
|
86
|
+
)
|
|
87
|
+
}
|