@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,596 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
3
|
-
type AnyNode,
|
|
4
|
-
type AnyNodeDefinition,
|
|
5
|
-
applyHeightPatch,
|
|
6
|
-
BuildingNode,
|
|
7
|
-
CeilingNode,
|
|
8
|
-
ColumnNode,
|
|
9
|
-
createTerrainField,
|
|
10
|
-
DoorNode,
|
|
11
|
-
ElevatorNode,
|
|
12
|
-
encodeTerrainField,
|
|
13
|
-
flattenPatch,
|
|
14
|
-
GeometryArtifactManifest,
|
|
15
|
-
LevelNode,
|
|
16
|
-
nodeRegistry,
|
|
17
|
-
registerNode,
|
|
18
|
-
ShelfNode,
|
|
19
|
-
SiteNode,
|
|
20
|
-
SlabNode,
|
|
21
|
-
StairNode,
|
|
22
|
-
sceneRegistry,
|
|
23
|
-
useInteractive,
|
|
24
|
-
useScene,
|
|
25
|
-
} from '@pascal-app/core'
|
|
26
|
-
import { hideFromScene, STAND_CLEARANCE, showInScene } from '@pascal-app/viewer'
|
|
27
|
-
import { BoxGeometry, Group, Mesh, MeshBasicMaterial, Raycaster, Vector3 } from 'three'
|
|
28
|
-
import { buildFirstPersonColliderWorldFromRegistry } from './build-collider-world'
|
|
29
|
-
|
|
30
|
-
function registerColliderDefinition(
|
|
31
|
-
kind: AnyNode['type'],
|
|
32
|
-
schema: AnyNodeDefinition['schema'],
|
|
33
|
-
category: AnyNodeDefinition['category'],
|
|
34
|
-
surfaceRole?: AnyNodeDefinition['surfaceRole'],
|
|
35
|
-
) {
|
|
36
|
-
registerNode({
|
|
37
|
-
kind,
|
|
38
|
-
schema,
|
|
39
|
-
schemaVersion: 1,
|
|
40
|
-
category,
|
|
41
|
-
surfaceRole,
|
|
42
|
-
capabilities: {},
|
|
43
|
-
} as AnyNodeDefinition)
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function mountNode(
|
|
47
|
-
node: AnyNode,
|
|
48
|
-
box: [number, number, number],
|
|
49
|
-
position: [number, number, number],
|
|
50
|
-
) {
|
|
51
|
-
const group = new Group()
|
|
52
|
-
const mesh = new Mesh(new BoxGeometry(box[0], box[1], box[2]), new MeshBasicMaterial())
|
|
53
|
-
mesh.position.set(position[0], position[1], position[2])
|
|
54
|
-
group.add(mesh)
|
|
55
|
-
group.updateMatrixWorld(true)
|
|
56
|
-
sceneRegistry.nodes.set(node.id, group)
|
|
57
|
-
sceneRegistry.byType[node.type]!.add(node.id)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function mountRegistryGroup(node: AnyNode, position: [number, number, number] = [0, 0, 0]) {
|
|
61
|
-
const group = new Group()
|
|
62
|
-
group.position.set(position[0], position[1], position[2])
|
|
63
|
-
group.updateMatrixWorld(true)
|
|
64
|
-
sceneRegistry.nodes.set(node.id, group)
|
|
65
|
-
sceneRegistry.byType[node.type]!.add(node.id)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function setSceneNodes(nodes: AnyNode[]) {
|
|
69
|
-
useScene.setState({
|
|
70
|
-
nodes: Object.fromEntries(nodes.map((node) => [node.id, node])),
|
|
71
|
-
rootNodeIds: nodes.map((node) => node.id),
|
|
72
|
-
} as never)
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
76
|
-
afterEach(() => {
|
|
77
|
-
sceneRegistry.clear()
|
|
78
|
-
nodeRegistry._reset()
|
|
79
|
-
useScene.setState({ nodes: {}, rootNodeIds: [] } as never)
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
test('includes structure and furnish nodes discovered through the node registry', () => {
|
|
83
|
-
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
84
|
-
registerColliderDefinition('shelf', ShelfNode, 'furnish')
|
|
85
|
-
|
|
86
|
-
const column = ColumnNode.parse({ id: 'column_test' })
|
|
87
|
-
const shelf = ShelfNode.parse({ id: 'shelf_test', position: [3, 0, 0] })
|
|
88
|
-
setSceneNodes([column, shelf])
|
|
89
|
-
mountNode(column, [1, 2, 1], [0, 1, 0])
|
|
90
|
-
mountNode(shelf, [2, 1, 1], [3, 0.5, 0])
|
|
91
|
-
|
|
92
|
-
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
93
|
-
|
|
94
|
-
expect(world).not.toBeNull()
|
|
95
|
-
expect(world?.bounds?.min.x).toBeCloseTo(-0.5)
|
|
96
|
-
expect(world?.bounds?.max.x).toBeCloseTo(4)
|
|
97
|
-
world?.dispose()
|
|
98
|
-
})
|
|
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
|
-
|
|
151
|
-
test('standing clearance and floor hits survive a slab source joining and leaving a batch', () => {
|
|
152
|
-
registerColliderDefinition('slab', SlabNode, 'structure', 'floor')
|
|
153
|
-
const slab = SlabNode.parse({ id: 'slab_clearance_batch', polygon: [] })
|
|
154
|
-
setSceneNodes([slab])
|
|
155
|
-
mountNode(slab, [4, 0.2, 4], [0, 2, 0])
|
|
156
|
-
const source = sceneRegistry.nodes.get(slab.id)!.children[0] as Mesh
|
|
157
|
-
const raycaster = new Raycaster()
|
|
158
|
-
for (const batched of [false, true, false]) {
|
|
159
|
-
if (batched) hideFromScene(source, 'batched')
|
|
160
|
-
else showInScene(source, 'batched')
|
|
161
|
-
const world = buildFirstPersonColliderWorldFromRegistry()!
|
|
162
|
-
expect(world).not.toBeNull()
|
|
163
|
-
try {
|
|
164
|
-
raycaster.set(new Vector3(0, 3, 0), new Vector3(0, -1, 0))
|
|
165
|
-
raycaster.far = STAND_CLEARANCE
|
|
166
|
-
expect(raycaster.intersectObject(world.mesh, false)[0]!.point.y).toBeCloseTo(2.1)
|
|
167
|
-
raycaster.set(new Vector3(0, 1, 0), new Vector3(0, 1, 0))
|
|
168
|
-
expect(raycaster.intersectObjects([world.mesh], false)[0]!.point.y).toBeCloseTo(1.9)
|
|
169
|
-
raycaster.set(new Vector3(0, 2.5, 0), new Vector3(0, 1, 0))
|
|
170
|
-
expect(raycaster.intersectObjects([world.mesh], false)).toHaveLength(0)
|
|
171
|
-
} finally {
|
|
172
|
-
world.dispose()
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
source.geometry.dispose()
|
|
176
|
-
;(source.material as MeshBasicMaterial).dispose()
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
test('excludes ceiling surfaces so the walkthrough player passes through them', () => {
|
|
180
|
-
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
181
|
-
registerColliderDefinition('ceiling', CeilingNode, 'structure', 'ceiling')
|
|
182
|
-
|
|
183
|
-
const column = ColumnNode.parse({ id: 'column_test' })
|
|
184
|
-
const ceiling = CeilingNode.parse({ id: 'ceiling_test', polygon: [] })
|
|
185
|
-
setSceneNodes([column, ceiling])
|
|
186
|
-
mountNode(column, [1, 2, 1], [0, 1, 0])
|
|
187
|
-
// A wide ceiling at head height — if it were collected, bounds would span ±5.
|
|
188
|
-
mountNode(ceiling, [10, 0.1, 10], [0, 2.5, 0])
|
|
189
|
-
|
|
190
|
-
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
191
|
-
|
|
192
|
-
expect(world).not.toBeNull()
|
|
193
|
-
// Bounds reflect only the 1×1 column; the ceiling contributed no geometry.
|
|
194
|
-
expect(world?.bounds?.min.x).toBeCloseTo(-0.5)
|
|
195
|
-
expect(world?.bounds?.max.x).toBeCloseTo(0.5)
|
|
196
|
-
world?.dispose()
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
test('skips meshes hidden by an invisible ancestor (stale roof segment CSG)', () => {
|
|
200
|
-
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
201
|
-
|
|
202
|
-
// Mirror the roof's segments-wrapper shape: the registered mesh's own
|
|
203
|
-
// visible flag stays true while a hidden wrapper hides it at render
|
|
204
|
-
// time. The collider must match the render, not the own-flag.
|
|
205
|
-
const column = ColumnNode.parse({ id: 'column_test' })
|
|
206
|
-
const visibleColumn = ColumnNode.parse({ id: 'column_visible', position: [3, 0, 0] })
|
|
207
|
-
setSceneNodes([column, visibleColumn])
|
|
208
|
-
|
|
209
|
-
const wrapper = new Group()
|
|
210
|
-
wrapper.visible = false
|
|
211
|
-
const hiddenMesh = new Mesh(new BoxGeometry(10, 2, 10), new MeshBasicMaterial())
|
|
212
|
-
wrapper.add(hiddenMesh)
|
|
213
|
-
wrapper.updateMatrixWorld(true)
|
|
214
|
-
sceneRegistry.nodes.set(column.id, hiddenMesh)
|
|
215
|
-
sceneRegistry.byType[column.type]!.add(column.id)
|
|
216
|
-
|
|
217
|
-
mountNode(visibleColumn, [1, 2, 1], [3, 1, 0])
|
|
218
|
-
|
|
219
|
-
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
220
|
-
|
|
221
|
-
expect(world).not.toBeNull()
|
|
222
|
-
// Bounds reflect only the visible 1×1 column at x = 3; the 10×10 mesh
|
|
223
|
-
// under the hidden wrapper contributed no geometry.
|
|
224
|
-
expect(world?.bounds?.min.x).toBeCloseTo(2.5)
|
|
225
|
-
expect(world?.bounds?.max.x).toBeCloseTo(3.5)
|
|
226
|
-
world?.dispose()
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
test('leaves elevators to their dedicated dynamic collider meshes', () => {
|
|
230
|
-
registerColliderDefinition('elevator', ElevatorNode, 'structure')
|
|
231
|
-
|
|
232
|
-
const elevator = ElevatorNode.parse({ id: 'elevator_test' })
|
|
233
|
-
setSceneNodes([elevator])
|
|
234
|
-
mountNode(elevator, [2, 3, 2], [0, 1.5, 0])
|
|
235
|
-
|
|
236
|
-
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
237
|
-
|
|
238
|
-
expect(world).toBeNull()
|
|
239
|
-
})
|
|
240
|
-
|
|
241
|
-
test('adds a fallback floor for a visible level with no slab', () => {
|
|
242
|
-
const level = LevelNode.parse({ id: 'level_test', level: 0 })
|
|
243
|
-
setSceneNodes([level])
|
|
244
|
-
mountRegistryGroup(level)
|
|
245
|
-
|
|
246
|
-
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
247
|
-
|
|
248
|
-
expect(world).not.toBeNull()
|
|
249
|
-
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
250
|
-
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
251
|
-
world?.dispose()
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
test('adds a fallback floor only for the lowest slab-less level in a building', () => {
|
|
255
|
-
const building = BuildingNode.parse({
|
|
256
|
-
id: 'building_test',
|
|
257
|
-
children: ['level_ground', 'level_upper'],
|
|
258
|
-
})
|
|
259
|
-
const groundLevel = LevelNode.parse({
|
|
260
|
-
id: 'level_ground',
|
|
261
|
-
parentId: building.id,
|
|
262
|
-
level: 0,
|
|
263
|
-
height: 3,
|
|
264
|
-
})
|
|
265
|
-
const upperLevel = LevelNode.parse({
|
|
266
|
-
id: 'level_upper',
|
|
267
|
-
parentId: building.id,
|
|
268
|
-
level: 1,
|
|
269
|
-
})
|
|
270
|
-
setSceneNodes([building, groundLevel, upperLevel])
|
|
271
|
-
mountRegistryGroup(groundLevel)
|
|
272
|
-
mountRegistryGroup(upperLevel, [0, 3, 0])
|
|
273
|
-
|
|
274
|
-
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
275
|
-
|
|
276
|
-
expect(world).not.toBeNull()
|
|
277
|
-
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
278
|
-
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
279
|
-
world?.dispose()
|
|
280
|
-
})
|
|
281
|
-
|
|
282
|
-
test('adds a site ground collider so a spawn on bare ground has a floor', () => {
|
|
283
|
-
const site = SiteNode.parse({ id: 'site_test' })
|
|
284
|
-
setSceneNodes([site])
|
|
285
|
-
mountRegistryGroup(site)
|
|
286
|
-
|
|
287
|
-
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
288
|
-
|
|
289
|
-
expect(world).not.toBeNull()
|
|
290
|
-
// Ground slab sits just below the site ground plane (y = 0).
|
|
291
|
-
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
292
|
-
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
293
|
-
// The ground collider extends far past the site polygon so stepping out of
|
|
294
|
-
// the site boundary never drops the player below the ground plane.
|
|
295
|
-
expect(world?.bounds?.min.x).toBeCloseTo(-1000)
|
|
296
|
-
expect(world?.bounds?.max.x).toBeCloseTo(1000)
|
|
297
|
-
expect(world?.bounds?.min.z).toBeCloseTo(-1000)
|
|
298
|
-
expect(world?.bounds?.max.z).toBeCloseTo(1000)
|
|
299
|
-
world?.dispose()
|
|
300
|
-
})
|
|
301
|
-
|
|
302
|
-
test('a sculpted site walks on its terrain instead of the flat ground slab', () => {
|
|
303
|
-
const base = createTerrainField({ cols: 17, rows: 17, spacing: 1, origin: [-8, -8] })
|
|
304
|
-
const patch = flattenPatch(base, { minX: 2, minZ: 2, maxX: 5, maxZ: 5 }, 2.5)
|
|
305
|
-
const field = applyHeightPatch(base, patch as never)
|
|
306
|
-
const site = SiteNode.parse({ id: 'site_test', terrain: encodeTerrainField(field) })
|
|
307
|
-
setSceneNodes([site])
|
|
308
|
-
mountRegistryGroup(site)
|
|
309
|
-
|
|
310
|
-
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
311
|
-
expect(world).not.toBeNull()
|
|
312
|
-
if (!world) return
|
|
313
|
-
|
|
314
|
-
// The hill is in the collider: a flat slab would top out at 0.
|
|
315
|
-
expect(world.bounds?.max.y).toBeCloseTo(2.5)
|
|
316
|
-
// And it still reaches past the site so stepping out does not drop the player.
|
|
317
|
-
expect(world.bounds?.min.x).toBeCloseTo(-1008)
|
|
318
|
-
expect(world.bounds?.max.x).toBeCloseTo(1008)
|
|
319
|
-
|
|
320
|
-
// What the player actually stands on, on the plateau and off it.
|
|
321
|
-
const raycaster = new Raycaster()
|
|
322
|
-
const standOn = (x: number, z: number) => {
|
|
323
|
-
raycaster.set(new Vector3(x, 500, z), new Vector3(0, -1, 0))
|
|
324
|
-
return raycaster.intersectObject(world.mesh, false)[0]?.point.y ?? null
|
|
325
|
-
}
|
|
326
|
-
expect(standOn(3, 3)).toBeCloseTo(2.5, 4)
|
|
327
|
-
expect(standOn(-3, -3)).toBeCloseTo(0, 4)
|
|
328
|
-
|
|
329
|
-
world.dispose()
|
|
330
|
-
})
|
|
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
|
-
})
|