@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
|
@@ -3,9 +3,11 @@ import {
|
|
|
3
3
|
type AnyNodeId,
|
|
4
4
|
type DoorNode,
|
|
5
5
|
getGarageVisibleOpeningRatio,
|
|
6
|
+
getLevelElevations,
|
|
6
7
|
isOperationDoorType,
|
|
7
8
|
nodeRegistry,
|
|
8
9
|
sceneRegistry,
|
|
10
|
+
terrainFieldOf,
|
|
9
11
|
useInteractive,
|
|
10
12
|
useScene,
|
|
11
13
|
} from '@pascal-app/core'
|
|
@@ -13,6 +15,7 @@ import * as THREE from 'three'
|
|
|
13
15
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
14
16
|
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh'
|
|
15
17
|
import { computeSceneBoundsXZ } from '../../../lib/scene-bounds'
|
|
18
|
+
import { createTerrainColliderGeometry } from './terrain-collider'
|
|
16
19
|
|
|
17
20
|
const SKIPPED_MESH_NAMES = new Set(['cutout', 'collision-mesh'])
|
|
18
21
|
const COLLIDER_NODE_CATEGORIES = new Set(['structure', 'furnish'])
|
|
@@ -27,6 +30,7 @@ const OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD = 0.85
|
|
|
27
30
|
const LEVEL_FALLBACK_FLOOR_THICKNESS = 0.08
|
|
28
31
|
const LEVEL_FALLBACK_FLOOR_PADDING = 2
|
|
29
32
|
const LEVEL_FALLBACK_FLOOR_MIN_SIZE = 30
|
|
33
|
+
const SITE_GROUND_COLLIDER_MIN_SIZE = 2000
|
|
30
34
|
|
|
31
35
|
export const FIRST_PERSON_SPAWN_EYE_HEIGHT = SPAWN_EYE_HEIGHT
|
|
32
36
|
|
|
@@ -49,6 +53,22 @@ function isMesh(object: THREE.Object3D): object is THREE.Mesh {
|
|
|
49
53
|
return 'isMesh' in object && (object as THREE.Mesh).isMesh
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
// Renderer-effective visibility: an invisible ancestor hides the whole
|
|
57
|
+
// subtree at render time even when the object's own flag is true. The
|
|
58
|
+
// collider world must match what's rendered — the roof keeps stale,
|
|
59
|
+
// UNCUT per-segment CSG inside its hidden `segments-wrapper` (full-edit
|
|
60
|
+
// exit hides the wrapper without stripping geometry), and cloning those
|
|
61
|
+
// meshes would block the walkthrough player at openings the visible
|
|
62
|
+
// merged shell has cut through.
|
|
63
|
+
function isEffectivelyVisible(object: THREE.Object3D) {
|
|
64
|
+
let current: THREE.Object3D | null = object
|
|
65
|
+
while (current) {
|
|
66
|
+
if (!current.visible) return false
|
|
67
|
+
current = current.parent
|
|
68
|
+
}
|
|
69
|
+
return true
|
|
70
|
+
}
|
|
71
|
+
|
|
52
72
|
function isColliderMaterialVisible(material: THREE.Material | THREE.Material[]) {
|
|
53
73
|
return Array.isArray(material) ? material.some((entry) => entry.visible) : material.visible
|
|
54
74
|
}
|
|
@@ -112,10 +132,12 @@ function createLevelFallbackFloorGeometry(level: LevelNode, nodes: SceneNodes) {
|
|
|
112
132
|
|
|
113
133
|
function collectLevelFallbackFloorGeometries(nodes: SceneNodes) {
|
|
114
134
|
const geometries: THREE.BufferGeometry[] = []
|
|
135
|
+
const levelElevations = getLevelElevations(nodes)
|
|
115
136
|
|
|
116
137
|
for (const levelId of sceneRegistry.byType.level!) {
|
|
117
138
|
const node = nodes[levelId as AnyNodeId]
|
|
118
139
|
if (node?.type !== 'level') continue
|
|
140
|
+
if (levelElevations.get(node.id)?.baseY !== 0) continue
|
|
119
141
|
|
|
120
142
|
const geometry = createLevelFallbackFloorGeometry(node, nodes)
|
|
121
143
|
if (geometry) geometries.push(geometry)
|
|
@@ -129,24 +151,41 @@ function collectLevelFallbackFloorGeometries(nodes: SceneNodes) {
|
|
|
129
151
|
// a dedicated collider, a spawn on the bare ground (no slab, or not parented to
|
|
130
152
|
// a level that triggers the per-level fallback) has no floor to stand on and the
|
|
131
153
|
// walkthrough player falls through. Derive a thin ground slab from node data (not
|
|
132
|
-
// the rendered mesh) so it exists regardless of geometry-mount timing
|
|
133
|
-
//
|
|
154
|
+
// the rendered mesh) so it exists regardless of geometry-mount timing. The slab
|
|
155
|
+
// is effectively unbounded (not sized to the site polygon): the ground plane must
|
|
156
|
+
// keep holding the player up even after they step past the site boundary,
|
|
157
|
+
// otherwise they fall below the ground plane into the void.
|
|
158
|
+
//
|
|
159
|
+
// A sculpted site takes the terrain path instead: the flat slab would hold the
|
|
160
|
+
// player at the datum inside an excavation and bury them inside a hill. Derived
|
|
161
|
+
// from the field rather than the rendered terrain mesh for the same
|
|
162
|
+
// mount-timing reason as the flat slab.
|
|
134
163
|
function createSiteGroundColliderGeometry(site: SiteNode, nodes: SceneNodes) {
|
|
135
164
|
if (site.visible === false) return null
|
|
136
165
|
|
|
137
166
|
const siteObject = sceneRegistry.nodes.get(site.id)
|
|
138
167
|
if (!siteObject?.visible) return null
|
|
139
168
|
|
|
169
|
+
const field = terrainFieldOf(site)
|
|
170
|
+
if (field) {
|
|
171
|
+
const terrainGeometry = createTerrainColliderGeometry(field)
|
|
172
|
+
if (terrainGeometry) {
|
|
173
|
+
siteObject.updateWorldMatrix(true, false)
|
|
174
|
+
terrainGeometry.applyMatrix4(siteObject.matrixWorld)
|
|
175
|
+
return terrainGeometry
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
140
179
|
const bounds = computeSceneBoundsXZ(nodes)
|
|
141
180
|
const [centerX, centerZ] = bounds?.center ?? [0, 0]
|
|
142
181
|
const [boundsWidth, boundsDepth] = bounds?.size ?? [0, 0]
|
|
143
182
|
const width = Math.max(
|
|
144
183
|
boundsWidth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
|
|
145
|
-
|
|
184
|
+
SITE_GROUND_COLLIDER_MIN_SIZE,
|
|
146
185
|
)
|
|
147
186
|
const depth = Math.max(
|
|
148
187
|
boundsDepth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
|
|
149
|
-
|
|
188
|
+
SITE_GROUND_COLLIDER_MIN_SIZE,
|
|
150
189
|
)
|
|
151
190
|
|
|
152
191
|
const geometry = createBoxColliderGeometry(width, LEVEL_FALLBACK_FLOOR_THICKNESS, depth)
|
|
@@ -316,9 +355,12 @@ function collectColliderGeometriesFromNode(
|
|
|
316
355
|
if (visitedMeshes.has(object)) return
|
|
317
356
|
visitedMeshes.add(object)
|
|
318
357
|
|
|
358
|
+
// Prune hidden subtrees — children of an invisible group never render,
|
|
359
|
+
// so they must not collide either (see isEffectivelyVisible).
|
|
360
|
+
if (!object.visible) return
|
|
361
|
+
|
|
319
362
|
if (
|
|
320
363
|
isMesh(object) &&
|
|
321
|
-
object.visible &&
|
|
322
364
|
isColliderMaterialVisible(object.material) &&
|
|
323
365
|
!SKIPPED_MESH_NAMES.has(object.name)
|
|
324
366
|
) {
|
|
@@ -361,6 +403,11 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider
|
|
|
361
403
|
const root = sceneRegistry.nodes.get(nodeId)
|
|
362
404
|
if (!root) continue
|
|
363
405
|
|
|
406
|
+
// Registered objects can sit inside a hidden wrapper (roof segments
|
|
407
|
+
// under `segments-wrapper`) — the per-node traversal starts AT the
|
|
408
|
+
// object, so the ancestor chain must be checked here.
|
|
409
|
+
if (!isEffectivelyVisible(root)) continue
|
|
410
|
+
|
|
364
411
|
if (node.type === 'door') {
|
|
365
412
|
const doorGeometry = createDoorLeafColliderGeometry(root, node)
|
|
366
413
|
if (doorGeometry) {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
applyHeightPatch,
|
|
4
|
+
createTerrainField,
|
|
5
|
+
flattenPatch,
|
|
6
|
+
heightAt,
|
|
7
|
+
type TerrainField,
|
|
8
|
+
} from '@pascal-app/core'
|
|
9
|
+
import { Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three'
|
|
10
|
+
import { createTerrainColliderGeometry } from './terrain-collider'
|
|
11
|
+
|
|
12
|
+
const DOWN = new Vector3(0, -1, 0)
|
|
13
|
+
const UP = new Vector3(0, 1, 0)
|
|
14
|
+
|
|
15
|
+
/** A 2.5 m plateau over x,z ∈ [2,5] on an otherwise flat field. */
|
|
16
|
+
function plateauField(): TerrainField {
|
|
17
|
+
const base = createTerrainField({ cols: 17, rows: 17, spacing: 1, origin: [-8, -8] })
|
|
18
|
+
const patch = flattenPatch(base, { minX: 2, minZ: 2, maxX: 5, maxZ: 5 }, 2.5)
|
|
19
|
+
return applyHeightPatch(base, patch as never)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A field sloping 0.1 m per metre along +x, so every sample differs. */
|
|
23
|
+
function slopedField(): TerrainField {
|
|
24
|
+
const field = createTerrainField({ cols: 9, rows: 9, spacing: 1, origin: [-4, -4] })
|
|
25
|
+
const heights = new Int16Array(field.heights)
|
|
26
|
+
for (let r = 0; r < field.rows; r++) {
|
|
27
|
+
for (let c = 0; c < field.cols; c++) {
|
|
28
|
+
heights[r * field.cols + c] = Math.round((c * 0.1) /* m */ / field.step)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { ...field, heights }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function colliderMesh(field: TerrainField) {
|
|
35
|
+
const geometry = createTerrainColliderGeometry(field)
|
|
36
|
+
if (!geometry) throw new Error('no collider geometry')
|
|
37
|
+
const mesh = new Mesh(geometry, new MeshBasicMaterial())
|
|
38
|
+
mesh.updateMatrixWorld(true)
|
|
39
|
+
return mesh
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The height a downward ray finds — what the walkthrough player stands on. */
|
|
43
|
+
function groundUnder(mesh: Mesh, x: number, z: number): number | null {
|
|
44
|
+
const raycaster = new Raycaster()
|
|
45
|
+
raycaster.set(new Vector3(x, 500, z), DOWN)
|
|
46
|
+
const hits = raycaster.intersectObject(mesh, false)
|
|
47
|
+
const floor = hits.find((hit) => {
|
|
48
|
+
if (!hit.face) return true
|
|
49
|
+
return hit.face.normal.clone().transformDirection(mesh.matrixWorld).dot(UP) > 0.2
|
|
50
|
+
})
|
|
51
|
+
return floor ? floor.point.y : null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('createTerrainColliderGeometry', () => {
|
|
55
|
+
test('the walkable surface is the field, not a plane', () => {
|
|
56
|
+
const field = plateauField()
|
|
57
|
+
const mesh = colliderMesh(field)
|
|
58
|
+
|
|
59
|
+
// On the plateau, off it, and on the slope between: the collider has to agree
|
|
60
|
+
// with `heightAt` at each, or what you see is not what you stand on.
|
|
61
|
+
for (const [x, z] of [
|
|
62
|
+
[3, 3],
|
|
63
|
+
[-3, -3],
|
|
64
|
+
[1.5, 3],
|
|
65
|
+
[4.5, 4.5],
|
|
66
|
+
] as Array<[number, number]>) {
|
|
67
|
+
expect(groundUnder(mesh, x, z)).toBeCloseTo(heightAt(field, x, z), 4)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('a sloped field is sampled per vertex, not averaged', () => {
|
|
72
|
+
const field = slopedField()
|
|
73
|
+
const mesh = colliderMesh(field)
|
|
74
|
+
|
|
75
|
+
// Sample midpoints, where a flat or decimated collider would disagree most.
|
|
76
|
+
for (const x of [-3.5, -0.5, 1.5, 3.5]) {
|
|
77
|
+
expect(groundUnder(mesh, x, 0)).toBeCloseTo(heightAt(field, x, 0), 4)
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('the skirt keeps holding the player far past the field', () => {
|
|
82
|
+
const field = slopedField()
|
|
83
|
+
const mesh = colliderMesh(field)
|
|
84
|
+
|
|
85
|
+
// Past every edge and past a corner: `heightAt` clamps to the border there, so
|
|
86
|
+
// the collider must too. Without the skirt these are null and the player falls.
|
|
87
|
+
for (const [x, z] of [
|
|
88
|
+
[400, 0],
|
|
89
|
+
[-400, 0],
|
|
90
|
+
[0, 400],
|
|
91
|
+
[0, -400],
|
|
92
|
+
[400, 400],
|
|
93
|
+
[-400, -400],
|
|
94
|
+
] as Array<[number, number]>) {
|
|
95
|
+
expect(groundUnder(mesh, x, z)).toBeCloseTo(heightAt(field, x, z), 4)
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('every triangle carries a normal, and the surface faces up', () => {
|
|
100
|
+
const geometry = createTerrainColliderGeometry(plateauField())
|
|
101
|
+
if (!geometry) throw new Error('no collider geometry')
|
|
102
|
+
|
|
103
|
+
const position = geometry.getAttribute('position')
|
|
104
|
+
const normal = geometry.getAttribute('normal')
|
|
105
|
+
expect(normal).toBeDefined()
|
|
106
|
+
expect(normal.count).toBe(position.count)
|
|
107
|
+
// Non-indexed triangles — `mergeGeometries` needs every contributor to match.
|
|
108
|
+
expect(geometry.index).toBeNull()
|
|
109
|
+
expect(position.count % 3).toBe(0)
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < normal.count; i++) {
|
|
112
|
+
expect(normal.getY(i)).toBeGreaterThan(0)
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('decimates rather than exploding on an absurd field', () => {
|
|
117
|
+
// 1025² is inside the schema's decode-bomb guard but 1M cells — an exact
|
|
118
|
+
// collider would be 2M triangles for a surface nobody can walk that far across.
|
|
119
|
+
const field = createTerrainField({ cols: 1025, rows: 1025, spacing: 0.5 })
|
|
120
|
+
const geometry = createTerrainColliderGeometry(field)
|
|
121
|
+
if (!geometry) throw new Error('no collider geometry')
|
|
122
|
+
|
|
123
|
+
const triangles = geometry.getAttribute('position').count / 3
|
|
124
|
+
expect(triangles).toBeLessThan(200_000)
|
|
125
|
+
// Still a real surface, not an empty one.
|
|
126
|
+
expect(triangles).toBeGreaterThan(1000)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('returns null for a degenerate field', () => {
|
|
130
|
+
expect(createTerrainColliderGeometry(createTerrainField({ cols: 1, rows: 1 }))).toBeNull()
|
|
131
|
+
})
|
|
132
|
+
})
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sculpted ground as walkthrough collision geometry.
|
|
3
|
+
*
|
|
4
|
+
* The first-person collider world is a single merged BVH mesh, and before terrain
|
|
5
|
+
* the ground contributed one flat 2 km box to it. That box is wrong in both
|
|
6
|
+
* directions on a sculpted site: it holds the player up at the datum inside an
|
|
7
|
+
* excavation, and buries them inside a hill. This module replaces it.
|
|
8
|
+
*
|
|
9
|
+
* Two properties it has to hold, because the whole point is that walking agrees
|
|
10
|
+
* with every other consumer of the field:
|
|
11
|
+
*
|
|
12
|
+
* - **The surface is the field**, sampled at every vertex — the same
|
|
13
|
+
* `heightAtSample`/`normalAt` the rendered mesh uses (`terrain-geometry.ts`),
|
|
14
|
+
* so what you see is what you stand on. It is deliberately not a decimated
|
|
15
|
+
* proxy at realistic field sizes.
|
|
16
|
+
* - **It extends past the field**, because `heightAt` clamps to the border
|
|
17
|
+
* outside it. A collider that stopped at the field edge would drop the player
|
|
18
|
+
* into the void one step past the site, which is exactly the bug the flat box
|
|
19
|
+
* existed to prevent. The skirt extrudes each border sample outward at its own
|
|
20
|
+
* height, so the collider and `heightAt` agree everywhere, not just inside.
|
|
21
|
+
*
|
|
22
|
+
* Non-indexed triangles with position + normal, matching what the other collider
|
|
23
|
+
* contributors emit — `mergeGeometries` requires every input to agree on both.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { heightAtSample, normalAt, type TerrainField } from '@pascal-app/core'
|
|
27
|
+
import * as THREE from 'three'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How far past the field the ground keeps holding the player, in metres. Matches
|
|
31
|
+
* the flat box's half-extent so a site's collider bounds do not change size when
|
|
32
|
+
* it gains terrain.
|
|
33
|
+
*/
|
|
34
|
+
const SKIRT_REACH = 1000
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cell budget before the collider decimates.
|
|
38
|
+
*
|
|
39
|
+
* 66k cells is a 257² field — the largest anything realistic produces, and ~131k
|
|
40
|
+
* triangles, which the BVH builds in well under a frame's worth of budget. The
|
|
41
|
+
* schema allows up to 4097² (a decode-bomb guard, not a real terrain); at that
|
|
42
|
+
* size an exact collider would be 100M triangles, so past the budget this steps
|
|
43
|
+
* the sample grid and the collider becomes a faithful-but-coarser version of the
|
|
44
|
+
* same surface rather than an out-of-memory crash.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_COLLIDER_CELLS = 66_000
|
|
47
|
+
|
|
48
|
+
function colliderStride(field: TerrainField): number {
|
|
49
|
+
const cells = Math.max(1, (field.cols - 1) * (field.rows - 1))
|
|
50
|
+
if (cells <= MAX_COLLIDER_CELLS) return 1
|
|
51
|
+
return Math.ceil(Math.sqrt(cells / MAX_COLLIDER_CELLS))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The sample indices the collider walks: every `stride`-th, always including the border. */
|
|
55
|
+
function sampleLine(count: number, stride: number): number[] {
|
|
56
|
+
const line: number[] = []
|
|
57
|
+
for (let i = 0; i < count - 1; i += stride) line.push(i)
|
|
58
|
+
line.push(count - 1)
|
|
59
|
+
return line
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type Vertex = { x: number; y: number; z: number }
|
|
63
|
+
|
|
64
|
+
class TriangleSink {
|
|
65
|
+
private readonly positions: number[] = []
|
|
66
|
+
private readonly normals: number[] = []
|
|
67
|
+
|
|
68
|
+
constructor(private readonly field: TerrainField) {}
|
|
69
|
+
|
|
70
|
+
private push(v: Vertex): void {
|
|
71
|
+
this.positions.push(v.x, v.y, v.z)
|
|
72
|
+
const [nx, ny, nz] = normalAt(this.field, v.x, v.z)
|
|
73
|
+
this.normals.push(nx, ny, nz)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* One cell, given its four corners named by axis order. Winding matches
|
|
78
|
+
* `buildTerrainMesh`: `(p00, p01, p10)` and `(p10, p01, p11)` faces +Y, which is
|
|
79
|
+
* what the spawn resolver's `normal.dot(UP) > 0.2` floor test needs.
|
|
80
|
+
*/
|
|
81
|
+
quad(p00: Vertex, p10: Vertex, p01: Vertex, p11: Vertex): void {
|
|
82
|
+
this.push(p00)
|
|
83
|
+
this.push(p01)
|
|
84
|
+
this.push(p10)
|
|
85
|
+
this.push(p10)
|
|
86
|
+
this.push(p01)
|
|
87
|
+
this.push(p11)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
build(): THREE.BufferGeometry | null {
|
|
91
|
+
if (this.positions.length === 0) return null
|
|
92
|
+
const geometry = new THREE.BufferGeometry()
|
|
93
|
+
geometry.setAttribute(
|
|
94
|
+
'position',
|
|
95
|
+
new THREE.BufferAttribute(new Float32Array(this.positions), 3),
|
|
96
|
+
)
|
|
97
|
+
geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(this.normals), 3))
|
|
98
|
+
return geometry
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Terrain collision geometry in the site's own frame — the caller applies the
|
|
104
|
+
* site's world matrix, exactly as it did for the flat box it replaces.
|
|
105
|
+
*/
|
|
106
|
+
export function createTerrainColliderGeometry(field: TerrainField): THREE.BufferGeometry | null {
|
|
107
|
+
if (field.cols < 2 || field.rows < 2) return null
|
|
108
|
+
|
|
109
|
+
const stride = colliderStride(field)
|
|
110
|
+
const cols = sampleLine(field.cols, stride)
|
|
111
|
+
const rows = sampleLine(field.rows, stride)
|
|
112
|
+
const sink = new TriangleSink(field)
|
|
113
|
+
|
|
114
|
+
const xAt = (col: number) => field.origin[0] + col * field.spacing
|
|
115
|
+
const zAt = (row: number) => field.origin[1] + row * field.spacing
|
|
116
|
+
const at = (col: number, row: number): Vertex => ({
|
|
117
|
+
x: xAt(col),
|
|
118
|
+
y: heightAtSample(field, col, row),
|
|
119
|
+
z: zAt(row),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
for (let ri = 0; ri < rows.length - 1; ri++) {
|
|
123
|
+
const r0 = rows[ri] as number
|
|
124
|
+
const r1 = rows[ri + 1] as number
|
|
125
|
+
for (let ci = 0; ci < cols.length - 1; ci++) {
|
|
126
|
+
const c0 = cols[ci] as number
|
|
127
|
+
const c1 = cols[ci + 1] as number
|
|
128
|
+
sink.quad(at(c0, r0), at(c1, r0), at(c0, r1), at(c1, r1))
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// The skirt: each border sample extruded outward at its own height, so stepping
|
|
133
|
+
// off the field lands on the border height rather than on nothing. This is the
|
|
134
|
+
// collider spelling of `heightAt`'s edge clamp.
|
|
135
|
+
const firstCol = 0
|
|
136
|
+
const lastCol = field.cols - 1
|
|
137
|
+
const firstRow = 0
|
|
138
|
+
const lastRow = field.rows - 1
|
|
139
|
+
const westX = xAt(firstCol) - SKIRT_REACH
|
|
140
|
+
const eastX = xAt(lastCol) + SKIRT_REACH
|
|
141
|
+
const northZ = zAt(firstRow) - SKIRT_REACH
|
|
142
|
+
const southZ = zAt(lastRow) + SKIRT_REACH
|
|
143
|
+
|
|
144
|
+
for (let ci = 0; ci < cols.length - 1; ci++) {
|
|
145
|
+
const c0 = cols[ci] as number
|
|
146
|
+
const c1 = cols[ci + 1] as number
|
|
147
|
+
const north0 = at(c0, firstRow)
|
|
148
|
+
const north1 = at(c1, firstRow)
|
|
149
|
+
sink.quad(
|
|
150
|
+
{ x: north0.x, y: north0.y, z: northZ },
|
|
151
|
+
{ x: north1.x, y: north1.y, z: northZ },
|
|
152
|
+
north0,
|
|
153
|
+
north1,
|
|
154
|
+
)
|
|
155
|
+
const south0 = at(c0, lastRow)
|
|
156
|
+
const south1 = at(c1, lastRow)
|
|
157
|
+
sink.quad(
|
|
158
|
+
south0,
|
|
159
|
+
south1,
|
|
160
|
+
{ x: south0.x, y: south0.y, z: southZ },
|
|
161
|
+
{
|
|
162
|
+
x: south1.x,
|
|
163
|
+
y: south1.y,
|
|
164
|
+
z: southZ,
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (let ri = 0; ri < rows.length - 1; ri++) {
|
|
170
|
+
const r0 = rows[ri] as number
|
|
171
|
+
const r1 = rows[ri + 1] as number
|
|
172
|
+
const west0 = at(firstCol, r0)
|
|
173
|
+
const west1 = at(firstCol, r1)
|
|
174
|
+
sink.quad(
|
|
175
|
+
{ x: westX, y: west0.y, z: west0.z },
|
|
176
|
+
west0,
|
|
177
|
+
{ x: westX, y: west1.y, z: west1.z },
|
|
178
|
+
west1,
|
|
179
|
+
)
|
|
180
|
+
const east0 = at(lastCol, r0)
|
|
181
|
+
const east1 = at(lastCol, r1)
|
|
182
|
+
sink.quad(east0, { x: eastX, y: east0.y, z: east0.z }, east1, {
|
|
183
|
+
x: eastX,
|
|
184
|
+
y: east1.y,
|
|
185
|
+
z: east1.z,
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Corner quads, flat at the corner sample's height — the diagonal region no edge
|
|
190
|
+
// strip covers, and where `heightAt` also reports the corner value.
|
|
191
|
+
const corners: Array<[Vertex, number, number]> = [
|
|
192
|
+
[at(firstCol, firstRow), westX, northZ],
|
|
193
|
+
[at(lastCol, firstRow), eastX, northZ],
|
|
194
|
+
[at(firstCol, lastRow), westX, southZ],
|
|
195
|
+
[at(lastCol, lastRow), eastX, southZ],
|
|
196
|
+
]
|
|
197
|
+
for (const [corner, farX, farZ] of corners) {
|
|
198
|
+
const minX = Math.min(corner.x, farX)
|
|
199
|
+
const maxX = Math.max(corner.x, farX)
|
|
200
|
+
const minZ = Math.min(corner.z, farZ)
|
|
201
|
+
const maxZ = Math.max(corner.z, farZ)
|
|
202
|
+
const y = corner.y
|
|
203
|
+
sink.quad(
|
|
204
|
+
{ x: minX, y, z: minZ },
|
|
205
|
+
{ x: maxX, y, z: minZ },
|
|
206
|
+
{ x: minX, y, z: maxZ },
|
|
207
|
+
{ x: maxX, y, z: maxZ },
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return sink.build()
|
|
212
|
+
}
|