@pascal-app/editor 1.0.3 → 1.0.5
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/README.md +106 -0
- package/package.json +42 -38
- package/src/components/editor/__bench__/room-label-placement.bench.ts +108 -0
- package/src/components/editor/action-menu-button.tsx +83 -0
- package/src/components/editor/bake-thumbnail.tsx +307 -5
- package/src/components/editor/capture-level-picker.tsx +72 -0
- package/src/components/editor/construction-reveal-audio.test.tsx +150 -0
- package/src/components/editor/construction-reveal-audio.tsx +40 -0
- package/src/components/editor/custom-camera-controls.tsx +275 -57
- package/src/components/editor/delete-confirmation-dialog.tsx +297 -7
- package/src/components/editor/duplicate-node.ts +244 -0
- package/src/components/editor/editor-layout-mobile.tsx +37 -26
- package/src/components/editor/editor-layout-v2.tsx +132 -59
- package/src/components/editor/export-manager.tsx +47 -26
- package/src/components/editor/first-person/build-collider-world.test.ts +320 -0
- package/src/components/editor/first-person/build-collider-world.ts +28 -7
- package/src/components/editor/first-person/ground-holes.test.ts +128 -0
- package/src/components/editor/first-person/ground-holes.ts +130 -0
- package/src/components/editor/first-person/mechanism-targets.test.ts +162 -0
- package/src/components/editor/first-person/mechanism-targets.ts +88 -0
- package/src/components/editor/first-person-controls.tsx +220 -9
- package/src/components/editor/floating-action-menu.tsx +63 -207
- package/src/components/editor/floor-aware-camera-controls.test.ts +466 -0
- package/src/components/editor/floor-aware-camera-controls.ts +324 -0
- package/src/components/editor/floor-openings-overlay.tsx +243 -0
- package/src/components/editor/floor-region-controls.tsx +846 -0
- package/src/components/editor/floor-region-webgpu-materials.test.ts +12 -0
- package/src/components/editor/floor-surface.test.ts +165 -0
- package/src/components/editor/floor-surface.ts +182 -0
- package/src/components/editor/floorplan-background-selection.test.ts +11 -2
- package/src/components/editor/floorplan-background-selection.ts +1 -3
- package/src/components/editor/floorplan-mode-coordinator.tsx +2 -2
- package/src/components/editor/floorplan-panel.tsx +414 -363
- package/src/components/editor/follow-pascal.test.tsx +912 -0
- package/src/components/editor/follow-pascal.tsx +354 -0
- package/src/components/editor/footprint-height-handle.tsx +204 -0
- package/src/components/editor/group-actions.ts +56 -2
- package/src/components/editor/group-move-3d.ts +2 -0
- package/src/components/editor/group-rotate-handle.tsx +2 -0
- package/src/components/editor/group-transform-shared.test.ts +460 -0
- package/src/components/editor/handles/handle-hit-priority.test.tsx +63 -0
- package/src/components/editor/handles/handle-hit-priority.tsx +37 -0
- package/src/components/editor/handles/swallow-next-click.test.ts +28 -0
- package/src/components/editor/handles/use-handle-drag.ts +269 -197
- package/src/components/editor/index.tsx +409 -197
- package/src/components/editor/join-walls-pill.tsx +44 -0
- package/src/components/editor/node-action-menu.tsx +68 -70
- package/src/components/editor/node-arrow-handles.tsx +25 -6
- package/src/components/editor/open-wall-ends-3d-layer.tsx +273 -0
- package/src/components/editor/open-wall-ends-hint.tsx +89 -0
- package/src/components/editor/quick-measurement-hud.tsx +3 -2
- package/src/components/editor/registry-action-contributions.test.tsx +63 -0
- package/src/components/editor/registry-action-contributions.tsx +80 -1
- package/src/components/editor/riser-diagram-panel.tsx +14 -7
- package/src/components/editor/room-controls.tsx +847 -0
- package/src/components/editor/room-floor-highlight.tsx +128 -0
- package/src/components/editor/room-handles-2d.tsx +354 -0
- package/src/components/editor/room-handles.tsx +675 -0
- package/src/components/editor/room-highlight.tsx +233 -0
- package/src/components/editor/room-labels.test.ts +850 -0
- package/src/components/editor/room-labels.tsx +1890 -0
- package/src/components/editor/room-transform-ghost.tsx +256 -0
- package/src/components/editor/room-webgpu-materials.test.ts +27 -0
- package/src/components/editor/selection-manager.tsx +474 -419
- package/src/components/editor/slab-hole-highlights.tsx +5 -2
- package/src/components/editor/snapshot-capture-overlay.tsx +7 -2
- package/src/components/editor/snapshot-capture.test.ts +119 -60
- package/src/components/editor/snapshot-capture.ts +36 -5
- package/src/components/editor/thumbnail-generator.test.ts +654 -0
- package/src/components/editor/thumbnail-generator.tsx +1103 -223
- package/src/components/editor/use-floorplan-background-placement.ts +34 -12
- package/src/components/editor/vector-edge-extractor.tsx +121 -0
- package/src/components/editor/views/host-view.tsx +61 -0
- package/src/components/editor/views/plugins-view.tsx +663 -0
- package/src/components/editor/views/scene-region.tsx +59 -0
- package/src/components/editor/views/use-editor-views.ts +57 -0
- package/src/components/editor/views/view-bar.test.tsx +54 -0
- package/src/components/editor/views/view-bar.tsx +343 -0
- package/src/components/editor/wall-justification.test.ts +140 -0
- package/src/components/editor/wall-move-side-handles.tsx +43 -142
- package/src/components/editor/wall-push-arrow.tsx +92 -0
- package/src/components/editor/wall-region-handles.tsx +509 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +8 -4
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +8 -4
- package/src/components/editor-2d/floorplan-group-move.tsx +3 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.test.tsx +86 -0
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +290 -0
- package/src/components/editor-2d/floorplan-registered-tool-layer.tsx +2 -2
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +11 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +116 -106
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.test.ts +156 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +278 -0
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +77 -0
- package/src/components/editor-2d/renderers/{floorplan-draft-wall-measurement.tsx → floorplan-draft-measurement.tsx} +8 -8
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +38 -8
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +319 -233
- package/src/components/editor-2d/renderers/floorplan-reveal.test.tsx +254 -0
- package/src/components/editor-2d/renderers/floorplan-reveal.ts +141 -0
- package/src/components/editor-2d/svg-paths.ts +12 -15
- package/src/components/point-ask/bubble.tsx +538 -0
- package/src/components/point-ask/hover-chip.tsx +91 -0
- package/src/components/point-ask/mode-pill.tsx +92 -0
- package/src/components/point-ask/optional-motion.ts +22 -0
- package/src/components/point-ask/outline-corners.tsx +93 -0
- package/src/components/point-ask/pins.tsx +711 -0
- package/src/components/point-ask/point-ask-layer.tsx +83 -0
- package/src/components/point-ask/point-ask-styles.ts +105 -0
- package/src/components/point-ask/region-marquee-styles.ts +15 -0
- package/src/components/point-ask/region-marquee.tsx +64 -0
- package/src/components/procedural-items/inspector.tsx +40 -15
- package/src/components/systems/ceiling/ceiling-bracket-batch.test.ts +12 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.test.ts +97 -14
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +18 -18
- package/src/components/systems/roof/roof-edit-system.tsx +1 -0
- package/src/components/systems/zone/zone-system.test.ts +47 -0
- package/src/components/systems/zone/zone-system.tsx +29 -68
- package/src/components/tools/distribution/connection-compatibility.test.ts +34 -0
- package/src/components/tools/distribution/connection-compatibility.ts +43 -0
- package/src/components/tools/distribution/distribution-public.ts +9 -0
- package/src/components/tools/distribution/distribution-run-contract.test.ts +26 -0
- package/src/components/tools/distribution/distribution-run-contract.ts +94 -0
- package/src/components/tools/distribution/distribution-run-tool.test.ts +241 -0
- package/src/components/tools/distribution/distribution-run-tool.test.tsx +125 -0
- package/src/components/tools/distribution/distribution-run-tool.tsx +1323 -0
- package/src/components/tools/distribution/draw-alignment.ts +35 -0
- package/src/components/tools/distribution/ports.test.ts +56 -0
- package/src/components/tools/distribution/ports.ts +378 -0
- package/src/components/tools/distribution/run-cursor.test.ts +135 -0
- package/src/components/tools/distribution/run-cursor.ts +36 -0
- package/src/components/tools/distribution/run-direction-feedback.test.ts +60 -0
- package/src/components/tools/distribution/run-direction-feedback.tsx +251 -0
- package/src/components/tools/distribution/run-port-snap.test.ts +79 -0
- package/src/components/tools/distribution/run-port-snap.ts +25 -0
- package/src/components/tools/distribution/run-screen-direction.test.ts +23 -0
- package/src/components/tools/distribution/run-screen-direction.ts +30 -0
- package/src/components/tools/elevator/elevator-tool.tsx +3 -6
- package/src/components/tools/elevator/move-elevator-tool.tsx +2 -3
- package/src/components/tools/item/item-surface-strategy-resolver.test.ts +67 -68
- package/src/components/tools/item/placement-history.test.tsx +512 -0
- package/src/components/tools/item/placement-strategies.test.ts +46 -1
- package/src/components/tools/item/placement-strategies.ts +11 -10
- package/src/components/tools/item/shelf-surface-strategy-resolver.test.ts +56 -56
- package/src/components/tools/item/use-draft-node.test.tsx +432 -1
- package/src/components/tools/item/use-draft-node.ts +166 -95
- package/src/components/tools/item/use-placement-coordinator.tsx +148 -95
- package/src/components/tools/paint-region/use-paint-tint.ts +37 -0
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +466 -0
- package/src/components/tools/registry/item-surface-move.test.ts +62 -63
- package/src/components/tools/registry/item-surface-move.ts +2 -0
- package/src/components/tools/registry/item-surface-resolver.test.ts +26 -26
- package/src/components/tools/registry/move-registry-node-tool.test.ts +268 -1
- package/src/components/tools/registry/move-registry-node-tool.tsx +66 -30
- package/src/components/tools/select/box-select-tool.tsx +2 -12
- package/src/components/tools/select/plane-box-select-tool.test.tsx +79 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +35 -22
- package/src/components/tools/select/select-candidates.test.ts +37 -4
- package/src/components/tools/select/select-candidates.ts +15 -11
- package/src/components/tools/shared/cursor-sphere.tsx +36 -12
- package/src/components/tools/site/site-boundary-editor.tsx +1 -2
- package/src/components/tools/tool-manager.tsx +59 -24
- package/src/components/tools/wall/wall-drafting.test.ts +175 -47
- package/src/components/tools/zone/zone-tool.tsx +4 -12
- package/src/components/ui/action-menu/action-button.tsx +9 -1
- package/src/components/ui/action-menu/camera-actions.tsx +9 -4
- package/src/components/ui/action-menu/control-modes.tsx +15 -42
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +32 -0
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +36 -0
- package/src/components/ui/action-menu/index.tsx +56 -33
- package/src/components/ui/action-menu/measurement-control.tsx +14 -19
- package/src/components/ui/action-menu/placement.ts +20 -0
- package/src/components/ui/action-menu/point-ask-button.tsx +52 -0
- package/src/components/ui/action-menu/view-toggles.tsx +47 -42
- package/src/components/ui/build-panel/build-panel.tsx +242 -0
- package/src/components/ui/command-palette/editor-commands.tsx +7 -42
- package/src/components/ui/command-palette/index.tsx +50 -39
- package/src/components/ui/command-palette/ranked-groups.test.ts +31 -0
- package/src/components/ui/controls/action-button.tsx +5 -2
- package/src/components/ui/controls/finish-swatch.tsx +110 -0
- package/src/components/ui/controls/material-paint-panel.tsx +97 -26
- package/src/components/ui/controls/material-picker.tsx +4 -1
- package/src/components/ui/controls/metric-control.tsx +1 -1
- package/src/components/ui/controls/scene-material-list.tsx +4 -32
- package/src/components/ui/controls/segmented-control.tsx +21 -8
- package/src/components/ui/controls/select-control.tsx +65 -0
- package/src/components/ui/controls/slider-control.tsx +1 -1
- package/src/components/ui/controls/toggle-control.tsx +3 -3
- package/src/components/ui/controls/tool-options-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +159 -59
- package/src/components/ui/helpers/building-helper.tsx +4 -1
- package/src/components/ui/helpers/contextual-helper-panel.tsx +181 -72
- package/src/components/ui/helpers/helper-manager.test.tsx +121 -0
- package/src/components/ui/helpers/helper-manager.tsx +331 -14
- package/src/components/ui/helpers/item-helper.tsx +17 -1
- package/src/components/ui/helpers/registered-tool-helper.tsx +8 -1
- package/src/components/ui/icon-ref.tsx +45 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/node-icon-image.tsx +16 -0
- package/src/components/ui/panels/collections/collections-popover.tsx +28 -22
- package/src/components/ui/panels/floor-foundation-panel.tsx +551 -0
- package/src/components/ui/panels/floor-opening-panel.tsx +143 -0
- package/src/components/ui/panels/mobile-panel-sheet.tsx +4 -4
- package/src/components/ui/panels/mobile-selection-bar.tsx +8 -5
- package/src/components/ui/panels/multi-field-value.test.ts +26 -3
- package/src/components/ui/panels/multi-field-value.ts +6 -3
- package/src/components/ui/panels/multi-parametric-inspector.tsx +52 -2
- package/src/components/ui/panels/multi-selection-panel.tsx +161 -5
- package/src/components/ui/panels/node-display.ts +8 -3
- package/src/components/ui/panels/panel-manager.tsx +103 -68
- package/src/components/ui/panels/panel-wrapper.tsx +130 -62
- package/src/components/ui/panels/parametric-field-control.tsx +3 -3
- package/src/components/ui/panels/parametric-inspector.tsx +7 -7
- package/src/components/ui/panels/reference-panel.tsx +2 -3
- package/src/components/ui/panels/room-construction-rows.tsx +774 -0
- package/src/components/ui/panels/room-name-options.test.ts +55 -0
- package/src/components/ui/panels/room-name-options.ts +47 -0
- package/src/components/ui/panels/room-openings-field.tsx +110 -0
- package/src/components/ui/panels/room-panel.tsx +790 -0
- package/src/components/ui/panels/shape-choice.tsx +58 -0
- package/src/components/ui/panels/wall-region-list.tsx +109 -0
- package/src/components/ui/primitives/badge.tsx +8 -0
- package/src/components/ui/primitives/color-dot.tsx +36 -24
- package/src/components/ui/primitives/sidebar.tsx +3 -1
- package/src/components/ui/right-stack.test.ts +29 -0
- package/src/components/ui/right-stack.tsx +164 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +8 -3
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +6 -6
- package/src/components/ui/sidebar/panels/settings-panel/animation-speed-setting.tsx +40 -0
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +7 -8
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.test.tsx +170 -0
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +92 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +138 -27
- package/src/components/ui/sidebar/panels/settings-panel/interface-theme.test.tsx +20 -0
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +5 -5
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +4 -10
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -11
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -4
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/collection-tree-node.tsx +197 -0
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +24 -11
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +181 -223
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +30 -8
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +4 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +1 -4
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +1 -3
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +21 -13
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +13 -37
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -3
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +1 -5
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +9 -4
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +2 -4
- package/src/components/ui/sidebar/tab-bar.tsx +14 -4
- package/src/components/ui/sidebar/use-plugin-panels.tsx +15 -5
- package/src/components/viewer/floorplan-compass-button.tsx +9 -1
- package/src/components/viewer/floorplan-preview.tsx +5 -6
- package/src/components/viewer/viewer-controls-bar.tsx +18 -6
- package/src/components/viewer/viewer-hover-label.tsx +34 -0
- package/src/components/viewer/viewer-rooms.tsx +191 -0
- package/src/components/viewer/viewer-scene-header.tsx +181 -145
- package/src/components/viewer/viewer-selection-manager.tsx +124 -0
- package/src/components/viewer/viewer-stage.tsx +2 -2
- package/src/components/viewer-zone-system.tsx +3 -8
- package/src/hooks/use-auto-save.integration.test.tsx +215 -43
- package/src/hooks/use-auto-save.ts +60 -9
- package/src/hooks/use-ceiling-events.test.ts +123 -118
- package/src/hooks/use-grid-events.test.ts +105 -105
- package/src/hooks/use-keyboard.test.ts +243 -19
- package/src/hooks/use-keyboard.ts +118 -83
- package/src/hooks/use-open-wall-ends.test.ts +99 -0
- package/src/hooks/use-open-wall-ends.ts +238 -0
- package/src/hooks/use-placement-notice.test.ts +71 -0
- package/src/hooks/use-placement-notice.ts +73 -0
- package/src/hooks/use-selected-room.ts +143 -0
- package/src/index.tsx +312 -58
- package/src/lib/__fixtures__/owner-victor-seattle-parking.json +1 -0
- package/src/lib/__fixtures__/owner-villa-aurelia-basement.json +1 -0
- package/src/lib/add-storey.test.ts +177 -0
- package/src/lib/add-storey.ts +59 -0
- package/src/lib/below-grade.test.tsx +171 -0
- package/src/lib/below-grade.ts +133 -0
- package/src/lib/camera-facing.test.ts +24 -0
- package/src/lib/camera-facing.ts +24 -0
- package/src/lib/ceiling-edit-session.test.ts +321 -0
- package/src/lib/ceiling-edit-session.ts +206 -0
- package/src/lib/ceiling-region-session.test.ts +179 -0
- package/src/lib/continuation.test.ts +19 -1
- package/src/lib/continuation.ts +25 -4
- package/src/lib/direct-manipulation.ts +2 -0
- package/src/lib/door-interaction.ts +14 -0
- package/src/lib/download-project-file.ts +17 -0
- package/src/lib/editor-level-display.test.ts +96 -0
- package/src/lib/editor-level-display.ts +55 -0
- package/src/lib/editor-views.ts +178 -0
- package/src/lib/export-geometry-wait.test.ts +90 -0
- package/src/lib/export-texture-utils.ts +2 -4
- package/src/lib/floor-edit-session.ts +153 -0
- package/src/lib/floor-footprints.test.ts +563 -0
- package/src/lib/floor-footprints.ts +395 -0
- package/src/lib/floor-foundation-support.test.ts +284 -0
- package/src/lib/floor-opening-draft.ts +218 -0
- package/src/lib/floor-openings.test.ts +216 -0
- package/src/lib/floor-region-geometry.ts +191 -0
- package/src/lib/floor-region-session.test.ts +485 -0
- package/src/lib/floor-region-session.ts +589 -0
- package/src/lib/floor-region-snap.ts +101 -0
- package/src/lib/floor-step-hover.test.ts +32 -0
- package/src/lib/floorplan/annotation-visibility.ts +17 -0
- package/src/lib/floorplan/dimension-drive.test.ts +360 -0
- package/src/lib/floorplan/dimension-drive.ts +479 -0
- package/src/lib/floorplan/floorplan-export.test.ts +12 -8
- package/src/lib/floorplan/floorplan-export.tsx +32 -8
- package/src/lib/floorplan/floorplan-extension.ts +32 -0
- package/src/lib/floorplan/floorplan-mode.test.ts +4 -0
- package/src/lib/floorplan/floorplan-mode.ts +2 -0
- package/src/lib/floorplan/floorplan-pdfkit-document.ts +363 -27
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +177 -8
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +60 -25
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +138 -0
- package/src/lib/floorplan/floorplan-readonly.ts +6 -0
- package/src/lib/floorplan/geometry.ts +13 -6
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/marks.test.ts +213 -0
- package/src/lib/floorplan/marks.ts +238 -0
- package/src/lib/floorplan/open-wall-ends.test.ts +268 -0
- package/src/lib/floorplan/open-wall-ends.ts +162 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/GeistMono-Regular.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-GeistMono.txt +92 -0
- package/src/lib/floorplan/pdf-fonts/LICENSE-LiberationSans.txt +102 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Bold.ttf +0 -0
- package/src/lib/floorplan/pdf-fonts/LiberationSans-Regular.ttf +0 -0
- package/src/lib/floorplan/pdfkit-standard-fonts.d.ts +5 -0
- package/src/lib/floorplan/schedules.test.ts +189 -0
- package/src/lib/floorplan/schedules.ts +339 -0
- package/src/lib/floorplan/sheet-export.test.ts +124 -0
- package/src/lib/floorplan/sheet-export.ts +318 -0
- package/src/lib/floorplan/site-plan/build-site-plan-drawing.ts +768 -0
- package/src/lib/floorplan/site-plan/contours.test.ts +43 -0
- package/src/lib/floorplan/site-plan/contributors.ts +97 -0
- package/src/lib/floorplan/site-plan/coverage.ts +230 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.test.ts +15 -0
- package/src/lib/floorplan/site-plan/drawing-type-switch.tsx +86 -0
- package/src/lib/floorplan/site-plan/front-edge.test.ts +103 -0
- package/src/lib/floorplan/site-plan/front-edge.ts +157 -0
- package/src/lib/floorplan/site-plan/geometry.test.ts +269 -0
- package/src/lib/floorplan/site-plan/geometry.ts +142 -0
- package/src/lib/floorplan/site-plan/index.ts +55 -0
- package/src/lib/floorplan/site-plan/site-annotations.test.ts +483 -0
- package/src/lib/floorplan/site-plan/site-annotations.ts +1084 -0
- package/src/lib/floorplan/site-plan/site-parts.ts +287 -0
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +138 -0
- package/src/lib/floorplan/site-plan/street-name.test.ts +69 -0
- package/src/lib/floorplan/site-plan/street-name.ts +302 -0
- package/src/lib/floorplan/site-plan/yards.test.ts +70 -0
- package/src/lib/floorplan/types.ts +1 -27
- package/src/lib/follow-pascal-exterior.test-fixture.ts +17 -0
- package/src/lib/follow-pascal-exterior.test.ts +171 -0
- package/src/lib/follow-pascal-exterior.ts +39 -0
- package/src/lib/follow-pascal-presentation.test.ts +203 -0
- package/src/lib/follow-pascal-presentation.ts +129 -0
- package/src/lib/follow-pascal-sight.ts +84 -0
- package/src/lib/follow-pascal-targets.test.ts +406 -0
- package/src/lib/follow-pascal-targets.ts +198 -0
- package/src/lib/follow-pascal.test.ts +350 -0
- package/src/lib/follow-pascal.ts +284 -0
- package/src/lib/fresh-planar-placement.test.ts +148 -0
- package/src/lib/fresh-planar-placement.ts +114 -37
- package/src/lib/geometry-script/author.ts +157 -0
- package/src/lib/geometry-script/client.ts +159 -0
- package/src/lib/geometry-script/finish-scripts.ts +127 -0
- package/src/lib/geometry-script/protocol.ts +13 -0
- package/src/lib/gesture-lifecycle.test.ts +595 -0
- package/src/lib/gesture-lifecycle.ts +290 -0
- package/src/lib/glb-export-zone-holes.test.ts +41 -0
- package/src/lib/glb-export.test.ts +636 -126
- package/src/lib/glb-export.ts +241 -120
- package/src/lib/history.test.ts +18 -7
- package/src/lib/history.ts +7 -0
- package/src/lib/hud-title.ts +109 -0
- package/src/lib/hydration-gated-storage.ts +49 -0
- package/src/lib/ifc-export.test.ts +174 -0
- package/src/lib/ifc-export.ts +201 -0
- package/src/lib/ifc-parts.ts +15 -0
- package/src/lib/inspector-expanded.test.ts +44 -0
- package/src/lib/inspector-expanded.ts +36 -0
- package/src/lib/interaction/scope.ts +22 -0
- package/src/lib/keyboard-pan.test.ts +129 -7
- package/src/lib/keyboard-pan.ts +161 -12
- package/src/lib/level-selection.test.ts +57 -0
- package/src/lib/level-selection.ts +16 -1
- package/src/lib/live-gesture-round-trip.test.ts +360 -0
- package/src/lib/lot/__fixtures__/map-dossier-miami-shores.json +1 -0
- package/src/lib/lot/__fixtures__/map-dossier-stpete.json +1 -0
- package/src/lib/lot/clean-ring.test.ts +187 -0
- package/src/lib/lot/clean-ring.ts +217 -0
- package/src/lib/lot/dossier.test.ts +202 -0
- package/src/lib/lot/dossier.ts +498 -0
- package/src/lib/lot/drop-in.test.ts +326 -0
- package/src/lib/lot/drop-in.ts +392 -0
- package/src/lib/lot/index.ts +43 -0
- package/src/lib/lot/lot-patch.test.ts +340 -0
- package/src/lib/lot/lot-patch.ts +394 -0
- package/src/lib/lot/parcel-provider.ts +43 -0
- package/src/lib/lot/terrain.test.ts +161 -0
- package/src/lib/lot/terrain.ts +266 -0
- package/src/lib/material-catalog-model.ts +3 -1
- package/src/lib/material-paint-panel-model.ts +9 -5
- package/src/lib/material-paint.ts +4 -0
- package/src/lib/measurement-kind.test.ts +6 -8
- package/src/lib/measurements.test.ts +23 -0
- package/src/lib/mezzanine-actions.test.ts +342 -0
- package/src/lib/mezzanine-draft.test.ts +501 -0
- package/src/lib/mezzanine-draft.ts +278 -0
- package/src/lib/mezzanine-messages.ts +26 -0
- package/src/lib/mezzanine-selection.test.ts +207 -0
- package/src/lib/mezzanine-stairs.ts +46 -0
- package/src/lib/model-export.ts +3 -1
- package/src/lib/node-action-movement.ts +17 -6
- package/src/lib/node-display-label.ts +37 -0
- package/src/lib/orbit-floor-clearance.test.ts +201 -0
- package/src/lib/orbit-floor-clearance.ts +154 -0
- package/src/lib/paint-commit-route.test.ts +37 -0
- package/src/lib/paint-commit-route.ts +17 -0
- package/src/lib/paint-eyedropper.test.tsx +165 -0
- package/src/lib/paint-eyedropper.ts +99 -0
- package/src/lib/paint-pass-through.test.ts +122 -0
- package/src/lib/paint-pass-through.ts +78 -0
- package/src/lib/paint-region-hover.ts +25 -0
- package/src/lib/paint-region-mode.ts +154 -0
- package/src/lib/paint-regions.test.ts +166 -0
- package/src/lib/paint-regions.ts +192 -0
- package/src/lib/paint-scope.test.ts +317 -35
- package/src/lib/paint-scope.ts +267 -25
- package/src/lib/panel-rows.ts +21 -0
- package/src/lib/parametric-node-update.ts +16 -7
- package/src/lib/plate-paint-affected.test.ts +300 -0
- package/src/lib/plate-paint-affected.ts +177 -0
- package/src/lib/plugin-chat.ts +65 -0
- package/src/lib/plugin-panels.test.ts +25 -0
- package/src/lib/plugin-panels.ts +94 -0
- package/src/lib/point-ask/__fixtures__/consumer-browser.tsx +76 -0
- package/src/lib/point-ask/__fixtures__/house.ts +81 -0
- package/src/lib/point-ask/ask-created.test.ts +195 -0
- package/src/lib/point-ask/ask-flow.test.ts +380 -0
- package/src/lib/point-ask/build-context.test.ts +173 -0
- package/src/lib/point-ask/build-context.ts +94 -0
- package/src/lib/point-ask/capture-crop.ts +129 -0
- package/src/lib/point-ask/choreography.ts +69 -0
- package/src/lib/point-ask/compare.test.ts +145 -0
- package/src/lib/point-ask/compare.ts +73 -0
- package/src/lib/point-ask/describe-target.test.ts +165 -0
- package/src/lib/point-ask/describe-target.ts +234 -0
- package/src/lib/point-ask/dictation.ts +79 -0
- package/src/lib/point-ask/edge-marker.test.ts +61 -0
- package/src/lib/point-ask/edge-marker.ts +27 -0
- package/src/lib/point-ask/frame-loop.ts +25 -0
- package/src/lib/point-ask/history-marks.test.ts +94 -0
- package/src/lib/point-ask/history-marks.ts +39 -0
- package/src/lib/point-ask/host-api.ts +32 -0
- package/src/lib/point-ask/outline-box.test.ts +30 -0
- package/src/lib/point-ask/outline-box.ts +11 -0
- package/src/lib/point-ask/overlay-metrics.ts +6 -0
- package/src/lib/point-ask/pick.test.ts +220 -0
- package/src/lib/point-ask/pick.ts +121 -0
- package/src/lib/point-ask/pin-machine.test.ts +494 -0
- package/src/lib/point-ask/pin-machine.ts +217 -0
- package/src/lib/point-ask/placement.test.ts +203 -0
- package/src/lib/point-ask/placement.ts +141 -0
- package/src/lib/point-ask/pointer-tracker.ts +34 -0
- package/src/lib/point-ask/projector.ts +83 -0
- package/src/lib/point-ask/region.test.ts +157 -0
- package/src/lib/point-ask/region.ts +136 -0
- package/src/lib/point-ask/retry.test.ts +217 -0
- package/src/lib/point-ask/retry.ts +37 -0
- package/src/lib/point-ask/scene-commit.test.ts +312 -0
- package/src/lib/point-ask/scene-commit.ts +32 -0
- package/src/lib/point-ask/session.ts +1391 -0
- package/src/lib/point-ask/spring-keyframes.test.ts +45 -0
- package/src/lib/point-ask/spring-keyframes.ts +38 -0
- package/src/lib/point-ask/spring.test.ts +63 -0
- package/src/lib/point-ask/spring.ts +107 -0
- package/src/lib/point-ask/suggestions.test.ts +80 -0
- package/src/lib/point-ask/suggestions.ts +125 -0
- package/src/lib/point-ask/target-scope.test.ts +622 -0
- package/src/lib/point-ask/target-scope.ts +338 -0
- package/src/lib/point-ask/types.ts +48 -0
- package/src/lib/portable-export.test.ts +34 -1
- package/src/lib/portable-export.ts +119 -53
- package/src/lib/print-feature-thickness.ts +10 -1
- package/src/lib/print-roof-solids.test.ts +39 -1
- package/src/lib/print-roof-solids.ts +43 -10
- package/src/lib/print-shell-compiler-manifold-core.ts +1 -1
- package/src/lib/print-shell-compiler.ts +1 -0
- package/src/lib/registered-draft-snap.ts +12 -0
- package/src/lib/roof-duplication.ts +10 -2
- package/src/lib/room-assembly-overlay.test.ts +275 -0
- package/src/lib/room-assembly-overlay.ts +265 -0
- package/src/lib/room-built-on.test.ts +379 -0
- package/src/lib/room-built-on.ts +138 -0
- package/src/lib/room-construction-commands.ts +712 -0
- package/src/lib/room-construction.test.ts +746 -0
- package/src/lib/room-construction.ts +385 -0
- package/src/lib/room-controls-phase6.test.ts +630 -0
- package/src/lib/room-divide-session.ts +523 -0
- package/src/lib/room-handle-drag.ts +1068 -0
- package/src/lib/room-name-catalog.test.ts +90 -0
- package/src/lib/room-name-catalog.ts +168 -0
- package/src/lib/room-push-dimensions.test.ts +311 -0
- package/src/lib/room-push-dimensions.ts +251 -0
- package/src/lib/room-selection-commands.ts +62 -0
- package/src/lib/room-selection.test.ts +1017 -0
- package/src/lib/room-selection.ts +417 -0
- package/src/lib/room-structure-commands.ts +94 -0
- package/src/lib/room-structure.test.ts +921 -0
- package/src/lib/room-transform-session.ts +570 -0
- package/src/lib/room-zone-routing.test.ts +294 -0
- package/src/lib/room-zone-routing.ts +197 -0
- package/src/lib/scene-clipboard.test.ts +610 -1
- package/src/lib/scene-clipboard.ts +269 -58
- package/src/lib/scene-material-refs.test.ts +79 -0
- package/src/lib/scene-material-refs.ts +79 -0
- package/src/lib/scene.test.ts +24 -36
- package/src/lib/scene.ts +33 -57
- package/src/lib/selection-routing.test.ts +129 -45
- package/src/lib/selection-routing.ts +54 -75
- package/src/lib/session-writes.test.ts +120 -0
- package/src/lib/session-writes.ts +205 -0
- package/src/lib/sfx-bus.ts +13 -0
- package/src/lib/sfx-player.test.ts +70 -9
- package/src/lib/sfx-player.ts +6 -0
- package/src/lib/sidebar-panel.ts +62 -0
- package/src/lib/site-boundary.test.ts +4 -6
- package/src/lib/site-boundary.ts +1 -1
- package/src/lib/snapping-mode.test.ts +4 -0
- package/src/lib/snapping-mode.ts +8 -1
- package/src/lib/stair-levels.test.ts +25 -0
- package/src/lib/stair-levels.ts +22 -3
- package/src/lib/terrace-draft.test.ts +145 -0
- package/src/lib/terrace-draft.ts +214 -0
- package/src/lib/tool-cursor-icon.test.ts +16 -0
- package/src/lib/tool-cursor-icon.ts +11 -0
- package/src/lib/typing-target.ts +9 -0
- package/src/lib/units.ts +77 -39
- package/src/lib/vector-edges.test.ts +58 -0
- package/src/lib/vector-edges.ts +400 -0
- package/src/lib/viewer-selection.test.ts +577 -0
- package/src/lib/viewer-selection.ts +580 -0
- package/src/lib/wall-draw-variant.test.ts +71 -0
- package/src/lib/wall-draw-variant.ts +91 -0
- package/src/lib/wall-paint-region-session.ts +246 -0
- package/src/lib/wall-paint-region.test.ts +373 -0
- package/src/lib/wall-placement-pointer.test.ts +42 -0
- package/src/lib/wall-placement-pointer.ts +57 -0
- package/src/lib/wall-polygon-draft.test.ts +262 -0
- package/src/lib/wall-polygon-draft.ts +286 -0
- package/src/lib/wall-push.test.ts +453 -0
- package/src/lib/wall-region-face.ts +352 -0
- package/src/lib/wall-region-handles.test.ts +304 -0
- package/src/lib/wall-region-handles.ts +368 -0
- package/src/lib/wall-region-list.test.ts +119 -0
- package/src/lib/wall-region-list.ts +136 -0
- package/src/lib/wall-region-snap.ts +110 -0
- package/src/lib/window-changes.ts +35 -0
- package/src/lib/window-interaction.ts +14 -0
- package/src/lib/zone-content.ts +4 -1
- package/src/lib/zone-name.test.ts +17 -0
- package/src/lib/zone-name.ts +17 -0
- package/src/store/opens-in-select-mode.test.ts +223 -0
- package/src/store/paint-mode.test.ts +188 -0
- package/src/store/terrain-sculpt-mode.test.ts +19 -21
- package/src/store/tool-mode.test.ts +137 -6
- package/src/store/use-audio.test.ts +146 -0
- package/src/store/use-audio.tsx +127 -3
- package/src/store/use-construction-reveal.test.ts +173 -0
- package/src/store/use-construction-reveal.ts +145 -0
- package/src/store/use-corner-snap-hint.test.ts +24 -0
- package/src/store/use-corner-snap-hint.ts +47 -0
- package/src/store/use-delete-confirmation.ts +28 -1
- package/src/store/use-drawing-view.test.ts +24 -3
- package/src/store/use-drawing-view.ts +29 -1
- package/src/store/use-editor.tsx +432 -321
- package/src/store/use-floorplan-draft-preview.ts +13 -0
- package/src/store/use-floorplan-marquee.ts +12 -2
- package/src/store/use-follow-pascal.test.ts +62 -0
- package/src/store/use-follow-pascal.ts +53 -0
- package/src/store/use-point-ask.ts +247 -0
- package/src/store/use-quick-measurement-hud.ts +3 -3
- package/src/store/use-stair-build-preview.ts +33 -5
- package/src/store/use-wall-move-ghosts.ts +4 -2
- package/src/store/use-wall-region-handles-selection.test.ts +222 -0
- package/src/store/use-wall-region-handles-selection.ts +139 -0
- package/src/store/view-layout.test.ts +201 -0
- package/src/store/view-layout.ts +199 -0
- package/src/test-utils/immediate-animation-frames.ts +16 -0
- package/src/test-utils/live-gesture-round-trip.ts +224 -0
- package/src/test-utils/render-current-markup.tsx +16 -0
- package/src/test-utils/selection-harness.ts +82 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +0 -499
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -333
- package/src/components/tools/fence/fence-drafting.test.ts +0 -100
- package/src/components/tools/fence/fence-drafting.ts +0 -266
- package/src/components/tools/stair/stair-click-guard.test.ts +0 -58
- package/src/components/tools/stair/stair-click-guard.ts +0 -72
- package/src/components/tools/stair/stair-defaults.ts +0 -18
- package/src/components/tools/stair/stair-tool.tsx +0 -634
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +0 -218
- package/src/components/viewer/viewer-units-panel.tsx +0 -116
- package/src/lib/floorplan/stairs.ts +0 -489
- package/src/lib/level-duplication.test.ts +0 -144
- package/src/lib/level-duplication.ts +0 -195
|
@@ -7,16 +7,20 @@ import {
|
|
|
7
7
|
CeilingNode,
|
|
8
8
|
ColumnNode,
|
|
9
9
|
createTerrainField,
|
|
10
|
+
DoorNode,
|
|
10
11
|
ElevatorNode,
|
|
11
12
|
encodeTerrainField,
|
|
12
13
|
flattenPatch,
|
|
14
|
+
GeometryArtifactManifest,
|
|
13
15
|
LevelNode,
|
|
14
16
|
nodeRegistry,
|
|
15
17
|
registerNode,
|
|
16
18
|
ShelfNode,
|
|
17
19
|
SiteNode,
|
|
18
20
|
SlabNode,
|
|
21
|
+
StairNode,
|
|
19
22
|
sceneRegistry,
|
|
23
|
+
useInteractive,
|
|
20
24
|
useScene,
|
|
21
25
|
} from '@pascal-app/core'
|
|
22
26
|
import { hideFromScene, STAND_CLEARANCE, showInScene } from '@pascal-app/viewer'
|
|
@@ -93,6 +97,57 @@ describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
|
93
97
|
world?.dispose()
|
|
94
98
|
})
|
|
95
99
|
|
|
100
|
+
test('keeps a procedural static body but excludes moving groups', () => {
|
|
101
|
+
registerColliderDefinition('shelf', ShelfNode, 'furnish')
|
|
102
|
+
const shelf = ShelfNode.parse({ id: 'shelf_motion_test' })
|
|
103
|
+
setSceneNodes([shelf])
|
|
104
|
+
mountNode(shelf, [1, 1, 1], [0, 0.5, 0])
|
|
105
|
+
const root = sceneRegistry.nodes.get(shelf.id)!
|
|
106
|
+
const motion = new Group()
|
|
107
|
+
motion.userData.proceduralMotion = { nodeId: shelf.id, partId: 'drawer' }
|
|
108
|
+
const movingMesh = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial())
|
|
109
|
+
movingMesh.position.set(5, 0.5, 0)
|
|
110
|
+
motion.add(movingMesh)
|
|
111
|
+
root.add(motion)
|
|
112
|
+
root.updateMatrixWorld(true)
|
|
113
|
+
|
|
114
|
+
const world = buildFirstPersonColliderWorldFromRegistry()!
|
|
115
|
+
expect(world.bounds?.min.x).toBeCloseTo(-0.5)
|
|
116
|
+
expect(world.bounds?.max.x).toBeCloseTo(0.5)
|
|
117
|
+
world.dispose()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('an idle joint tree collides with its static split parts, not its merged rest pose', () => {
|
|
121
|
+
registerColliderDefinition('shelf', ShelfNode, 'furnish')
|
|
122
|
+
const shelf = ShelfNode.parse({ id: 'shelf_rest_test' })
|
|
123
|
+
setSceneNodes([shelf])
|
|
124
|
+
mountRegistryGroup(shelf)
|
|
125
|
+
const root = sceneRegistry.nodes.get(shelf.id)!
|
|
126
|
+
const box = (x: number) => {
|
|
127
|
+
const mesh = new Mesh(new BoxGeometry(1, 1, 1), new MeshBasicMaterial())
|
|
128
|
+
mesh.position.set(x, 0.5, 0)
|
|
129
|
+
return mesh
|
|
130
|
+
}
|
|
131
|
+
// The rest pose merges the body with the gate leaf at x = 5; the hidden split draws them apart.
|
|
132
|
+
const rest = new Group()
|
|
133
|
+
rest.userData.pascalProceduralRest = true
|
|
134
|
+
rest.add(box(0), box(5))
|
|
135
|
+
const split = new Group()
|
|
136
|
+
split.userData.pascalProceduralSplit = true
|
|
137
|
+
split.visible = false
|
|
138
|
+
const motion = new Group()
|
|
139
|
+
motion.userData.proceduralMotion = { nodeId: shelf.id, partId: 'gate' }
|
|
140
|
+
motion.add(box(5))
|
|
141
|
+
split.add(box(0), motion)
|
|
142
|
+
root.add(rest, split)
|
|
143
|
+
root.updateMatrixWorld(true)
|
|
144
|
+
|
|
145
|
+
const world = buildFirstPersonColliderWorldFromRegistry()!
|
|
146
|
+
expect(world.bounds?.min.x).toBeCloseTo(-0.5)
|
|
147
|
+
expect(world.bounds?.max.x).toBeCloseTo(0.5)
|
|
148
|
+
world.dispose()
|
|
149
|
+
})
|
|
150
|
+
|
|
96
151
|
test('standing clearance and floor hits survive a slab source joining and leaving a batch', () => {
|
|
97
152
|
registerColliderDefinition('slab', SlabNode, 'structure', 'floor')
|
|
98
153
|
const slab = SlabNode.parse({ id: 'slab_clearance_batch', polygon: [] })
|
|
@@ -274,3 +329,268 @@ describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
|
274
329
|
world.dispose()
|
|
275
330
|
})
|
|
276
331
|
})
|
|
332
|
+
|
|
333
|
+
// A door built from a script is drawn by its model: the leaves are the model's own and its `open`
|
|
334
|
+
// clip moves them. What the player is stopped by is a stand-in leaf box at the door's frame, and
|
|
335
|
+
// that box has to follow the door's own open state, not the native `operationState` the clip
|
|
336
|
+
// never touches.
|
|
337
|
+
describe('a door built from a script', () => {
|
|
338
|
+
const source = {
|
|
339
|
+
kind: 'script' as const,
|
|
340
|
+
script: 'b'.repeat(64),
|
|
341
|
+
artifact: 'a'.repeat(64),
|
|
342
|
+
manifest: GeometryArtifactManifest.parse({
|
|
343
|
+
bounds: { min: [-0.6, 0, -0.11], max: [0.6, 2.55, 0.11] },
|
|
344
|
+
triangles: 12,
|
|
345
|
+
animations: [{ name: 'open', duration: 1.3 }],
|
|
346
|
+
}),
|
|
347
|
+
}
|
|
348
|
+
// The door mechanism's open state for a script: the first control toggle of the item.
|
|
349
|
+
const mechanism = {
|
|
350
|
+
verb: 'open',
|
|
351
|
+
icon: 'door',
|
|
352
|
+
has: (node: AnyNode) => Boolean((node as { source?: unknown }).source),
|
|
353
|
+
isOn: (node: AnyNode, state: ReturnType<typeof useInteractive.getState>) =>
|
|
354
|
+
Boolean(state.items[node.id as never]?.controlValues[0]),
|
|
355
|
+
set: () => {},
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function mountScriptedDoor(language: 'three' | 'bpy' = 'three') {
|
|
359
|
+
registerNode({
|
|
360
|
+
kind: 'door',
|
|
361
|
+
schema: DoorNode,
|
|
362
|
+
schemaVersion: 1,
|
|
363
|
+
category: 'structure',
|
|
364
|
+
capabilities: { mechanism },
|
|
365
|
+
} as unknown as AnyNodeDefinition)
|
|
366
|
+
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
367
|
+
const door = DoorNode.parse({
|
|
368
|
+
id: 'door_shoji',
|
|
369
|
+
doorType: 'sliding',
|
|
370
|
+
width: 1.2,
|
|
371
|
+
height: 2.55,
|
|
372
|
+
source: { ...source, language },
|
|
373
|
+
})
|
|
374
|
+
const column = ColumnNode.parse({ id: 'column_far' })
|
|
375
|
+
setSceneNodes([door, column])
|
|
376
|
+
mountNode(column, [1, 2, 1], [10, 1, 0])
|
|
377
|
+
// The door's registered group, as its renderer builds it: the compiled model under it, with
|
|
378
|
+
// the agent's 1 m deep `cutout` box and two leaves.
|
|
379
|
+
const group = new Group()
|
|
380
|
+
group.position.set(0, 1.275, 0)
|
|
381
|
+
const box = (name: string, size: [number, number, number], x: number, z: number) => {
|
|
382
|
+
const mesh = new Mesh(new BoxGeometry(...size), new MeshBasicMaterial())
|
|
383
|
+
mesh.name = name
|
|
384
|
+
mesh.position.set(x, 0, z)
|
|
385
|
+
group.add(mesh)
|
|
386
|
+
}
|
|
387
|
+
box('cutout', [1.2, 2.55, 1], 0, 0)
|
|
388
|
+
box('leaf_0', [0.5, 2.4, 0.02], -0.255, 0.025)
|
|
389
|
+
box('leaf_1', [0.5, 2.4, 0.02], 0.255, -0.025)
|
|
390
|
+
group.updateMatrixWorld(true)
|
|
391
|
+
sceneRegistry.nodes.set(door.id, group)
|
|
392
|
+
sceneRegistry.byType.door!.add(door.id)
|
|
393
|
+
return door
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** Walk straight at the doorway from the front, at hip height: where the first thing is hit. */
|
|
397
|
+
function firstHitAcrossDoorway() {
|
|
398
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
399
|
+
if (!world) return null
|
|
400
|
+
const raycaster = new Raycaster(new Vector3(0, 1, -3), new Vector3(0, 0, 1))
|
|
401
|
+
const hit = raycaster.intersectObject(world.mesh, false)[0]
|
|
402
|
+
world.dispose()
|
|
403
|
+
return hit ? { z: hit.point.z } : null
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
afterEach(() => {
|
|
407
|
+
useInteractive.setState({ items: {} } as never)
|
|
408
|
+
sceneRegistry.clear()
|
|
409
|
+
nodeRegistry._reset()
|
|
410
|
+
useScene.setState({ nodes: {}, rootNodeIds: [] } as never)
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test('a closed one stops the player at its leaf, not at the model’s cutout box', () => {
|
|
414
|
+
mountScriptedDoor()
|
|
415
|
+
useInteractive.setState({ items: { door_shoji: { controlValues: [false] } } } as never)
|
|
416
|
+
|
|
417
|
+
const hit = firstHitAcrossDoorway()
|
|
418
|
+
expect(hit).not.toBeNull()
|
|
419
|
+
// the stand-in leaf is 0.06 m deep at the frame; the cutout would stop the player at z = -0.5
|
|
420
|
+
expect(hit!.z).toBeCloseTo(-0.03, 3)
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
test('an open one can be walked through, and its cutout never collides', () => {
|
|
424
|
+
mountScriptedDoor()
|
|
425
|
+
useInteractive.setState({ items: { door_shoji: { controlValues: [true] } } } as never)
|
|
426
|
+
|
|
427
|
+
expect(firstHitAcrossDoorway()).toBeNull()
|
|
428
|
+
})
|
|
429
|
+
test('a Blender-built door follows its mechanism the same way: the language makes no difference', () => {
|
|
430
|
+
mountScriptedDoor('bpy')
|
|
431
|
+
useInteractive.setState({ items: { door_shoji: { controlValues: [false] } } } as never)
|
|
432
|
+
expect(firstHitAcrossDoorway()?.z).toBeCloseTo(-0.03, 3)
|
|
433
|
+
|
|
434
|
+
useInteractive.setState({ items: { door_shoji: { controlValues: [true] } } } as never)
|
|
435
|
+
expect(firstHitAcrossDoorway()).toBeNull()
|
|
436
|
+
})
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
// The ground is a sheet across the lot, and under a house with a basement the sheet ran across the
|
|
440
|
+
// stairwell: a walker going down the stair met it at grade. The ground is drawn open over what the
|
|
441
|
+
// house covers (`coveredBelowGradeFootprints`), and the walker's ground is cut open over the same.
|
|
442
|
+
describe('a basement under the house', () => {
|
|
443
|
+
afterEach(() => {
|
|
444
|
+
sceneRegistry.clear()
|
|
445
|
+
nodeRegistry._reset()
|
|
446
|
+
useScene.setState({ nodes: {}, rootNodeIds: [] } as never)
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
type Box = { size: [number, number, number]; at: [number, number, number] }
|
|
450
|
+
|
|
451
|
+
function mountBoxes(node: AnyNode, boxes: Box[]) {
|
|
452
|
+
const group = new Group()
|
|
453
|
+
for (const { size, at } of boxes) {
|
|
454
|
+
const mesh = new Mesh(new BoxGeometry(...size), new MeshBasicMaterial())
|
|
455
|
+
mesh.position.set(...at)
|
|
456
|
+
group.add(mesh)
|
|
457
|
+
}
|
|
458
|
+
group.updateMatrixWorld(true)
|
|
459
|
+
sceneRegistry.nodes.set(node.id, group)
|
|
460
|
+
sceneRegistry.byType[node.type]!.add(node.id)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const rect = (x0: number, x1: number, z0: number, z1: number): Array<[number, number]> => [
|
|
464
|
+
[x0, z0],
|
|
465
|
+
[x1, z0],
|
|
466
|
+
[x1, z1],
|
|
467
|
+
[x0, z1],
|
|
468
|
+
]
|
|
469
|
+
const TREADS = 14
|
|
470
|
+
const RISE = 0.2
|
|
471
|
+
const FLOOR_G = 0.05
|
|
472
|
+
const FLOOR_B = -2.95
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* An 8 m square house on a flat site (or a flat sculpted one) over a basement whose floor lies at
|
|
476
|
+
* -2.95 m, its plan 1.3..6.7 × 1.4..6.6. The ground floor is a slab with the stairwell cut out of
|
|
477
|
+
* it, and a stair of 14 treads, 0.2 m each, goes down along +x from x = 2 at z 3..4.
|
|
478
|
+
*/
|
|
479
|
+
function house(sculpted: boolean) {
|
|
480
|
+
registerColliderDefinition('slab', SlabNode, 'structure', 'floor')
|
|
481
|
+
registerColliderDefinition('stair', StairNode, 'structure')
|
|
482
|
+
const field = createTerrainField({ cols: 21, rows: 21, spacing: 1, origin: [-6, -6] })
|
|
483
|
+
const site = SiteNode.parse({
|
|
484
|
+
id: 'site_test',
|
|
485
|
+
children: ['building_a'],
|
|
486
|
+
...(sculpted ? { terrain: encodeTerrainField(field) } : {}),
|
|
487
|
+
})
|
|
488
|
+
const building = BuildingNode.parse({
|
|
489
|
+
id: 'building_a',
|
|
490
|
+
parentId: site.id,
|
|
491
|
+
children: ['level_b', 'level_g'],
|
|
492
|
+
})
|
|
493
|
+
const basement = LevelNode.parse({
|
|
494
|
+
id: 'level_b',
|
|
495
|
+
parentId: building.id,
|
|
496
|
+
level: -1,
|
|
497
|
+
baseElevation: -3,
|
|
498
|
+
height: 3,
|
|
499
|
+
children: ['slab_b'],
|
|
500
|
+
})
|
|
501
|
+
const ground = LevelNode.parse({
|
|
502
|
+
id: 'level_g',
|
|
503
|
+
parentId: building.id,
|
|
504
|
+
level: 0,
|
|
505
|
+
height: 3,
|
|
506
|
+
children: ['slab_g'],
|
|
507
|
+
})
|
|
508
|
+
const slabB = SlabNode.parse({
|
|
509
|
+
id: 'slab_b',
|
|
510
|
+
parentId: basement.id,
|
|
511
|
+
polygon: rect(1.3, 6.7, 1.4, 6.6),
|
|
512
|
+
elevation: 0.05,
|
|
513
|
+
})
|
|
514
|
+
const slabG = SlabNode.parse({
|
|
515
|
+
id: 'slab_g',
|
|
516
|
+
parentId: ground.id,
|
|
517
|
+
polygon: rect(0, 8, 0, 8),
|
|
518
|
+
elevation: FLOOR_G,
|
|
519
|
+
})
|
|
520
|
+
const stair = StairNode.parse({ id: 'stair_a', parentId: basement.id })
|
|
521
|
+
setSceneNodes([site, building, basement, ground, slabB, slabG, stair])
|
|
522
|
+
mountRegistryGroup(site)
|
|
523
|
+
mountBoxes(slabB, [{ size: [5.4, 0.1, 5.2], at: [4, FLOOR_B - 0.05, 4] }])
|
|
524
|
+
// The ground floor around the stairwell (x 2..5.4, z 3..4).
|
|
525
|
+
mountBoxes(slabG, [
|
|
526
|
+
{ size: [2, 0.1, 8], at: [1, FLOOR_G - 0.05, 4] },
|
|
527
|
+
{ size: [2.6, 0.1, 8], at: [6.7, FLOOR_G - 0.05, 4] },
|
|
528
|
+
{ size: [3.4, 0.1, 3], at: [3.7, FLOOR_G - 0.05, 1.5] },
|
|
529
|
+
{ size: [3.4, 0.1, 4], at: [3.7, FLOOR_G - 0.05, 6] },
|
|
530
|
+
])
|
|
531
|
+
mountBoxes(
|
|
532
|
+
stair,
|
|
533
|
+
Array.from({ length: TREADS }, (_, i) => ({
|
|
534
|
+
size: [0.3, RISE, 1] as [number, number, number],
|
|
535
|
+
at: [2.15 + 0.3 * i, FLOOR_G - RISE * (i + 1) - RISE / 2, 3.5] as [number, number, number],
|
|
536
|
+
})),
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const UP = new Vector3(0, 1, 0)
|
|
541
|
+
/** The first upward-facing surface under (x, z), looking down from `from`. */
|
|
542
|
+
function floorUnder(mesh: Mesh, x: number, z: number, from: number): number | null {
|
|
543
|
+
const raycaster = new Raycaster()
|
|
544
|
+
raycaster.set(new Vector3(x, from, z), new Vector3(0, -1, 0))
|
|
545
|
+
const hit = raycaster
|
|
546
|
+
.intersectObject(mesh, false)
|
|
547
|
+
.find(
|
|
548
|
+
(h) => !h.face || h.face.normal.clone().transformDirection(mesh.matrixWorld).dot(UP) > 0.2,
|
|
549
|
+
)
|
|
550
|
+
return hit ? hit.point.y : null
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
for (const sculpted of [false, true]) {
|
|
554
|
+
describe(sculpted ? 'on sculpted ground' : 'on flat ground', () => {
|
|
555
|
+
test('a walker goes down the stair from the ground floor to the basement', () => {
|
|
556
|
+
house(sculpted)
|
|
557
|
+
const world = buildFirstPersonColliderWorldFromRegistry()!
|
|
558
|
+
expect(world).not.toBeNull()
|
|
559
|
+
|
|
560
|
+
// Eye-level search from half a metre over the floor the walker stands on, a step at a time
|
|
561
|
+
// along the stair's run: the floor found is the next one he stands on.
|
|
562
|
+
let floor = floorUnder(world.mesh, 1, 3.5, 3)!
|
|
563
|
+
expect(floor).toBeCloseTo(FLOOR_G, 6)
|
|
564
|
+
const floors = [floor]
|
|
565
|
+
for (let x = 1.1; x < 6.5; x += 0.1) {
|
|
566
|
+
floor = floorUnder(world.mesh, x, 3.5, floor + 0.5) ?? Number.NaN
|
|
567
|
+
floors.push(floor)
|
|
568
|
+
}
|
|
569
|
+
// He never stands on the ground sheet (0 m) once past the stairwell's edge, and ends on
|
|
570
|
+
// the basement's floor; no step is deeper than a tread.
|
|
571
|
+
expect(Math.min(...floors)).toBeCloseTo(FLOOR_B, 6)
|
|
572
|
+
for (let i = 1; i < floors.length; i += 1)
|
|
573
|
+
expect(floors[i - 1]! - floors[i]!).toBeLessThanOrEqual(RISE + 1e-6)
|
|
574
|
+
world.dispose()
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
test('the ground is open over the part of the basement the house covers, and closed past it', () => {
|
|
578
|
+
house(sculpted)
|
|
579
|
+
const world = buildFirstPersonColliderWorldFromRegistry()!
|
|
580
|
+
// Under the ground floor's slab, looking down from just under it: the ground sheet is
|
|
581
|
+
// there outside the basement's plan and gone inside it, where the basement's floor is.
|
|
582
|
+
const under = (x: number, z: number) => floorUnder(world.mesh, x, z, FLOOR_G - 0.03)
|
|
583
|
+
expect(under(1.2, 5)).toBeCloseTo(0, 6)
|
|
584
|
+
expect(under(1.4, 5)).toBeCloseTo(FLOOR_B, 6)
|
|
585
|
+
expect(under(6.6, 5)).toBeCloseTo(FLOOR_B, 6)
|
|
586
|
+
expect(under(6.8, 5)).toBeCloseTo(0, 6)
|
|
587
|
+
expect(under(4, 1.3)).toBeCloseTo(0, 6)
|
|
588
|
+
expect(under(4, 1.5)).toBeCloseTo(FLOOR_B, 6)
|
|
589
|
+
// Outside the house: the garden is walked on as before.
|
|
590
|
+
expect(floorUnder(world.mesh, 12, 5, 3)).toBeCloseTo(0, 6)
|
|
591
|
+
expect(floorUnder(world.mesh, -3, -3, 3)).toBeCloseTo(0, 6)
|
|
592
|
+
world.dispose()
|
|
593
|
+
})
|
|
594
|
+
})
|
|
595
|
+
}
|
|
596
|
+
})
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
|
+
coveredBelowGradeFootprints,
|
|
4
5
|
type DoorNode,
|
|
5
6
|
getGarageVisibleOpeningRatio,
|
|
6
7
|
getLevelElevations,
|
|
7
8
|
isOperationDoorType,
|
|
9
|
+
nodeMechanism,
|
|
8
10
|
nodeRegistry,
|
|
9
11
|
sceneRegistry,
|
|
10
12
|
terrainFieldOf,
|
|
@@ -15,6 +17,7 @@ import * as THREE from 'three'
|
|
|
15
17
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
16
18
|
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh'
|
|
17
19
|
import { computeSceneBoundsXZ } from '../../../lib/scene-bounds'
|
|
20
|
+
import { cutGroundCollider } from './ground-holes'
|
|
18
21
|
import { createTerrainColliderGeometry } from './terrain-collider'
|
|
19
22
|
|
|
20
23
|
const SKIPPED_MESH_NAMES = new Set(['cutout', 'collision-mesh'])
|
|
@@ -160,19 +163,28 @@ function collectLevelFallbackFloorGeometries(nodes: SceneNodes) {
|
|
|
160
163
|
// player at the datum inside an excavation and bury them inside a hill. Derived
|
|
161
164
|
// from the field rather than the rendered terrain mesh for the same
|
|
162
165
|
// mount-timing reason as the flat slab.
|
|
166
|
+
//
|
|
167
|
+
// Either way the ground is cut open over what the house covers of its basement
|
|
168
|
+
// (`coveredBelowGradeFootprints`, the rings the ground is drawn open over): a sheet
|
|
169
|
+
// across the stairwell would hold the player at grade on the way down, with the
|
|
170
|
+
// stair and the basement's floor below it. The house covers that part, so it is cut
|
|
171
|
+
// whatever the view does with the ground; past it the ground is walked on as before.
|
|
163
172
|
function createSiteGroundColliderGeometry(site: SiteNode, nodes: SceneNodes) {
|
|
164
173
|
if (site.visible === false) return null
|
|
165
174
|
|
|
166
175
|
const siteObject = sceneRegistry.nodes.get(site.id)
|
|
167
176
|
if (!siteObject?.visible) return null
|
|
168
177
|
|
|
178
|
+
const covered = coveredBelowGradeFootprints(nodes)
|
|
179
|
+
|
|
169
180
|
const field = terrainFieldOf(site)
|
|
170
181
|
if (field) {
|
|
171
182
|
const terrainGeometry = createTerrainColliderGeometry(field)
|
|
172
183
|
if (terrainGeometry) {
|
|
184
|
+
const cut = cutGroundCollider(terrainGeometry, covered)
|
|
173
185
|
siteObject.updateWorldMatrix(true, false)
|
|
174
|
-
|
|
175
|
-
return
|
|
186
|
+
cut.applyMatrix4(siteObject.matrixWorld)
|
|
187
|
+
return cut
|
|
176
188
|
}
|
|
177
189
|
}
|
|
178
190
|
|
|
@@ -194,8 +206,9 @@ function createSiteGroundColliderGeometry(site: SiteNode, nodes: SceneNodes) {
|
|
|
194
206
|
geometry.applyMatrix4(
|
|
195
207
|
new THREE.Matrix4().makeTranslation(centerX, -LEVEL_FALLBACK_FLOOR_THICKNESS / 2, centerZ),
|
|
196
208
|
)
|
|
197
|
-
geometry
|
|
198
|
-
|
|
209
|
+
const cut = cutGroundCollider(geometry, covered)
|
|
210
|
+
cut.applyMatrix4(siteObject.matrixWorld)
|
|
211
|
+
return cut
|
|
199
212
|
}
|
|
200
213
|
|
|
201
214
|
function collectSiteGroundColliderGeometries(nodes: SceneNodes) {
|
|
@@ -281,6 +294,10 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
|
|
|
281
294
|
const hasLeafContent = node.segments.some((segment) => segment.type !== 'empty')
|
|
282
295
|
if (!hasLeafContent) return null
|
|
283
296
|
|
|
297
|
+
// A door built from a script opens by its model's `open` clip, which the mechanism runs through
|
|
298
|
+
// its own control: the native `operationState` below never moves for it, so open is read there.
|
|
299
|
+
if (node.source && nodeMechanism(node)?.isOn(node, useInteractive.getState())) return null
|
|
300
|
+
|
|
284
301
|
const leafW = node.width - 2 * node.frameThickness
|
|
285
302
|
const leafH = node.height - node.frameThickness
|
|
286
303
|
if (leafW <= 0 || leafH <= 0) return null
|
|
@@ -356,8 +373,12 @@ function collectColliderGeometriesFromNode(
|
|
|
356
373
|
visitedMeshes.add(object)
|
|
357
374
|
|
|
358
375
|
// Prune hidden subtrees — children of an invisible group never render,
|
|
359
|
-
// so they must not collide either (see isEffectivelyVisible).
|
|
360
|
-
|
|
376
|
+
// so they must not collide either (see isEffectivelyVisible). An idle joint
|
|
377
|
+
// tree draws a merged rest pose that includes its moving parts; its hidden
|
|
378
|
+
// split group holds the same static parts apart from them.
|
|
379
|
+
if (object.userData.pascalProceduralRest) return
|
|
380
|
+
if (!object.visible && !object.userData.pascalProceduralSplit) return
|
|
381
|
+
if (object.userData.proceduralMotion) return
|
|
361
382
|
|
|
362
383
|
if (
|
|
363
384
|
isMesh(object) &&
|
|
@@ -453,7 +474,7 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider
|
|
|
453
474
|
;(bvhGeometry as any).computeBoundsTree = computeBoundsTree
|
|
454
475
|
;(bvhGeometry as any).disposeBoundsTree = disposeBoundsTree
|
|
455
476
|
bvhGeometry.computeBoundsTree?.({
|
|
456
|
-
|
|
477
|
+
targetLeafSize: 12,
|
|
457
478
|
strategy: 0,
|
|
458
479
|
} as never)
|
|
459
480
|
bvhGeometry.computeBoundingBox()
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
BoxGeometry,
|
|
4
|
+
BufferAttribute,
|
|
5
|
+
BufferGeometry,
|
|
6
|
+
Mesh,
|
|
7
|
+
MeshBasicMaterial,
|
|
8
|
+
Raycaster,
|
|
9
|
+
Vector3,
|
|
10
|
+
} from 'three'
|
|
11
|
+
import { cutGroundCollider } from './ground-holes'
|
|
12
|
+
|
|
13
|
+
/** A 10 × 10 m square on the plane y = 0.1 x (two triangles, facing up), plan 0..10 × 0..10. */
|
|
14
|
+
function slopedSquare(flip = false) {
|
|
15
|
+
const corner = (x: number, z: number): [number, number, number] => [x, 0.1 * x, z]
|
|
16
|
+
const up = [
|
|
17
|
+
corner(0, 0),
|
|
18
|
+
corner(0, 10),
|
|
19
|
+
corner(10, 0),
|
|
20
|
+
corner(10, 0),
|
|
21
|
+
corner(0, 10),
|
|
22
|
+
corner(10, 10),
|
|
23
|
+
]
|
|
24
|
+
const vertices = flip ? [...up].reverse() : up
|
|
25
|
+
const geometry = new BufferGeometry()
|
|
26
|
+
geometry.setAttribute('position', new BufferAttribute(new Float32Array(vertices.flat()), 3))
|
|
27
|
+
const slope = new Vector3(-0.1, 1, 0).normalize()
|
|
28
|
+
const normal = flip ? slope.clone().negate() : slope
|
|
29
|
+
geometry.setAttribute(
|
|
30
|
+
'normal',
|
|
31
|
+
new BufferAttribute(
|
|
32
|
+
new Float32Array(vertices.flatMap(() => [normal.x, normal.y, normal.z])),
|
|
33
|
+
3,
|
|
34
|
+
),
|
|
35
|
+
)
|
|
36
|
+
return geometry
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const HOLE: Array<[number, number]> = [
|
|
40
|
+
[3, 3],
|
|
41
|
+
[7, 3],
|
|
42
|
+
[7, 6],
|
|
43
|
+
[3, 6],
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
/** The up (1) or down (-1) facing of every triangle of a non-indexed geometry. */
|
|
47
|
+
function triangles(geometry: BufferGeometry) {
|
|
48
|
+
const position = geometry.getAttribute('position')
|
|
49
|
+
const out: Array<{ facing: number }> = []
|
|
50
|
+
for (let i = 0; i < position.count; i += 3) {
|
|
51
|
+
const [a, b, c] = [0, 1, 2].map(
|
|
52
|
+
(k) => new Vector3(position.getX(i + k), position.getY(i + k), position.getZ(i + k)),
|
|
53
|
+
) as [Vector3, Vector3, Vector3]
|
|
54
|
+
const normal = b.clone().sub(a).cross(c.clone().sub(a))
|
|
55
|
+
out.push({ facing: Math.sign(normal.y) })
|
|
56
|
+
}
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hitUnder(geometry: BufferGeometry, x: number, z: number): number | null {
|
|
61
|
+
const mesh = new Mesh(geometry, new MeshBasicMaterial())
|
|
62
|
+
mesh.updateMatrixWorld(true)
|
|
63
|
+
const raycaster = new Raycaster()
|
|
64
|
+
raycaster.set(new Vector3(x, 50, z), new Vector3(0, -1, 0))
|
|
65
|
+
return raycaster.intersectObject(mesh, false)[0]?.point.y ?? null
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe('cutGroundCollider', () => {
|
|
69
|
+
test('without holes the geometry is returned as it is', () => {
|
|
70
|
+
const geometry = slopedSquare()
|
|
71
|
+
expect(cutGroundCollider(geometry, [])).toBe(geometry)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('the ground is gone over the hole and is the same surface everywhere else', () => {
|
|
75
|
+
const cut = cutGroundCollider(slopedSquare(), [HOLE])
|
|
76
|
+
expect(hitUnder(cut, 5, 4)).toBeNull()
|
|
77
|
+
expect(hitUnder(cut, 3.2, 3.2)).toBeNull()
|
|
78
|
+
expect(hitUnder(cut, 6.8, 5.8)).toBeNull()
|
|
79
|
+
// Past the rim, on the slope the uncut triangle had: y = 0.1 x.
|
|
80
|
+
for (const [x, z] of [
|
|
81
|
+
[2.8, 4],
|
|
82
|
+
[7.2, 4],
|
|
83
|
+
[5, 2.8],
|
|
84
|
+
[5, 6.2],
|
|
85
|
+
[1, 1],
|
|
86
|
+
[9, 9],
|
|
87
|
+
] as const)
|
|
88
|
+
expect(hitUnder(cut, x, z)).toBeCloseTo(0.1 * x, 5)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('what is left has the area of the ground less the hole, and faces as the ground did', () => {
|
|
92
|
+
for (const flip of [false, true]) {
|
|
93
|
+
const cut = cutGroundCollider(slopedSquare(flip), [HOLE])
|
|
94
|
+
const pieces = triangles(cut)
|
|
95
|
+
// The slope stretches the surface, so compare in plan: 100 - 12 m².
|
|
96
|
+
const position = cut.getAttribute('position')
|
|
97
|
+
let plan = 0
|
|
98
|
+
for (let i = 0; i < position.count; i += 3) {
|
|
99
|
+
const [a, b, c] = [0, 1, 2].map((k) => [position.getX(i + k), position.getZ(i + k)]) as [
|
|
100
|
+
[number, number],
|
|
101
|
+
[number, number],
|
|
102
|
+
[number, number],
|
|
103
|
+
]
|
|
104
|
+
plan += Math.abs((b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])) / 2
|
|
105
|
+
}
|
|
106
|
+
expect(plan).toBeCloseTo(100 - 12, 3)
|
|
107
|
+
for (const piece of pieces) expect(piece.facing).toBe(flip ? -1 : 1)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('the normals follow the ground and stay unit length', () => {
|
|
112
|
+
const cut = cutGroundCollider(slopedSquare(), [HOLE])
|
|
113
|
+
const normal = cut.getAttribute('normal')
|
|
114
|
+
for (let i = 0; i < normal.count; i += 1) {
|
|
115
|
+
expect(Math.hypot(normal.getX(i), normal.getY(i), normal.getZ(i))).toBeCloseTo(1, 5)
|
|
116
|
+
expect(normal.getX(i)).toBeCloseTo(-0.1 / Math.hypot(0.1, 1), 5)
|
|
117
|
+
}
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('a triangle wholly inside the hole is dropped, vertical faces are kept', () => {
|
|
121
|
+
const box = new BoxGeometry(2, 2, 2).toNonIndexed()
|
|
122
|
+
box.translate(5, 0, 4.5)
|
|
123
|
+
const before = box.getAttribute('position').count
|
|
124
|
+
const cut = cutGroundCollider(box, [HOLE])
|
|
125
|
+
// Top and bottom faces lie wholly inside the hole and go; the four sides (no plan area) stay.
|
|
126
|
+
expect(cut.getAttribute('position').count).toBe(before - 12)
|
|
127
|
+
})
|
|
128
|
+
})
|