@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
|
@@ -6,19 +6,28 @@ import type {
|
|
|
6
6
|
GridEvent,
|
|
7
7
|
ItemEvent,
|
|
8
8
|
ItemNode,
|
|
9
|
+
RoofEvent,
|
|
10
|
+
RoofNode,
|
|
11
|
+
RoofSegmentNode,
|
|
12
|
+
RoofWallFaceId,
|
|
9
13
|
ShelfEvent,
|
|
10
14
|
ShelfNode,
|
|
11
15
|
WallEvent,
|
|
12
16
|
WallNode,
|
|
13
17
|
} from '@pascal-app/core'
|
|
14
18
|
import {
|
|
19
|
+
canHostOnTop,
|
|
20
|
+
clampRectToRoofWallFace,
|
|
21
|
+
getRoofSegmentWallFace,
|
|
15
22
|
getScaledDimensions,
|
|
16
23
|
isLowProfileItemSurface,
|
|
17
24
|
nodeRegistry,
|
|
25
|
+
roofFacePointToSegment,
|
|
18
26
|
sceneRegistry,
|
|
19
27
|
useScene,
|
|
20
28
|
} from '@pascal-app/core'
|
|
21
29
|
import { Euler, Matrix3, Quaternion, Vector3 } from 'three'
|
|
30
|
+
import { hasRoofFaceChildOverlap, resolveRoofWallHit } from '../../../lib/roof-wall-hit'
|
|
22
31
|
import { snapWorldXZForActiveBuilding } from '../../../lib/world-grid-snap'
|
|
23
32
|
import {
|
|
24
33
|
calculateCursorRotation,
|
|
@@ -56,6 +65,7 @@ function isUpwardItemSurfaceHit(event: ItemEvent): boolean {
|
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
function getSurfacePlacementHeight(surfaceItem: ItemNode, event: ItemEvent, localPos: Vector3) {
|
|
68
|
+
if (!canHostOnTop(surfaceItem)) return null
|
|
59
69
|
if (isLowProfileItemSurface(surfaceItem)) return null
|
|
60
70
|
if (!isUpwardItemSurfaceHit(event)) return null
|
|
61
71
|
|
|
@@ -105,10 +115,13 @@ export const floorStrategy = {
|
|
|
105
115
|
// is rotated; then project the world point back into building-local
|
|
106
116
|
// for storage. Without this, a rotated building drags placement off
|
|
107
117
|
// the world grid.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const
|
|
111
|
-
|
|
118
|
+
// Snapping is governed by the active mode (snapToGrid returns raw in Off /
|
|
119
|
+
// non-grid modes); Alt is force-place only and never bypasses snapping here.
|
|
120
|
+
const [x, z] = snapWorldXZForActiveBuilding(
|
|
121
|
+
snapToGrid(event.position[0], swapDims ? dimZ : dimX),
|
|
122
|
+
snapToGrid(event.position[2], swapDims ? dimX : dimZ),
|
|
123
|
+
0,
|
|
124
|
+
).local
|
|
112
125
|
const y = ctx.gridPosition.y
|
|
113
126
|
|
|
114
127
|
return {
|
|
@@ -132,6 +145,7 @@ export const floorStrategy = {
|
|
|
132
145
|
): CommitResult | null {
|
|
133
146
|
if (ctx.state.surface !== 'floor') return null
|
|
134
147
|
if (!(ctx.levelId && ctx.draftItem)) return null
|
|
148
|
+
if (ctx.draftItem.asset.attachTo) return null
|
|
135
149
|
|
|
136
150
|
const pos: [number, number, number] = [
|
|
137
151
|
ctx.gridPosition.x,
|
|
@@ -211,10 +225,13 @@ export const wallStrategy = {
|
|
|
211
225
|
const adjustedY = validation.adjustedY ?? y
|
|
212
226
|
|
|
213
227
|
return {
|
|
214
|
-
stateUpdate: { surface: 'wall', wallId: event.node.id },
|
|
228
|
+
stateUpdate: { surface: 'wall', wallId: event.node.id, roofSegmentId: null },
|
|
215
229
|
nodeUpdate: {
|
|
216
230
|
position: [x, adjustedY, z],
|
|
217
231
|
parentId: event.node.id,
|
|
232
|
+
// The draft may arrive from a roof-segment wall face.
|
|
233
|
+
roofSegmentId: undefined,
|
|
234
|
+
roofFace: undefined,
|
|
218
235
|
side,
|
|
219
236
|
rotation: [0, itemRotation, 0],
|
|
220
237
|
},
|
|
@@ -313,6 +330,8 @@ export const wallStrategy = {
|
|
|
313
330
|
nodeUpdate: {
|
|
314
331
|
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
315
332
|
parentId: event.node.id,
|
|
333
|
+
roofSegmentId: undefined,
|
|
334
|
+
roofFace: undefined,
|
|
316
335
|
side: ctx.draftItem.side,
|
|
317
336
|
rotation: ctx.draftItem.rotation,
|
|
318
337
|
metadata: stripTransient(ctx.draftItem.metadata),
|
|
@@ -342,6 +361,225 @@ export const wallStrategy = {
|
|
|
342
361
|
},
|
|
343
362
|
}
|
|
344
363
|
|
|
364
|
+
// ============================================================================
|
|
365
|
+
// ROOF WALL STRATEGY
|
|
366
|
+
// ============================================================================
|
|
367
|
+
|
|
368
|
+
type RoofWallTarget = {
|
|
369
|
+
segment: RoofSegmentNode
|
|
370
|
+
faceId: RoofWallFaceId
|
|
371
|
+
faceYaw: number
|
|
372
|
+
/** Stored node position: FACE-LOCAL, y = bottom edge. */
|
|
373
|
+
position: [number, number, number]
|
|
374
|
+
/** Face-coord center of the placed rect (for the overlap guard). */
|
|
375
|
+
centerU: number
|
|
376
|
+
centerV: number
|
|
377
|
+
width: number
|
|
378
|
+
height: number
|
|
379
|
+
cursorPosition: [number, number, number]
|
|
380
|
+
cursorRotationY: number
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Resolve a roof pointer event to an item placement on a segment wall
|
|
385
|
+
* face. Items snap u / bottom-v to the 0.5m grid, then the rect is
|
|
386
|
+
* clamped inside the face profile (sliding under the gable slopes).
|
|
387
|
+
* Position frame matches wall hosting: y anchors the BOTTOM edge;
|
|
388
|
+
* `wall-side` items mount on the outer surface, `wall` items center in
|
|
389
|
+
* the wall thickness.
|
|
390
|
+
*
|
|
391
|
+
* `freePlace` mirrors the wall flow's Alt override (stubbed
|
|
392
|
+
* validators): the profile clamp is skipped, so the rect may overhang
|
|
393
|
+
* the face edges — placement follows the snapped cursor as-is.
|
|
394
|
+
*/
|
|
395
|
+
function resolveRoofWallTarget(
|
|
396
|
+
ctx: PlacementContext,
|
|
397
|
+
event: RoofEvent,
|
|
398
|
+
freePlace = false,
|
|
399
|
+
): RoofWallTarget | null {
|
|
400
|
+
const attachTo = ctx.asset.attachTo
|
|
401
|
+
if (attachTo !== 'wall' && attachTo !== 'wall-side') return null
|
|
402
|
+
|
|
403
|
+
const hit = resolveRoofWallHit(event.node as RoofNode, event.position, event.normal, event.object)
|
|
404
|
+
if (!hit) return null
|
|
405
|
+
|
|
406
|
+
const rawDims = ctx.draftItem
|
|
407
|
+
? getScaledDimensions(ctx.draftItem)
|
|
408
|
+
: (ctx.asset.dimensions ?? DEFAULT_DIMENSIONS)
|
|
409
|
+
const dims = getGridAlignedDimensions(rawDims, attachTo)
|
|
410
|
+
const [width, height] = dims
|
|
411
|
+
|
|
412
|
+
// Snap follows the active mode (snapToHalf returns raw in Off/non-grid);
|
|
413
|
+
// `freePlace` (Alt) is force-place — it only skips the face-fit validity gate.
|
|
414
|
+
const u = snapToHalf(hit.u)
|
|
415
|
+
const centerV = snapToHalf(hit.v) + height / 2
|
|
416
|
+
const fitted = freePlace ? null : clampRectToRoofWallFace(hit.face, u, centerV, width, height)
|
|
417
|
+
if (!fitted && !freePlace) return null
|
|
418
|
+
const finalU = fitted?.u ?? u
|
|
419
|
+
const finalV = fitted?.v ?? centerV
|
|
420
|
+
|
|
421
|
+
// FACE-LOCAL storage (z = 0 → wall mid-plane; ItemSystem pushes
|
|
422
|
+
// wall-side items to the outer surface, exactly like wall hosting).
|
|
423
|
+
// The renderer mounts the node inside the live face frame, so items
|
|
424
|
+
// track segment resizes without any re-anchoring.
|
|
425
|
+
const position: [number, number, number] = [finalU, finalV - height / 2, 0]
|
|
426
|
+
|
|
427
|
+
const segObj = sceneRegistry.nodes.get(hit.segment.id)
|
|
428
|
+
if (!segObj) return null
|
|
429
|
+
segObj.updateWorldMatrix(true, false)
|
|
430
|
+
const segLocal = roofFacePointToSegment(hit.segment, hit.face.id, position)
|
|
431
|
+
const worldPos = segObj.localToWorld(new Vector3(segLocal[0], segLocal[1], segLocal[2]))
|
|
432
|
+
|
|
433
|
+
const nodes = useScene.getState().nodes
|
|
434
|
+
const roof = hit.segment.parentId
|
|
435
|
+
? (nodes[hit.segment.parentId as AnyNodeId] as RoofNode | undefined)
|
|
436
|
+
: undefined
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
segment: hit.segment,
|
|
440
|
+
faceId: hit.face.id,
|
|
441
|
+
faceYaw: hit.face.yaw,
|
|
442
|
+
position,
|
|
443
|
+
centerU: finalU,
|
|
444
|
+
centerV: finalV,
|
|
445
|
+
width,
|
|
446
|
+
height,
|
|
447
|
+
cursorPosition: [worldPos.x, worldPos.y, worldPos.z],
|
|
448
|
+
cursorRotationY: (roof?.rotation ?? 0) + (hit.segment.rotation ?? 0) + hit.face.yaw,
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Validation half of `checkCanPlace` for the roof-wall surface. */
|
|
453
|
+
function canPlaceOnRoofWall(ctx: PlacementContext): boolean {
|
|
454
|
+
const segmentId = ctx.state.roofSegmentId
|
|
455
|
+
if (!(segmentId && ctx.draftItem)) return false
|
|
456
|
+
const segment = useScene.getState().nodes[segmentId as AnyNodeId] as RoofSegmentNode | undefined
|
|
457
|
+
if (segment?.type !== 'roof-segment') return false
|
|
458
|
+
const faceId = ctx.draftItem.roofFace
|
|
459
|
+
if (!faceId) return false
|
|
460
|
+
const face = getRoofSegmentWallFace(segment, faceId)
|
|
461
|
+
|
|
462
|
+
const dims = getGridAlignedDimensions(
|
|
463
|
+
getScaledDimensions(ctx.draftItem),
|
|
464
|
+
ctx.draftItem.asset.attachTo,
|
|
465
|
+
)
|
|
466
|
+
const [width, height] = dims
|
|
467
|
+
// gridPosition carries the stored FACE-LOCAL coords (u, bottom-v, z).
|
|
468
|
+
const u = ctx.gridPosition.x
|
|
469
|
+
const centerV = ctx.gridPosition.y + height / 2
|
|
470
|
+
const clamped = clampRectToRoofWallFace(face, u, centerV, width, height)
|
|
471
|
+
if (!clamped || Math.abs(clamped.u - u) > 1e-3 || Math.abs(clamped.v - centerV) > 1e-3) {
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
return !hasRoofFaceChildOverlap(segment, faceId, u, centerV, width, height, ctx.draftItem.id)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export const roofWallStrategy = {
|
|
478
|
+
/**
|
|
479
|
+
* Handle roof:enter / first hover — transition onto a segment wall
|
|
480
|
+
* face. Returns null when the item doesn't wall-attach or the pointer
|
|
481
|
+
* isn't over a placeable face.
|
|
482
|
+
*/
|
|
483
|
+
enter(ctx: PlacementContext, event: RoofEvent, freePlace = false): TransitionResult | null {
|
|
484
|
+
const target = resolveRoofWallTarget(ctx, event, freePlace)
|
|
485
|
+
if (!target) return null
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
stateUpdate: { surface: 'roof-wall', roofSegmentId: target.segment.id, wallId: null },
|
|
489
|
+
nodeUpdate: {
|
|
490
|
+
position: target.position,
|
|
491
|
+
parentId: target.segment.id,
|
|
492
|
+
roofSegmentId: target.segment.id,
|
|
493
|
+
roofFace: target.faceId,
|
|
494
|
+
wallId: undefined,
|
|
495
|
+
side: 'front',
|
|
496
|
+
rotation: [0, 0, 0],
|
|
497
|
+
},
|
|
498
|
+
cursorRotationY: target.cursorRotationY,
|
|
499
|
+
gridPosition: target.position,
|
|
500
|
+
cursorPosition: target.cursorPosition,
|
|
501
|
+
stopPropagation: true,
|
|
502
|
+
}
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Handle roof:move while on a segment wall face. Returns null when the
|
|
507
|
+
* pointer resolves to a DIFFERENT segment (the coordinator re-enters —
|
|
508
|
+
* segment transitions inside one roof never re-fire roof:enter) or to
|
|
509
|
+
* no placeable face.
|
|
510
|
+
*/
|
|
511
|
+
move(ctx: PlacementContext, event: RoofEvent, freePlace = false): PlacementResult | null {
|
|
512
|
+
if (ctx.state.surface !== 'roof-wall') return null
|
|
513
|
+
if (!ctx.draftItem) return null
|
|
514
|
+
|
|
515
|
+
const target = resolveRoofWallTarget(ctx, event, freePlace)
|
|
516
|
+
if (!target) return null
|
|
517
|
+
if (target.segment.id !== ctx.state.roofSegmentId) return null
|
|
518
|
+
|
|
519
|
+
return {
|
|
520
|
+
gridPosition: target.position,
|
|
521
|
+
cursorPosition: target.cursorPosition,
|
|
522
|
+
cursorRotationY: target.cursorRotationY,
|
|
523
|
+
nodeUpdate: {
|
|
524
|
+
position: target.position,
|
|
525
|
+
side: 'front',
|
|
526
|
+
rotation: [0, 0, 0],
|
|
527
|
+
roofFace: target.faceId,
|
|
528
|
+
},
|
|
529
|
+
stopPropagation: true,
|
|
530
|
+
// Items don't cut the roof — no geometry rebuild needed.
|
|
531
|
+
dirtyNodeId: null,
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Handle roof:click — commit placement on the segment wall face.
|
|
537
|
+
*/
|
|
538
|
+
click(ctx: PlacementContext, _event: RoofEvent, freePlace = false): CommitResult | null {
|
|
539
|
+
if (ctx.state.surface !== 'roof-wall') return null
|
|
540
|
+
if (!(ctx.draftItem && ctx.state.roofSegmentId)) return null
|
|
541
|
+
// Alt mirrors the wall flow's stubbed validators: skip profile-fit
|
|
542
|
+
// and overlap checks entirely.
|
|
543
|
+
if (!freePlace && !canPlaceOnRoofWall(ctx)) return null
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
nodeUpdate: {
|
|
547
|
+
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
548
|
+
parentId: ctx.state.roofSegmentId,
|
|
549
|
+
roofSegmentId: ctx.state.roofSegmentId,
|
|
550
|
+
roofFace: ctx.draftItem.roofFace,
|
|
551
|
+
wallId: undefined,
|
|
552
|
+
side: 'front',
|
|
553
|
+
rotation: [0, 0, 0],
|
|
554
|
+
metadata: stripTransient(ctx.draftItem.metadata),
|
|
555
|
+
},
|
|
556
|
+
stopPropagation: true,
|
|
557
|
+
dirtyNodeId: null,
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Handle roof:leave — transition back to floor surface.
|
|
563
|
+
*/
|
|
564
|
+
leave(ctx: PlacementContext): TransitionResult | null {
|
|
565
|
+
if (ctx.state.surface !== 'roof-wall') return null
|
|
566
|
+
|
|
567
|
+
return {
|
|
568
|
+
stateUpdate: { surface: 'floor', roofSegmentId: null },
|
|
569
|
+
nodeUpdate: {
|
|
570
|
+
position: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
571
|
+
parentId: ctx.levelId,
|
|
572
|
+
roofSegmentId: undefined,
|
|
573
|
+
roofFace: undefined,
|
|
574
|
+
},
|
|
575
|
+
cursorRotationY: 0,
|
|
576
|
+
gridPosition: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
577
|
+
cursorPosition: [ctx.gridPosition.x, ctx.gridPosition.y, ctx.gridPosition.z],
|
|
578
|
+
stopPropagation: true,
|
|
579
|
+
}
|
|
580
|
+
},
|
|
581
|
+
}
|
|
582
|
+
|
|
345
583
|
// ============================================================================
|
|
346
584
|
// CEILING STRATEGY
|
|
347
585
|
// ============================================================================
|
|
@@ -794,6 +1032,9 @@ export function checkCanPlace(ctx: PlacementContext, validators: SpatialValidato
|
|
|
794
1032
|
}
|
|
795
1033
|
|
|
796
1034
|
if (attachTo === 'wall' || attachTo === 'wall-side') {
|
|
1035
|
+
if (ctx.state.surface === 'roof-wall') {
|
|
1036
|
+
return canPlaceOnRoofWall(ctx)
|
|
1037
|
+
}
|
|
797
1038
|
if (ctx.state.surface !== 'wall' || !ctx.state.wallId) return false
|
|
798
1039
|
return validators.canPlaceOnWall(
|
|
799
1040
|
ctx.levelId,
|
|
@@ -12,7 +12,13 @@ import type { Vector3 } from 'three'
|
|
|
12
12
|
// PLACEMENT STATE
|
|
13
13
|
// ============================================================================
|
|
14
14
|
|
|
15
|
-
export type SurfaceType =
|
|
15
|
+
export type SurfaceType =
|
|
16
|
+
| 'floor'
|
|
17
|
+
| 'wall'
|
|
18
|
+
| 'roof-wall'
|
|
19
|
+
| 'ceiling'
|
|
20
|
+
| 'item-surface'
|
|
21
|
+
| 'shelf-surface'
|
|
16
22
|
|
|
17
23
|
/**
|
|
18
24
|
* Tracks which surface the draft item is currently on.
|
|
@@ -21,6 +27,12 @@ export type SurfaceType = 'floor' | 'wall' | 'ceiling' | 'item-surface' | 'shelf
|
|
|
21
27
|
export interface PlacementState {
|
|
22
28
|
surface: SurfaceType
|
|
23
29
|
wallId: string | null
|
|
30
|
+
/**
|
|
31
|
+
* Active roof-segment when `surface === 'roof-wall'` — wall-attach
|
|
32
|
+
* items also host on the vertical wall faces a roof segment generates
|
|
33
|
+
* (base walls + coplanar gable ends).
|
|
34
|
+
*/
|
|
35
|
+
roofSegmentId: string | null
|
|
24
36
|
ceilingId: string | null
|
|
25
37
|
surfaceItemId: string | null
|
|
26
38
|
/**
|
|
@@ -2,12 +2,14 @@ import {
|
|
|
2
2
|
type AnyNodeId,
|
|
3
3
|
type AssetInput,
|
|
4
4
|
ItemNode,
|
|
5
|
+
resolveSupportSlabPatch,
|
|
5
6
|
sceneRegistry,
|
|
6
7
|
useScene,
|
|
7
8
|
} from '@pascal-app/core'
|
|
8
9
|
import { useViewer } from '@pascal-app/viewer'
|
|
9
10
|
import { useCallback, useMemo, useRef } from 'react'
|
|
10
11
|
import type { Vector3 } from 'three'
|
|
12
|
+
import usePlacementPreview from '../../../store/use-placement-preview'
|
|
11
13
|
import { stripTransient } from './placement-math'
|
|
12
14
|
|
|
13
15
|
interface OriginalState {
|
|
@@ -15,6 +17,10 @@ interface OriginalState {
|
|
|
15
17
|
rotation: [number, number, number]
|
|
16
18
|
side: ItemNode['side']
|
|
17
19
|
parentId: string | null
|
|
20
|
+
// Roof-segment wall hosting — cleared/changed by surface transitions
|
|
21
|
+
// mid-move, so reverts must restore it alongside parentId.
|
|
22
|
+
roofSegmentId: ItemNode['roofSegmentId']
|
|
23
|
+
roofFace: ItemNode['roofFace']
|
|
18
24
|
metadata: ItemNode['metadata']
|
|
19
25
|
}
|
|
20
26
|
|
|
@@ -23,17 +29,25 @@ export interface DraftNodeHandle {
|
|
|
23
29
|
readonly current: ItemNode | null
|
|
24
30
|
/** Whether the current draft was adopted (move mode) vs created (create mode) */
|
|
25
31
|
readonly isAdopted: boolean
|
|
26
|
-
/** Create a new draft item at the given position. Returns the created node or null.
|
|
32
|
+
/** Create a new draft item at the given position. Returns the created node or null.
|
|
33
|
+
* `slots` seeds painted slot overrides so duplicates keep their materials. */
|
|
27
34
|
create: (
|
|
28
35
|
gridPosition: Vector3,
|
|
29
36
|
asset: AssetInput,
|
|
30
37
|
rotation?: [number, number, number],
|
|
31
38
|
scale?: [number, number, number],
|
|
39
|
+
slots?: ItemNode['slots'],
|
|
32
40
|
) => ItemNode | null
|
|
33
41
|
/** Take ownership of an existing scene node as the draft (for move mode). */
|
|
34
42
|
adopt: (node: ItemNode) => void
|
|
35
|
-
/** Commit the current draft. Create mode: delete+recreate. Move mode: update in place.
|
|
36
|
-
|
|
43
|
+
/** Commit the current draft. Create mode: delete+recreate. Move mode: update in place.
|
|
44
|
+
* `supportElevationCap` (floor commits) is the pointer-decided surface
|
|
45
|
+
* elevation — it caps the persisted `supportSlabId` election so the
|
|
46
|
+
* commit lands on the surface the cursor pointed at. */
|
|
47
|
+
commit: (
|
|
48
|
+
finalUpdate: Partial<ItemNode>,
|
|
49
|
+
options?: { supportElevationCap?: number | null },
|
|
50
|
+
) => string | null
|
|
37
51
|
/** Destroy the current draft. Create mode: delete node. Move mode: restore original state. */
|
|
38
52
|
destroy: () => void
|
|
39
53
|
}
|
|
@@ -57,6 +71,7 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
57
71
|
asset: AssetInput,
|
|
58
72
|
rotation?: [number, number, number],
|
|
59
73
|
scale?: [number, number, number],
|
|
74
|
+
slots?: ItemNode['slots'],
|
|
60
75
|
): ItemNode | null => {
|
|
61
76
|
const currentLevelId = useViewer.getState().selection.levelId
|
|
62
77
|
if (!currentLevelId) return null
|
|
@@ -69,9 +84,13 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
69
84
|
asset,
|
|
70
85
|
parentId: currentLevelId,
|
|
71
86
|
metadata: { isTransient: true },
|
|
87
|
+
...(slots ? { slots } : {}),
|
|
72
88
|
})
|
|
73
89
|
|
|
74
90
|
useScene.getState().createNode(node, currentLevelId)
|
|
91
|
+
usePlacementPreview
|
|
92
|
+
.getState()
|
|
93
|
+
.set(node, useScene.getState().nodes[currentLevelId as AnyNodeId] ?? null)
|
|
75
94
|
draftRef.current = node
|
|
76
95
|
adoptedRef.current = false
|
|
77
96
|
originalStateRef.current = null
|
|
@@ -92,6 +111,8 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
92
111
|
rotation: [...node.rotation] as [number, number, number],
|
|
93
112
|
side: node.side,
|
|
94
113
|
parentId: node.parentId,
|
|
114
|
+
roofSegmentId: node.roofSegmentId,
|
|
115
|
+
roofFace: node.roofFace,
|
|
95
116
|
metadata: node.metadata,
|
|
96
117
|
}
|
|
97
118
|
|
|
@@ -102,82 +123,142 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
102
123
|
useScene.getState().updateNode(node.id, {
|
|
103
124
|
metadata: { ...meta, isTransient: true },
|
|
104
125
|
})
|
|
126
|
+
usePlacementPreview
|
|
127
|
+
.getState()
|
|
128
|
+
.set(
|
|
129
|
+
node,
|
|
130
|
+
node.parentId ? (useScene.getState().nodes[node.parentId as AnyNodeId] ?? null) : null,
|
|
131
|
+
)
|
|
105
132
|
}, [])
|
|
106
133
|
|
|
107
|
-
const commit = useCallback(
|
|
108
|
-
|
|
109
|
-
|
|
134
|
+
const commit = useCallback(
|
|
135
|
+
(
|
|
136
|
+
finalUpdate: Partial<ItemNode>,
|
|
137
|
+
options?: { supportElevationCap?: number | null },
|
|
138
|
+
): string | null => {
|
|
139
|
+
const draft = draftRef.current
|
|
140
|
+
if (!draft) return null
|
|
141
|
+
|
|
142
|
+
if (adoptedRef.current) {
|
|
143
|
+
// Move mode: update in place (single undoable action)
|
|
144
|
+
const { parentId: newParentId, ...updateProps } = finalUpdate
|
|
145
|
+
const parentId =
|
|
146
|
+
newParentId ??
|
|
147
|
+
originalStateRef.current?.parentId ??
|
|
148
|
+
useViewer.getState().selection.levelId
|
|
149
|
+
const original = originalStateRef.current!
|
|
150
|
+
|
|
151
|
+
// Restore original state while paused — so the undo baseline is clean
|
|
152
|
+
useScene.getState().updateNode(draft.id, {
|
|
153
|
+
position: original.position,
|
|
154
|
+
rotation: original.rotation,
|
|
155
|
+
side: original.side,
|
|
156
|
+
parentId: original.parentId,
|
|
157
|
+
roofSegmentId: original.roofSegmentId,
|
|
158
|
+
roofFace: original.roofFace,
|
|
159
|
+
metadata: original.metadata,
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
// Resume → tracked update (undo reverts to original)
|
|
163
|
+
useScene.temporal.getState().resume()
|
|
164
|
+
|
|
165
|
+
const effectiveNode = ItemNode.parse({
|
|
166
|
+
...draft,
|
|
167
|
+
...updateProps,
|
|
168
|
+
parentId,
|
|
169
|
+
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
170
|
+
})
|
|
110
171
|
|
|
111
|
-
|
|
112
|
-
|
|
172
|
+
useScene.getState().updateNode(draft.id, {
|
|
173
|
+
position: updateProps.position ?? draft.position,
|
|
174
|
+
rotation: updateProps.rotation ?? draft.rotation,
|
|
175
|
+
side: updateProps.side ?? draft.side,
|
|
176
|
+
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
177
|
+
parentId: parentId as string,
|
|
178
|
+
// Forward the roof host explicitly: strategies set it on every
|
|
179
|
+
// commit (segment id on a roof face, undefined elsewhere), and
|
|
180
|
+
// dropping it here strands the item in the roof frame without
|
|
181
|
+
// the segment transform.
|
|
182
|
+
roofSegmentId: updateProps.roofSegmentId,
|
|
183
|
+
roofFace: updateProps.roofFace,
|
|
184
|
+
// Only when the strategy decided about wallId (roof commits clear
|
|
185
|
+
// it) — floor/ceiling commits never managed the field.
|
|
186
|
+
...('wallId' in updateProps ? { wallId: updateProps.wallId } : {}),
|
|
187
|
+
...resolveSupportSlabPatch(effectiveNode, useScene.getState().nodes, {
|
|
188
|
+
maxElevation: options?.supportElevationCap,
|
|
189
|
+
}),
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
useScene.temporal.getState().pause()
|
|
193
|
+
|
|
194
|
+
const id = draft.id
|
|
195
|
+
if (usePlacementPreview.getState().node?.id === id) {
|
|
196
|
+
usePlacementPreview.getState().clear()
|
|
197
|
+
}
|
|
198
|
+
draftRef.current = null
|
|
199
|
+
adoptedRef.current = false
|
|
200
|
+
originalStateRef.current = null
|
|
201
|
+
return id
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Create mode: delete draft (paused), resume, create fresh node (tracked), re-pause
|
|
113
205
|
const { parentId: newParentId, ...updateProps } = finalUpdate
|
|
114
|
-
const parentId =
|
|
115
|
-
|
|
116
|
-
const original = originalStateRef.current!
|
|
206
|
+
const parentId = (newParentId ?? useViewer.getState().selection.levelId) as AnyNodeId
|
|
207
|
+
if (!parentId) return null
|
|
117
208
|
|
|
118
|
-
//
|
|
119
|
-
useScene.getState().
|
|
120
|
-
|
|
121
|
-
rotation: original.rotation,
|
|
122
|
-
side: original.side,
|
|
123
|
-
parentId: original.parentId,
|
|
124
|
-
metadata: original.metadata,
|
|
125
|
-
})
|
|
209
|
+
// Delete draft while paused (invisible to undo)
|
|
210
|
+
useScene.getState().deleteNode(draft.id)
|
|
211
|
+
draftRef.current = null
|
|
126
212
|
|
|
127
|
-
//
|
|
213
|
+
// Briefly resume → create fresh node (the single undoable action)
|
|
128
214
|
useScene.temporal.getState().resume()
|
|
129
215
|
|
|
130
|
-
|
|
216
|
+
const finalNode = ItemNode.parse({
|
|
217
|
+
name: draft.name,
|
|
218
|
+
asset: draft.asset,
|
|
131
219
|
position: updateProps.position ?? draft.position,
|
|
132
220
|
rotation: updateProps.rotation ?? draft.rotation,
|
|
221
|
+
scale: updateProps.scale ?? draft.scale,
|
|
133
222
|
side: updateProps.side ?? draft.side,
|
|
223
|
+
// Carry painted slot overrides so a duplicated item keeps its materials.
|
|
224
|
+
...(draft.slots ? { slots: draft.slots } : {}),
|
|
225
|
+
// Roof host — see the move-mode commit above for why this must be
|
|
226
|
+
// forwarded explicitly.
|
|
227
|
+
roofSegmentId: updateProps.roofSegmentId,
|
|
228
|
+
roofFace: updateProps.roofFace,
|
|
229
|
+
...('wallId' in updateProps ? { wallId: updateProps.wallId } : {}),
|
|
134
230
|
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
135
|
-
parentId
|
|
231
|
+
parentId,
|
|
136
232
|
})
|
|
233
|
+
const nodes = useScene.getState().nodes
|
|
234
|
+
const committedNode = ItemNode.parse({
|
|
235
|
+
...finalNode,
|
|
236
|
+
...resolveSupportSlabPatch(
|
|
237
|
+
finalNode,
|
|
238
|
+
{ ...nodes, [finalNode.id]: finalNode },
|
|
239
|
+
{ maxElevation: options?.supportElevationCap },
|
|
240
|
+
),
|
|
241
|
+
})
|
|
242
|
+
useScene.getState().createNode(committedNode, parentId)
|
|
243
|
+
if (usePlacementPreview.getState().node?.id === draft.id) {
|
|
244
|
+
usePlacementPreview.getState().clear()
|
|
245
|
+
}
|
|
137
246
|
|
|
247
|
+
// Re-pause for next draft cycle
|
|
138
248
|
useScene.temporal.getState().pause()
|
|
139
249
|
|
|
140
|
-
const id = draft.id
|
|
141
|
-
draftRef.current = null
|
|
142
250
|
adoptedRef.current = false
|
|
143
251
|
originalStateRef.current = null
|
|
144
|
-
return id
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const { parentId: newParentId, ...updateProps } = finalUpdate
|
|
149
|
-
const parentId = (newParentId ?? useViewer.getState().selection.levelId) as AnyNodeId
|
|
150
|
-
if (!parentId) return null
|
|
151
|
-
|
|
152
|
-
// Delete draft while paused (invisible to undo)
|
|
153
|
-
useScene.getState().deleteNode(draft.id)
|
|
154
|
-
draftRef.current = null
|
|
155
|
-
|
|
156
|
-
// Briefly resume → create fresh node (the single undoable action)
|
|
157
|
-
useScene.temporal.getState().resume()
|
|
158
|
-
|
|
159
|
-
const finalNode = ItemNode.parse({
|
|
160
|
-
name: draft.name,
|
|
161
|
-
asset: draft.asset,
|
|
162
|
-
position: updateProps.position ?? draft.position,
|
|
163
|
-
rotation: updateProps.rotation ?? draft.rotation,
|
|
164
|
-
scale: updateProps.scale ?? draft.scale,
|
|
165
|
-
side: updateProps.side ?? draft.side,
|
|
166
|
-
metadata: updateProps.metadata ?? stripTransient(draft.metadata),
|
|
167
|
-
})
|
|
168
|
-
useScene.getState().createNode(finalNode, parentId)
|
|
169
|
-
|
|
170
|
-
// Re-pause for next draft cycle
|
|
171
|
-
useScene.temporal.getState().pause()
|
|
172
|
-
|
|
173
|
-
adoptedRef.current = false
|
|
174
|
-
originalStateRef.current = null
|
|
175
|
-
return finalNode.id
|
|
176
|
-
}, [])
|
|
252
|
+
return committedNode.id
|
|
253
|
+
},
|
|
254
|
+
[],
|
|
255
|
+
)
|
|
177
256
|
|
|
178
257
|
const destroy = useCallback(() => {
|
|
179
258
|
if (!draftRef.current) return
|
|
180
259
|
|
|
260
|
+
const draftId = draftRef.current.id
|
|
261
|
+
|
|
181
262
|
if (adoptedRef.current && originalStateRef.current) {
|
|
182
263
|
// Move mode: restore original state instead of deleting — but only
|
|
183
264
|
// if no other system has already committed a new position for this
|
|
@@ -199,6 +280,9 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
199
280
|
draftRef.current = null
|
|
200
281
|
adoptedRef.current = false
|
|
201
282
|
originalStateRef.current = null
|
|
283
|
+
if (usePlacementPreview.getState().node?.id === draftId) {
|
|
284
|
+
usePlacementPreview.getState().clear()
|
|
285
|
+
}
|
|
202
286
|
return
|
|
203
287
|
}
|
|
204
288
|
|
|
@@ -207,6 +291,8 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
207
291
|
rotation: original.rotation,
|
|
208
292
|
side: original.side,
|
|
209
293
|
parentId: original.parentId,
|
|
294
|
+
roofSegmentId: original.roofSegmentId,
|
|
295
|
+
roofFace: original.roofFace,
|
|
210
296
|
metadata: original.metadata,
|
|
211
297
|
})
|
|
212
298
|
|
|
@@ -227,6 +313,9 @@ export function useDraftNode(): DraftNodeHandle {
|
|
|
227
313
|
draftRef.current = null
|
|
228
314
|
adoptedRef.current = false
|
|
229
315
|
originalStateRef.current = null
|
|
316
|
+
if (usePlacementPreview.getState().node?.id === draftId) {
|
|
317
|
+
usePlacementPreview.getState().clear()
|
|
318
|
+
}
|
|
230
319
|
}, [])
|
|
231
320
|
|
|
232
321
|
return useMemo(
|