@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,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
type AlignmentAnchor,
|
|
2
3
|
type AnyNode,
|
|
3
4
|
type AnyNodeId,
|
|
4
5
|
collectAlignmentAnchors,
|
|
@@ -7,33 +8,94 @@ import {
|
|
|
7
8
|
type LevelNode,
|
|
8
9
|
RoofNode,
|
|
9
10
|
RoofSegmentNode,
|
|
11
|
+
resolveBuildingForLevel,
|
|
10
12
|
sceneRegistry,
|
|
11
|
-
snapScalar,
|
|
12
13
|
useScene,
|
|
14
|
+
type WallNode,
|
|
15
|
+
wallSegmentAnchors,
|
|
13
16
|
} from '@pascal-app/core'
|
|
14
|
-
import {
|
|
17
|
+
import { clearSurfacePlanSnapFeedback, resolveSurfacePlanPointSnap } from '@pascal-app/editor'
|
|
15
18
|
import { useViewer } from '@pascal-app/viewer'
|
|
16
19
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
17
20
|
import * as THREE from 'three'
|
|
18
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
BufferGeometry,
|
|
23
|
+
DoubleSide,
|
|
24
|
+
Float32BufferAttribute,
|
|
25
|
+
type Group,
|
|
26
|
+
type Line,
|
|
27
|
+
Vector3,
|
|
28
|
+
} from 'three'
|
|
19
29
|
import { markToolCancelConsumed } from '../../../hooks/use-keyboard'
|
|
20
30
|
import { EDITOR_LAYER } from '../../../lib/constants'
|
|
21
31
|
import { sfxEmitter } from '../../../lib/sfx-bus'
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
} from '../../../lib/world-grid-snap'
|
|
26
|
-
import useEditor from '../../../store/use-editor'
|
|
32
|
+
import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
|
|
33
|
+
import useEditor, { isGridSnapActive, isMagneticSnapActive } from '../../../store/use-editor'
|
|
34
|
+
import { useFloorplanDraftPreview } from '../../../store/use-floorplan-draft-preview'
|
|
27
35
|
import { CursorSphere } from '../shared/cursor-sphere'
|
|
28
36
|
|
|
29
37
|
const DEFAULT_WALL_HEIGHT = 0.5
|
|
30
38
|
const DEFAULT_PITCH_DEG = 40
|
|
31
39
|
const GRID_OFFSET = 0.02
|
|
32
|
-
/** Figma-style alignment-snap threshold (meters), matching the move tools. */
|
|
33
|
-
const ALIGNMENT_THRESHOLD_M = 0.08
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
// Walls that are direct children of a level.
|
|
42
|
+
function getLevelWalls(
|
|
43
|
+
levelId: string | null,
|
|
44
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
45
|
+
): WallNode[] {
|
|
46
|
+
if (!levelId) return []
|
|
47
|
+
const levelNode = nodes[levelId]
|
|
48
|
+
if (levelNode?.type !== 'level') return []
|
|
49
|
+
return (levelNode as LevelNode).children
|
|
50
|
+
.map((childId) => nodes[childId])
|
|
51
|
+
.filter((node): node is WallNode => node?.type === 'wall')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Walls on the level directly beneath the active one. Levels share the same
|
|
55
|
+
// local XZ origin (they only differ in world Y), so these walls live in the
|
|
56
|
+
// identical coordinate frame and feed straight into both the alignment pool
|
|
57
|
+
// and the magnetic wall-snap pipeline — letting a roof drawn on the upper
|
|
58
|
+
// floor snap onto the wall corners of the floor below.
|
|
59
|
+
function getBelowLevelWalls(
|
|
60
|
+
currentLevelId: string | null,
|
|
61
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
62
|
+
): WallNode[] {
|
|
63
|
+
if (!currentLevelId) return []
|
|
64
|
+
const currentLevel = nodes[currentLevelId]
|
|
65
|
+
if (currentLevel?.type !== 'level') return []
|
|
66
|
+
const buildingId = resolveBuildingForLevel(currentLevel.id, nodes)
|
|
67
|
+
if (!buildingId) return []
|
|
68
|
+
const building = nodes[buildingId]
|
|
69
|
+
if (building?.type !== 'building') return []
|
|
70
|
+
const currentIndex = (currentLevel as LevelNode).level
|
|
71
|
+
const belowLevel = (building.children ?? [])
|
|
72
|
+
.map((childId) => nodes[childId])
|
|
73
|
+
.filter((node): node is LevelNode => node?.type === 'level' && node.level < currentIndex)
|
|
74
|
+
.sort((a, b) => b.level - a.level)[0]
|
|
75
|
+
return getLevelWalls(belowLevel?.id ?? null, nodes)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Current-level + floor-below walls — the magnetic snap targets the roof draft
|
|
79
|
+
// locks onto (corners, midpoints, crossings, wall bodies), matching the wall
|
|
80
|
+
// tool. Same coordinate frame, so no transform is needed.
|
|
81
|
+
function getRoofSnapWalls(
|
|
82
|
+
currentLevelId: string | null,
|
|
83
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
84
|
+
): WallNode[] {
|
|
85
|
+
return [...getLevelWalls(currentLevelId, nodes), ...getBelowLevelWalls(currentLevelId, nodes)]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Current-level alignment anchors plus the floor-below wall corners.
|
|
89
|
+
function collectRoofAlignmentAnchors(
|
|
90
|
+
nodes: Readonly<Record<string, AnyNode>>,
|
|
91
|
+
currentLevelId: string | null,
|
|
92
|
+
): AlignmentAnchor[] {
|
|
93
|
+
return [
|
|
94
|
+
...collectAlignmentAnchors(nodes, '', currentLevelId),
|
|
95
|
+
...getBelowLevelWalls(currentLevelId, nodes).flatMap((wall) =>
|
|
96
|
+
wallSegmentAnchors(wall.id, wall.start, wall.end, wall.thickness),
|
|
97
|
+
),
|
|
98
|
+
]
|
|
37
99
|
}
|
|
38
100
|
|
|
39
101
|
/**
|
|
@@ -148,14 +210,168 @@ type PreviewState = {
|
|
|
148
210
|
levelY: number
|
|
149
211
|
}
|
|
150
212
|
|
|
213
|
+
function buildRoofGhostGeometry(
|
|
214
|
+
width: number,
|
|
215
|
+
depth: number,
|
|
216
|
+
wallHeight: number,
|
|
217
|
+
pitchDeg: number,
|
|
218
|
+
) {
|
|
219
|
+
const safeWidth = Math.max(width, 0.1)
|
|
220
|
+
const safeDepth = Math.max(depth, 0.1)
|
|
221
|
+
const halfWidth = safeWidth / 2
|
|
222
|
+
const halfDepth = safeDepth / 2
|
|
223
|
+
const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
|
|
224
|
+
|
|
225
|
+
const vertices = [
|
|
226
|
+
// Front slope
|
|
227
|
+
-halfWidth,
|
|
228
|
+
wallHeight,
|
|
229
|
+
-halfDepth,
|
|
230
|
+
halfWidth,
|
|
231
|
+
wallHeight,
|
|
232
|
+
-halfDepth,
|
|
233
|
+
halfWidth,
|
|
234
|
+
ridgeHeight,
|
|
235
|
+
0,
|
|
236
|
+
|
|
237
|
+
-halfWidth,
|
|
238
|
+
wallHeight,
|
|
239
|
+
-halfDepth,
|
|
240
|
+
halfWidth,
|
|
241
|
+
ridgeHeight,
|
|
242
|
+
0,
|
|
243
|
+
-halfWidth,
|
|
244
|
+
ridgeHeight,
|
|
245
|
+
0,
|
|
246
|
+
|
|
247
|
+
// Back slope
|
|
248
|
+
-halfWidth,
|
|
249
|
+
ridgeHeight,
|
|
250
|
+
0,
|
|
251
|
+
halfWidth,
|
|
252
|
+
ridgeHeight,
|
|
253
|
+
0,
|
|
254
|
+
halfWidth,
|
|
255
|
+
wallHeight,
|
|
256
|
+
halfDepth,
|
|
257
|
+
|
|
258
|
+
-halfWidth,
|
|
259
|
+
ridgeHeight,
|
|
260
|
+
0,
|
|
261
|
+
halfWidth,
|
|
262
|
+
wallHeight,
|
|
263
|
+
halfDepth,
|
|
264
|
+
-halfWidth,
|
|
265
|
+
wallHeight,
|
|
266
|
+
halfDepth,
|
|
267
|
+
|
|
268
|
+
// Left gable
|
|
269
|
+
-halfWidth,
|
|
270
|
+
wallHeight,
|
|
271
|
+
-halfDepth,
|
|
272
|
+
-halfWidth,
|
|
273
|
+
ridgeHeight,
|
|
274
|
+
0,
|
|
275
|
+
-halfWidth,
|
|
276
|
+
wallHeight,
|
|
277
|
+
halfDepth,
|
|
278
|
+
|
|
279
|
+
// Right gable
|
|
280
|
+
halfWidth,
|
|
281
|
+
wallHeight,
|
|
282
|
+
-halfDepth,
|
|
283
|
+
halfWidth,
|
|
284
|
+
wallHeight,
|
|
285
|
+
halfDepth,
|
|
286
|
+
halfWidth,
|
|
287
|
+
ridgeHeight,
|
|
288
|
+
0,
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
const geometry = new BufferGeometry()
|
|
292
|
+
geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
|
|
293
|
+
geometry.computeVertexNormals()
|
|
294
|
+
return geometry
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function buildRoofGhostEdges(width: number, depth: number, wallHeight: number, pitchDeg: number) {
|
|
298
|
+
const safeWidth = Math.max(width, 0.1)
|
|
299
|
+
const safeDepth = Math.max(depth, 0.1)
|
|
300
|
+
const halfWidth = safeWidth / 2
|
|
301
|
+
const halfDepth = safeDepth / 2
|
|
302
|
+
const ridgeHeight = wallHeight + Math.tan((pitchDeg * Math.PI) / 180) * halfDepth
|
|
303
|
+
|
|
304
|
+
const vertices = [
|
|
305
|
+
// Base rectangle
|
|
306
|
+
-halfWidth,
|
|
307
|
+
wallHeight,
|
|
308
|
+
-halfDepth,
|
|
309
|
+
halfWidth,
|
|
310
|
+
wallHeight,
|
|
311
|
+
-halfDepth,
|
|
312
|
+
halfWidth,
|
|
313
|
+
wallHeight,
|
|
314
|
+
-halfDepth,
|
|
315
|
+
halfWidth,
|
|
316
|
+
wallHeight,
|
|
317
|
+
halfDepth,
|
|
318
|
+
halfWidth,
|
|
319
|
+
wallHeight,
|
|
320
|
+
halfDepth,
|
|
321
|
+
-halfWidth,
|
|
322
|
+
wallHeight,
|
|
323
|
+
halfDepth,
|
|
324
|
+
-halfWidth,
|
|
325
|
+
wallHeight,
|
|
326
|
+
halfDepth,
|
|
327
|
+
-halfWidth,
|
|
328
|
+
wallHeight,
|
|
329
|
+
-halfDepth,
|
|
330
|
+
|
|
331
|
+
// Ridge + gable edges
|
|
332
|
+
-halfWidth,
|
|
333
|
+
ridgeHeight,
|
|
334
|
+
0,
|
|
335
|
+
halfWidth,
|
|
336
|
+
ridgeHeight,
|
|
337
|
+
0,
|
|
338
|
+
-halfWidth,
|
|
339
|
+
wallHeight,
|
|
340
|
+
-halfDepth,
|
|
341
|
+
-halfWidth,
|
|
342
|
+
ridgeHeight,
|
|
343
|
+
0,
|
|
344
|
+
-halfWidth,
|
|
345
|
+
ridgeHeight,
|
|
346
|
+
0,
|
|
347
|
+
-halfWidth,
|
|
348
|
+
wallHeight,
|
|
349
|
+
halfDepth,
|
|
350
|
+
halfWidth,
|
|
351
|
+
wallHeight,
|
|
352
|
+
-halfDepth,
|
|
353
|
+
halfWidth,
|
|
354
|
+
ridgeHeight,
|
|
355
|
+
0,
|
|
356
|
+
halfWidth,
|
|
357
|
+
ridgeHeight,
|
|
358
|
+
0,
|
|
359
|
+
halfWidth,
|
|
360
|
+
wallHeight,
|
|
361
|
+
halfDepth,
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
const geometry = new BufferGeometry()
|
|
365
|
+
geometry.setAttribute('position', new Float32BufferAttribute(vertices, 3))
|
|
366
|
+
return geometry
|
|
367
|
+
}
|
|
368
|
+
|
|
151
369
|
export const RoofTool: React.FC = () => {
|
|
152
370
|
const cursorRef = useRef<Group>(null)
|
|
153
371
|
const outlineRef = useRef<Line>(null!)
|
|
154
372
|
const currentLevelId = useViewer((state) => state.selection.levelId)
|
|
155
373
|
const selectedIds = useViewer((state) => state.selection.selectedIds)
|
|
156
374
|
const setSelection = useViewer((state) => state.setSelection)
|
|
157
|
-
const setTool = useEditor((state) => state.setTool)
|
|
158
|
-
const setMode = useEditor((state) => state.setMode)
|
|
159
375
|
|
|
160
376
|
const selectedIdsRef = useRef(selectedIds)
|
|
161
377
|
useEffect(() => {
|
|
@@ -179,43 +395,38 @@ export const RoofTool: React.FC = () => {
|
|
|
179
395
|
|
|
180
396
|
outlineRef.current.geometry = new BufferGeometry()
|
|
181
397
|
|
|
182
|
-
// Alignment candidates — anchors of every alignable object
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
398
|
+
// Alignment candidates — anchors of every alignable object on the active
|
|
399
|
+
// level plus the wall corners of the floor directly below, so a roof drawn
|
|
400
|
+
// on the upper floor aligns to the walls beneath it. Refreshed after each
|
|
401
|
+
// roof commits. Both corners of the rectangle align.
|
|
402
|
+
let alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
|
|
403
|
+
|
|
404
|
+
// Resolve a grid:move/click into the drafted corner via the shared surface
|
|
405
|
+
// snap pipeline: magnetic lock onto wall corners / midpoints / crossings /
|
|
406
|
+
// bodies on the active level + floor below (raising the green beacon),
|
|
407
|
+
// falling back to alignment guides, then to the world-grid snap. The same
|
|
408
|
+
// path the slab/ceiling tools use, so the beacon and coloring match. The
|
|
409
|
+
// pipeline reads the snapping mode itself (Shift bypass, magnetic on/off),
|
|
410
|
+
// so this tool never inspects the flags. `levelId` is intentionally omitted
|
|
411
|
+
// so the explicit floor-below `walls` aren't filtered back out.
|
|
412
|
+
const resolveDraftPoint = (event: GridEvent): [number, number] => {
|
|
413
|
+
const rawPoint: [number, number] = [event.localPosition[0], event.localPosition[2]]
|
|
414
|
+
const gridFallback: [number, number] = isGridSnapActive()
|
|
415
|
+
? snapWorldXZForActiveBuilding(
|
|
416
|
+
event.position[0],
|
|
417
|
+
event.position[2],
|
|
418
|
+
useEditor.getState().gridSnapStep,
|
|
419
|
+
).local
|
|
420
|
+
: rawPoint
|
|
421
|
+
const nodes = useScene.getState().nodes
|
|
422
|
+
return resolveSurfacePlanPointSnap({
|
|
423
|
+
rawPoint,
|
|
424
|
+
fallbackPoint: gridFallback,
|
|
425
|
+
walls: getRoofSnapWalls(currentLevelId, nodes),
|
|
204
426
|
candidates: alignmentCandidates,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
useAlignmentGuides.getState().clear()
|
|
209
|
-
return [gridX, gridZ]
|
|
210
|
-
}
|
|
211
|
-
useAlignmentGuides.getState().set(ar.guides)
|
|
212
|
-
let x = gridX
|
|
213
|
-
let z = gridZ
|
|
214
|
-
for (const guide of ar.guides) {
|
|
215
|
-
if (guide.axis === 'x') x = guide.coord
|
|
216
|
-
else z = guide.coord
|
|
217
|
-
}
|
|
218
|
-
return [x, z]
|
|
427
|
+
movingId: '__roof-draft__',
|
|
428
|
+
highlightWalls: true,
|
|
429
|
+
}).point
|
|
219
430
|
}
|
|
220
431
|
|
|
221
432
|
const updateOutline = (
|
|
@@ -240,20 +451,7 @@ export const RoofTool: React.FC = () => {
|
|
|
240
451
|
const onGridMove = (event: GridEvent) => {
|
|
241
452
|
if (!cursorRef.current) return
|
|
242
453
|
|
|
243
|
-
|
|
244
|
-
// used to drag every roof corner off the visible grid.
|
|
245
|
-
const snapped = snapWorldXZForActiveBuilding(
|
|
246
|
-
event.position[0],
|
|
247
|
-
event.position[2],
|
|
248
|
-
useEditor.getState().gridSnapStep,
|
|
249
|
-
).local
|
|
250
|
-
const [gridX, gridZ] = alignPoint(
|
|
251
|
-
snapped[0],
|
|
252
|
-
snapped[1],
|
|
253
|
-
event.localPosition[0],
|
|
254
|
-
event.localPosition[2],
|
|
255
|
-
event.nativeEvent?.altKey === true,
|
|
256
|
-
)
|
|
454
|
+
const [gridX, gridZ] = resolveDraftPoint(event)
|
|
257
455
|
const y = event.localPosition[1]
|
|
258
456
|
|
|
259
457
|
const cursorPosition: [number, number, number] = [gridX, y, gridZ]
|
|
@@ -262,6 +460,7 @@ export const RoofTool: React.FC = () => {
|
|
|
262
460
|
cursorRef.current.position.set(gridX, gridY, gridZ)
|
|
263
461
|
|
|
264
462
|
if (
|
|
463
|
+
(isGridSnapActive() || isMagneticSnapActive()) &&
|
|
265
464
|
corner1Ref.current &&
|
|
266
465
|
previousGridPosRef.current &&
|
|
267
466
|
(gridX !== previousGridPosRef.current[0] || gridZ !== previousGridPosRef.current[1])
|
|
@@ -278,6 +477,9 @@ export const RoofTool: React.FC = () => {
|
|
|
278
477
|
})
|
|
279
478
|
|
|
280
479
|
if (corner1Ref.current) {
|
|
480
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
481
|
+
draftPreview.setRoofDraftStart([corner1Ref.current[0], corner1Ref.current[2]])
|
|
482
|
+
draftPreview.setRoofDraftEnd([gridX, gridZ])
|
|
281
483
|
updateOutline(corner1Ref.current, cursorPosition)
|
|
282
484
|
}
|
|
283
485
|
}
|
|
@@ -285,20 +487,7 @@ export const RoofTool: React.FC = () => {
|
|
|
285
487
|
const onGridClick = (event: GridEvent) => {
|
|
286
488
|
if (!currentLevelId) return
|
|
287
489
|
|
|
288
|
-
|
|
289
|
-
// used to drag every roof corner off the visible grid.
|
|
290
|
-
const snapped = snapWorldXZForActiveBuilding(
|
|
291
|
-
event.position[0],
|
|
292
|
-
event.position[2],
|
|
293
|
-
useEditor.getState().gridSnapStep,
|
|
294
|
-
).local
|
|
295
|
-
const [gridX, gridZ] = alignPoint(
|
|
296
|
-
snapped[0],
|
|
297
|
-
snapped[1],
|
|
298
|
-
event.localPosition[0],
|
|
299
|
-
event.localPosition[2],
|
|
300
|
-
event.nativeEvent?.altKey === true,
|
|
301
|
-
)
|
|
490
|
+
const [gridX, gridZ] = resolveDraftPoint(event)
|
|
302
491
|
const y = event.localPosition[1]
|
|
303
492
|
|
|
304
493
|
if (corner1Ref.current) {
|
|
@@ -312,11 +501,17 @@ export const RoofTool: React.FC = () => {
|
|
|
312
501
|
setSelection({ selectedIds: [roofId as AnyNode['id']] })
|
|
313
502
|
|
|
314
503
|
corner1Ref.current = null
|
|
504
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
505
|
+
draftPreview.setRoofDraftStart(null)
|
|
506
|
+
draftPreview.setRoofDraftEnd(null)
|
|
315
507
|
outlineRef.current.visible = false
|
|
316
|
-
alignmentCandidates =
|
|
317
|
-
|
|
508
|
+
alignmentCandidates = collectRoofAlignmentAnchors(useScene.getState().nodes, currentLevelId)
|
|
509
|
+
clearSurfacePlanSnapFeedback()
|
|
318
510
|
} else {
|
|
319
511
|
corner1Ref.current = [gridX, y, gridZ]
|
|
512
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
513
|
+
draftPreview.setRoofDraftStart([gridX, gridZ])
|
|
514
|
+
draftPreview.setRoofDraftEnd([gridX, gridZ])
|
|
320
515
|
sfxEmitter.emit('sfx:structure-build-start')
|
|
321
516
|
setPreview((prev) => ({
|
|
322
517
|
...prev,
|
|
@@ -329,10 +524,13 @@ export const RoofTool: React.FC = () => {
|
|
|
329
524
|
if (corner1Ref.current) {
|
|
330
525
|
markToolCancelConsumed()
|
|
331
526
|
corner1Ref.current = null
|
|
527
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
528
|
+
draftPreview.setRoofDraftStart(null)
|
|
529
|
+
draftPreview.setRoofDraftEnd(null)
|
|
332
530
|
outlineRef.current.visible = false
|
|
333
531
|
setPreview((prev) => ({ ...prev, corner1: null }))
|
|
334
532
|
}
|
|
335
|
-
|
|
533
|
+
clearSurfacePlanSnapFeedback()
|
|
336
534
|
}
|
|
337
535
|
|
|
338
536
|
emitter.on('grid:move', onGridMove)
|
|
@@ -343,9 +541,12 @@ export const RoofTool: React.FC = () => {
|
|
|
343
541
|
emitter.off('grid:move', onGridMove)
|
|
344
542
|
emitter.off('grid:click', onGridClick)
|
|
345
543
|
emitter.off('tool:cancel', onCancel)
|
|
346
|
-
|
|
544
|
+
clearSurfacePlanSnapFeedback()
|
|
347
545
|
|
|
348
546
|
corner1Ref.current = null
|
|
547
|
+
const draftPreview = useFloorplanDraftPreview.getState()
|
|
548
|
+
draftPreview.setRoofDraftStart(null)
|
|
549
|
+
draftPreview.setRoofDraftEnd(null)
|
|
349
550
|
}
|
|
350
551
|
}, [currentLevelId, setSelection])
|
|
351
552
|
|
|
@@ -360,6 +561,34 @@ export const RoofTool: React.FC = () => {
|
|
|
360
561
|
return { length, width, centerX, centerZ }
|
|
361
562
|
}, [corner1, cursorPosition])
|
|
362
563
|
|
|
564
|
+
const roofGhostGeometry = useMemo(() => {
|
|
565
|
+
if (!previewDimensions) return null
|
|
566
|
+
return buildRoofGhostGeometry(
|
|
567
|
+
previewDimensions.length,
|
|
568
|
+
previewDimensions.width,
|
|
569
|
+
DEFAULT_WALL_HEIGHT,
|
|
570
|
+
DEFAULT_PITCH_DEG,
|
|
571
|
+
)
|
|
572
|
+
}, [previewDimensions])
|
|
573
|
+
|
|
574
|
+
const roofGhostEdges = useMemo(() => {
|
|
575
|
+
if (!previewDimensions) return null
|
|
576
|
+
return buildRoofGhostEdges(
|
|
577
|
+
previewDimensions.length,
|
|
578
|
+
previewDimensions.width,
|
|
579
|
+
DEFAULT_WALL_HEIGHT,
|
|
580
|
+
DEFAULT_PITCH_DEG,
|
|
581
|
+
)
|
|
582
|
+
}, [previewDimensions])
|
|
583
|
+
|
|
584
|
+
useEffect(
|
|
585
|
+
() => () => {
|
|
586
|
+
roofGhostGeometry?.dispose()
|
|
587
|
+
roofGhostEdges?.dispose()
|
|
588
|
+
},
|
|
589
|
+
[roofGhostEdges, roofGhostGeometry],
|
|
590
|
+
)
|
|
591
|
+
|
|
363
592
|
return (
|
|
364
593
|
<group>
|
|
365
594
|
<CursorSphere ref={cursorRef} />
|
|
@@ -393,21 +622,34 @@ export const RoofTool: React.FC = () => {
|
|
|
393
622
|
)}
|
|
394
623
|
|
|
395
624
|
{previewDimensions && previewDimensions.length > 0.1 && previewDimensions.width > 0.1 && (
|
|
396
|
-
<
|
|
625
|
+
<group
|
|
397
626
|
layers={EDITOR_LAYER}
|
|
398
627
|
position={[previewDimensions.centerX, levelY + GRID_OFFSET, previewDimensions.centerZ]}
|
|
399
|
-
rotation={[-Math.PI / 2, 0, 0]}
|
|
400
628
|
>
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
629
|
+
{roofGhostGeometry && (
|
|
630
|
+
<mesh geometry={roofGhostGeometry} layers={EDITOR_LAYER} renderOrder={1}>
|
|
631
|
+
<meshBasicMaterial
|
|
632
|
+
color="#818cf8"
|
|
633
|
+
depthTest={false}
|
|
634
|
+
depthWrite={false}
|
|
635
|
+
opacity={0.16}
|
|
636
|
+
side={DoubleSide}
|
|
637
|
+
transparent
|
|
638
|
+
/>
|
|
639
|
+
</mesh>
|
|
640
|
+
)}
|
|
641
|
+
{roofGhostEdges && (
|
|
642
|
+
<lineSegments geometry={roofGhostEdges} layers={EDITOR_LAYER} renderOrder={2}>
|
|
643
|
+
<lineBasicMaterial
|
|
644
|
+
color="#818cf8"
|
|
645
|
+
depthTest={false}
|
|
646
|
+
depthWrite={false}
|
|
647
|
+
opacity={0.5}
|
|
648
|
+
transparent
|
|
649
|
+
/>
|
|
650
|
+
</lineSegments>
|
|
651
|
+
)}
|
|
652
|
+
</group>
|
|
411
653
|
)}
|
|
412
654
|
</group>
|
|
413
655
|
)
|
|
@@ -9,6 +9,10 @@ type PointerEventLike = {
|
|
|
9
9
|
nativeEvent?: PointerEvent | PointerEventLike
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
type SuppressBoxSelectOptions = {
|
|
13
|
+
markHandled?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
function pointerIdFor(event: PointerEvent | PointerEventLike): number | null {
|
|
13
17
|
if ('pointerId' in event && typeof event.pointerId === 'number') {
|
|
14
18
|
return event.pointerId
|
|
@@ -28,8 +32,12 @@ export function markBoxSelectHandled() {
|
|
|
28
32
|
}, 50)
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
export function suppressBoxSelectForPointer(
|
|
32
|
-
|
|
35
|
+
export function suppressBoxSelectForPointer(
|
|
36
|
+
event: PointerEvent | PointerEventLike,
|
|
37
|
+
options: SuppressBoxSelectOptions = {},
|
|
38
|
+
) {
|
|
39
|
+
const markHandled = options.markHandled ?? true
|
|
40
|
+
if (markHandled) markBoxSelectHandled()
|
|
33
41
|
|
|
34
42
|
const pointerId = pointerIdFor(event)
|
|
35
43
|
if (pointerId === null || suppressedPointerIds.has(pointerId)) return
|
|
@@ -38,7 +46,7 @@ export function suppressBoxSelectForPointer(event: PointerEvent | PointerEventLi
|
|
|
38
46
|
|
|
39
47
|
const clear = (releaseEvent?: PointerEvent) => {
|
|
40
48
|
if (releaseEvent && releaseEvent.pointerId !== pointerId) return
|
|
41
|
-
markBoxSelectHandled()
|
|
49
|
+
if (markHandled) markBoxSelectHandled()
|
|
42
50
|
suppressedPointerIds.delete(pointerId)
|
|
43
51
|
const cleanup = suppressionCleanups.get(pointerId)
|
|
44
52
|
suppressionCleanups.delete(pointerId)
|
|
@@ -48,15 +56,18 @@ export function suppressBoxSelectForPointer(event: PointerEvent | PointerEventLi
|
|
|
48
56
|
const onPointerUp = (releaseEvent: PointerEvent) => clear(releaseEvent)
|
|
49
57
|
const onPointerCancel = (releaseEvent: PointerEvent) => clear(releaseEvent)
|
|
50
58
|
const onBlur = () => clear()
|
|
59
|
+
// Click-preserving handle interactions need suppression cleared before
|
|
60
|
+
// canvas-level pointerup handlers decide whether to block the follow-up click.
|
|
61
|
+
const releaseListenerOptions = markHandled ? undefined : { capture: true }
|
|
51
62
|
const cleanup = () => {
|
|
52
|
-
window.removeEventListener('pointerup', onPointerUp)
|
|
53
|
-
window.removeEventListener('pointercancel', onPointerCancel)
|
|
63
|
+
window.removeEventListener('pointerup', onPointerUp, releaseListenerOptions)
|
|
64
|
+
window.removeEventListener('pointercancel', onPointerCancel, releaseListenerOptions)
|
|
54
65
|
window.removeEventListener('blur', onBlur)
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
suppressionCleanups.set(pointerId, cleanup)
|
|
58
|
-
window.addEventListener('pointerup', onPointerUp)
|
|
59
|
-
window.addEventListener('pointercancel', onPointerCancel)
|
|
69
|
+
window.addEventListener('pointerup', onPointerUp, releaseListenerOptions)
|
|
70
|
+
window.addEventListener('pointercancel', onPointerCancel, releaseListenerOptions)
|
|
60
71
|
window.addEventListener('blur', onBlur)
|
|
61
72
|
}
|
|
62
73
|
|