@pascal-app/editor 0.9.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +14 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +49 -0
- package/src/components/editor/bake-thumbnail.tsx +95 -0
- package/src/components/editor/camera-dragging-lifecycle.test.ts +49 -0
- package/src/components/editor/camera-dragging-lifecycle.ts +41 -0
- package/src/components/editor/custom-camera-controls.tsx +332 -199
- package/src/components/editor/delete-confirmation-dialog.tsx +54 -0
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/elevation-3d-guide-layer.tsx +120 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +102 -5
- package/src/components/editor/first-person/build-collider-world.ts +52 -5
- package/src/components/editor/first-person/terrain-collider.test.ts +132 -0
- package/src/components/editor/first-person/terrain-collider.ts +212 -0
- package/src/components/editor/first-person-controls.tsx +410 -136
- package/src/components/editor/floating-action-menu.tsx +435 -64
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +72 -0
- package/src/components/editor/floorplan-background-selection.ts +7 -20
- package/src/components/editor/floorplan-camera-sync.test.ts +249 -0
- package/src/components/editor/floorplan-camera-sync.ts +213 -0
- package/src/components/editor/floorplan-mode-coordinator.tsx +82 -0
- package/src/components/editor/floorplan-navigation-presentation.test.ts +142 -0
- package/src/components/editor/floorplan-navigation-presentation.ts +156 -0
- package/src/components/editor/floorplan-panel.tsx +2517 -1492
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +597 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/handle-drag-history.test.ts +96 -0
- package/src/components/editor/handles/handle-drag-history.ts +14 -0
- package/src/components/editor/handles/preview-overrides.test.ts +26 -0
- package/src/components/editor/handles/preview-overrides.ts +13 -0
- package/src/components/editor/handles/resize-snap.test.ts +52 -0
- package/src/components/editor/handles/resize-snap.ts +23 -0
- package/src/components/editor/handles/use-handle-drag.ts +49 -6
- package/src/components/editor/index.tsx +289 -35
- package/src/components/editor/measurement-pill.tsx +68 -23
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +409 -180
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +76 -0
- package/src/components/editor/quick-measurement-hud.tsx +34 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +814 -466
- package/src/components/editor/site-edge-labels.tsx +28 -6
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +258 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +118 -253
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-hit-testing.test.ts +82 -0
- package/src/components/editor/use-floorplan-hit-testing.ts +144 -161
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +27 -0
- package/src/components/editor/wall-measurement-label.tsx +10 -28
- package/src/components/editor/wall-move-side-handles.tsx +229 -39
- package/src/components/editor/wall-opening-highlights.tsx +63 -32
- package/src/components/editor/wall-snap-beacon-layer.tsx +162 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +2 -1
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +40 -9
- package/src/components/editor-2d/floorplan-cursor-indicator-position.test.ts +31 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-position.ts +39 -0
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +744 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1672 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +72 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +219 -20
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +327 -59
- package/src/components/editor-2d/floorplan-render-context.test.ts +27 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +113 -8
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +285 -0
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +516 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +236 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +618 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +369 -0
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +431 -32
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +89 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +106 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +94 -26
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +587 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +2280 -578
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +20 -10
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +425 -61
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +107 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +146 -57
- package/src/components/tools/item/use-placement-coordinator.tsx +813 -125
- package/src/components/tools/registry/move-registry-node-tool.tsx +636 -103
- package/src/components/tools/roof/roof-tool.tsx +336 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/floor-stack-preview.ts +4 -0
- package/src/components/tools/shared/horizontal-construction-plane.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/pointer-support-cap.test.ts +98 -0
- package/src/components/tools/shared/pointer-support-cap.ts +273 -0
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +101 -42
- package/src/components/tools/site/stroke-pointer-ownership.test.ts +91 -0
- package/src/components/tools/site/stroke-pointer-ownership.ts +57 -0
- package/src/components/tools/site/terrain-brush-cursor.tsx +247 -0
- package/src/components/tools/site/terrain-sculpt-grid.tsx +192 -0
- package/src/components/tools/site/terrain-sculpt-tool.tsx +356 -0
- package/src/components/tools/stair/stair-click-guard.test.ts +58 -0
- package/src/components/tools/stair/stair-click-guard.ts +72 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +164 -42
- package/src/components/tools/tool-manager.tsx +149 -30
- package/src/components/tools/wall/wall-drafting.test.ts +447 -0
- package/src/components/tools/wall/wall-drafting.ts +304 -108
- package/src/components/tools/wall/wall-snap-geometry.test.ts +52 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +137 -9
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +98 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +44 -94
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +267 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +16 -3
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +93 -18
- package/src/components/ui/controls/material-picker.tsx +162 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +193 -0
- package/src/components/ui/floating-level-selector.tsx +74 -20
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +273 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/multi-selection-panel.tsx +57 -0
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +36 -14
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +19 -4
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/panels/selection-breakdown.test.ts +24 -0
- package/src/components/ui/panels/selection-breakdown.ts +20 -0
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +82 -41
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +76 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +42 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer/viewer-controls-bar.tsx +466 -0
- package/src/components/viewer/viewer-scene-header.tsx +235 -0
- package/src/components/viewer-overlay.tsx +36 -596
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/components/walkthrough-hud.tsx +111 -0
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-grid-events.ts +24 -0
- package/src/hooks/use-keyboard.ts +438 -70
- package/src/index.tsx +355 -9
- package/src/lib/active-placement-surface.test.ts +24 -0
- package/src/lib/active-placement-surface.ts +42 -0
- package/src/lib/camera-pose.test.ts +237 -0
- package/src/lib/camera-pose.ts +172 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +275 -0
- package/src/lib/direct-manipulation.ts +118 -0
- package/src/lib/door-interaction.ts +1 -1
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/elevation-guides.test.ts +107 -0
- package/src/lib/elevation-guides.ts +236 -0
- package/src/lib/floorplan/annotation-visibility.test.ts +327 -0
- package/src/lib/floorplan/annotation-visibility.ts +133 -0
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/drawing-coordination.test.ts +46 -0
- package/src/lib/floorplan/drawing-coordination.ts +13 -0
- package/src/lib/floorplan/floorplan-export.test.ts +319 -0
- package/src/lib/floorplan/floorplan-export.tsx +978 -0
- package/src/lib/floorplan/floorplan-extension.test.ts +43 -0
- package/src/lib/floorplan/floorplan-extension.ts +181 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +120 -0
- package/src/lib/floorplan/floorplan-mode.ts +73 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +194 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +107 -0
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +680 -0
- package/src/lib/floorplan/geometry.ts +52 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +283 -3
- package/src/lib/fresh-planar-placement.ts +85 -1
- package/src/lib/glb-export.test.ts +556 -0
- package/src/lib/glb-export.ts +1071 -0
- package/src/lib/ground-surface.ts +95 -0
- package/src/lib/history.test.ts +96 -0
- package/src/lib/history.ts +31 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +52 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +243 -0
- package/src/lib/level-duplication.test.ts +2 -1
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +188 -0
- package/src/lib/measurements.ts +210 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-action-feedback.ts +33 -0
- package/src/lib/quick-action-nodes.test.ts +94 -0
- package/src/lib/quick-action-nodes.ts +47 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +325 -0
- package/src/lib/scene-clipboard.ts +310 -21
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +313 -0
- package/src/lib/selection-routing.ts +180 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +82 -14
- package/src/lib/sfx-player.test.ts +266 -0
- package/src/lib/sfx-player.ts +262 -69
- package/src/lib/site-boundary.test.ts +53 -0
- package/src/lib/site-boundary.ts +27 -0
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +165 -0
- package/src/lib/snapping-mode.ts +191 -0
- package/src/lib/stair-duplication.test.ts +103 -0
- package/src/lib/stair-duplication.ts +58 -86
- package/src/lib/stair-levels.ts +2 -0
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/terrain-sculpt.test.ts +429 -0
- package/src/lib/terrain-sculpt.ts +301 -0
- package/src/lib/terrain-verb-color.test.ts +51 -0
- package/src/lib/terrain-verb-color.ts +58 -0
- package/src/lib/touch-gesture-priority.test.ts +45 -0
- package/src/lib/touch-gesture-priority.ts +30 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/window-interaction.ts +1 -1
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/camera-pose-store.test.ts +29 -0
- package/src/store/camera-pose-store.ts +27 -0
- package/src/store/live-draft-preview-stores.test.ts +136 -0
- package/src/store/navigation-sync-pose-store.test.ts +34 -0
- package/src/store/navigation-sync-pose-store.ts +27 -0
- package/src/store/terrain-sculpt-mode.test.ts +223 -0
- package/src/store/use-delete-confirmation.ts +27 -0
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-drawing-view.test.ts +23 -0
- package/src/store/use-drawing-view.ts +84 -0
- package/src/store/use-editor.tsx +665 -126
- package/src/store/use-elevation-guides.ts +40 -0
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +51 -0
- package/src/store/use-first-person-hud.ts +40 -0
- package/src/store/use-floorplan-annotation-visibility.ts +60 -0
- package/src/store/use-floorplan-draft-preview.ts +151 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-floorplan-mode.ts +110 -0
- package/src/store/use-interaction-scope.test.ts +206 -0
- package/src/store/use-interaction-scope.ts +140 -0
- package/src/store/use-measurement-draft.test.ts +559 -0
- package/src/store/use-measurement-draft.ts +570 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-path-draft-preview.ts +90 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +53 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/floorplan/selection-tool.ts +0 -271
- package/src/lib/level-name.ts +0 -11
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
createTerrainField,
|
|
4
|
+
DEFAULT_TERRAIN_SPACING,
|
|
5
|
+
heightAt,
|
|
6
|
+
minBrushRadius,
|
|
7
|
+
type SiteNode,
|
|
8
|
+
terrainFieldOf,
|
|
9
|
+
useLiveTerrain,
|
|
10
|
+
useScene,
|
|
11
|
+
} from '@pascal-app/core'
|
|
12
|
+
import {
|
|
13
|
+
brushRadiusRange,
|
|
14
|
+
clampBrushRadius,
|
|
15
|
+
clipTerrainPatchToSite,
|
|
16
|
+
fieldExtentForSite,
|
|
17
|
+
flattenSite,
|
|
18
|
+
resetSiteTerrain,
|
|
19
|
+
resolveFlattenTarget,
|
|
20
|
+
sculptFieldForSite,
|
|
21
|
+
terrainPointInsideSite,
|
|
22
|
+
} from './terrain-sculpt'
|
|
23
|
+
|
|
24
|
+
// `updateNode` batches its dirty-node flush through rAF, which bun's runtime does
|
|
25
|
+
// not provide. Running the callback synchronously is what the other scene-writing
|
|
26
|
+
// unit tests do (`handle-drag-history.test.ts`).
|
|
27
|
+
type RafFn = (callback: (time: number) => void) => number
|
|
28
|
+
;(globalThis as { requestAnimationFrame?: RafFn }).requestAnimationFrame ??= (callback) => {
|
|
29
|
+
callback(0)
|
|
30
|
+
return 0
|
|
31
|
+
}
|
|
32
|
+
;(globalThis as { cancelAnimationFrame?: (id: number) => void }).cancelAnimationFrame ??= () => {}
|
|
33
|
+
|
|
34
|
+
function site(points: Array<[number, number]>): SiteNode {
|
|
35
|
+
return {
|
|
36
|
+
id: 'site_1',
|
|
37
|
+
type: 'site',
|
|
38
|
+
children: [],
|
|
39
|
+
polygon: { type: 'polygon', points },
|
|
40
|
+
} as unknown as SiteNode
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('fieldExtentForSite', () => {
|
|
44
|
+
test('covers the whole polygon with padding to spare', () => {
|
|
45
|
+
const extent = fieldExtentForSite(
|
|
46
|
+
site([
|
|
47
|
+
[-10, -10],
|
|
48
|
+
[10, -10],
|
|
49
|
+
[10, 10],
|
|
50
|
+
[-10, 10],
|
|
51
|
+
]),
|
|
52
|
+
)
|
|
53
|
+
const maxX = extent.origin[0] + (extent.cols - 1) * extent.spacing
|
|
54
|
+
const maxZ = extent.origin[1] + (extent.rows - 1) * extent.spacing
|
|
55
|
+
|
|
56
|
+
// Strictly outside the lot on all four sides: the field must not end at the
|
|
57
|
+
// property line, or the terrain mesh stops exactly where the boundary is
|
|
58
|
+
// drawn and leaves a visible cliff.
|
|
59
|
+
expect(extent.origin[0]).toBeLessThan(-10)
|
|
60
|
+
expect(extent.origin[1]).toBeLessThan(-10)
|
|
61
|
+
expect(maxX).toBeGreaterThan(10)
|
|
62
|
+
expect(maxZ).toBeGreaterThan(10)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('is square and 2ⁿ+1 so a future LOD halving lands on real samples', () => {
|
|
66
|
+
for (const half of [4, 12, 30, 80]) {
|
|
67
|
+
const extent = fieldExtentForSite(
|
|
68
|
+
site([
|
|
69
|
+
[-half, -half],
|
|
70
|
+
[half, -half],
|
|
71
|
+
[half, half],
|
|
72
|
+
[-half, half],
|
|
73
|
+
]),
|
|
74
|
+
)
|
|
75
|
+
expect(extent.cols).toBe(extent.rows)
|
|
76
|
+
expect([33, 65, 129, 257]).toContain(extent.cols)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test('centres on the lot even when the lot is off-origin', () => {
|
|
81
|
+
const extent = fieldExtentForSite(
|
|
82
|
+
site([
|
|
83
|
+
[100, 40],
|
|
84
|
+
[120, 40],
|
|
85
|
+
[120, 60],
|
|
86
|
+
[100, 60],
|
|
87
|
+
]),
|
|
88
|
+
)
|
|
89
|
+
const centerX = extent.origin[0] + ((extent.cols - 1) / 2) * extent.spacing
|
|
90
|
+
const centerZ = extent.origin[1] + ((extent.rows - 1) / 2) * extent.spacing
|
|
91
|
+
expect(centerX).toBeCloseTo(110, 6)
|
|
92
|
+
expect(centerZ).toBeCloseTo(50, 6)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('holds the default spacing while the lot fits in 257 samples', () => {
|
|
96
|
+
const extent = fieldExtentForSite(
|
|
97
|
+
site([
|
|
98
|
+
[-20, -20],
|
|
99
|
+
[20, -20],
|
|
100
|
+
[20, 20],
|
|
101
|
+
[-20, 20],
|
|
102
|
+
]),
|
|
103
|
+
)
|
|
104
|
+
expect(extent.spacing).toBeCloseTo(DEFAULT_TERRAIN_SPACING, 6)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('stretches spacing rather than stopping short on a huge lot', () => {
|
|
108
|
+
// A 400 m lot needs 801 samples at 0.5 m. Capping the sample count and
|
|
109
|
+
// coarsening is the right trade: coarse ground everywhere beats fine ground
|
|
110
|
+
// that ends mid-lot.
|
|
111
|
+
const extent = fieldExtentForSite(
|
|
112
|
+
site([
|
|
113
|
+
[-200, -200],
|
|
114
|
+
[200, -200],
|
|
115
|
+
[200, 200],
|
|
116
|
+
[-200, 200],
|
|
117
|
+
]),
|
|
118
|
+
)
|
|
119
|
+
expect(extent.cols).toBe(257)
|
|
120
|
+
expect(extent.spacing).toBeGreaterThan(DEFAULT_TERRAIN_SPACING)
|
|
121
|
+
const maxX = extent.origin[0] + (extent.cols - 1) * extent.spacing
|
|
122
|
+
expect(maxX).toBeGreaterThan(200)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('a degenerate polygon still yields a usable field', () => {
|
|
126
|
+
for (const points of [[], [[0, 0]] as Array<[number, number]>]) {
|
|
127
|
+
const extent = fieldExtentForSite(site(points as Array<[number, number]>))
|
|
128
|
+
expect(extent.cols).toBe(65)
|
|
129
|
+
expect(extent.spacing).toBeCloseTo(DEFAULT_TERRAIN_SPACING, 6)
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
test('a null site does not throw', () => {
|
|
134
|
+
expect(fieldExtentForSite(null).cols).toBe(65)
|
|
135
|
+
expect(fieldExtentForSite(undefined).cols).toBe(65)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('sculptFieldForSite', () => {
|
|
140
|
+
test('a virgin site gets a fresh flat field sized to its lot', () => {
|
|
141
|
+
const target = site([
|
|
142
|
+
[-10, -10],
|
|
143
|
+
[10, -10],
|
|
144
|
+
[10, 10],
|
|
145
|
+
[-10, 10],
|
|
146
|
+
])
|
|
147
|
+
const field = sculptFieldForSite(target)
|
|
148
|
+
expect(field.cols).toBe(fieldExtentForSite(target).cols)
|
|
149
|
+
expect(heightAt(field, 0, 0)).toBeCloseTo(0, 6)
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe('site footprint', () => {
|
|
154
|
+
const concaveSite = site([
|
|
155
|
+
[0, 0],
|
|
156
|
+
[4, 0],
|
|
157
|
+
[4, 2],
|
|
158
|
+
[2, 2],
|
|
159
|
+
[2, 4],
|
|
160
|
+
[0, 4],
|
|
161
|
+
])
|
|
162
|
+
|
|
163
|
+
test('includes the boundary but excludes the padded field and concave notch', () => {
|
|
164
|
+
expect(terrainPointInsideSite(concaveSite, 0, 2)).toBe(true)
|
|
165
|
+
expect(terrainPointInsideSite(concaveSite, 1, 3)).toBe(true)
|
|
166
|
+
expect(terrainPointInsideSite(concaveSite, 3, 3)).toBe(false)
|
|
167
|
+
expect(terrainPointInsideSite(concaveSite, -1, 2)).toBe(false)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('a brush patch preserves samples outside the property polygon', () => {
|
|
171
|
+
const field = createTerrainField({ cols: 5, rows: 5, spacing: 1, origin: [0, 0] })
|
|
172
|
+
const heights = new Int16Array(25)
|
|
173
|
+
heights.fill(100)
|
|
174
|
+
const clipped = clipTerrainPatchToSite(
|
|
175
|
+
field,
|
|
176
|
+
{ col0: 0, row0: 0, cols: 5, rows: 5, heights },
|
|
177
|
+
concaveSite,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
expect(clipped.heights[1 * 5 + 1]).toBe(100)
|
|
181
|
+
expect(clipped.heights[3 * 5 + 3]).toBe(0)
|
|
182
|
+
expect(clipped.heights[2 * 5 + 0]).toBe(100)
|
|
183
|
+
expect(clipped.heights[2 * 5 + 4]).toBe(100)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('brushRadiusRange', () => {
|
|
188
|
+
const hugeLot = site([
|
|
189
|
+
[-200, -200],
|
|
190
|
+
[200, -200],
|
|
191
|
+
[200, 200],
|
|
192
|
+
[-200, 200],
|
|
193
|
+
])
|
|
194
|
+
const smallLot = site([
|
|
195
|
+
[-10, -10],
|
|
196
|
+
[10, -10],
|
|
197
|
+
[10, 10],
|
|
198
|
+
[-10, 10],
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
test('the low end is the field floor, not a constant', () => {
|
|
202
|
+
// The bug: both the slider and the `[`/`]` keys hardcoded 0.5 m. On a lot big
|
|
203
|
+
// enough that `fieldExtentForSite` stretches spacing, 0.5 m is under the
|
|
204
|
+
// sample gap and the brush silently paints nothing. The floor has to move
|
|
205
|
+
// with the lot.
|
|
206
|
+
const [smallMin] = brushRadiusRange(smallLot)
|
|
207
|
+
const [hugeMin] = brushRadiusRange(hugeLot)
|
|
208
|
+
expect(smallMin).toBeCloseTo(minBrushRadius({ spacing: DEFAULT_TERRAIN_SPACING }), 6)
|
|
209
|
+
expect(hugeMin).toBeGreaterThan(smallMin)
|
|
210
|
+
expect(hugeMin).toBeCloseTo(minBrushRadius(sculptFieldForSite(hugeLot)), 6)
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
test('reads the spacing of terrain that already exists', () => {
|
|
214
|
+
// A lot whose field was created earlier (or imported from a DEM at its own
|
|
215
|
+
// resolution) must be measured by *that* field, not by what a fresh one
|
|
216
|
+
// would get — otherwise the floor is wrong for exactly the sites that have
|
|
217
|
+
// been sculpted.
|
|
218
|
+
const existing = { ...smallLot, terrain: { spacing: 4 } } as unknown as SiteNode
|
|
219
|
+
const [min] = brushRadiusRange(existing)
|
|
220
|
+
expect(min).toBeCloseTo(minBrushRadius({ spacing: 4 }), 6)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
test('a site with no lot yet still yields a usable range', () => {
|
|
224
|
+
for (const target of [null, undefined]) {
|
|
225
|
+
const [min, max] = brushRadiusRange(target)
|
|
226
|
+
expect(min).toBeGreaterThan(0)
|
|
227
|
+
expect(max).toBeGreaterThan(min)
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
test('the range never inverts', () => {
|
|
232
|
+
// Reachable: a lot coarse enough that the floor passes MAX_BRUSH_RADIUS would
|
|
233
|
+
// otherwise give min > max, and a slider whose min exceeds its max clamps
|
|
234
|
+
// every value to a different number depending on which bound is applied last.
|
|
235
|
+
for (const spacing of [0.5, 4, 20, 100]) {
|
|
236
|
+
const target = { ...smallLot, terrain: { spacing } } as unknown as SiteNode
|
|
237
|
+
const [min, max] = brushRadiusRange(target)
|
|
238
|
+
expect(max).toBeGreaterThanOrEqual(min)
|
|
239
|
+
expect(clampBrushRadius(target, 1)).toBeGreaterThanOrEqual(min)
|
|
240
|
+
expect(clampBrushRadius(target, 1e6)).toBeLessThanOrEqual(max)
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
describe('clampBrushRadius', () => {
|
|
246
|
+
const lot = site([
|
|
247
|
+
[-10, -10],
|
|
248
|
+
[10, -10],
|
|
249
|
+
[10, 10],
|
|
250
|
+
[-10, 10],
|
|
251
|
+
])
|
|
252
|
+
|
|
253
|
+
test('holds a radius already in range untouched', () => {
|
|
254
|
+
expect(clampBrushRadius(lot, 5)).toBe(5)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
test('the `[`/`]` ladder stays inside the range and reverses', () => {
|
|
258
|
+
// The keys step multiplicatively (×1.25) and quantize to decimetres, which is
|
|
259
|
+
// where a ratchet would hide: if `]` then `[` did not return, the pair would
|
|
260
|
+
// walk the radius away from where the user left it. Only the clamp at the top
|
|
261
|
+
// is allowed to be one-way.
|
|
262
|
+
const step = (radius: number, up: boolean) =>
|
|
263
|
+
clampBrushRadius(lot, Math.round(radius * (up ? 1.25 : 1 / 1.25) * 10) / 10)
|
|
264
|
+
const [min, max] = brushRadiusRange(lot)
|
|
265
|
+
|
|
266
|
+
let radius = min
|
|
267
|
+
const visited: number[] = []
|
|
268
|
+
for (let i = 0; i < 40 && radius < max; i++) {
|
|
269
|
+
const next = step(radius, true)
|
|
270
|
+
if (next === radius) break
|
|
271
|
+
visited.push(next)
|
|
272
|
+
radius = next
|
|
273
|
+
}
|
|
274
|
+
// The ladder has to actually traverse the range, or `]` is a dead key
|
|
275
|
+
// somewhere in the middle.
|
|
276
|
+
expect(visited.at(-1)).toBe(max)
|
|
277
|
+
expect(visited.length).toBeGreaterThan(8)
|
|
278
|
+
|
|
279
|
+
for (const value of visited) {
|
|
280
|
+
expect(value).toBeGreaterThanOrEqual(min)
|
|
281
|
+
expect(value).toBeLessThanOrEqual(max)
|
|
282
|
+
// Below the top, up-then-down returns. At the ceiling it cannot: one step
|
|
283
|
+
// down from the max is a genuine 20% reduction, which is correct.
|
|
284
|
+
if (step(value, true) !== max) expect(step(step(value, true), false)).toBe(value)
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
describe('resolveFlattenTarget', () => {
|
|
290
|
+
test('an explicit target wins over the ground under the cursor', () => {
|
|
291
|
+
const field = createTerrainField({ cols: 33, rows: 33, spacing: 0.5, origin: [-8, -8] })
|
|
292
|
+
expect(resolveFlattenTarget(field, 3.25, 0, 0)).toBeCloseTo(3.25, 6)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('with no explicit target the first click samples the ground', () => {
|
|
296
|
+
// This is what makes flatten usable without ever opening a number field.
|
|
297
|
+
const field = createTerrainField({ cols: 33, rows: 33, spacing: 0.5, origin: [-8, -8] })
|
|
298
|
+
const raised = { ...field, heights: field.heights.slice() }
|
|
299
|
+
// Sample [16,16] is world (0,0) — put it at 1.5 m.
|
|
300
|
+
raised.heights[16 * 33 + 16] = Math.round(1.5 / raised.step)
|
|
301
|
+
expect(resolveFlattenTarget(raised, null, 0, 0)).toBeCloseTo(1.5, 6)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
test('an explicit target of 0 is honoured, not treated as absent', () => {
|
|
305
|
+
// `?? ` rather than `||` — flatten-to-datum is the single most common
|
|
306
|
+
// explicit target, and `||` would silently resample instead.
|
|
307
|
+
const field = createTerrainField({ cols: 33, rows: 33, spacing: 0.5, origin: [-8, -8] })
|
|
308
|
+
const raised = { ...field, heights: field.heights.slice() }
|
|
309
|
+
raised.heights[16 * 33 + 16] = Math.round(2 / raised.step)
|
|
310
|
+
expect(resolveFlattenTarget(raised, 0, 0, 0)).toBe(0)
|
|
311
|
+
})
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* What Escape mid-stroke actually promises.
|
|
316
|
+
*
|
|
317
|
+
* The tool's `tool:cancel` handler ends the live stroke *without* calling
|
|
318
|
+
* `commitStroke`, which is the whole mechanism: the live stroke is what every reader
|
|
319
|
+
* prefers over the persisted terrain, so dropping it un-committed reverts the scene
|
|
320
|
+
* by construction. The half worth asserting is that it reverts to the *right* place —
|
|
321
|
+
* a site sculpted before the abandoned stroke must keep that earlier work — and that
|
|
322
|
+
* the abandon leaves no live stroke behind for the next reader to pick up.
|
|
323
|
+
*/
|
|
324
|
+
describe('abandoning a stroke — the Escape contract', () => {
|
|
325
|
+
const siteId = 'site_cancel' as SiteNode['id']
|
|
326
|
+
|
|
327
|
+
function fieldAt(metres: number): ReturnType<typeof createTerrainField> {
|
|
328
|
+
const field = createTerrainField({ cols: 33, rows: 33, spacing: 0.5, origin: [-8, -8] })
|
|
329
|
+
const heights = field.heights.slice()
|
|
330
|
+
heights.fill(Math.round(metres / field.step))
|
|
331
|
+
return { ...field, heights }
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
test('ends the live stroke and commits nothing', () => {
|
|
335
|
+
const committed = fieldAt(1.5)
|
|
336
|
+
useLiveTerrain.getState().begin(siteId, committed)
|
|
337
|
+
useLiveTerrain.getState().advance(siteId, fieldAt(9), null as never)
|
|
338
|
+
expect(useLiveTerrain.getState().fieldOf(siteId)).toBeDefined()
|
|
339
|
+
|
|
340
|
+
// Exactly what the tool's `onCancel` does: end, no `commitStroke`.
|
|
341
|
+
useLiveTerrain.getState().end(siteId)
|
|
342
|
+
|
|
343
|
+
// No stroke left armed — a leaked one shows ground the scene graph never had.
|
|
344
|
+
expect(useLiveTerrain.getState().strokes.has(siteId)).toBe(false)
|
|
345
|
+
expect(useLiveTerrain.getState().fieldOf(siteId)).toBeUndefined()
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
test('the snapshot taken at begin is never mutated by the dabs it survives', () => {
|
|
349
|
+
// The anti-compounding invariant, from the cancel side: if a dab could write
|
|
350
|
+
// through to the snapshot, Escape would revert to a *partially* sculpted field
|
|
351
|
+
// rather than the baseline, and the user's earlier work would drift every stroke.
|
|
352
|
+
const committed = fieldAt(1.5)
|
|
353
|
+
const baseline = Array.from(committed.heights)
|
|
354
|
+
useLiveTerrain.getState().begin(siteId, committed)
|
|
355
|
+
useLiveTerrain.getState().advance(siteId, fieldAt(9), null as never)
|
|
356
|
+
useLiveTerrain.getState().end(siteId)
|
|
357
|
+
expect(Array.from(committed.heights)).toEqual(baseline)
|
|
358
|
+
})
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The lot-wide writes have to win over a stroke in flight.
|
|
363
|
+
*
|
|
364
|
+
* Both replace the entire field, and every reader prefers the live stroke over the
|
|
365
|
+
* persisted terrain — so landing one *under* a stroke is invisible twice over: the
|
|
366
|
+
* ground keeps showing the stroke, and the stroke's commit (computed from a snapshot
|
|
367
|
+
* predating the write) then overwrites it. A button that silently does nothing.
|
|
368
|
+
*
|
|
369
|
+
* Not a hypothetical race. On touch one finger holds a stroke while the other taps
|
|
370
|
+
* the panel; with a mouse a stroke survives a wheel zoom's 500 ms `cameraDragging`
|
|
371
|
+
* window and a held pointer that has left the canvas.
|
|
372
|
+
*/
|
|
373
|
+
describe('lot-wide terrain writes vs a stroke in flight', () => {
|
|
374
|
+
const lotSiteId = 'site_lotwide' as SiteNode['id']
|
|
375
|
+
|
|
376
|
+
function siteNodeWithTerrain(terrain: SiteNode['terrain']): SiteNode {
|
|
377
|
+
return {
|
|
378
|
+
id: lotSiteId,
|
|
379
|
+
type: 'site',
|
|
380
|
+
children: [],
|
|
381
|
+
polygon: {
|
|
382
|
+
type: 'polygon',
|
|
383
|
+
points: [
|
|
384
|
+
[-10, -10],
|
|
385
|
+
[10, -10],
|
|
386
|
+
[10, 10],
|
|
387
|
+
[-10, 10],
|
|
388
|
+
],
|
|
389
|
+
},
|
|
390
|
+
terrain,
|
|
391
|
+
} as unknown as SiteNode
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
beforeEach(() => {
|
|
395
|
+
useLiveTerrain.getState().endAll()
|
|
396
|
+
useScene.setState({ nodes: {}, rootNodeIds: [], dirtyNodes: new Set() } as never)
|
|
397
|
+
useScene.temporal.getState().clear()
|
|
398
|
+
useScene.temporal.getState().resume()
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
test('levelling the lot drops the stroke instead of hiding behind it', () => {
|
|
402
|
+
const site = siteNodeWithTerrain(undefined)
|
|
403
|
+
useScene.setState({ nodes: { [lotSiteId]: site }, rootNodeIds: [lotSiteId] } as never)
|
|
404
|
+
useLiveTerrain.getState().begin(lotSiteId, sculptFieldForSite(site))
|
|
405
|
+
|
|
406
|
+
flattenSite(site, 3)
|
|
407
|
+
|
|
408
|
+
// The assertion that fails without the abandon: with a stroke still live,
|
|
409
|
+
// `terrainFieldOf` — and so the mesh, the raycast and the next stroke's
|
|
410
|
+
// snapshot — would keep reading the pre-flatten ground.
|
|
411
|
+
expect(useLiveTerrain.getState().fieldOf(lotSiteId)).toBeUndefined()
|
|
412
|
+
const written = useScene.getState().nodes[lotSiteId] as SiteNode
|
|
413
|
+
expect(heightAt(terrainFieldOf(written) as never, 0, 0)).toBeCloseTo(3, 6)
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
test('clearing terrain drops the stroke even when there is nothing to clear', () => {
|
|
417
|
+
// The early-return path: a first-ever stroke on a virgin site leaves
|
|
418
|
+
// `site.terrain` undefined, so `resetSiteTerrain` returns before writing —
|
|
419
|
+
// and a live stroke surviving that shows ground the scene graph has none of.
|
|
420
|
+
const site = siteNodeWithTerrain(undefined)
|
|
421
|
+
useScene.setState({ nodes: { [lotSiteId]: site }, rootNodeIds: [lotSiteId] } as never)
|
|
422
|
+
useLiveTerrain.getState().begin(lotSiteId, sculptFieldForSite(site))
|
|
423
|
+
|
|
424
|
+
resetSiteTerrain(site)
|
|
425
|
+
|
|
426
|
+
expect(useLiveTerrain.getState().fieldOf(lotSiteId)).toBeUndefined()
|
|
427
|
+
expect(terrainFieldOf(useScene.getState().nodes[lotSiteId] as SiteNode)).toBeNull()
|
|
428
|
+
})
|
|
429
|
+
})
|