@pascal-app/editor 1.1.0 → 1.1.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 +9 -15
- package/src/components/editor/action-menu-button.tsx +1 -1
- package/src/components/editor/alignment-3d-guide-layer.tsx +1 -1
- package/src/components/editor/custom-camera-controls.tsx +11 -11
- package/src/components/editor/editor-layout-v2.tsx +1 -1
- package/src/components/editor/elevation-3d-guide-layer.tsx +1 -1
- package/src/components/editor/first-person-controls.tsx +20 -1981
- package/src/components/editor/floating-action-menu.tsx +1 -1
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floor-region-controls.tsx +6 -2
- package/src/components/editor/floorplan-navigation-presentation.test.ts +0 -40
- package/src/components/editor/floorplan-navigation-presentation.ts +0 -66
- package/src/components/editor/floorplan-panel.tsx +26 -22
- package/src/components/editor/floorplan-wall-typing-keys.ts +1 -1
- package/src/components/editor/follow-pascal.test.tsx +1 -2
- package/src/components/editor/follow-pascal.tsx +1 -1
- package/src/components/editor/footprint-height-handle.tsx +1 -1
- package/src/components/editor/group-actions.ts +1 -2
- package/src/components/editor/group-floating-action-menu.tsx +1 -1
- package/src/components/editor/group-move-3d.ts +1 -2
- package/src/components/editor/group-rotate-handle.tsx +1 -2
- package/src/components/editor/handles/use-handle-drag.ts +1 -2
- package/src/components/editor/index.tsx +24 -14
- package/src/components/editor/inspector-extension-actions.tsx +1 -1
- package/src/components/editor/node-arrow-handles.tsx +1 -1
- package/src/components/editor/open-wall-ends-3d-layer.tsx +1 -1
- package/src/components/editor/opening-guides-3d-layer.tsx +1 -1
- package/src/components/editor/room-controls.tsx +7 -4
- package/src/components/editor/room-handles-2d.tsx +2 -3
- package/src/components/editor/room-handles.tsx +1 -2
- package/src/components/editor/room-highlight.tsx +6 -105
- package/src/components/editor/room-labels.test.ts +8 -822
- package/src/components/editor/room-labels.tsx +40 -1671
- package/src/components/editor/room-webgpu-materials.test.ts +1 -1
- package/src/components/editor/selection-manager.tsx +5 -3
- package/src/components/editor/site-edge-labels.tsx +1 -1
- package/src/components/editor/snapshot-capture-overlay.tsx +2 -5
- package/src/components/editor/use-floorplan-background-placement.ts +2 -1
- package/src/components/editor/views/plugins-view.tsx +1 -1
- package/src/components/editor/views/view-bar.tsx +9 -9
- package/src/components/editor/wall-measurement-label.tsx +1 -1
- package/src/components/editor/wall-move-side-handles.tsx +1 -1
- package/src/components/editor/wall-pull-grip.tsx +2 -2
- package/src/components/editor-2d/floorplan-group-move.tsx +2 -3
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-open-wall-ends-layer.tsx +1 -1
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.test.ts +0 -37
- package/src/components/editor-2d/renderers/floorplan-annotation-layout.ts +0 -30
- package/src/components/editor-2d/renderers/floorplan-dimension-edit-overlay.tsx +13 -3
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +1 -2
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +8 -10
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +25 -33
- package/src/components/systems/roof/roof-edit-system.tsx +4 -3
- package/src/components/tools/distribution/distribution-run-tool.tsx +1 -1
- package/src/components/tools/item/placement-history.test.tsx +1 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +2 -3
- package/src/components/tools/paint-region/wall-paint-region-tool.tsx +6 -2
- package/src/components/tools/registry/move-registry-node-tool.tsx +1 -2
- package/src/components/tools/shared/cursor-sphere.tsx +1 -1
- package/src/components/tools/shared/draft-measurement-label.tsx +1 -1
- package/src/components/tools/shared/placement-box.tsx +1 -1
- package/src/components/tools/shared/placement-dimension-guides.tsx +1 -1
- package/src/components/tools/site/terrain-brush-cursor.tsx +1 -1
- package/src/components/ui/action-menu/camera-actions.tsx +2 -2
- package/src/components/ui/action-menu/control-modes.tsx +1 -1
- package/src/components/ui/action-menu/follow-pascal-pill.tsx +1 -1
- package/src/components/ui/action-menu/index.tsx +3 -3
- package/src/components/ui/action-menu/measurement-control.tsx +3 -3
- package/src/components/ui/action-menu/placement.ts +0 -18
- package/src/components/ui/action-menu/point-ask-button.tsx +1 -1
- package/src/components/ui/action-menu/view-toggles.tsx +5 -5
- package/src/components/ui/controls/finish-swatch.tsx +1 -1
- package/src/components/ui/controls/material-paint-panel.tsx +2 -2
- package/src/components/ui/controls/scene-material-list.tsx +2 -2
- package/src/components/ui/controls/select-control.tsx +1 -1
- package/src/components/ui/controls/terrain-sculpt-panel.tsx +1 -1
- package/src/components/ui/floating-level-selector.tsx +3 -3
- package/src/components/ui/helpers/contextual-helper-panel.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.test.tsx +1 -1
- package/src/components/ui/helpers/helper-manager.tsx +1 -1
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/collections/collections-popover.tsx +2 -2
- package/src/components/ui/panels/floor-foundation-panel.tsx +2 -2
- package/src/components/ui/panels/floor-opening-panel.tsx +4 -4
- package/src/components/ui/panels/node-display.ts +1 -1
- package/src/components/ui/panels/panel-manager.tsx +1 -1
- package/src/components/ui/panels/panel-wrapper.tsx +1 -1
- package/src/components/ui/panels/parametric-inspector.tsx +1 -1
- package/src/components/ui/panels/room-construction-rows.tsx +6 -6
- package/src/components/ui/panels/room-name-options.ts +1 -5
- package/src/components/ui/panels/room-panel.tsx +6 -8
- package/src/components/ui/primitives/color-dot.tsx +2 -2
- package/src/components/ui/primitives/opacity-control.tsx +2 -2
- package/src/components/ui/primitives/sidebar.tsx +3 -3
- package/src/components/ui/right-stack.tsx +1 -1
- package/src/components/ui/sidebar/icon-rail.tsx +1 -1
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/audio-settings-dialog.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/construction-animation-setting.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +1 -1
- package/src/components/ui/sidebar/panels/settings-panel/print-export-button.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/inline-rename-input.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +46 -7
- package/src/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node-drag.tsx +14 -5
- package/src/components/ui/sidebar/panels/site-panel/unit-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-editor-dialog.tsx +1 -1
- package/src/components/ui/wall-type/wall-type-menu.tsx +1 -1
- package/src/components/viewer-overlay.tsx +1 -2
- package/src/hooks/use-auto-frame.ts +1 -2
- package/src/hooks/use-keyboard.ts +1 -2
- package/src/hooks/use-selected-room.ts +5 -7
- package/src/i18n/floor-footprint-name.ts +1 -1
- package/src/i18n/locales/en/messages.po +180 -182
- package/src/i18n/locales/es/messages.po +180 -182
- package/src/i18n/locales/fr/messages.po +180 -182
- package/src/i18n/locales/ja/messages.po +180 -182
- package/src/i18n/locales/zh-CN/messages.po +180 -182
- package/src/i18n/locales.test.ts +1 -1
- package/src/index.tsx +109 -131
- package/src/lib/below-grade.test.tsx +4 -4
- package/src/lib/camera-facing.test.ts +1 -1
- package/src/lib/camera-facing.ts +2 -3
- package/src/lib/editor-level-display.ts +1 -1
- package/src/lib/floor-footprints.test.ts +2 -2
- package/src/lib/floor-opening-draft.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.test.ts +1 -1
- package/src/lib/floorplan/annotation-visibility.ts +1 -1
- package/src/lib/floorplan/floorplan-export.test.ts +1 -1
- package/src/lib/floorplan/floorplan-export.tsx +8 -11
- package/src/lib/floorplan/floorplan-extension.ts +4 -157
- package/src/lib/floorplan/floorplan-mode.ts +2 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.test.ts +1 -1
- package/src/lib/floorplan/floorplan-pdfkit-renderer.ts +8 -6
- package/src/lib/floorplan/floorplan-plugin-install.test.tsx +3 -4
- package/src/lib/floorplan/geometry.ts +2 -112
- package/src/lib/floorplan/index.ts +0 -9
- package/src/lib/floorplan/items.ts +2 -1
- package/src/lib/floorplan/schedules.ts +6 -3
- package/src/lib/floorplan/sheet-export.ts +7 -3
- package/src/lib/floorplan/site-plan/site-parts.ts +7 -1
- package/src/lib/floorplan/site-plan/site-plan-layer.tsx +1 -1
- package/src/lib/follow-pascal-presentation.ts +1 -1
- package/src/lib/glb-export.ts +1 -1
- package/src/lib/material-paint.ts +1 -1
- package/src/lib/measurements.test.ts +2 -1
- package/src/lib/mezzanine-selection.test.ts +1 -1
- package/src/lib/paint-region-mode.ts +1 -1
- package/src/lib/point-ask/session.ts +1 -2
- package/src/lib/roof-duplication.test.ts +100 -0
- package/src/lib/roof-duplication.ts +30 -5
- package/src/lib/room-handle-drag.ts +2 -12
- package/src/lib/room-name-catalog.test.ts +2 -1
- package/src/lib/room-name-messages.test.ts +2 -1
- package/src/lib/room-name-messages.ts +1 -118
- package/src/lib/room-name-storage.test.ts +2 -1
- package/src/lib/room-selection-commands.ts +2 -2
- package/src/lib/room-selection.test.ts +6 -6
- package/src/lib/room-transform-session.ts +1 -2
- package/src/lib/room-zone-routing.ts +2 -2
- package/src/lib/selection-routing.test.ts +0 -28
- package/src/lib/selection-routing.ts +1 -40
- package/src/lib/sfx-bus.ts +8 -32
- package/src/lib/sfx-player.test.ts +72 -1
- package/src/lib/sfx-player.ts +49 -337
- package/src/lib/viewer-selection.test.ts +38 -10
- package/src/lib/wall-pull.test.ts +1 -1
- package/src/lib/wall-pull.ts +1 -27
- package/src/store/use-editor.tsx +17 -26
- package/src/store/use-floorplan-annotation-visibility.ts +5 -5
- package/src/store/use-wall-region-handles-selection.ts +1 -2
- package/src/components/editor/__bench__/room-label-placement.bench.ts +0 -108
- package/src/components/editor/first-person/build-collider-world.test.ts +0 -596
- package/src/components/editor/first-person/build-collider-world.ts +0 -549
- package/src/components/editor/first-person/ground-holes.test.ts +0 -128
- package/src/components/editor/first-person/ground-holes.ts +0 -130
- package/src/components/editor/first-person/mechanism-targets.test.ts +0 -162
- package/src/components/editor/first-person/mechanism-targets.ts +0 -88
- package/src/components/editor/first-person/terrain-collider.test.ts +0 -132
- package/src/components/editor/first-person/terrain-collider.ts +0 -212
- package/src/components/editor/floorplan-camera-sync.test.ts +0 -249
- package/src/components/editor/floorplan-camera-sync.ts +0 -213
- package/src/components/editor/localized-html.tsx +0 -22
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.test.tsx +0 -236
- package/src/components/editor-2d/renderers/floorplan-dimension-renderer.tsx +0 -708
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.test.tsx +0 -385
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +0 -670
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +0 -89
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +0 -106
- package/src/components/editor-2d/svg-paths.ts +0 -124
- package/src/components/ui/action-menu/action-button.tsx +0 -89
- package/src/components/ui/action-menu/follow-pascal-toggle.tsx +0 -42
- package/src/components/ui/primitives/button.tsx +0 -69
- package/src/components/ui/primitives/dropdown-menu.tsx +0 -232
- package/src/components/ui/primitives/popover.tsx +0 -42
- package/src/components/ui/primitives/switch.tsx +0 -29
- package/src/components/ui/primitives/tooltip.tsx +0 -57
- package/src/components/ui/scene-loader.tsx +0 -73
- package/src/components/viewer/floorplan-compass-button.tsx +0 -67
- package/src/components/viewer/floorplan-preview-geometry.test.ts +0 -107
- package/src/components/viewer/floorplan-preview-geometry.ts +0 -281
- package/src/components/viewer/floorplan-preview-navigation.test.ts +0 -43
- package/src/components/viewer/floorplan-preview-navigation.ts +0 -62
- package/src/components/viewer/floorplan-preview-visibility.test.ts +0 -35
- package/src/components/viewer/floorplan-preview-visibility.ts +0 -19
- package/src/components/viewer/floorplan-preview.tsx +0 -1065
- package/src/components/viewer/render-settings-menu.tsx +0 -294
- package/src/components/viewer/sky-control.tsx +0 -710
- package/src/components/viewer/use-viewer-camera-navigation-sync.ts +0 -140
- package/src/components/viewer/use-viewer-ui-sounds.test.ts +0 -15
- package/src/components/viewer/use-viewer-ui-sounds.ts +0 -46
- package/src/components/viewer/viewer-controls-bar.tsx +0 -496
- package/src/components/viewer/viewer-hover-label.tsx +0 -42
- package/src/components/viewer/viewer-rooms.tsx +0 -248
- package/src/components/viewer/viewer-scene-header.tsx +0 -286
- package/src/components/viewer/viewer-selection-manager.tsx +0 -124
- package/src/components/viewer/viewer-stage-modes.test.ts +0 -38
- package/src/components/viewer/viewer-stage-modes.ts +0 -49
- package/src/components/viewer/viewer-stage-switcher.tsx +0 -97
- package/src/components/viewer/viewer-stage.test.tsx +0 -47
- package/src/components/viewer/viewer-stage.tsx +0 -221
- package/src/components/walkthrough-hud.tsx +0 -193
- package/src/hooks/use-mobile.ts +0 -19
- package/src/i18n/level-name.ts +0 -39
- package/src/i18n/scene-theme-name.ts +0 -16
- package/src/lib/below-grade.ts +0 -133
- package/src/lib/camera-pose.test.ts +0 -237
- package/src/lib/camera-pose.ts +0 -172
- package/src/lib/floorplan/floorplan-extension.test.ts +0 -42
- package/src/lib/floorplan/floorplan-readonly.test.ts +0 -47
- package/src/lib/floorplan/floorplan-readonly.ts +0 -95
- package/src/lib/floorplan/marks.test.ts +0 -213
- package/src/lib/floorplan/marks.ts +0 -238
- package/src/lib/measurements.ts +0 -8
- package/src/lib/node-display-label.ts +0 -37
- package/src/lib/room-assembly-overlay.test.ts +0 -275
- package/src/lib/room-assembly-overlay.ts +0 -265
- package/src/lib/room-selection.ts +0 -417
- package/src/lib/scene-bounds.test.ts +0 -327
- package/src/lib/scene-bounds.ts +0 -233
- package/src/lib/typing-target.ts +0 -9
- package/src/lib/viewer-selection.ts +0 -595
- package/src/lib/zone-content.ts +0 -133
- package/src/store/camera-pose-store.test.ts +0 -29
- package/src/store/camera-pose-store.ts +0 -27
- package/src/store/navigation-sync-pose-store.test.ts +0 -34
- package/src/store/navigation-sync-pose-store.ts +0 -27
- package/src/store/use-audio.test.ts +0 -146
- package/src/store/use-audio.tsx +0 -169
- package/src/store/use-first-person-hud.ts +0 -40
- package/src/store/use-follow-pascal.test.ts +0 -62
- package/src/store/use-follow-pascal.ts +0 -53
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Holes in the site ground's collision geometry.
|
|
3
|
-
*
|
|
4
|
-
* The walkthrough stands on one merged BVH mesh, and the site's ground (a flat slab, or the terrain)
|
|
5
|
-
* is a sheet across the whole lot. Where the house stands over a basement the ground is drawn open
|
|
6
|
-
* (`coveredBelowGradeFootprints`), but the sheet under the stairwell stayed: the player descending
|
|
7
|
-
* the stair met it at grade and could go no deeper. Cutting the collider by the same rings the
|
|
8
|
-
* ground is drawn open over lets the player follow the building's floors and stairs there.
|
|
9
|
-
*
|
|
10
|
-
* Every triangle that reaches a hole is cut against it in plan; the pieces keep the source
|
|
11
|
-
* triangle's plane (height and normal interpolated from its corners) and its facing, so the cut
|
|
12
|
-
* surface is exactly the one the uncut triangle was.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { difference, type Ring, union } from '@pascal-app/core'
|
|
16
|
-
import { BufferAttribute, BufferGeometry, ShapeUtils, Vector2 } from 'three'
|
|
17
|
-
|
|
18
|
-
/** Triangles flatter than this in plan (m²) are vertical faces: they hold nothing up. */
|
|
19
|
-
const MIN_PLAN_AREA = 1e-9
|
|
20
|
-
|
|
21
|
-
type Corner = { x: number; y: number; z: number; nx: number; ny: number; nz: number }
|
|
22
|
-
|
|
23
|
-
/** Twice the signed plan area of `a b c` (x right, z down). */
|
|
24
|
-
function orient(a: [number, number], b: [number, number], c: [number, number]): number {
|
|
25
|
-
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* `geometry` (non-indexed, `position` and `normal`, in the frame the rings are in) without the part
|
|
30
|
-
* of it that lies over `holes`. A triangle the clip fails on is kept whole: the ground closed is
|
|
31
|
-
* the safe side. The geometry is returned as it is when there are no holes.
|
|
32
|
-
*/
|
|
33
|
-
export function cutGroundCollider(
|
|
34
|
-
geometry: BufferGeometry,
|
|
35
|
-
holes: readonly Ring[],
|
|
36
|
-
): BufferGeometry {
|
|
37
|
-
if (holes.length === 0) return geometry
|
|
38
|
-
const cutouts = union(holes)
|
|
39
|
-
if (cutouts.length === 0) return geometry
|
|
40
|
-
const boxes = cutouts.map(({ outer }) => ({
|
|
41
|
-
minX: Math.min(...outer.map(([x]) => x)),
|
|
42
|
-
maxX: Math.max(...outer.map(([x]) => x)),
|
|
43
|
-
minZ: Math.min(...outer.map(([, z]) => z)),
|
|
44
|
-
maxZ: Math.max(...outer.map(([, z]) => z)),
|
|
45
|
-
}))
|
|
46
|
-
|
|
47
|
-
const position = geometry.getAttribute('position')
|
|
48
|
-
const normal = geometry.getAttribute('normal')
|
|
49
|
-
const positions: number[] = []
|
|
50
|
-
const normals: number[] = []
|
|
51
|
-
const push = (corner: Corner) => {
|
|
52
|
-
positions.push(corner.x, corner.y, corner.z)
|
|
53
|
-
normals.push(corner.nx, corner.ny, corner.nz)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
for (let i = 0; i + 2 < position.count; i += 3) {
|
|
57
|
-
const corners: Corner[] = [0, 1, 2].map((k) => ({
|
|
58
|
-
x: position.getX(i + k),
|
|
59
|
-
y: position.getY(i + k),
|
|
60
|
-
z: position.getZ(i + k),
|
|
61
|
-
nx: normal.getX(i + k),
|
|
62
|
-
ny: normal.getY(i + k),
|
|
63
|
-
nz: normal.getZ(i + k),
|
|
64
|
-
}))
|
|
65
|
-
const [a, b, c] = corners as [Corner, Corner, Corner]
|
|
66
|
-
const minX = Math.min(a.x, b.x, c.x)
|
|
67
|
-
const maxX = Math.max(a.x, b.x, c.x)
|
|
68
|
-
const minZ = Math.min(a.z, b.z, c.z)
|
|
69
|
-
const maxZ = Math.max(a.z, b.z, c.z)
|
|
70
|
-
const reached = boxes.some(
|
|
71
|
-
(box) => box.maxX > minX && box.minX < maxX && box.maxZ > minZ && box.minZ < maxZ,
|
|
72
|
-
)
|
|
73
|
-
const plan: [[number, number], [number, number], [number, number]] = [
|
|
74
|
-
[a.x, a.z],
|
|
75
|
-
[b.x, b.z],
|
|
76
|
-
[c.x, c.z],
|
|
77
|
-
]
|
|
78
|
-
const determinant = orient(plan[0], plan[1], plan[2])
|
|
79
|
-
if (!reached || Math.abs(determinant) < MIN_PLAN_AREA) {
|
|
80
|
-
corners.forEach(push)
|
|
81
|
-
continue
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
let pieces: ReturnType<typeof difference>
|
|
85
|
-
try {
|
|
86
|
-
pieces = difference(plan, cutouts, { throwOnError: true })
|
|
87
|
-
} catch {
|
|
88
|
-
corners.forEach(push)
|
|
89
|
-
continue
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const at = (x: number, z: number): Corner => {
|
|
93
|
-
const wa = ((b.z - c.z) * (x - c.x) + (c.x - b.x) * (z - c.z)) / determinant
|
|
94
|
-
const wb = ((c.z - a.z) * (x - c.x) + (a.x - c.x) * (z - c.z)) / determinant
|
|
95
|
-
const wc = 1 - wa - wb
|
|
96
|
-
const nx = a.nx * wa + b.nx * wb + c.nx * wc
|
|
97
|
-
const ny = a.ny * wa + b.ny * wb + c.ny * wc
|
|
98
|
-
const nz = a.nz * wa + b.nz * wb + c.nz * wc
|
|
99
|
-
const length = Math.hypot(nx, ny, nz) || 1
|
|
100
|
-
return {
|
|
101
|
-
x,
|
|
102
|
-
y: a.y * wa + b.y * wb + c.y * wc,
|
|
103
|
-
z,
|
|
104
|
-
nx: nx / length,
|
|
105
|
-
ny: ny / length,
|
|
106
|
-
nz: nz / length,
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
for (const piece of pieces) {
|
|
111
|
-
const rings = [piece.outer, ...piece.holes]
|
|
112
|
-
const points = rings.flat()
|
|
113
|
-
const contour = piece.outer.map(([x, z]) => new Vector2(x, z))
|
|
114
|
-
const voids = piece.holes.map((ring) => ring.map(([x, z]) => new Vector2(x, z)))
|
|
115
|
-
for (const [p, q, r] of ShapeUtils.triangulateShape(contour, voids)) {
|
|
116
|
-
const corner = [p, q, r].map((index) => points[index as number] as [number, number])
|
|
117
|
-
const swap =
|
|
118
|
-
Math.sign(orient(corner[0]!, corner[1]!, corner[2]!)) !== Math.sign(determinant)
|
|
119
|
-
const ordered = swap ? [corner[0]!, corner[2]!, corner[1]!] : corner
|
|
120
|
-
for (const [x, z] of ordered) push(at(x, z))
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const cut = new BufferGeometry()
|
|
126
|
-
cut.setAttribute('position', new BufferAttribute(new Float32Array(positions), 3))
|
|
127
|
-
cut.setAttribute('normal', new BufferAttribute(new Float32Array(normals), 3))
|
|
128
|
-
geometry.dispose()
|
|
129
|
-
return cut
|
|
130
|
-
}
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
type AnyNode,
|
|
4
|
-
type AnyNodeId,
|
|
5
|
-
nodeRegistry,
|
|
6
|
-
registerNode,
|
|
7
|
-
sceneRegistry,
|
|
8
|
-
useInteractive,
|
|
9
|
-
useScene,
|
|
10
|
-
} from '@pascal-app/core'
|
|
11
|
-
import { Object3D } from 'three'
|
|
12
|
-
import {
|
|
13
|
-
hasNativeLeaf,
|
|
14
|
-
mechanismHudInteract,
|
|
15
|
-
mechanismTargetIds,
|
|
16
|
-
nativeOpeningIds,
|
|
17
|
-
watchScriptedDoors,
|
|
18
|
-
} from './mechanism-targets'
|
|
19
|
-
|
|
20
|
-
const switchMechanism = {
|
|
21
|
-
has: (node: AnyNode) => (node as unknown as { joints: number }).joints > 0,
|
|
22
|
-
isOn: (node: AnyNode, state: ReturnType<typeof useInteractive.getState>) =>
|
|
23
|
-
Boolean(state.mechanisms[node.id]),
|
|
24
|
-
set: (node: AnyNode, on: boolean) => useInteractive.getState().setMechanism(node.id, on),
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const mounted: [string, string][] = []
|
|
28
|
-
|
|
29
|
-
function mount(kind: string, id: string, fields: Record<string, unknown> = {}): AnyNode {
|
|
30
|
-
const node = { object: 'node', id, type: kind, ...fields } as unknown as AnyNode
|
|
31
|
-
useScene.setState((state) => ({ nodes: { ...state.nodes, [id]: node } }) as never)
|
|
32
|
-
sceneRegistry.nodes.set(id, new Object3D())
|
|
33
|
-
sceneRegistry.byType[kind]!.add(id)
|
|
34
|
-
mounted.push([kind, id])
|
|
35
|
-
return node
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function registerKind(capabilities: Record<string, unknown>): string {
|
|
39
|
-
const kind = `test:walk-${crypto.randomUUID()}`
|
|
40
|
-
registerNode({
|
|
41
|
-
kind,
|
|
42
|
-
schemaVersion: 1,
|
|
43
|
-
category: 'furnish',
|
|
44
|
-
defaults: () => ({}),
|
|
45
|
-
capabilities,
|
|
46
|
-
} as never)
|
|
47
|
-
return kind
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
afterEach(() => {
|
|
51
|
-
for (const [kind, id] of mounted.splice(0)) {
|
|
52
|
-
sceneRegistry.nodes.delete(id)
|
|
53
|
-
sceneRegistry.byType[kind]!.delete(id)
|
|
54
|
-
useInteractive.getState().removeMechanism(id as AnyNodeId)
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
describe('walkthrough mechanism targets', () => {
|
|
59
|
-
test('any mounted node whose kind declares a mechanism is a target, once', () => {
|
|
60
|
-
const kind = registerKind({ mechanism: switchMechanism })
|
|
61
|
-
mount(kind, 'articulated_1', { joints: 2 })
|
|
62
|
-
mount(kind, 'articulated_still', { joints: 0 })
|
|
63
|
-
mount(registerKind({}), 'plain_1')
|
|
64
|
-
const covered = mount(kind, 'articulated_covered', { joints: 1 })
|
|
65
|
-
|
|
66
|
-
expect(mechanismTargetIds(new Set([covered.id]))).toEqual(['articulated_1' as AnyNodeId])
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
test('the HUD names the node and says what E will do', () => {
|
|
70
|
-
const kind = registerKind({ mechanism: switchMechanism })
|
|
71
|
-
const node = mount(kind, 'articulated_hud', { joints: 1, name: 'Desk lamp' })
|
|
72
|
-
expect(mechanismHudInteract(node)).toEqual({ label: 'Desk lamp', verb: 'turn on' })
|
|
73
|
-
useInteractive.getState().setMechanism(node.id, true)
|
|
74
|
-
expect(mechanismHudInteract(node)).toEqual({ label: 'Desk lamp', verb: 'turn off' })
|
|
75
|
-
|
|
76
|
-
const lid = mount(registerKind({ mechanism: { ...switchMechanism, verb: 'open' } }), 'lid_1', {
|
|
77
|
-
joints: 1,
|
|
78
|
-
})
|
|
79
|
-
expect(mechanismHudInteract(lid)).toEqual({ label: 'item', verb: 'open' })
|
|
80
|
-
expect(mechanismHudInteract(mount(registerKind({}), 'plain_hud'))).toBeNull()
|
|
81
|
-
})
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// A door or window built from a script has no native leaf or sash: the walkthrough's own door and
|
|
85
|
-
// window targets only start a native swing on it, which draws nothing. Its `open` clip runs
|
|
86
|
-
// through its mechanism, as an authored object's does.
|
|
87
|
-
describe('walkthrough doors and windows', () => {
|
|
88
|
-
const script = { source: { kind: 'script', script: 'b'.repeat(64), artifact: 'a'.repeat(64) } }
|
|
89
|
-
|
|
90
|
-
test('the doors and windows it swings itself are the ones with a native leaf', () => {
|
|
91
|
-
const native = mount('door', 'door_native')
|
|
92
|
-
const scripted = mount('door', 'door_scripted', script)
|
|
93
|
-
mount('window', 'window_native')
|
|
94
|
-
mount('window', 'window_scripted', script)
|
|
95
|
-
|
|
96
|
-
expect(nativeOpeningIds().sort()).toEqual(['door_native', 'window_native'] as AnyNodeId[])
|
|
97
|
-
expect(hasNativeLeaf(native)).toBe(true)
|
|
98
|
-
expect(hasNativeLeaf(scripted)).toBe(false)
|
|
99
|
-
expect(hasNativeLeaf(undefined)).toBe(true)
|
|
100
|
-
})
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// The walkthrough's collider world is rebuilt when a native door's tween ends. A door built from a
|
|
104
|
-
// script opens by a clip that no tween system reports: its collider follows its mechanism, so
|
|
105
|
-
// the world has to be rebuilt when that flips.
|
|
106
|
-
describe('walkthrough scripted doors', () => {
|
|
107
|
-
const mechanism = {
|
|
108
|
-
verb: 'open',
|
|
109
|
-
has: (node: AnyNode) => Boolean((node as unknown as { source?: unknown }).source),
|
|
110
|
-
isOn: (node: AnyNode, state: ReturnType<typeof useInteractive.getState>) =>
|
|
111
|
-
Boolean(state.items[node.id as AnyNodeId]?.controlValues[0]),
|
|
112
|
-
set: () => {},
|
|
113
|
-
}
|
|
114
|
-
let restoreRegistry = () => {}
|
|
115
|
-
|
|
116
|
-
beforeEach(() => {
|
|
117
|
-
restoreRegistry = nodeRegistry._snapshot()
|
|
118
|
-
registerNode({
|
|
119
|
-
kind: 'door',
|
|
120
|
-
schemaVersion: 1,
|
|
121
|
-
category: 'structure',
|
|
122
|
-
defaults: () => ({}),
|
|
123
|
-
capabilities: { mechanism },
|
|
124
|
-
} as never)
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
afterEach(() => {
|
|
128
|
-
useInteractive.setState({ items: {} } as never)
|
|
129
|
-
restoreRegistry()
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
test('a rebuild is asked for when a scripted door opens or closes, and for nothing else', () => {
|
|
133
|
-
mount('door', 'door_scripted_watch', { source: { kind: 'script' } })
|
|
134
|
-
mount('door', 'door_native_watch')
|
|
135
|
-
let rebuilds = 0
|
|
136
|
-
const stop = watchScriptedDoors(() => rebuilds++)
|
|
137
|
-
const setControls = (scripted: boolean, native: boolean) =>
|
|
138
|
-
useInteractive.setState({
|
|
139
|
-
items: {
|
|
140
|
-
door_scripted_watch: { controlValues: [scripted] },
|
|
141
|
-
door_native_watch: { controlValues: [native] },
|
|
142
|
-
},
|
|
143
|
-
} as never)
|
|
144
|
-
try {
|
|
145
|
-
// its model initialising the item closed is not a change
|
|
146
|
-
setControls(false, false)
|
|
147
|
-
expect(rebuilds).toBe(0)
|
|
148
|
-
setControls(true, false)
|
|
149
|
-
expect(rebuilds).toBe(1)
|
|
150
|
-
// the same state again, and a native door's control, are not its business
|
|
151
|
-
setControls(true, false)
|
|
152
|
-
setControls(true, true)
|
|
153
|
-
expect(rebuilds).toBe(1)
|
|
154
|
-
setControls(false, true)
|
|
155
|
-
expect(rebuilds).toBe(2)
|
|
156
|
-
} finally {
|
|
157
|
-
stop()
|
|
158
|
-
}
|
|
159
|
-
setControls(true, true)
|
|
160
|
-
expect(rebuilds).toBe(2)
|
|
161
|
-
})
|
|
162
|
-
})
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type AnyNode,
|
|
3
|
-
type AnyNodeId,
|
|
4
|
-
isScriptedNode,
|
|
5
|
-
nodeMechanism,
|
|
6
|
-
nodeRegistry,
|
|
7
|
-
sceneRegistry,
|
|
8
|
-
useInteractive,
|
|
9
|
-
useScene,
|
|
10
|
-
} from '@pascal-app/core'
|
|
11
|
-
import type { WalkthroughInteract } from '../../../store/use-first-person-hud'
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Whether the walkthrough swings this door or window itself, through the native `operationState`
|
|
15
|
-
* its door and window systems draw. One built from a script has no native leaf or sash: a swing
|
|
16
|
-
* would draw nothing, so its clips run through its mechanism, as an authored object's do.
|
|
17
|
-
*/
|
|
18
|
-
export function hasNativeLeaf(node: AnyNode | undefined): boolean {
|
|
19
|
-
return !isScriptedNode(node)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Mounted doors and windows the walkthrough's own door and window targets cover. */
|
|
23
|
-
export function nativeOpeningIds(): AnyNodeId[] {
|
|
24
|
-
const nodes = useScene.getState().nodes
|
|
25
|
-
const ids: AnyNodeId[] = []
|
|
26
|
-
for (const kind of ['door', 'window'] as const) {
|
|
27
|
-
for (const id of sceneRegistry.byType[kind] ?? []) {
|
|
28
|
-
if (hasNativeLeaf(nodes[id as AnyNodeId])) ids.push(id as AnyNodeId)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return ids
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Mounted nodes the walkthrough runs through `capabilities.mechanism`, any kind
|
|
36
|
-
* included. `covered` holds the nodes a finer target already tested (procedural
|
|
37
|
-
* parts, catalog item toggles), so no node is targeted twice.
|
|
38
|
-
*/
|
|
39
|
-
export function mechanismTargetIds(covered: ReadonlySet<string>): AnyNodeId[] {
|
|
40
|
-
const nodes = useScene.getState().nodes
|
|
41
|
-
const ids: AnyNodeId[] = []
|
|
42
|
-
for (const [kind, kindIds] of Object.entries(sceneRegistry.byType)) {
|
|
43
|
-
if (!nodeRegistry.get(kind)?.capabilities.mechanism) continue
|
|
44
|
-
for (const id of kindIds) {
|
|
45
|
-
if (!covered.has(id) && nodeMechanism(nodes[id as AnyNodeId])) ids.push(id as AnyNodeId)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
return ids
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** The walkthrough prompt for a mechanism target: its name and what E does next. */
|
|
52
|
-
export function mechanismHudInteract(node: AnyNode): WalkthroughInteract {
|
|
53
|
-
const mechanism = nodeMechanism(node)
|
|
54
|
-
if (!mechanism) return null
|
|
55
|
-
const on = mechanism.isOn(node, useInteractive.getState())
|
|
56
|
-
const verb = mechanism.verb === 'open' ? (on ? 'close' : 'open') : on ? 'turn off' : 'turn on'
|
|
57
|
-
return { label: node.name ?? 'item', verb }
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Ids of the mounted doors built from a script that are open, by their mechanism. */
|
|
61
|
-
function openScriptedDoors(): string {
|
|
62
|
-
const nodes = useScene.getState().nodes
|
|
63
|
-
const state = useInteractive.getState()
|
|
64
|
-
const open: string[] = []
|
|
65
|
-
for (const id of sceneRegistry.byType.door ?? []) {
|
|
66
|
-
const node = nodes[id as AnyNodeId]
|
|
67
|
-
if (isScriptedNode(node) && nodeMechanism(node)?.isOn(node, state)) open.push(id)
|
|
68
|
-
}
|
|
69
|
-
return open.sort().join(',')
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Calls `onChange` when a mounted door built from a script opens or closes. The walkthrough's
|
|
74
|
-
* collider follows the door's mechanism, and its model's clip is moved by no tween system that
|
|
75
|
-
* would report the end, as a native door's does: without this the world keeps the leaf that was
|
|
76
|
-
* there when it was last built. It fires at the toggle: the doorway is passable as the leaves
|
|
77
|
-
* start to move, and solid again as they start to close.
|
|
78
|
-
*/
|
|
79
|
-
export function watchScriptedDoors(onChange: () => void): () => void {
|
|
80
|
-
let known = openScriptedDoors()
|
|
81
|
-
return useInteractive.subscribe((state, previous) => {
|
|
82
|
-
if (state.items === previous.items) return
|
|
83
|
-
const next = openScriptedDoors()
|
|
84
|
-
if (next === known) return
|
|
85
|
-
known = next
|
|
86
|
-
onChange()
|
|
87
|
-
})
|
|
88
|
-
}
|
|
@@ -1,132 +0,0 @@
|
|
|
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
|
-
})
|