@pascal-app/viewer 1.0.7 → 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/README.md +209 -19
- package/dist/components/viewer/drone-controller.d.ts.map +1 -1
- package/dist/components/viewer/drone-controller.js +1 -5
- package/dist/components/viewer/first-person/build-collider-world.d.ts +14 -0
- package/dist/components/viewer/first-person/build-collider-world.d.ts.map +1 -0
- package/dist/components/viewer/first-person/build-collider-world.js +426 -0
- package/dist/components/viewer/first-person/ground-holes.d.ts +22 -0
- package/dist/components/viewer/first-person/ground-holes.d.ts.map +1 -0
- package/dist/components/viewer/first-person/ground-holes.js +116 -0
- package/dist/components/viewer/first-person/mechanism-targets.d.ts +27 -0
- package/dist/components/viewer/first-person/mechanism-targets.d.ts.map +1 -0
- package/dist/components/viewer/first-person/mechanism-targets.js +79 -0
- package/dist/components/viewer/first-person/terrain-collider.d.ts +32 -0
- package/dist/components/viewer/first-person/terrain-collider.d.ts.map +1 -0
- package/dist/components/viewer/first-person/terrain-collider.js +175 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
- package/dist/components/viewer/glb-replace-instances.js +5 -4
- package/dist/components/viewer/glb-scene.d.ts +3 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +2 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +3 -4
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +8 -4
- package/dist/components/viewer/live-walkthrough-controller.d.ts +17 -0
- package/dist/components/viewer/live-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/live-walkthrough-controller.js +1449 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +22 -1
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +2 -2
- package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
- package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -1
- package/dist/components/viewer/use-daylight-rooms.js +75 -33
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/lib/asset-url.d.ts +0 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +3 -3
- package/dist/lib/daylight/room-context.d.ts.map +1 -1
- package/dist/lib/daylight/room-context.js +3 -2
- package/dist/lib/daylight/room-map.d.ts +149 -0
- package/dist/lib/daylight/room-map.d.ts.map +1 -0
- package/dist/lib/daylight/room-map.js +813 -0
- package/dist/lib/daylight/room-scene.d.ts +4 -2
- package/dist/lib/daylight/room-scene.d.ts.map +1 -1
- package/dist/lib/daylight/room-scene.js +81 -40
- package/dist/lib/daylight/rooms.d.ts +71 -32
- package/dist/lib/daylight/rooms.d.ts.map +1 -1
- package/dist/lib/daylight/rooms.js +371 -286
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +2 -1
- package/dist/lib/measurement-label.d.ts +4 -0
- package/dist/lib/measurement-label.d.ts.map +1 -0
- package/dist/lib/measurement-label.js +54 -0
- package/dist/lib/measurement-presentation.d.ts +7 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -0
- package/dist/lib/measurement-presentation.js +14 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +3 -3
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +4 -3
- package/dist/lib/typing-target.d.ts +2 -0
- package/dist/lib/typing-target.d.ts.map +1 -0
- package/dist/lib/typing-target.js +7 -0
- package/dist/lib/viewer-config.d.ts +27 -0
- package/dist/lib/viewer-config.d.ts.map +1 -0
- package/dist/lib/viewer-config.js +23 -0
- package/dist/lib/viewer-preference-storage.d.ts +7 -0
- package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
- package/dist/lib/viewer-preference-storage.js +23 -0
- package/dist/sfx/audio-settings.d.ts +81 -0
- package/dist/sfx/audio-settings.d.ts.map +1 -0
- package/dist/sfx/audio-settings.js +166 -0
- package/dist/sfx/index.d.ts +6 -0
- package/dist/sfx/index.d.ts.map +1 -0
- package/dist/sfx/index.js +5 -0
- package/dist/sfx/interaction-sounds.d.ts +7 -0
- package/dist/sfx/interaction-sounds.d.ts.map +1 -0
- package/dist/sfx/interaction-sounds.js +39 -0
- package/dist/sfx/player.d.ts +31 -0
- package/dist/sfx/player.d.ts.map +1 -0
- package/dist/sfx/player.js +255 -0
- package/dist/sfx/sounds.d.ts +60 -0
- package/dist/sfx/sounds.d.ts.map +1 -0
- package/dist/sfx/sounds.js +25 -0
- package/dist/sfx/use-viewer-ui-sounds.d.ts +8 -0
- package/dist/sfx/use-viewer-ui-sounds.d.ts.map +1 -0
- package/dist/sfx/use-viewer-ui-sounds.js +42 -0
- package/dist/store/use-first-person-hud.d.ts +13 -0
- package/dist/store/use-first-person-hud.d.ts.map +1 -0
- package/dist/store/use-first-person-hud.js +21 -0
- package/dist/store/use-viewer.d.ts +2 -56
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +3 -1
- package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +41 -8
- package/dist/systems/geometry/geometry-status.d.ts +8 -0
- package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-status.js +24 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +134 -78
- package/dist/systems/wall/wall-system.d.ts +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +56 -10
- package/dist/ui/components/action-button.d.ts +14 -0
- package/dist/ui/components/action-button.d.ts.map +1 -0
- package/dist/ui/components/action-button.js +21 -0
- package/dist/ui/components/action-menu-placement.d.ts +7 -0
- package/dist/ui/components/action-menu-placement.d.ts.map +1 -0
- package/dist/ui/components/action-menu-placement.js +10 -0
- package/dist/ui/components/floorplan-compass-button.d.ts +10 -0
- package/dist/ui/components/floorplan-compass-button.d.ts.map +1 -0
- package/dist/ui/components/floorplan-compass-button.js +22 -0
- package/dist/ui/components/follow-pascal-toggle.d.ts +6 -0
- package/dist/ui/components/follow-pascal-toggle.d.ts.map +1 -0
- package/dist/ui/components/follow-pascal-toggle.js +21 -0
- package/dist/ui/components/localized-html.d.ts +9 -0
- package/dist/ui/components/localized-html.d.ts.map +1 -0
- package/dist/ui/components/localized-html.js +16 -0
- package/dist/ui/components/render-settings-menu.d.ts +9 -0
- package/dist/ui/components/render-settings-menu.d.ts.map +1 -0
- package/dist/ui/components/render-settings-menu.js +145 -0
- package/dist/ui/components/rooms/room-highlight.d.ts +10 -0
- package/dist/ui/components/rooms/room-highlight.d.ts.map +1 -0
- package/dist/ui/components/rooms/room-highlight.js +56 -0
- package/dist/ui/components/rooms/room-labels.d.ts +278 -0
- package/dist/ui/components/rooms/room-labels.d.ts.map +1 -0
- package/dist/ui/components/rooms/room-labels.js +1295 -0
- package/dist/ui/components/rooms/viewer-rooms.d.ts +14 -0
- package/dist/ui/components/rooms/viewer-rooms.d.ts.map +1 -0
- package/dist/ui/components/rooms/viewer-rooms.js +123 -0
- package/dist/ui/components/scene-loader.d.ts +18 -0
- package/dist/ui/components/scene-loader.d.ts.map +1 -0
- package/dist/ui/components/scene-loader.js +29 -0
- package/dist/ui/components/sky-control.d.ts +37 -0
- package/dist/ui/components/sky-control.d.ts.map +1 -0
- package/dist/ui/components/sky-control.js +382 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts +4 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts.map +1 -0
- package/dist/ui/components/use-viewer-camera-navigation-sync.js +117 -0
- package/dist/ui/components/viewer-controls-bar.d.ts +20 -0
- package/dist/ui/components/viewer-controls-bar.d.ts.map +1 -0
- package/dist/ui/components/viewer-controls-bar.js +130 -0
- package/dist/ui/components/viewer-hover-label.d.ts +14 -0
- package/dist/ui/components/viewer-hover-label.d.ts.map +1 -0
- package/dist/ui/components/viewer-hover-label.js +28 -0
- package/dist/ui/components/viewer-scene-header.d.ts +34 -0
- package/dist/ui/components/viewer-scene-header.d.ts.map +1 -0
- package/dist/ui/components/viewer-scene-header.js +92 -0
- package/dist/ui/components/viewer-selection-manager.d.ts +7 -0
- package/dist/ui/components/viewer-selection-manager.d.ts.map +1 -0
- package/dist/ui/components/viewer-selection-manager.js +116 -0
- package/dist/ui/components/viewer-stage-modes.d.ts +7 -0
- package/dist/ui/components/viewer-stage-modes.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage-modes.js +42 -0
- package/dist/ui/components/viewer-stage-switcher.d.ts +10 -0
- package/dist/ui/components/viewer-stage-switcher.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage-switcher.js +16 -0
- package/dist/ui/components/viewer-stage.d.ts +25 -0
- package/dist/ui/components/viewer-stage.d.ts.map +1 -0
- package/dist/ui/components/viewer-stage.js +107 -0
- package/dist/ui/components/walkthrough-hud.d.ts +29 -0
- package/dist/ui/components/walkthrough-hud.d.ts.map +1 -0
- package/dist/ui/components/walkthrough-hud.js +55 -0
- package/dist/ui/floorplan/floorplan-camera-sync.d.ts +22 -0
- package/dist/ui/floorplan/floorplan-camera-sync.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-camera-sync.js +157 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts +61 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-dimension-renderer.js +280 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts +54 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-geometry-renderer.js +317 -0
- package/dist/ui/floorplan/floorplan-label-angle.d.ts +24 -0
- package/dist/ui/floorplan/floorplan-label-angle.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-label-angle.js +60 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.d.ts +18 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-navigation-sync.js +41 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.d.ts +26 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-geometry.js +173 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.d.ts +22 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-navigation.js +41 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.d.ts +8 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview-visibility.js +21 -0
- package/dist/ui/floorplan/floorplan-preview.d.ts +19 -0
- package/dist/ui/floorplan/floorplan-preview.d.ts.map +1 -0
- package/dist/ui/floorplan/floorplan-preview.js +700 -0
- package/dist/ui/hooks/use-mobile.d.ts +2 -0
- package/dist/ui/hooks/use-mobile.d.ts.map +1 -0
- package/dist/ui/hooks/use-mobile.js +14 -0
- package/dist/ui/index.d.ts +49 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +48 -0
- package/dist/ui/lib/below-grade.d.ts +38 -0
- package/dist/ui/lib/below-grade.d.ts.map +1 -0
- package/dist/ui/lib/below-grade.js +102 -0
- package/dist/ui/lib/camera-pose.d.ts +19 -0
- package/dist/ui/lib/camera-pose.d.ts.map +1 -0
- package/dist/ui/lib/camera-pose.js +113 -0
- package/dist/ui/lib/cn.d.ts +3 -0
- package/dist/ui/lib/cn.d.ts.map +1 -0
- package/dist/ui/lib/cn.js +5 -0
- package/dist/ui/lib/level-name.d.ts +10 -0
- package/dist/ui/lib/level-name.d.ts.map +1 -0
- package/dist/ui/lib/level-name.js +33 -0
- package/dist/ui/lib/node-display-label.d.ts +8 -0
- package/dist/ui/lib/node-display-label.d.ts.map +1 -0
- package/dist/ui/lib/node-display-label.js +36 -0
- package/dist/ui/lib/room-area.d.ts +3 -0
- package/dist/ui/lib/room-area.d.ts.map +1 -0
- package/dist/ui/lib/room-area.js +8 -0
- package/dist/ui/lib/room-assembly-overlay.d.ts +46 -0
- package/dist/ui/lib/room-assembly-overlay.d.ts.map +1 -0
- package/dist/ui/lib/room-assembly-overlay.js +222 -0
- package/dist/ui/lib/room-name-messages.d.ts +28 -0
- package/dist/ui/lib/room-name-messages.d.ts.map +1 -0
- package/dist/ui/lib/room-name-messages.js +112 -0
- package/dist/ui/lib/room-selection.d.ts +58 -0
- package/dist/ui/lib/room-selection.d.ts.map +1 -0
- package/dist/ui/lib/room-selection.js +297 -0
- package/dist/ui/lib/scene-theme-name.d.ts +4 -0
- package/dist/ui/lib/scene-theme-name.d.ts.map +1 -0
- package/dist/ui/lib/scene-theme-name.js +22 -0
- package/dist/ui/lib/selection-modifiers.d.ts +20 -0
- package/dist/ui/lib/selection-modifiers.d.ts.map +1 -0
- package/dist/ui/lib/selection-modifiers.js +15 -0
- package/dist/ui/lib/viewer-selection.d.ts +132 -0
- package/dist/ui/lib/viewer-selection.d.ts.map +1 -0
- package/dist/ui/lib/viewer-selection.js +462 -0
- package/dist/ui/lib/zone-content.d.ts +6 -0
- package/dist/ui/lib/zone-content.d.ts.map +1 -0
- package/dist/ui/lib/zone-content.js +96 -0
- package/dist/ui/primitives/button.d.ts +11 -0
- package/dist/ui/primitives/button.d.ts.map +1 -0
- package/dist/ui/primitives/button.js +35 -0
- package/dist/ui/primitives/dropdown-menu.d.ts +26 -0
- package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
- package/dist/ui/primitives/dropdown-menu.js +53 -0
- package/dist/ui/primitives/popover.d.ts +8 -0
- package/dist/ui/primitives/popover.d.ts.map +1 -0
- package/dist/ui/primitives/popover.js +17 -0
- package/dist/ui/primitives/switch.d.ts +5 -0
- package/dist/ui/primitives/switch.d.ts.map +1 -0
- package/dist/ui/primitives/switch.js +8 -0
- package/dist/ui/primitives/tooltip.d.ts +8 -0
- package/dist/ui/primitives/tooltip.d.ts.map +1 -0
- package/dist/ui/primitives/tooltip.js +17 -0
- package/dist/ui/store/camera-pose-store.d.ts +17 -0
- package/dist/ui/store/camera-pose-store.d.ts.map +1 -0
- package/dist/ui/store/camera-pose-store.js +15 -0
- package/dist/ui/store/navigation-sync-pose-store.d.ts +26 -0
- package/dist/ui/store/navigation-sync-pose-store.d.ts.map +1 -0
- package/dist/ui/store/navigation-sync-pose-store.js +16 -0
- package/dist/ui/store/use-follow-pascal.d.ts +30 -0
- package/dist/ui/store/use-follow-pascal.d.ts.map +1 -0
- package/dist/ui/store/use-follow-pascal.js +28 -0
- package/dist/ui/theme.css +339 -0
- package/package.json +34 -7
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { computeSceneBoundsXZ, coveredBelowGradeFootprints, getGarageVisibleOpeningRatio, getLevelElevations, isOperationDoorType, nodeMechanism, nodeRegistry, sceneRegistry, terrainFieldOf, useInteractive, useScene, } from '@pascal-app/core';
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
4
|
+
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh';
|
|
5
|
+
import { cutGroundCollider } from './ground-holes';
|
|
6
|
+
import { createTerrainColliderGeometry } from './terrain-collider';
|
|
7
|
+
const SKIPPED_MESH_NAMES = new Set(['cutout', 'collision-mesh']);
|
|
8
|
+
const COLLIDER_NODE_CATEGORIES = new Set(['structure', 'furnish']);
|
|
9
|
+
const DEDICATED_COLLIDER_NODE_TYPES = new Set(['elevator']);
|
|
10
|
+
const COLLIDER_MATERIAL = new THREE.MeshBasicMaterial();
|
|
11
|
+
const DOWN = new THREE.Vector3(0, -1, 0);
|
|
12
|
+
const UP = new THREE.Vector3(0, 1, 0);
|
|
13
|
+
const SPAWN_EYE_HEIGHT = 1.65;
|
|
14
|
+
const RAYCAST_CLEARANCE = 25;
|
|
15
|
+
const DOOR_LEAF_COLLIDER_DEPTH = 0.06;
|
|
16
|
+
const OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD = 0.85;
|
|
17
|
+
const LEVEL_FALLBACK_FLOOR_THICKNESS = 0.08;
|
|
18
|
+
const LEVEL_FALLBACK_FLOOR_PADDING = 2;
|
|
19
|
+
const LEVEL_FALLBACK_FLOOR_MIN_SIZE = 30;
|
|
20
|
+
const SITE_GROUND_COLLIDER_MIN_SIZE = 2000;
|
|
21
|
+
export const FIRST_PERSON_SPAWN_EYE_HEIGHT = SPAWN_EYE_HEIGHT;
|
|
22
|
+
function isMesh(object) {
|
|
23
|
+
return 'isMesh' in object && object.isMesh;
|
|
24
|
+
}
|
|
25
|
+
// Renderer-effective visibility: an invisible ancestor hides the whole
|
|
26
|
+
// subtree at render time even when the object's own flag is true. The
|
|
27
|
+
// collider world must match what's rendered — the roof keeps stale,
|
|
28
|
+
// UNCUT per-segment CSG inside its hidden `segments-wrapper` (full-edit
|
|
29
|
+
// exit hides the wrapper without stripping geometry), and cloning those
|
|
30
|
+
// meshes would block the walkthrough player at openings the visible
|
|
31
|
+
// merged shell has cut through.
|
|
32
|
+
function isEffectivelyVisible(object) {
|
|
33
|
+
let current = object;
|
|
34
|
+
while (current) {
|
|
35
|
+
if (!current.visible)
|
|
36
|
+
return false;
|
|
37
|
+
current = current.parent;
|
|
38
|
+
}
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
function isColliderMaterialVisible(material) {
|
|
42
|
+
return Array.isArray(material) ? material.some((entry) => entry.visible) : material.visible;
|
|
43
|
+
}
|
|
44
|
+
function isGenericColliderNode(node) {
|
|
45
|
+
if (node.visible === false)
|
|
46
|
+
return false;
|
|
47
|
+
if (DEDICATED_COLLIDER_NODE_TYPES.has(node.type))
|
|
48
|
+
return false;
|
|
49
|
+
const def = nodeRegistry.get(node.type);
|
|
50
|
+
// Ceilings are a transparent mount surface for fixtures (lights, fans), not a
|
|
51
|
+
// walkable or blocking structure — the walkthrough player must pass through
|
|
52
|
+
// them rather than be held up as if standing on a floor slab.
|
|
53
|
+
if (def?.surfaceRole === 'ceiling')
|
|
54
|
+
return false;
|
|
55
|
+
return COLLIDER_NODE_CATEGORIES.has(def?.category ?? '');
|
|
56
|
+
}
|
|
57
|
+
function createBoxColliderGeometry(width, height, depth) {
|
|
58
|
+
const sourceGeometry = new THREE.BoxGeometry(width, height, depth).toNonIndexed();
|
|
59
|
+
const geometry = new THREE.BufferGeometry();
|
|
60
|
+
geometry.setAttribute('position', sourceGeometry.getAttribute('position').clone());
|
|
61
|
+
geometry.setAttribute('normal', sourceGeometry.getAttribute('normal').clone());
|
|
62
|
+
sourceGeometry.dispose();
|
|
63
|
+
return geometry;
|
|
64
|
+
}
|
|
65
|
+
function getVisibleLevelChildren(level, nodes) {
|
|
66
|
+
return level.children
|
|
67
|
+
.map((childId) => nodes[childId])
|
|
68
|
+
.filter((child) => Boolean(child && child.visible !== false));
|
|
69
|
+
}
|
|
70
|
+
function createLevelFallbackFloorGeometry(level, nodes) {
|
|
71
|
+
if (level.visible === false)
|
|
72
|
+
return null;
|
|
73
|
+
const children = getVisibleLevelChildren(level, nodes);
|
|
74
|
+
if (children.some((child) => child.type === 'slab'))
|
|
75
|
+
return null;
|
|
76
|
+
const levelObject = sceneRegistry.nodes.get(level.id);
|
|
77
|
+
if (!levelObject?.visible)
|
|
78
|
+
return null;
|
|
79
|
+
const bounds = computeSceneBoundsXZ(children);
|
|
80
|
+
const [centerX, centerZ] = bounds?.center ?? [0, 0];
|
|
81
|
+
const [boundsWidth, boundsDepth] = bounds?.size ?? [0, 0];
|
|
82
|
+
const width = Math.max(boundsWidth + LEVEL_FALLBACK_FLOOR_PADDING * 2, LEVEL_FALLBACK_FLOOR_MIN_SIZE);
|
|
83
|
+
const depth = Math.max(boundsDepth + LEVEL_FALLBACK_FLOOR_PADDING * 2, LEVEL_FALLBACK_FLOOR_MIN_SIZE);
|
|
84
|
+
const geometry = createBoxColliderGeometry(width, LEVEL_FALLBACK_FLOOR_THICKNESS, depth);
|
|
85
|
+
levelObject.updateWorldMatrix(true, false);
|
|
86
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(centerX, -LEVEL_FALLBACK_FLOOR_THICKNESS / 2, centerZ));
|
|
87
|
+
geometry.applyMatrix4(levelObject.matrixWorld);
|
|
88
|
+
return geometry;
|
|
89
|
+
}
|
|
90
|
+
function collectLevelFallbackFloorGeometries(nodes) {
|
|
91
|
+
const geometries = [];
|
|
92
|
+
const levelElevations = getLevelElevations(nodes);
|
|
93
|
+
for (const levelId of sceneRegistry.byType.level) {
|
|
94
|
+
const node = nodes[levelId];
|
|
95
|
+
if (node?.type !== 'level')
|
|
96
|
+
continue;
|
|
97
|
+
if (levelElevations.get(node.id)?.baseY !== 0)
|
|
98
|
+
continue;
|
|
99
|
+
const geometry = createLevelFallbackFloorGeometry(node, nodes);
|
|
100
|
+
if (geometry)
|
|
101
|
+
geometries.push(geometry);
|
|
102
|
+
}
|
|
103
|
+
return geometries;
|
|
104
|
+
}
|
|
105
|
+
// The visible ground is the site node's ground mesh, but `site` is a `site`
|
|
106
|
+
// category node and therefore excluded from the generic collider sweep. Without
|
|
107
|
+
// a dedicated collider, a spawn on the bare ground (no slab, or not parented to
|
|
108
|
+
// a level that triggers the per-level fallback) has no floor to stand on and the
|
|
109
|
+
// walkthrough player falls through. Derive a thin ground slab from node data (not
|
|
110
|
+
// the rendered mesh) so it exists regardless of geometry-mount timing. The slab
|
|
111
|
+
// is effectively unbounded (not sized to the site polygon): the ground plane must
|
|
112
|
+
// keep holding the player up even after they step past the site boundary,
|
|
113
|
+
// otherwise they fall below the ground plane into the void.
|
|
114
|
+
//
|
|
115
|
+
// A sculpted site takes the terrain path instead: the flat slab would hold the
|
|
116
|
+
// player at the datum inside an excavation and bury them inside a hill. Derived
|
|
117
|
+
// from the field rather than the rendered terrain mesh for the same
|
|
118
|
+
// mount-timing reason as the flat slab.
|
|
119
|
+
//
|
|
120
|
+
// Either way the ground is cut open over what the house covers of its basement
|
|
121
|
+
// (`coveredBelowGradeFootprints`, the rings the ground is drawn open over): a sheet
|
|
122
|
+
// across the stairwell would hold the player at grade on the way down, with the
|
|
123
|
+
// stair and the basement's floor below it. The house covers that part, so it is cut
|
|
124
|
+
// whatever the view does with the ground; past it the ground is walked on as before.
|
|
125
|
+
function createSiteGroundColliderGeometry(site, nodes) {
|
|
126
|
+
if (site.visible === false)
|
|
127
|
+
return null;
|
|
128
|
+
const siteObject = sceneRegistry.nodes.get(site.id);
|
|
129
|
+
if (!siteObject?.visible)
|
|
130
|
+
return null;
|
|
131
|
+
const covered = coveredBelowGradeFootprints(nodes);
|
|
132
|
+
const field = terrainFieldOf(site);
|
|
133
|
+
if (field) {
|
|
134
|
+
const terrainGeometry = createTerrainColliderGeometry(field);
|
|
135
|
+
if (terrainGeometry) {
|
|
136
|
+
const cut = cutGroundCollider(terrainGeometry, covered);
|
|
137
|
+
siteObject.updateWorldMatrix(true, false);
|
|
138
|
+
cut.applyMatrix4(siteObject.matrixWorld);
|
|
139
|
+
return cut;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const bounds = computeSceneBoundsXZ(nodes);
|
|
143
|
+
const [centerX, centerZ] = bounds?.center ?? [0, 0];
|
|
144
|
+
const [boundsWidth, boundsDepth] = bounds?.size ?? [0, 0];
|
|
145
|
+
const width = Math.max(boundsWidth + LEVEL_FALLBACK_FLOOR_PADDING * 2, SITE_GROUND_COLLIDER_MIN_SIZE);
|
|
146
|
+
const depth = Math.max(boundsDepth + LEVEL_FALLBACK_FLOOR_PADDING * 2, SITE_GROUND_COLLIDER_MIN_SIZE);
|
|
147
|
+
const geometry = createBoxColliderGeometry(width, LEVEL_FALLBACK_FLOOR_THICKNESS, depth);
|
|
148
|
+
siteObject.updateWorldMatrix(true, false);
|
|
149
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(centerX, -LEVEL_FALLBACK_FLOOR_THICKNESS / 2, centerZ));
|
|
150
|
+
const cut = cutGroundCollider(geometry, covered);
|
|
151
|
+
cut.applyMatrix4(siteObject.matrixWorld);
|
|
152
|
+
return cut;
|
|
153
|
+
}
|
|
154
|
+
function collectSiteGroundColliderGeometries(nodes) {
|
|
155
|
+
const geometries = [];
|
|
156
|
+
for (const siteId of sceneRegistry.byType.site ?? []) {
|
|
157
|
+
const node = nodes[siteId];
|
|
158
|
+
if (node?.type !== 'site')
|
|
159
|
+
continue;
|
|
160
|
+
const geometry = createSiteGroundColliderGeometry(node, nodes);
|
|
161
|
+
if (geometry)
|
|
162
|
+
geometries.push(geometry);
|
|
163
|
+
}
|
|
164
|
+
return geometries;
|
|
165
|
+
}
|
|
166
|
+
// Decode any attribute (interleaved, quantized/normalized integer, Float64…) into a
|
|
167
|
+
// plain, non-normalized Float32Array BufferAttribute. mergeGeometries() requires every
|
|
168
|
+
// merged geometry to share the same typed-array constructor for matching attributes, so
|
|
169
|
+
// imported item GLBs using KHR_mesh_quantization or interleaved buffers must be coerced
|
|
170
|
+
// to Float32 to match wall/slab geometry.
|
|
171
|
+
function toFloat32Attribute(source) {
|
|
172
|
+
const itemSize = source.itemSize;
|
|
173
|
+
const array = new Float32Array(source.count * itemSize);
|
|
174
|
+
for (let i = 0; i < source.count; i++) {
|
|
175
|
+
const offset = i * itemSize;
|
|
176
|
+
array[offset] = source.getX(i);
|
|
177
|
+
if (itemSize > 1)
|
|
178
|
+
array[offset + 1] = source.getY(i);
|
|
179
|
+
if (itemSize > 2)
|
|
180
|
+
array[offset + 2] = source.getZ(i);
|
|
181
|
+
if (itemSize > 3)
|
|
182
|
+
array[offset + 3] = source.getW(i);
|
|
183
|
+
}
|
|
184
|
+
return new THREE.BufferAttribute(array, itemSize);
|
|
185
|
+
}
|
|
186
|
+
function cloneWorldGeometry(mesh) {
|
|
187
|
+
const sourceGeometry = mesh.geometry;
|
|
188
|
+
const position = sourceGeometry.getAttribute('position');
|
|
189
|
+
if (!position || position.count < 3)
|
|
190
|
+
return null;
|
|
191
|
+
const workingGeometry = sourceGeometry.index
|
|
192
|
+
? sourceGeometry.toNonIndexed()
|
|
193
|
+
: sourceGeometry.clone();
|
|
194
|
+
const cleanGeometry = new THREE.BufferGeometry();
|
|
195
|
+
cleanGeometry.setAttribute('position', toFloat32Attribute(workingGeometry.getAttribute('position')));
|
|
196
|
+
const normal = workingGeometry.getAttribute('normal');
|
|
197
|
+
if (normal) {
|
|
198
|
+
cleanGeometry.setAttribute('normal', toFloat32Attribute(normal));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
cleanGeometry.computeVertexNormals();
|
|
202
|
+
}
|
|
203
|
+
cleanGeometry.applyMatrix4(mesh.matrixWorld);
|
|
204
|
+
workingGeometry.dispose();
|
|
205
|
+
const worldPosition = cleanGeometry.getAttribute('position');
|
|
206
|
+
if (!worldPosition || worldPosition.count < 3) {
|
|
207
|
+
cleanGeometry.dispose();
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return cleanGeometry;
|
|
211
|
+
}
|
|
212
|
+
function shouldSkipColliderNode(node) {
|
|
213
|
+
if (node.type === 'window') {
|
|
214
|
+
return node.openingKind === 'opening';
|
|
215
|
+
}
|
|
216
|
+
if (node.type !== 'door')
|
|
217
|
+
return false;
|
|
218
|
+
if (!node.segments.length)
|
|
219
|
+
return true;
|
|
220
|
+
if (node.openingKind === 'opening')
|
|
221
|
+
return true;
|
|
222
|
+
return node.segments.every((segment) => segment.type === 'empty');
|
|
223
|
+
}
|
|
224
|
+
function createDoorLeafColliderGeometry(root, node) {
|
|
225
|
+
const hasLeafContent = node.segments.some((segment) => segment.type !== 'empty');
|
|
226
|
+
if (!hasLeafContent)
|
|
227
|
+
return null;
|
|
228
|
+
// A door built from a script opens by its model's `open` clip, which the mechanism runs through
|
|
229
|
+
// its own control: the native `operationState` below never moves for it, so open is read there.
|
|
230
|
+
if (node.source && nodeMechanism(node)?.isOn(node, useInteractive.getState()))
|
|
231
|
+
return null;
|
|
232
|
+
const leafW = node.width - 2 * node.frameThickness;
|
|
233
|
+
const leafH = node.height - node.frameThickness;
|
|
234
|
+
if (leafW <= 0 || leafH <= 0)
|
|
235
|
+
return null;
|
|
236
|
+
const leafCenterY = -node.frameThickness / 2;
|
|
237
|
+
const runtimeDoorState = useInteractive.getState().doors[node.id];
|
|
238
|
+
const operationState = runtimeDoorState?.operationState ?? node.operationState;
|
|
239
|
+
const swingAngle = runtimeDoorState?.swingAngle ?? node.swingAngle;
|
|
240
|
+
root.updateWorldMatrix(true, false);
|
|
241
|
+
if (node.doorType === 'garage-sectional' || node.doorType === 'garage-rollup') {
|
|
242
|
+
const openAmount = getGarageVisibleOpeningRatio(node.doorType, operationState);
|
|
243
|
+
const visibleHeight = leafH * (1 - openAmount);
|
|
244
|
+
if (visibleHeight <= 0.12)
|
|
245
|
+
return null;
|
|
246
|
+
const geometry = createBoxColliderGeometry(leafW, visibleHeight, DOOR_LEAF_COLLIDER_DEPTH);
|
|
247
|
+
const visibleCenterY = leafCenterY - leafH / 2 + visibleHeight / 2;
|
|
248
|
+
geometry.applyMatrix4(root.matrixWorld.clone().multiply(new THREE.Matrix4().makeTranslation(0, visibleCenterY, 0)));
|
|
249
|
+
return geometry;
|
|
250
|
+
}
|
|
251
|
+
if (isOperationDoorType(node.doorType) &&
|
|
252
|
+
(operationState ?? 0) >= OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const hingeX = node.hingesSide === 'right' ? leafW / 2 : -leafW / 2;
|
|
256
|
+
const swingDirectionSign = node.swingDirection === 'inward' ? 1 : -1;
|
|
257
|
+
const hingeDirectionSign = node.hingesSide === 'right' ? 1 : -1;
|
|
258
|
+
const clampedSwingAngle = Math.max(0, Math.min(Math.PI / 2, swingAngle ?? 0));
|
|
259
|
+
const leafSwingRotation = clampedSwingAngle * swingDirectionSign * hingeDirectionSign;
|
|
260
|
+
const geometry = createBoxColliderGeometry(leafW, leafH, DOOR_LEAF_COLLIDER_DEPTH);
|
|
261
|
+
const matrix = root.matrixWorld
|
|
262
|
+
.clone()
|
|
263
|
+
.multiply(new THREE.Matrix4().makeTranslation(hingeX, 0, 0))
|
|
264
|
+
.multiply(new THREE.Matrix4().makeRotationY(leafSwingRotation))
|
|
265
|
+
.multiply(new THREE.Matrix4().makeTranslation(-hingeX, leafCenterY, 0));
|
|
266
|
+
geometry.applyMatrix4(matrix);
|
|
267
|
+
return geometry;
|
|
268
|
+
}
|
|
269
|
+
function buildRegisteredColliderNodeIds(nodes) {
|
|
270
|
+
const nodeIds = new Set();
|
|
271
|
+
for (const nodeId of sceneRegistry.nodes.keys()) {
|
|
272
|
+
const node = nodes[nodeId];
|
|
273
|
+
if (!node || !isGenericColliderNode(node))
|
|
274
|
+
continue;
|
|
275
|
+
if (shouldSkipColliderNode(node))
|
|
276
|
+
continue;
|
|
277
|
+
nodeIds.add(nodeId);
|
|
278
|
+
}
|
|
279
|
+
return nodeIds;
|
|
280
|
+
}
|
|
281
|
+
function collectColliderGeometriesFromNode(root, rootNodeId, visitedMeshes, registeredObjectIds, registeredColliderNodeIds) {
|
|
282
|
+
const geometries = [];
|
|
283
|
+
const visit = (object) => {
|
|
284
|
+
if (visitedMeshes.has(object))
|
|
285
|
+
return;
|
|
286
|
+
visitedMeshes.add(object);
|
|
287
|
+
// Prune hidden subtrees — children of an invisible group never render,
|
|
288
|
+
// so they must not collide either (see isEffectivelyVisible). An idle joint
|
|
289
|
+
// tree draws a merged rest pose that includes its moving parts; its hidden
|
|
290
|
+
// split group holds the same static parts apart from them.
|
|
291
|
+
if (object.userData.pascalProceduralRest)
|
|
292
|
+
return;
|
|
293
|
+
if (!object.visible && !object.userData.pascalProceduralSplit)
|
|
294
|
+
return;
|
|
295
|
+
if (object.userData.proceduralMotion)
|
|
296
|
+
return;
|
|
297
|
+
if (isMesh(object) &&
|
|
298
|
+
isColliderMaterialVisible(object.material) &&
|
|
299
|
+
!SKIPPED_MESH_NAMES.has(object.name)) {
|
|
300
|
+
const geometry = cloneWorldGeometry(object);
|
|
301
|
+
if (geometry) {
|
|
302
|
+
geometries.push(geometry);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const child of object.children) {
|
|
306
|
+
const childNodeId = registeredObjectIds.get(child);
|
|
307
|
+
if (childNodeId && childNodeId !== rootNodeId && registeredColliderNodeIds.has(childNodeId)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
visit(child);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
visit(root);
|
|
314
|
+
return geometries;
|
|
315
|
+
}
|
|
316
|
+
export function buildFirstPersonColliderWorldFromRegistry() {
|
|
317
|
+
const nodes = useScene.getState().nodes;
|
|
318
|
+
const geometries = [];
|
|
319
|
+
const visitedMeshes = new WeakSet();
|
|
320
|
+
const registeredColliderNodeIds = buildRegisteredColliderNodeIds(nodes);
|
|
321
|
+
const registeredObjectIds = new Map();
|
|
322
|
+
for (const [nodeId, object] of sceneRegistry.nodes) {
|
|
323
|
+
registeredObjectIds.set(object, nodeId);
|
|
324
|
+
}
|
|
325
|
+
for (const nodeId of registeredColliderNodeIds) {
|
|
326
|
+
const node = nodes[nodeId];
|
|
327
|
+
if (!node)
|
|
328
|
+
continue;
|
|
329
|
+
const root = sceneRegistry.nodes.get(nodeId);
|
|
330
|
+
if (!root)
|
|
331
|
+
continue;
|
|
332
|
+
// Registered objects can sit inside a hidden wrapper (roof segments
|
|
333
|
+
// under `segments-wrapper`) — the per-node traversal starts AT the
|
|
334
|
+
// object, so the ancestor chain must be checked here.
|
|
335
|
+
if (!isEffectivelyVisible(root))
|
|
336
|
+
continue;
|
|
337
|
+
if (node.type === 'door') {
|
|
338
|
+
const doorGeometry = createDoorLeafColliderGeometry(root, node);
|
|
339
|
+
if (doorGeometry) {
|
|
340
|
+
geometries.push(doorGeometry);
|
|
341
|
+
}
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
root.updateMatrixWorld(true);
|
|
345
|
+
geometries.push(...collectColliderGeometriesFromNode(root, nodeId, visitedMeshes, registeredObjectIds, registeredColliderNodeIds));
|
|
346
|
+
}
|
|
347
|
+
geometries.push(...collectLevelFallbackFloorGeometries(nodes));
|
|
348
|
+
geometries.push(...collectSiteGroundColliderGeometries(nodes));
|
|
349
|
+
if (geometries.length === 0) {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
const mergedGeometry = mergeGeometries(geometries, false);
|
|
353
|
+
for (const geometry of geometries) {
|
|
354
|
+
geometry.dispose();
|
|
355
|
+
}
|
|
356
|
+
if (!mergedGeometry || mergedGeometry.getAttribute('position') == null) {
|
|
357
|
+
mergedGeometry?.dispose();
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
const bvhGeometry = mergedGeometry;
|
|
361
|
+
bvhGeometry.computeBoundsTree = computeBoundsTree;
|
|
362
|
+
bvhGeometry.disposeBoundsTree = disposeBoundsTree;
|
|
363
|
+
bvhGeometry.computeBoundsTree?.({
|
|
364
|
+
targetLeafSize: 12,
|
|
365
|
+
strategy: 0,
|
|
366
|
+
});
|
|
367
|
+
bvhGeometry.computeBoundingBox();
|
|
368
|
+
const mesh = new THREE.Mesh(bvhGeometry, COLLIDER_MATERIAL);
|
|
369
|
+
mesh.raycast = acceleratedRaycast;
|
|
370
|
+
mesh.visible = true;
|
|
371
|
+
mesh.userData = {
|
|
372
|
+
type: 'STATIC',
|
|
373
|
+
friction: 0.8,
|
|
374
|
+
restitution: 0.05,
|
|
375
|
+
excludeFloatHit: false,
|
|
376
|
+
excludeCollisionCheck: false,
|
|
377
|
+
};
|
|
378
|
+
mesh.updateMatrixWorld(true);
|
|
379
|
+
return {
|
|
380
|
+
mesh,
|
|
381
|
+
bounds: bvhGeometry.boundingBox?.clone() ?? null,
|
|
382
|
+
dispose: () => {
|
|
383
|
+
bvhGeometry.disposeBoundsTree?.();
|
|
384
|
+
bvhGeometry.dispose();
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
export function deriveFirstPersonSpawn(camera, world) {
|
|
389
|
+
const direction = new THREE.Vector3();
|
|
390
|
+
camera.getWorldDirection(direction);
|
|
391
|
+
direction.y = 0;
|
|
392
|
+
if (direction.lengthSq() < 1e-6) {
|
|
393
|
+
direction.set(0, 0, -1);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
direction.normalize();
|
|
397
|
+
}
|
|
398
|
+
const yaw = Math.atan2(-direction.x, -direction.z);
|
|
399
|
+
const raycaster = new THREE.Raycaster();
|
|
400
|
+
const candidates = [[camera.position.x, camera.position.z]];
|
|
401
|
+
const boundsCenter = world.bounds?.getCenter(new THREE.Vector3());
|
|
402
|
+
if (boundsCenter) {
|
|
403
|
+
candidates.push([boundsCenter.x, boundsCenter.z]);
|
|
404
|
+
}
|
|
405
|
+
for (const [x, z] of candidates) {
|
|
406
|
+
const topY = Math.max(world.bounds?.max.y ?? camera.position.y, camera.position.y) + RAYCAST_CLEARANCE;
|
|
407
|
+
raycaster.set(new THREE.Vector3(x, topY, z), DOWN);
|
|
408
|
+
const intersections = raycaster.intersectObject(world.mesh, false);
|
|
409
|
+
const hit = intersections.find((intersection) => {
|
|
410
|
+
if (!intersection.face)
|
|
411
|
+
return true;
|
|
412
|
+
const normal = intersection.face.normal.clone().transformDirection(world.mesh.matrixWorld);
|
|
413
|
+
return normal.dot(UP) > 0.2;
|
|
414
|
+
});
|
|
415
|
+
if (hit) {
|
|
416
|
+
return {
|
|
417
|
+
position: [hit.point.x, hit.point.y + SPAWN_EYE_HEIGHT, hit.point.z],
|
|
418
|
+
yaw,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
position: [camera.position.x, Math.max(camera.position.y, SPAWN_EYE_HEIGHT), camera.position.z],
|
|
424
|
+
yaw,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
import { type Ring } from '@pascal-app/core';
|
|
15
|
+
import { BufferGeometry } from 'three';
|
|
16
|
+
/**
|
|
17
|
+
* `geometry` (non-indexed, `position` and `normal`, in the frame the rings are in) without the part
|
|
18
|
+
* of it that lies over `holes`. A triangle the clip fails on is kept whole: the ground closed is
|
|
19
|
+
* the safe side. The geometry is returned as it is when there are no holes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function cutGroundCollider(geometry: BufferGeometry, holes: readonly Ring[]): BufferGeometry;
|
|
22
|
+
//# sourceMappingURL=ground-holes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ground-holes.d.ts","sourceRoot":"","sources":["../../../../src/components/viewer/first-person/ground-holes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAc,KAAK,IAAI,EAAS,MAAM,kBAAkB,CAAA;AAC/D,OAAO,EAAmB,cAAc,EAAuB,MAAM,OAAO,CAAA;AAY5E;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,cAAc,EACxB,KAAK,EAAE,SAAS,IAAI,EAAE,GACrB,cAAc,CA8FhB"}
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
+
import { difference, union } from '@pascal-app/core';
|
|
15
|
+
import { BufferAttribute, BufferGeometry, ShapeUtils, Vector2 } from 'three';
|
|
16
|
+
/** Triangles flatter than this in plan (m²) are vertical faces: they hold nothing up. */
|
|
17
|
+
const MIN_PLAN_AREA = 1e-9;
|
|
18
|
+
/** Twice the signed plan area of `a b c` (x right, z down). */
|
|
19
|
+
function orient(a, b, c) {
|
|
20
|
+
return (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* `geometry` (non-indexed, `position` and `normal`, in the frame the rings are in) without the part
|
|
24
|
+
* of it that lies over `holes`. A triangle the clip fails on is kept whole: the ground closed is
|
|
25
|
+
* the safe side. The geometry is returned as it is when there are no holes.
|
|
26
|
+
*/
|
|
27
|
+
export function cutGroundCollider(geometry, holes) {
|
|
28
|
+
if (holes.length === 0)
|
|
29
|
+
return geometry;
|
|
30
|
+
const cutouts = union(holes);
|
|
31
|
+
if (cutouts.length === 0)
|
|
32
|
+
return geometry;
|
|
33
|
+
const boxes = cutouts.map(({ outer }) => ({
|
|
34
|
+
minX: Math.min(...outer.map(([x]) => x)),
|
|
35
|
+
maxX: Math.max(...outer.map(([x]) => x)),
|
|
36
|
+
minZ: Math.min(...outer.map(([, z]) => z)),
|
|
37
|
+
maxZ: Math.max(...outer.map(([, z]) => z)),
|
|
38
|
+
}));
|
|
39
|
+
const position = geometry.getAttribute('position');
|
|
40
|
+
const normal = geometry.getAttribute('normal');
|
|
41
|
+
const positions = [];
|
|
42
|
+
const normals = [];
|
|
43
|
+
const push = (corner) => {
|
|
44
|
+
positions.push(corner.x, corner.y, corner.z);
|
|
45
|
+
normals.push(corner.nx, corner.ny, corner.nz);
|
|
46
|
+
};
|
|
47
|
+
for (let i = 0; i + 2 < position.count; i += 3) {
|
|
48
|
+
const corners = [0, 1, 2].map((k) => ({
|
|
49
|
+
x: position.getX(i + k),
|
|
50
|
+
y: position.getY(i + k),
|
|
51
|
+
z: position.getZ(i + k),
|
|
52
|
+
nx: normal.getX(i + k),
|
|
53
|
+
ny: normal.getY(i + k),
|
|
54
|
+
nz: normal.getZ(i + k),
|
|
55
|
+
}));
|
|
56
|
+
const [a, b, c] = corners;
|
|
57
|
+
const minX = Math.min(a.x, b.x, c.x);
|
|
58
|
+
const maxX = Math.max(a.x, b.x, c.x);
|
|
59
|
+
const minZ = Math.min(a.z, b.z, c.z);
|
|
60
|
+
const maxZ = Math.max(a.z, b.z, c.z);
|
|
61
|
+
const reached = boxes.some((box) => box.maxX > minX && box.minX < maxX && box.maxZ > minZ && box.minZ < maxZ);
|
|
62
|
+
const plan = [
|
|
63
|
+
[a.x, a.z],
|
|
64
|
+
[b.x, b.z],
|
|
65
|
+
[c.x, c.z],
|
|
66
|
+
];
|
|
67
|
+
const determinant = orient(plan[0], plan[1], plan[2]);
|
|
68
|
+
if (!reached || Math.abs(determinant) < MIN_PLAN_AREA) {
|
|
69
|
+
corners.forEach(push);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
let pieces;
|
|
73
|
+
try {
|
|
74
|
+
pieces = difference(plan, cutouts, { throwOnError: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
corners.forEach(push);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const at = (x, z) => {
|
|
81
|
+
const wa = ((b.z - c.z) * (x - c.x) + (c.x - b.x) * (z - c.z)) / determinant;
|
|
82
|
+
const wb = ((c.z - a.z) * (x - c.x) + (a.x - c.x) * (z - c.z)) / determinant;
|
|
83
|
+
const wc = 1 - wa - wb;
|
|
84
|
+
const nx = a.nx * wa + b.nx * wb + c.nx * wc;
|
|
85
|
+
const ny = a.ny * wa + b.ny * wb + c.ny * wc;
|
|
86
|
+
const nz = a.nz * wa + b.nz * wb + c.nz * wc;
|
|
87
|
+
const length = Math.hypot(nx, ny, nz) || 1;
|
|
88
|
+
return {
|
|
89
|
+
x,
|
|
90
|
+
y: a.y * wa + b.y * wb + c.y * wc,
|
|
91
|
+
z,
|
|
92
|
+
nx: nx / length,
|
|
93
|
+
ny: ny / length,
|
|
94
|
+
nz: nz / length,
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
for (const piece of pieces) {
|
|
98
|
+
const rings = [piece.outer, ...piece.holes];
|
|
99
|
+
const points = rings.flat();
|
|
100
|
+
const contour = piece.outer.map(([x, z]) => new Vector2(x, z));
|
|
101
|
+
const voids = piece.holes.map((ring) => ring.map(([x, z]) => new Vector2(x, z)));
|
|
102
|
+
for (const [p, q, r] of ShapeUtils.triangulateShape(contour, voids)) {
|
|
103
|
+
const corner = [p, q, r].map((index) => points[index]);
|
|
104
|
+
const swap = Math.sign(orient(corner[0], corner[1], corner[2])) !== Math.sign(determinant);
|
|
105
|
+
const ordered = swap ? [corner[0], corner[2], corner[1]] : corner;
|
|
106
|
+
for (const [x, z] of ordered)
|
|
107
|
+
push(at(x, z));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
const cut = new BufferGeometry();
|
|
112
|
+
cut.setAttribute('position', new BufferAttribute(new Float32Array(positions), 3));
|
|
113
|
+
cut.setAttribute('normal', new BufferAttribute(new Float32Array(normals), 3));
|
|
114
|
+
geometry.dispose();
|
|
115
|
+
return cut;
|
|
116
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type AnyNode, type AnyNodeId } from '@pascal-app/core';
|
|
2
|
+
import type { WalkthroughInteract } from '../../../store/use-first-person-hud';
|
|
3
|
+
/**
|
|
4
|
+
* Whether the walkthrough swings this door or window itself, through the native `operationState`
|
|
5
|
+
* its door and window systems draw. One built from a script has no native leaf or sash: a swing
|
|
6
|
+
* would draw nothing, so its clips run through its mechanism, as an authored object's do.
|
|
7
|
+
*/
|
|
8
|
+
export declare function hasNativeLeaf(node: AnyNode | undefined): boolean;
|
|
9
|
+
/** Mounted doors and windows the walkthrough's own door and window targets cover. */
|
|
10
|
+
export declare function nativeOpeningIds(): AnyNodeId[];
|
|
11
|
+
/**
|
|
12
|
+
* Mounted nodes the walkthrough runs through `capabilities.mechanism`, any kind
|
|
13
|
+
* included. `covered` holds the nodes a finer target already tested (procedural
|
|
14
|
+
* parts, catalog item toggles), so no node is targeted twice.
|
|
15
|
+
*/
|
|
16
|
+
export declare function mechanismTargetIds(covered: ReadonlySet<string>): AnyNodeId[];
|
|
17
|
+
/** The walkthrough prompt for a mechanism target: its name and what E does next. */
|
|
18
|
+
export declare function mechanismHudInteract(node: AnyNode): WalkthroughInteract;
|
|
19
|
+
/**
|
|
20
|
+
* Calls `onChange` when a mounted door built from a script opens or closes. The walkthrough's
|
|
21
|
+
* collider follows the door's mechanism, and its model's clip is moved by no tween system that
|
|
22
|
+
* would report the end, as a native door's does: without this the world keeps the leaf that was
|
|
23
|
+
* there when it was last built. It fires at the toggle: the doorway is passable as the leaves
|
|
24
|
+
* start to move, and solid again as they start to close.
|
|
25
|
+
*/
|
|
26
|
+
export declare function watchScriptedDoors(onChange: () => void): () => void;
|
|
27
|
+
//# sourceMappingURL=mechanism-targets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mechanism-targets.d.ts","sourceRoot":"","sources":["../../../../src/components/viewer/first-person/mechanism-targets.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAOf,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qCAAqC,CAAA;AAE9E;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,OAAO,CAEhE;AAED,qFAAqF;AACrF,wBAAgB,gBAAgB,IAAI,SAAS,EAAE,CAS9C;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,SAAS,EAAE,CAU5E;AAED,oFAAoF;AACpF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,OAAO,GAAG,mBAAmB,CAMvE;AAcD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI,CASnE"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isScriptedNode, nodeMechanism, nodeRegistry, sceneRegistry, useInteractive, useScene, } from '@pascal-app/core';
|
|
2
|
+
/**
|
|
3
|
+
* Whether the walkthrough swings this door or window itself, through the native `operationState`
|
|
4
|
+
* its door and window systems draw. One built from a script has no native leaf or sash: a swing
|
|
5
|
+
* would draw nothing, so its clips run through its mechanism, as an authored object's do.
|
|
6
|
+
*/
|
|
7
|
+
export function hasNativeLeaf(node) {
|
|
8
|
+
return !isScriptedNode(node);
|
|
9
|
+
}
|
|
10
|
+
/** Mounted doors and windows the walkthrough's own door and window targets cover. */
|
|
11
|
+
export function nativeOpeningIds() {
|
|
12
|
+
const nodes = useScene.getState().nodes;
|
|
13
|
+
const ids = [];
|
|
14
|
+
for (const kind of ['door', 'window']) {
|
|
15
|
+
for (const id of sceneRegistry.byType[kind] ?? []) {
|
|
16
|
+
if (hasNativeLeaf(nodes[id]))
|
|
17
|
+
ids.push(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return ids;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Mounted nodes the walkthrough runs through `capabilities.mechanism`, any kind
|
|
24
|
+
* included. `covered` holds the nodes a finer target already tested (procedural
|
|
25
|
+
* parts, catalog item toggles), so no node is targeted twice.
|
|
26
|
+
*/
|
|
27
|
+
export function mechanismTargetIds(covered) {
|
|
28
|
+
const nodes = useScene.getState().nodes;
|
|
29
|
+
const ids = [];
|
|
30
|
+
for (const [kind, kindIds] of Object.entries(sceneRegistry.byType)) {
|
|
31
|
+
if (!nodeRegistry.get(kind)?.capabilities.mechanism)
|
|
32
|
+
continue;
|
|
33
|
+
for (const id of kindIds) {
|
|
34
|
+
if (!covered.has(id) && nodeMechanism(nodes[id]))
|
|
35
|
+
ids.push(id);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return ids;
|
|
39
|
+
}
|
|
40
|
+
/** The walkthrough prompt for a mechanism target: its name and what E does next. */
|
|
41
|
+
export function mechanismHudInteract(node) {
|
|
42
|
+
const mechanism = nodeMechanism(node);
|
|
43
|
+
if (!mechanism)
|
|
44
|
+
return null;
|
|
45
|
+
const on = mechanism.isOn(node, useInteractive.getState());
|
|
46
|
+
const verb = mechanism.verb === 'open' ? (on ? 'close' : 'open') : on ? 'turn off' : 'turn on';
|
|
47
|
+
return { label: node.name ?? 'item', verb };
|
|
48
|
+
}
|
|
49
|
+
/** Ids of the mounted doors built from a script that are open, by their mechanism. */
|
|
50
|
+
function openScriptedDoors() {
|
|
51
|
+
const nodes = useScene.getState().nodes;
|
|
52
|
+
const state = useInteractive.getState();
|
|
53
|
+
const open = [];
|
|
54
|
+
for (const id of sceneRegistry.byType.door ?? []) {
|
|
55
|
+
const node = nodes[id];
|
|
56
|
+
if (isScriptedNode(node) && nodeMechanism(node)?.isOn(node, state))
|
|
57
|
+
open.push(id);
|
|
58
|
+
}
|
|
59
|
+
return open.sort().join(',');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Calls `onChange` when a mounted door built from a script opens or closes. The walkthrough's
|
|
63
|
+
* collider follows the door's mechanism, and its model's clip is moved by no tween system that
|
|
64
|
+
* would report the end, as a native door's does: without this the world keeps the leaf that was
|
|
65
|
+
* there when it was last built. It fires at the toggle: the doorway is passable as the leaves
|
|
66
|
+
* start to move, and solid again as they start to close.
|
|
67
|
+
*/
|
|
68
|
+
export function watchScriptedDoors(onChange) {
|
|
69
|
+
let known = openScriptedDoors();
|
|
70
|
+
return useInteractive.subscribe((state, previous) => {
|
|
71
|
+
if (state.items === previous.items)
|
|
72
|
+
return;
|
|
73
|
+
const next = openScriptedDoors();
|
|
74
|
+
if (next === known)
|
|
75
|
+
return;
|
|
76
|
+
known = next;
|
|
77
|
+
onChange();
|
|
78
|
+
});
|
|
79
|
+
}
|