@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,12 +1,15 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
+
type AnyNode,
|
|
4
5
|
type AnyNodeId,
|
|
5
|
-
DEFAULT_WALL_HEIGHT,
|
|
6
6
|
type FenceNode,
|
|
7
|
+
getWallBaseElevationForNodes,
|
|
7
8
|
getWallCurveFrameAt,
|
|
9
|
+
getWallEffectiveHeightForNodes,
|
|
8
10
|
getWallThickness,
|
|
9
11
|
isCurvedWall,
|
|
12
|
+
MIN_WALL_HEIGHT,
|
|
10
13
|
sceneRegistry,
|
|
11
14
|
useLiveNodeOverrides,
|
|
12
15
|
useScene,
|
|
@@ -32,12 +35,27 @@ import {
|
|
|
32
35
|
} from 'three'
|
|
33
36
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
34
37
|
import { MeshBasicNodeMaterial } from 'three/webgpu'
|
|
38
|
+
import {
|
|
39
|
+
clearStructuralElevationGuide,
|
|
40
|
+
publishStructuralElevationGuide,
|
|
41
|
+
resolveStructuralElevationSnap,
|
|
42
|
+
} from '../../lib/elevation-guides'
|
|
43
|
+
import { isHistoryShortcut } from '../../lib/history'
|
|
44
|
+
import { endpointReshapeScope } from '../../lib/interaction/scope'
|
|
35
45
|
import { sfxEmitter } from '../../lib/sfx-bus'
|
|
36
|
-
import useEditor from '../../store/use-editor'
|
|
46
|
+
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../store/use-editor'
|
|
47
|
+
import useInteractionScope, {
|
|
48
|
+
useEndpointReshape,
|
|
49
|
+
useIsCurveReshape,
|
|
50
|
+
useMovingNode,
|
|
51
|
+
} from '../../store/use-interaction-scope'
|
|
37
52
|
import { suppressBoxSelectForPointer } from '../tools/select/box-select-state'
|
|
53
|
+
import { resolveResizeSnapValue } from './handles/resize-snap'
|
|
54
|
+
import { type HandleDragControls, useHandleDrag } from './handles/use-handle-drag'
|
|
38
55
|
import {
|
|
39
56
|
createArrowHitAreaGeometry,
|
|
40
57
|
createEndpointHitAreaGeometry,
|
|
58
|
+
HandleArrow,
|
|
41
59
|
InvisibleHandleHitArea,
|
|
42
60
|
NO_RAYCAST,
|
|
43
61
|
useInvisibleHitAreaMaterial,
|
|
@@ -48,13 +66,12 @@ const HANDLE_MIN_OFFSET = 0.33
|
|
|
48
66
|
const HANDLE_MIN_HEIGHT = 0.4
|
|
49
67
|
const HANDLE_TOP_INSET = 0.08
|
|
50
68
|
const HEIGHT_HANDLE_OFFSET = 0.26
|
|
51
|
-
const MIN_WALL_HEIGHT = 0.5
|
|
52
69
|
const ARROW_COLOR = '#8381ed'
|
|
53
70
|
const ARROW_HOVER_COLOR = '#a5b4fc'
|
|
54
71
|
// Match the door arrows: scale the rendered chevron down to ~two-thirds
|
|
55
72
|
// so the in-world handles read as a single UI family.
|
|
56
73
|
const ARROW_SCALE = 0.65
|
|
57
|
-
const CORNER_HEX_RADIUS = 0.
|
|
74
|
+
const CORNER_HEX_RADIUS = 0.11
|
|
58
75
|
const CORNER_DASH_SIZE = 0.1
|
|
59
76
|
const CORNER_GAP_SIZE = 0.07
|
|
60
77
|
const CORNER_DASH_THICKNESS = 0.006
|
|
@@ -118,11 +135,9 @@ export function WallMoveSideHandles() {
|
|
|
118
135
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
119
136
|
const mode = useEditor((state) => state.mode)
|
|
120
137
|
const isFloorplanHovered = useEditor((state) => state.isFloorplanHovered)
|
|
121
|
-
const movingNode =
|
|
122
|
-
const
|
|
123
|
-
const
|
|
124
|
-
const curvingWall = useEditor((state) => state.curvingWall)
|
|
125
|
-
const curvingFence = useEditor((state) => state.curvingFence)
|
|
138
|
+
const movingNode = useMovingNode()
|
|
139
|
+
const endpointReshape = useEndpointReshape()
|
|
140
|
+
const isCurveReshape = useIsCurveReshape()
|
|
126
141
|
|
|
127
142
|
const selectedId = selectedIds.length === 1 ? selectedIds[0] : null
|
|
128
143
|
// Fence side-move / height / corner-pickers now flow through the
|
|
@@ -140,10 +155,8 @@ export function WallMoveSideHandles() {
|
|
|
140
155
|
!isFloorplanHovered &&
|
|
141
156
|
mode !== 'delete' &&
|
|
142
157
|
!movingNode &&
|
|
143
|
-
!
|
|
144
|
-
!
|
|
145
|
-
!curvingWall &&
|
|
146
|
-
!curvingFence
|
|
158
|
+
!endpointReshape &&
|
|
159
|
+
!isCurveReshape
|
|
147
160
|
|
|
148
161
|
if (!shouldRender || !selectedNode) return null
|
|
149
162
|
|
|
@@ -151,6 +164,7 @@ export function WallMoveSideHandles() {
|
|
|
151
164
|
}
|
|
152
165
|
|
|
153
166
|
function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
167
|
+
const nodes = useScene((state) => state.nodes)
|
|
154
168
|
// Merge the in-flight drag override so every handle (side-move arrows,
|
|
155
169
|
// height arrow, corner leaders) tracks the live height in real time
|
|
156
170
|
// during a height drag — the scene store stays at the pre-drag value
|
|
@@ -194,24 +208,34 @@ function WallMoveSideHandlesForWall({ wall }: { wall: WallNode }) {
|
|
|
194
208
|
}
|
|
195
209
|
}, [wall.parentId])
|
|
196
210
|
|
|
197
|
-
const
|
|
211
|
+
const baseElevation = getWallBaseElevationForNodes(effectiveWall, nodes)
|
|
212
|
+
const handles = useMemo(() => getWallMoveHandles(effectiveWall, nodes), [effectiveWall, nodes])
|
|
198
213
|
|
|
199
214
|
if (!levelObject || handles.length === 0) return null
|
|
200
215
|
|
|
201
216
|
return createPortal(
|
|
202
|
-
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
217
|
+
<>
|
|
218
|
+
<group position={[0, baseElevation, 0]}>
|
|
219
|
+
{handles.map((handle) => (
|
|
220
|
+
<WallMoveArrowHandle handle={handle} key={handle.key} wall={effectiveWall} />
|
|
221
|
+
))}
|
|
222
|
+
<WallHeightArrowHandle wall={effectiveWall} />
|
|
223
|
+
<WallCornerLeaderHandle endpoint="start" wall={effectiveWall} />
|
|
224
|
+
<WallCornerLeaderHandle endpoint="end" wall={effectiveWall} />
|
|
225
|
+
</group>
|
|
226
|
+
<WallBaseElevationHandle
|
|
227
|
+
baseElevation={baseElevation}
|
|
228
|
+
levelObject={levelObject}
|
|
229
|
+
wall={effectiveWall}
|
|
230
|
+
/>
|
|
231
|
+
</>,
|
|
210
232
|
levelObject,
|
|
211
233
|
)
|
|
212
234
|
}
|
|
213
235
|
|
|
214
236
|
function buildDashedVerticalGeometry(height: number) {
|
|
237
|
+
if (!(Number.isFinite(height) && height > 0)) return new BufferGeometry()
|
|
238
|
+
|
|
215
239
|
// Build each dash as a thin cylinder section so thickness is
|
|
216
240
|
// controllable — native `lineSegments` lock to 1px on WebGL/WebGPU.
|
|
217
241
|
const dashes: BufferGeometry[] = []
|
|
@@ -224,7 +248,10 @@ function buildDashedVerticalGeometry(height: number) {
|
|
|
224
248
|
dashes.push(cylinder)
|
|
225
249
|
y = end + CORNER_GAP_SIZE
|
|
226
250
|
}
|
|
227
|
-
const merged =
|
|
251
|
+
const merged =
|
|
252
|
+
dashes.length > 0
|
|
253
|
+
? (mergeGeometries(dashes, false) ?? new BufferGeometry())
|
|
254
|
+
: new BufferGeometry()
|
|
228
255
|
for (const dash of dashes) dash.dispose()
|
|
229
256
|
return merged
|
|
230
257
|
}
|
|
@@ -241,7 +268,7 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
|
|
|
241
268
|
const corner = endpoint === 'start' ? wall.start : wall.end
|
|
242
269
|
const x = corner[0]
|
|
243
270
|
const z = corner[1]
|
|
244
|
-
const wallHeight = wall.
|
|
271
|
+
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
245
272
|
|
|
246
273
|
const dashedGeometry = useMemo(() => buildDashedVerticalGeometry(wallHeight), [wallHeight])
|
|
247
274
|
const hitGeometry = useMemo(() => createEndpointHitAreaGeometry(CORNER_HEX_RADIUS), [])
|
|
@@ -329,11 +356,12 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
|
|
|
329
356
|
}, [])
|
|
330
357
|
|
|
331
358
|
const activateEndpointMove = (event: ThreeEvent<PointerEvent>) => {
|
|
359
|
+
if (event.button !== 0) return
|
|
332
360
|
event.stopPropagation()
|
|
333
361
|
suppressBoxSelectForPointer(event)
|
|
334
362
|
sfxEmitter.emit('sfx:item-pick')
|
|
335
363
|
document.body.style.cursor = 'grabbing'
|
|
336
|
-
|
|
364
|
+
useInteractionScope.getState().begin(endpointReshapeScope(wall.id, endpoint))
|
|
337
365
|
}
|
|
338
366
|
|
|
339
367
|
return (
|
|
@@ -377,6 +405,155 @@ function WallCornerLeaderHandle({ wall, endpoint }: { wall: WallNode; endpoint:
|
|
|
377
405
|
)
|
|
378
406
|
}
|
|
379
407
|
|
|
408
|
+
function WallBaseElevationHandle({
|
|
409
|
+
wall,
|
|
410
|
+
baseElevation,
|
|
411
|
+
levelObject,
|
|
412
|
+
}: {
|
|
413
|
+
wall: WallNode
|
|
414
|
+
baseElevation: number
|
|
415
|
+
levelObject: Object3D
|
|
416
|
+
}) {
|
|
417
|
+
const [isHovered, setIsHovered] = useState(false)
|
|
418
|
+
const [isDragging, setIsDragging] = useState(false)
|
|
419
|
+
const { camera } = useThree()
|
|
420
|
+
const zoom = camera instanceof OrthographicCamera ? 1 / camera.zoom : 1
|
|
421
|
+
const curveFrame = isCurvedWall(wall) ? getWallCurveFrameAt(wall, 0.5) : null
|
|
422
|
+
const midpoint: [number, number] = curveFrame
|
|
423
|
+
? [curveFrame.point.x, curveFrame.point.y]
|
|
424
|
+
: [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
|
|
425
|
+
const elevationGuideSource = {
|
|
426
|
+
nodeId: wall.id,
|
|
427
|
+
levelId: wall.parentId,
|
|
428
|
+
anchor: midpoint,
|
|
429
|
+
}
|
|
430
|
+
const leaderBottom = Math.min(0, baseElevation)
|
|
431
|
+
const leaderHeight = Math.abs(baseElevation)
|
|
432
|
+
const dashedGeometry = useMemo(
|
|
433
|
+
() => (leaderHeight > 0.0001 ? buildDashedVerticalGeometry(leaderHeight) : null),
|
|
434
|
+
[leaderHeight],
|
|
435
|
+
)
|
|
436
|
+
const dashMaterial = useMemo(
|
|
437
|
+
() =>
|
|
438
|
+
new MeshBasicNodeMaterial({
|
|
439
|
+
color: new Color(ARROW_COLOR),
|
|
440
|
+
transparent: true,
|
|
441
|
+
opacity: 0.85,
|
|
442
|
+
depthTest: false,
|
|
443
|
+
depthWrite: false,
|
|
444
|
+
}),
|
|
445
|
+
[],
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
const isActive = isHovered || isDragging
|
|
449
|
+
useEffect(() => {
|
|
450
|
+
dashMaterial.color.set(isActive ? ARROW_HOVER_COLOR : ARROW_COLOR)
|
|
451
|
+
}, [dashMaterial, isActive])
|
|
452
|
+
useEffect(() => () => dashedGeometry?.dispose(), [dashedGeometry])
|
|
453
|
+
useEffect(() => () => dashMaterial.dispose(), [dashMaterial])
|
|
454
|
+
const dragControls = useMemo<HandleDragControls>(
|
|
455
|
+
() => ({
|
|
456
|
+
onStart: () => {},
|
|
457
|
+
onEnd: () => {},
|
|
458
|
+
}),
|
|
459
|
+
[],
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
const activateElevationMove = useHandleDrag({
|
|
463
|
+
kind: 'drag',
|
|
464
|
+
cursor: 'ns-resize',
|
|
465
|
+
dragControls,
|
|
466
|
+
handleIndex: 0,
|
|
467
|
+
node: wall,
|
|
468
|
+
rideObject: levelObject,
|
|
469
|
+
setIsDragging,
|
|
470
|
+
onStart: ({ event, initialNode, intersectPlane, nodeId, sceneApi }) => {
|
|
471
|
+
if (initialNode.type !== 'wall') return null
|
|
472
|
+
|
|
473
|
+
levelObject.updateWorldMatrix(true, false)
|
|
474
|
+
const initialBase = getWallBaseElevationForNodes(initialNode, sceneApi.nodes())
|
|
475
|
+
const supportBase = initialBase - (initialNode.supportOffset ?? 0)
|
|
476
|
+
const initialHeight = getWallEffectiveHeightForNodes(initialNode, sceneApi.nodes())
|
|
477
|
+
const midpointWorld = new Vector3(midpoint[0], initialBase, midpoint[1]).applyMatrix4(
|
|
478
|
+
levelObject.matrixWorld,
|
|
479
|
+
)
|
|
480
|
+
const planeNormal = new Vector3().subVectors(camera.position, midpointWorld).setY(0)
|
|
481
|
+
if (planeNormal.lengthSq() === 0) return null
|
|
482
|
+
planeNormal.normalize()
|
|
483
|
+
const plane = new Plane().setFromNormalAndCoplanarPoint(planeNormal, midpointWorld)
|
|
484
|
+
const initialHit = new Vector3()
|
|
485
|
+
if (
|
|
486
|
+
!intersectPlane(event.nativeEvent.clientX, event.nativeEvent.clientY, plane, initialHit)
|
|
487
|
+
) {
|
|
488
|
+
return null
|
|
489
|
+
}
|
|
490
|
+
const initialPointerY = levelObject.worldToLocal(initialHit).y
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
onBegin: () => {
|
|
494
|
+
useInteractionScope.getState().begin({ kind: 'handle-drag', nodeId, handle: 'elevation' })
|
|
495
|
+
},
|
|
496
|
+
onEnd: () => {
|
|
497
|
+
useInteractionScope.getState().endIf((scope) => scope.kind === 'handle-drag')
|
|
498
|
+
clearStructuralElevationGuide(initialNode.id)
|
|
499
|
+
},
|
|
500
|
+
move: ({ event: moveEvent, intersectPlane: intersectMovePlane }) => {
|
|
501
|
+
const hit = new Vector3()
|
|
502
|
+
if (!intersectMovePlane(moveEvent.clientX, moveEvent.clientY, plane, hit)) return null
|
|
503
|
+
const pointerY = levelObject.worldToLocal(hit).y
|
|
504
|
+
const nextBase = resolveResizeSnapValue({
|
|
505
|
+
rawValue: initialBase + pointerY - initialPointerY,
|
|
506
|
+
gridSnapEnabled: true,
|
|
507
|
+
gridSnapActive: isGridSnapActive(),
|
|
508
|
+
gridSnapStep: useEditor.getState().gridSnapStep,
|
|
509
|
+
magneticSnapActive: isMagneticSnapActive(),
|
|
510
|
+
magneticSnap: (value) =>
|
|
511
|
+
resolveStructuralElevationSnap(elevationGuideSource, value, sceneApi.nodes()),
|
|
512
|
+
})
|
|
513
|
+
publishStructuralElevationGuide(elevationGuideSource, nextBase, sceneApi.nodes())
|
|
514
|
+
if (Math.abs(nextBase - initialBase) < 1e-6) {
|
|
515
|
+
return {
|
|
516
|
+
supportOffset: initialNode.supportOffset,
|
|
517
|
+
height: initialNode.height,
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const nextOffset = nextBase - supportBase
|
|
521
|
+
return {
|
|
522
|
+
supportOffset: Math.abs(nextOffset) < 1e-6 ? undefined : nextOffset,
|
|
523
|
+
height: initialHeight,
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<>
|
|
532
|
+
{dashedGeometry ? (
|
|
533
|
+
<mesh
|
|
534
|
+
frustumCulled={false}
|
|
535
|
+
geometry={dashedGeometry}
|
|
536
|
+
material={dashMaterial}
|
|
537
|
+
position={[midpoint[0], leaderBottom, midpoint[1]]}
|
|
538
|
+
renderOrder={1001}
|
|
539
|
+
/>
|
|
540
|
+
) : null}
|
|
541
|
+
<HandleArrow
|
|
542
|
+
cursor="ns-resize"
|
|
543
|
+
hover={isActive}
|
|
544
|
+
hoverScale={1.25}
|
|
545
|
+
onHoverChange={setIsHovered}
|
|
546
|
+
onPointerDown={activateElevationMove}
|
|
547
|
+
placement={{
|
|
548
|
+
position: [midpoint[0], baseElevation, midpoint[1]],
|
|
549
|
+
baseScale: zoom,
|
|
550
|
+
}}
|
|
551
|
+
shape="tracker"
|
|
552
|
+
/>
|
|
553
|
+
</>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
|
|
380
557
|
function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
381
558
|
const [isHovered, setIsHovered] = useState(false)
|
|
382
559
|
const arrowGeometry = useMemo(() => createArrowHandleGeometry(), [])
|
|
@@ -429,10 +606,11 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
429
606
|
const wallAngle = Math.atan2(-dirZ, dirX)
|
|
430
607
|
// `wall` is the override-merged effective wall (see
|
|
431
608
|
// WallMoveSideHandlesForWall), so this height is already live during a drag.
|
|
432
|
-
const wallHeight = wall.
|
|
609
|
+
const wallHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
433
610
|
const handleY = wallHeight + HEIGHT_HANDLE_OFFSET
|
|
434
611
|
|
|
435
612
|
const activateHeightResize = (event: ThreeEvent<PointerEvent>) => {
|
|
613
|
+
if (event.button !== 0) return
|
|
436
614
|
event.stopPropagation()
|
|
437
615
|
suppressBoxSelectForPointer(event)
|
|
438
616
|
const levelObject = wall.parentId ? sceneRegistry.nodes.get(wall.parentId) : null
|
|
@@ -461,14 +639,16 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
461
639
|
const hit = new Vector3()
|
|
462
640
|
if (!raycaster.ray.intersectPlane(plane, hit)) return
|
|
463
641
|
|
|
464
|
-
|
|
642
|
+
// Dragging the top makes the wall custom-height; seed from the resolved
|
|
643
|
+
// effective height so a plane-bound wall's drag starts at its real top.
|
|
644
|
+
const initialHeight = getWallEffectiveHeightForNodes(wall, useScene.getState().nodes)
|
|
465
645
|
const initialY = hit.y
|
|
466
646
|
const wallId = wall.id as AnyNodeId
|
|
467
647
|
let pendingHeight = initialHeight
|
|
468
648
|
|
|
469
649
|
document.body.style.cursor = 'ns-resize'
|
|
470
650
|
sfxEmitter.emit('sfx:item-pick')
|
|
471
|
-
|
|
651
|
+
useInteractionScope.getState().begin({ kind: 'handle-drag', nodeId: wallId, handle: 'height' })
|
|
472
652
|
// Suppress R3F node pointer events until pointerup completes so the
|
|
473
653
|
// synthesized click doesn't reroute selection to whatever mesh sits
|
|
474
654
|
// under the cursor at release.
|
|
@@ -494,11 +674,12 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
494
674
|
window.removeEventListener('pointermove', onMove)
|
|
495
675
|
window.removeEventListener('pointerup', onUp)
|
|
496
676
|
window.removeEventListener('pointercancel', onCancel)
|
|
677
|
+
window.removeEventListener('keydown', onKeyDown, true)
|
|
497
678
|
if (document.body.style.cursor === 'ns-resize') {
|
|
498
679
|
document.body.style.cursor = ''
|
|
499
680
|
}
|
|
500
681
|
useScene.temporal.getState().resume()
|
|
501
|
-
|
|
682
|
+
useInteractionScope.getState().endIf((sc) => sc.kind === 'handle-drag')
|
|
502
683
|
useViewer.getState().setInputDragging(false)
|
|
503
684
|
dragCleanupRef.current = null
|
|
504
685
|
}
|
|
@@ -523,10 +704,21 @@ function WallHeightArrowHandle({ wall }: { wall: WallNode }) {
|
|
|
523
704
|
cleanup()
|
|
524
705
|
}
|
|
525
706
|
|
|
707
|
+
// Escape / ⌘Z abort the drag — capture phase so they win over the global
|
|
708
|
+
// use-keyboard arms (⌘Z must never history-jump under a live pointer).
|
|
709
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
710
|
+
if (e.key !== 'Escape' && !isHistoryShortcut(e)) return
|
|
711
|
+
e.preventDefault()
|
|
712
|
+
e.stopPropagation()
|
|
713
|
+
swallowNextClick()
|
|
714
|
+
onCancel()
|
|
715
|
+
}
|
|
716
|
+
|
|
526
717
|
dragCleanupRef.current = cleanup
|
|
527
718
|
window.addEventListener('pointermove', onMove)
|
|
528
719
|
window.addEventListener('pointerup', onUp)
|
|
529
720
|
window.addEventListener('pointercancel', onCancel)
|
|
721
|
+
window.addEventListener('keydown', onKeyDown, true)
|
|
530
722
|
}
|
|
531
723
|
|
|
532
724
|
return (
|
|
@@ -605,16 +797,15 @@ function WallMoveArrowHandle({ wall, handle }: { wall: WallNode; handle: WallMov
|
|
|
605
797
|
useEffect(() => () => arrowMaterial.dispose(), [arrowMaterial])
|
|
606
798
|
|
|
607
799
|
const activateWallMove = (event: ThreeEvent<PointerEvent>) => {
|
|
800
|
+
if (event.button !== 0) return
|
|
608
801
|
event.stopPropagation()
|
|
609
802
|
suppressBoxSelectForPointer(event)
|
|
610
803
|
document.body.style.cursor = 'grabbing'
|
|
611
804
|
|
|
612
805
|
sfxEmitter.emit('sfx:item-pick')
|
|
613
806
|
useEditor.getState().setMovingNode(wall)
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
useEditor.getState().setCurvingWall(null)
|
|
617
|
-
useEditor.getState().setCurvingFence(null)
|
|
807
|
+
useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'endpoint')
|
|
808
|
+
useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'curve')
|
|
618
809
|
// Keep the wall selected so it stays the active item once the move
|
|
619
810
|
// commits; the `!movingNode` guard on the handles hides them mid-drag.
|
|
620
811
|
}
|
|
@@ -696,16 +887,15 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
|
|
|
696
887
|
useEffect(() => () => arrowMaterial.dispose(), [arrowMaterial])
|
|
697
888
|
|
|
698
889
|
const activateFenceMove = (event: ThreeEvent<PointerEvent>) => {
|
|
890
|
+
if (event.button !== 0) return
|
|
699
891
|
event.stopPropagation()
|
|
700
892
|
suppressBoxSelectForPointer(event)
|
|
701
893
|
document.body.style.cursor = 'grabbing'
|
|
702
894
|
|
|
703
895
|
sfxEmitter.emit('sfx:item-pick')
|
|
704
896
|
useEditor.getState().setMovingNode(fence)
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
useEditor.getState().setCurvingWall(null)
|
|
708
|
-
useEditor.getState().setCurvingFence(null)
|
|
897
|
+
useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'endpoint')
|
|
898
|
+
useInteractionScope.getState().endIf((s) => s.kind === 'reshaping' && s.reshape === 'curve')
|
|
709
899
|
// Keep the fence selected so it stays active once the move commits.
|
|
710
900
|
}
|
|
711
901
|
|
|
@@ -743,7 +933,7 @@ function FenceMoveArrowHandle({ fence, handle }: { fence: FenceNode; handle: Wal
|
|
|
743
933
|
)
|
|
744
934
|
}
|
|
745
935
|
|
|
746
|
-
function getWallMoveHandles(wall: WallNode): WallMoveHandle[] {
|
|
936
|
+
function getWallMoveHandles(wall: WallNode, nodes: Record<string, AnyNode>): WallMoveHandle[] {
|
|
747
937
|
const dx = wall.end[0] - wall.start[0]
|
|
748
938
|
const dz = wall.end[1] - wall.start[1]
|
|
749
939
|
const length = Math.hypot(dx, dz)
|
|
@@ -759,7 +949,7 @@ function getWallMoveHandles(wall: WallNode): WallMoveHandle[] {
|
|
|
759
949
|
const midpoint: [number, number] = frame
|
|
760
950
|
? [frame.point.x, frame.point.y]
|
|
761
951
|
: [(wall.start[0] + wall.end[0]) / 2, (wall.start[1] + wall.end[1]) / 2]
|
|
762
|
-
const wallHeight = wall
|
|
952
|
+
const wallHeight = getWallEffectiveHeightForNodes(wall, nodes)
|
|
763
953
|
const handleHeight = Math.max(wallHeight - HANDLE_TOP_INSET, HANDLE_MIN_HEIGHT)
|
|
764
954
|
const offset = Math.max(getWallThickness(wall) / 2 + HANDLE_OFFSET, HANDLE_MIN_OFFSET)
|
|
765
955
|
|
|
@@ -3,8 +3,16 @@
|
|
|
3
3
|
import { type AnyNodeId, sceneRegistry, useLiveNodeOverrides, useScene } from '@pascal-app/core'
|
|
4
4
|
import { useViewer } from '@pascal-app/viewer'
|
|
5
5
|
import { createPortal, useFrame, useThree } from '@react-three/fiber'
|
|
6
|
-
import { useEffect, useMemo, useRef } from 'react'
|
|
7
|
-
import {
|
|
6
|
+
import { useEffect, useLayoutEffect, useMemo, useRef } from 'react'
|
|
7
|
+
import {
|
|
8
|
+
BoxGeometry,
|
|
9
|
+
type BufferGeometry,
|
|
10
|
+
EdgesGeometry,
|
|
11
|
+
type Group,
|
|
12
|
+
type Mesh,
|
|
13
|
+
MeshBasicMaterial,
|
|
14
|
+
Vector3,
|
|
15
|
+
} from 'three'
|
|
8
16
|
import { LineBasicNodeMaterial, MeshBasicNodeMaterial } from 'three/webgpu'
|
|
9
17
|
import { EDITOR_LAYER } from '../../lib/constants'
|
|
10
18
|
|
|
@@ -41,6 +49,13 @@ const fillMaterial = new MeshBasicNodeMaterial({
|
|
|
41
49
|
depthWrite: false,
|
|
42
50
|
})
|
|
43
51
|
|
|
52
|
+
// Keep the cutout in the render list for the outliner's override material
|
|
53
|
+
// without letting its source material alter the normal scene pass.
|
|
54
|
+
const outlineProxyMaterial = new MeshBasicMaterial({
|
|
55
|
+
colorWrite: false,
|
|
56
|
+
depthWrite: false,
|
|
57
|
+
})
|
|
58
|
+
|
|
44
59
|
function makeOutlineGeometry(width: number, height: number, depth: number): BufferGeometry {
|
|
45
60
|
const box = new BoxGeometry(width + PAD, height + PAD, depth + PAD)
|
|
46
61
|
const edges = new EdgesGeometry(box)
|
|
@@ -60,26 +75,60 @@ function makeOutlineGeometry(width: number, height: number, depth: number): Buff
|
|
|
60
75
|
*/
|
|
61
76
|
export function WallOpeningHighlights() {
|
|
62
77
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
78
|
+
const hoveredId = useViewer((state) => state.hoveredId)
|
|
63
79
|
const { scene } = useThree()
|
|
80
|
+
const outlineProxyIds = Array.from(new Set(hoveredId ? [...selectedIds, hoveredId] : selectedIds))
|
|
64
81
|
|
|
65
|
-
if (selectedIds.length === 0) return null
|
|
82
|
+
if (selectedIds.length === 0 && !hoveredId) return null
|
|
66
83
|
|
|
67
|
-
return
|
|
84
|
+
return (
|
|
68
85
|
<>
|
|
69
|
-
{
|
|
70
|
-
<
|
|
86
|
+
{outlineProxyIds.map((id) => (
|
|
87
|
+
<OpeningOutlineProxy key={id} openingId={id} />
|
|
71
88
|
))}
|
|
72
|
-
|
|
73
|
-
|
|
89
|
+
{selectedIds.length > 0 &&
|
|
90
|
+
createPortal(
|
|
91
|
+
<>
|
|
92
|
+
{selectedIds.map((id) => (
|
|
93
|
+
<SelectionOpeningHighlights key={id} selectedId={id} />
|
|
94
|
+
))}
|
|
95
|
+
</>,
|
|
96
|
+
scene,
|
|
97
|
+
)}
|
|
98
|
+
</>
|
|
74
99
|
)
|
|
75
100
|
}
|
|
76
101
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
102
|
+
function OpeningOutlineProxy({ openingId }: { openingId: string }) {
|
|
103
|
+
const node = useScene((state) => state.nodes[openingId as AnyNodeId])
|
|
104
|
+
const geometryRevision = useViewer((state) => state.geometryRevision)
|
|
105
|
+
const proxyRef = useRef<Mesh | null>(null)
|
|
106
|
+
const isFramelessOpening = node?.type === 'door' && node.openingKind === 'opening'
|
|
107
|
+
|
|
108
|
+
useLayoutEffect(() => {
|
|
109
|
+
void geometryRevision
|
|
110
|
+
if (!isFramelessOpening) return
|
|
111
|
+
const root = sceneRegistry.nodes.get(openingId as AnyNodeId)
|
|
112
|
+
const proxy = root?.getObjectByName('cutout') as Mesh | undefined
|
|
113
|
+
if (!proxy) return
|
|
114
|
+
|
|
115
|
+
proxy.material = outlineProxyMaterial
|
|
116
|
+
proxy.visible = true
|
|
117
|
+
proxyRef.current = proxy
|
|
118
|
+
|
|
119
|
+
return () => {
|
|
120
|
+
proxy.visible = false
|
|
121
|
+
proxyRef.current = null
|
|
122
|
+
}
|
|
123
|
+
}, [geometryRevision, isFramelessOpening, openingId])
|
|
124
|
+
|
|
125
|
+
useFrame(() => {
|
|
126
|
+
if (proxyRef.current) proxyRef.current.visible = true
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return null
|
|
130
|
+
}
|
|
131
|
+
|
|
83
132
|
function SelectionOpeningHighlights({ selectedId }: { selectedId: string }) {
|
|
84
133
|
const node = useScene((state) => state.nodes[selectedId as AnyNodeId])
|
|
85
134
|
|
|
@@ -94,27 +143,9 @@ function SelectionOpeningHighlights({ selectedId }: { selectedId: string }) {
|
|
|
94
143
|
)
|
|
95
144
|
}
|
|
96
145
|
|
|
97
|
-
if (node?.type === 'door' && node.openingKind === 'opening') {
|
|
98
|
-
return <SelectedOpeningHighlight openingId={selectedId} parentId={node.parentId} />
|
|
99
|
-
}
|
|
100
|
-
|
|
101
146
|
return null
|
|
102
147
|
}
|
|
103
148
|
|
|
104
|
-
// A frameless opening selected on its own. Pulls the cutout depth from its
|
|
105
|
-
// host wall's thickness so the fill block matches the wall it sits in.
|
|
106
|
-
function SelectedOpeningHighlight({
|
|
107
|
-
openingId,
|
|
108
|
-
parentId,
|
|
109
|
-
}: {
|
|
110
|
-
openingId: string
|
|
111
|
-
parentId: string | null
|
|
112
|
-
}) {
|
|
113
|
-
const parent = useScene((state) => (parentId ? state.nodes[parentId as AnyNodeId] : undefined))
|
|
114
|
-
const depth = parent?.type === 'wall' ? (parent.thickness ?? 0.1) : 0.1
|
|
115
|
-
return <OpeningHighlight depth={depth} openingId={openingId} />
|
|
116
|
-
}
|
|
117
|
-
|
|
118
149
|
function OpeningHighlight({ openingId, depth }: { openingId: string; depth: number }) {
|
|
119
150
|
const node = useScene((state) => state.nodes[openingId as AnyNodeId])
|
|
120
151
|
// Resize arrows publish width/height to the live-override store during the
|