@pascal-app/editor 0.7.0 → 0.9.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 +12 -13
- package/src/components/editor/alignment-3d-guide-layer.tsx +160 -0
- package/src/components/editor/custom-camera-controls.tsx +755 -25
- package/src/components/editor/editor-layout-mobile.tsx +3 -3
- package/src/components/editor/editor-layout-v2.tsx +62 -54
- package/src/components/editor/export-manager.tsx +32 -3
- package/src/components/editor/first-person/build-collider-world.test.ts +149 -0
- package/src/components/editor/first-person/build-collider-world.ts +203 -87
- package/src/components/editor/first-person/bvh-ecctrl.tsx +127 -62
- package/src/components/editor/first-person-controls.tsx +836 -31
- package/src/components/editor/floating-action-menu.tsx +287 -196
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-panel.tsx +5837 -12006
- package/src/components/editor/grid.tsx +51 -17
- package/src/components/editor/group-move-handle.tsx +316 -0
- package/src/components/editor/group-rotate-handle.tsx +382 -0
- package/src/components/editor/group-transform-shared.test.ts +205 -0
- package/src/components/editor/group-transform-shared.ts +256 -0
- package/src/components/editor/handles/handle-arrow.tsx +523 -0
- package/src/components/editor/handles/use-handle-drag.ts +213 -0
- package/src/components/editor/index.tsx +234 -190
- package/src/components/editor/measurement-pill.tsx +75 -0
- package/src/components/editor/node-arrow-handles.tsx +1453 -0
- package/src/components/editor/selection-manager.tsx +435 -158
- package/src/components/editor/site-edge-labels.tsx +44 -20
- package/src/components/editor/slab-hole-highlights.tsx +466 -0
- package/src/components/editor/snapshot-capture-overlay.tsx +539 -0
- package/src/components/editor/thumbnail-generator.tsx +27 -64
- package/src/components/editor/use-floorplan-background-placement.ts +122 -35
- package/src/components/editor/use-floorplan-hit-testing.ts +24 -0
- package/src/components/editor/use-floorplan-scene-data.ts +14 -2
- package/src/components/editor/wall-measurement-label.tsx +11 -6
- package/src/components/editor/wall-move-side-handles.tsx +855 -0
- package/src/components/editor/wall-opening-highlights.tsx +181 -0
- package/src/components/editor/wall-snap-beacon-layer.tsx +150 -0
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +6 -1
- package/src/components/editor-2d/floorplan-alignment-guide-layer.tsx +142 -0
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +5 -3
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +227 -0
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +616 -0
- package/src/components/editor-2d/floorplan-render-context.tsx +62 -0
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +75 -0
- package/src/components/editor-2d/floorplan-wall-move-ghost-layer.tsx +47 -0
- package/src/components/editor-2d/renderers/floorplan-draft-layer.tsx +6 -1
- package/src/components/editor-2d/renderers/floorplan-geometry-renderer.tsx +266 -0
- package/src/components/editor-2d/renderers/floorplan-measurements-layer.tsx +108 -61
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +59 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1963 -0
- package/src/components/editor-2d/renderers/floorplan-stair-layer.tsx +29 -14
- package/src/components/editor-2d/svg-paths.ts +10 -2
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +30 -9
- package/src/components/systems/ceiling/ceiling-system.tsx +12 -8
- package/src/components/systems/roof/roof-edit-system.tsx +86 -12
- package/src/components/systems/stair/stair-edit-system.tsx +2 -2
- package/src/components/systems/zone/zone-label-editor-system.tsx +0 -0
- package/src/components/systems/zone/zone-system.tsx +0 -0
- package/src/components/tools/elevator/elevator-defaults.ts +8 -0
- package/src/components/tools/elevator/elevator-tool.tsx +349 -0
- package/src/components/tools/elevator/move-elevator-tool.tsx +252 -0
- package/src/components/tools/fence/fence-drafting.ts +64 -22
- package/src/components/tools/item/move-tool.tsx +40 -107
- package/src/components/tools/item/placement-math.test.ts +10 -0
- package/src/components/tools/item/placement-math.ts +33 -9
- package/src/components/tools/item/placement-strategies.ts +201 -25
- package/src/components/tools/item/placement-types.ts +10 -1
- package/src/components/tools/item/use-draft-node.ts +20 -2
- package/src/components/tools/item/use-placement-coordinator.tsx +700 -299
- package/src/components/tools/registry/move-registry-node-tool.tsx +602 -0
- package/src/components/tools/roof/roof-tool.tsx +108 -12
- package/src/components/tools/select/box-select-state.ts +77 -0
- package/src/components/tools/select/box-select-tool.tsx +324 -543
- package/src/components/tools/select/plane-box-select-tool.tsx +577 -0
- package/src/components/tools/select/screen-rectangle-selection.ts +84 -0
- package/src/components/tools/select/select-candidates.test.ts +109 -0
- package/src/components/tools/select/select-candidates.ts +85 -0
- package/src/components/tools/shared/affordance-dispatch.ts +30 -0
- package/src/components/tools/shared/drag-bounding-box.tsx +171 -0
- package/src/components/tools/shared/floor-stack-preview.ts +25 -0
- package/src/components/tools/shared/fresh-placement-visibility.ts +61 -0
- package/src/components/tools/shared/placement-box-geometry.ts +123 -0
- package/src/components/tools/shared/placement-box.tsx +123 -0
- package/src/components/tools/shared/polygon-editor.tsx +617 -167
- package/src/components/tools/shared/segment-angle.ts +57 -1
- package/src/components/tools/site/site-boundary-editor.tsx +464 -12
- package/src/components/tools/site/site-flag-model.ts +18 -0
- package/src/components/tools/stair/stair-defaults.ts +1 -1
- package/src/components/tools/stair/stair-tool.tsx +269 -35
- package/src/components/tools/tool-manager.tsx +172 -56
- package/src/components/tools/wall/wall-drafting.ts +98 -98
- package/src/components/tools/wall/wall-snap-geometry.test.ts +79 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +270 -0
- package/src/components/tools/zone/zone-tool.tsx +36 -11
- package/src/components/ui/action-menu/action-button.tsx +21 -1
- package/src/components/ui/action-menu/camera-actions.tsx +0 -0
- package/src/components/ui/action-menu/control-modes.tsx +5 -76
- package/src/components/ui/action-menu/furnish-tools.tsx +6 -92
- package/src/components/ui/action-menu/index.tsx +33 -196
- package/src/components/ui/action-menu/structure-tools.tsx +13 -88
- package/src/components/ui/action-menu/view-toggles.tsx +262 -37
- package/src/components/ui/command-palette/editor-commands.tsx +24 -17
- package/src/components/ui/command-palette/index.tsx +6 -256
- package/src/components/ui/controls/material-paint-panel.tsx +83 -0
- package/src/components/ui/controls/material-picker.tsx +17 -35
- package/src/components/ui/controls/metric-control.tsx +33 -11
- package/src/components/ui/floating-level-selector.tsx +48 -11
- package/src/components/ui/helpers/helper-manager.tsx +21 -17
- package/src/components/ui/helpers/registered-tool-helper.tsx +25 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +1743 -315
- package/src/components/ui/item-catalog/item-catalog.tsx +91 -46
- package/src/components/ui/level-duplicate-dialog.tsx +5 -6
- package/src/components/ui/panels/node-display.ts +1 -0
- package/src/components/ui/panels/paint-panel.tsx +66 -41
- package/src/components/ui/panels/panel-manager.tsx +33 -74
- package/src/components/ui/panels/panel-wrapper.tsx +217 -12
- package/src/components/ui/panels/parametric-inspector.tsx +469 -0
- package/src/components/ui/panels/reference-panel.tsx +28 -13
- package/src/components/ui/primitives/dropdown-menu.tsx +12 -8
- package/src/components/ui/primitives/number-input.tsx +1 -1
- package/src/components/ui/primitives/sidebar.tsx +0 -0
- package/src/components/ui/scene-loader.tsx +1 -3
- package/src/components/ui/sidebar/app-sidebar.tsx +0 -0
- package/src/components/ui/sidebar/icon-rail.tsx +0 -0
- package/src/components/ui/sidebar/panels/items-panel/function-tree-panel.tsx +263 -0
- package/src/components/ui/sidebar/panels/items-panel/index.tsx +404 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +59 -9
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +6 -1
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +259 -0
- package/src/components/ui/sidebar/panels/site-panel/building-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +89 -0
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +75 -0
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +8 -12
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +2 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +93 -0
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +41 -5
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +128 -0
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +5 -2
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +83 -0
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +68 -34
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -1
- package/src/components/ui/sidebar/panels/zone-panel/index.tsx +52 -3
- package/src/components/ui/sidebar/tab-bar.tsx +61 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/viewer-overlay.tsx +169 -52
- package/src/components/viewer-zone-system.tsx +1 -1
- package/src/hooks/use-auto-save.ts +24 -3
- package/src/hooks/use-ceiling-events.ts +175 -0
- package/src/hooks/use-drag-action.ts +124 -0
- package/src/hooks/use-keyboard.ts +125 -62
- package/src/hooks/use-selection.ts +64 -0
- package/src/index.tsx +243 -4
- package/src/lib/constants.ts +6 -2
- package/src/lib/editor-api.ts +62 -0
- package/src/lib/elevator-support.ts +96 -0
- package/src/lib/floorplan/apply-alignment.ts +111 -0
- package/src/lib/floorplan/index.ts +7 -0
- package/src/lib/floorplan/selection-tool.ts +40 -0
- package/src/lib/floorplan/stairs.ts +12 -1
- package/src/lib/fresh-planar-placement.test.ts +102 -0
- package/src/lib/fresh-planar-placement.ts +63 -0
- package/src/lib/history.ts +2 -1
- package/src/lib/level-duplication.test.ts +0 -2
- package/src/lib/level-duplication.ts +55 -43
- package/src/lib/level-name.ts +11 -0
- package/src/lib/material-paint.ts +212 -20
- package/src/lib/placement-metadata.ts +29 -0
- package/src/lib/planar-cursor-placement.test.ts +43 -0
- package/src/lib/planar-cursor-placement.ts +42 -0
- package/src/lib/roof-duplication.ts +2 -2
- package/src/lib/scene-bounds.ts +1 -1
- package/src/lib/scene-clipboard.ts +267 -0
- package/src/lib/scene.ts +13 -4
- package/src/lib/sfx-bus.ts +25 -1
- package/src/lib/sfx-player.ts +75 -27
- package/src/lib/site-boundary.ts +1 -0
- package/src/lib/stair-duplication.ts +2 -2
- package/src/lib/stair-levels.test.ts +146 -0
- package/src/lib/stair-levels.ts +177 -0
- package/src/lib/world-grid-snap.ts +199 -0
- package/src/lib/zone-content.ts +130 -0
- package/src/store/use-alignment-guides.ts +21 -0
- package/src/store/use-editor.tsx +342 -91
- package/src/store/use-placement-preview.ts +31 -0
- package/src/store/use-wall-move-ghosts.ts +36 -0
- package/src/store/use-wall-snap-indicator.ts +32 -0
- package/tsconfig.json +2 -1
- package/src/components/editor/preset-thumbnail-generator.tsx +0 -125
- package/src/components/editor-2d/renderers/floorplan-roof-layer.tsx +0 -113
- package/src/components/feedback-dialog.tsx +0 -265
- package/src/components/pascal-radio.tsx +0 -280
- package/src/components/preview-button.tsx +0 -16
- package/src/components/tools/building/move-building-tool.tsx +0 -157
- package/src/components/tools/ceiling/ceiling-boundary-editor.tsx +0 -43
- package/src/components/tools/ceiling/ceiling-hole-editor.tsx +0 -49
- package/src/components/tools/ceiling/ceiling-tool.tsx +0 -465
- package/src/components/tools/ceiling/move-ceiling-tool.tsx +0 -257
- package/src/components/tools/column/column-tool.tsx +0 -97
- package/src/components/tools/column/move-column-tool.tsx +0 -105
- package/src/components/tools/door/door-math.ts +0 -110
- package/src/components/tools/door/door-tool.tsx +0 -324
- package/src/components/tools/door/move-door-tool.tsx +0 -412
- package/src/components/tools/fence/curve-fence-tool.tsx +0 -179
- package/src/components/tools/fence/fence-tool.tsx +0 -346
- package/src/components/tools/fence/move-fence-endpoint-tool.tsx +0 -438
- package/src/components/tools/fence/move-fence-tool.tsx +0 -298
- package/src/components/tools/item/item-tool.tsx +0 -26
- package/src/components/tools/roof/move-roof-tool.tsx +0 -359
- package/src/components/tools/slab/move-slab-tool.tsx +0 -182
- package/src/components/tools/slab/slab-boundary-editor.tsx +0 -43
- package/src/components/tools/slab/slab-hole-editor.tsx +0 -49
- package/src/components/tools/slab/slab-tool.tsx +0 -322
- package/src/components/tools/spawn/move-spawn-tool.tsx +0 -101
- package/src/components/tools/spawn/spawn-tool.tsx +0 -130
- package/src/components/tools/wall/curve-wall-tool.tsx +0 -176
- package/src/components/tools/wall/move-wall-endpoint-tool.tsx +0 -423
- package/src/components/tools/wall/move-wall-tool.tsx +0 -356
- package/src/components/tools/wall/wall-tool.tsx +0 -332
- package/src/components/tools/window/move-window-tool.tsx +0 -447
- package/src/components/tools/window/window-math.ts +0 -117
- package/src/components/tools/window/window-tool.tsx +0 -332
- package/src/components/ui/helpers/ceiling-helper.tsx +0 -20
- package/src/components/ui/helpers/slab-helper.tsx +0 -20
- package/src/components/ui/helpers/wall-helper.tsx +0 -20
- package/src/components/ui/panels/ceiling-panel.tsx +0 -250
- package/src/components/ui/panels/column-panel.tsx +0 -715
- package/src/components/ui/panels/door-panel.tsx +0 -1292
- package/src/components/ui/panels/fence-panel.tsx +0 -227
- package/src/components/ui/panels/item-panel.tsx +0 -306
- package/src/components/ui/panels/presets/presets-popover.tsx +0 -511
- package/src/components/ui/panels/roof-panel.tsx +0 -230
- package/src/components/ui/panels/roof-segment-panel.tsx +0 -311
- package/src/components/ui/panels/slab-panel.tsx +0 -250
- package/src/components/ui/panels/spawn-panel.tsx +0 -155
- package/src/components/ui/panels/stair-panel.tsx +0 -525
- package/src/components/ui/panels/stair-segment-panel.tsx +0 -325
- package/src/components/ui/panels/wall-panel.tsx +0 -177
- package/src/components/ui/panels/window-panel.tsx +0 -960
- package/src/components/ui/viewer-toolbar.tsx +0 -436
- package/src/contexts/presets-context.tsx +0 -121
- package/src/hooks/use-contextual-tools.ts +0 -61
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
type AnyNode,
|
|
3
3
|
type AnyNodeId,
|
|
4
4
|
type DoorNode,
|
|
5
|
+
getGarageVisibleOpeningRatio,
|
|
5
6
|
isOperationDoorType,
|
|
7
|
+
nodeRegistry,
|
|
6
8
|
sceneRegistry,
|
|
7
9
|
useInteractive,
|
|
8
10
|
useScene,
|
|
@@ -10,21 +12,11 @@ import {
|
|
|
10
12
|
import * as THREE from 'three'
|
|
11
13
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js'
|
|
12
14
|
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh'
|
|
13
|
-
|
|
14
|
-
const COLLIDER_NODE_TYPES = [
|
|
15
|
-
'wall',
|
|
16
|
-
'fence',
|
|
17
|
-
'slab',
|
|
18
|
-
'stair',
|
|
19
|
-
'stair-segment',
|
|
20
|
-
'roof',
|
|
21
|
-
'roof-segment',
|
|
22
|
-
'door',
|
|
23
|
-
'window',
|
|
24
|
-
'item',
|
|
25
|
-
] as const
|
|
15
|
+
import { computeSceneBoundsXZ } from '../../../lib/scene-bounds'
|
|
26
16
|
|
|
27
17
|
const SKIPPED_MESH_NAMES = new Set(['cutout', 'collision-mesh'])
|
|
18
|
+
const COLLIDER_NODE_CATEGORIES = new Set(['structure', 'furnish'])
|
|
19
|
+
const DEDICATED_COLLIDER_NODE_TYPES = new Set<AnyNode['type']>(['elevator'])
|
|
28
20
|
const COLLIDER_MATERIAL = new THREE.MeshBasicMaterial()
|
|
29
21
|
const DOWN = new THREE.Vector3(0, -1, 0)
|
|
30
22
|
const UP = new THREE.Vector3(0, 1, 0)
|
|
@@ -32,6 +24,9 @@ const SPAWN_EYE_HEIGHT = 1.65
|
|
|
32
24
|
const RAYCAST_CLEARANCE = 25
|
|
33
25
|
const DOOR_LEAF_COLLIDER_DEPTH = 0.06
|
|
34
26
|
const OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD = 0.85
|
|
27
|
+
const LEVEL_FALLBACK_FLOOR_THICKNESS = 0.08
|
|
28
|
+
const LEVEL_FALLBACK_FLOOR_PADDING = 2
|
|
29
|
+
const LEVEL_FALLBACK_FLOOR_MIN_SIZE = 30
|
|
35
30
|
|
|
36
31
|
export const FIRST_PERSON_SPAWN_EYE_HEIGHT = SPAWN_EYE_HEIGHT
|
|
37
32
|
|
|
@@ -46,7 +41,9 @@ export type FirstPersonSpawn = {
|
|
|
46
41
|
yaw: number
|
|
47
42
|
}
|
|
48
43
|
|
|
49
|
-
type
|
|
44
|
+
type LevelNode = Extract<AnyNode, { type: 'level' }>
|
|
45
|
+
type SiteNode = Extract<AnyNode, { type: 'site' }>
|
|
46
|
+
type SceneNodes = ReturnType<typeof useScene.getState>['nodes']
|
|
50
47
|
|
|
51
48
|
function isMesh(object: THREE.Object3D): object is THREE.Mesh {
|
|
52
49
|
return 'isMesh' in object && (object as THREE.Mesh).isMesh
|
|
@@ -56,6 +53,144 @@ function isColliderMaterialVisible(material: THREE.Material | THREE.Material[])
|
|
|
56
53
|
return Array.isArray(material) ? material.some((entry) => entry.visible) : material.visible
|
|
57
54
|
}
|
|
58
55
|
|
|
56
|
+
function isGenericColliderNode(node: AnyNode) {
|
|
57
|
+
if (node.visible === false) return false
|
|
58
|
+
if (DEDICATED_COLLIDER_NODE_TYPES.has(node.type)) return false
|
|
59
|
+
const def = nodeRegistry.get(node.type)
|
|
60
|
+
// Ceilings are a transparent mount surface for fixtures (lights, fans), not a
|
|
61
|
+
// walkable or blocking structure — the walkthrough player must pass through
|
|
62
|
+
// them rather than be held up as if standing on a floor slab.
|
|
63
|
+
if (def?.surfaceRole === 'ceiling') return false
|
|
64
|
+
return COLLIDER_NODE_CATEGORIES.has(def?.category ?? '')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function createBoxColliderGeometry(width: number, height: number, depth: number) {
|
|
68
|
+
const sourceGeometry = new THREE.BoxGeometry(width, height, depth).toNonIndexed()
|
|
69
|
+
const geometry = new THREE.BufferGeometry()
|
|
70
|
+
geometry.setAttribute('position', sourceGeometry.getAttribute('position').clone())
|
|
71
|
+
geometry.setAttribute('normal', sourceGeometry.getAttribute('normal').clone())
|
|
72
|
+
sourceGeometry.dispose()
|
|
73
|
+
return geometry
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getVisibleLevelChildren(level: LevelNode, nodes: SceneNodes) {
|
|
77
|
+
return level.children
|
|
78
|
+
.map((childId) => nodes[childId as AnyNodeId])
|
|
79
|
+
.filter((child): child is AnyNode => Boolean(child && child.visible !== false))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function createLevelFallbackFloorGeometry(level: LevelNode, nodes: SceneNodes) {
|
|
83
|
+
if (level.visible === false) return null
|
|
84
|
+
|
|
85
|
+
const children = getVisibleLevelChildren(level, nodes)
|
|
86
|
+
if (children.some((child) => child.type === 'slab')) return null
|
|
87
|
+
|
|
88
|
+
const levelObject = sceneRegistry.nodes.get(level.id)
|
|
89
|
+
if (!levelObject?.visible) return null
|
|
90
|
+
|
|
91
|
+
const bounds = computeSceneBoundsXZ(children)
|
|
92
|
+
const [centerX, centerZ] = bounds?.center ?? [0, 0]
|
|
93
|
+
const [boundsWidth, boundsDepth] = bounds?.size ?? [0, 0]
|
|
94
|
+
const width = Math.max(
|
|
95
|
+
boundsWidth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
|
|
96
|
+
LEVEL_FALLBACK_FLOOR_MIN_SIZE,
|
|
97
|
+
)
|
|
98
|
+
const depth = Math.max(
|
|
99
|
+
boundsDepth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
|
|
100
|
+
LEVEL_FALLBACK_FLOOR_MIN_SIZE,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
const geometry = createBoxColliderGeometry(width, LEVEL_FALLBACK_FLOOR_THICKNESS, depth)
|
|
104
|
+
|
|
105
|
+
levelObject.updateWorldMatrix(true, false)
|
|
106
|
+
geometry.applyMatrix4(
|
|
107
|
+
new THREE.Matrix4().makeTranslation(centerX, -LEVEL_FALLBACK_FLOOR_THICKNESS / 2, centerZ),
|
|
108
|
+
)
|
|
109
|
+
geometry.applyMatrix4(levelObject.matrixWorld)
|
|
110
|
+
return geometry
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function collectLevelFallbackFloorGeometries(nodes: SceneNodes) {
|
|
114
|
+
const geometries: THREE.BufferGeometry[] = []
|
|
115
|
+
|
|
116
|
+
for (const levelId of sceneRegistry.byType.level!) {
|
|
117
|
+
const node = nodes[levelId as AnyNodeId]
|
|
118
|
+
if (node?.type !== 'level') continue
|
|
119
|
+
|
|
120
|
+
const geometry = createLevelFallbackFloorGeometry(node, nodes)
|
|
121
|
+
if (geometry) geometries.push(geometry)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return geometries
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// The visible ground is the site node's ground mesh, but `site` is a `site`
|
|
128
|
+
// category node and therefore excluded from the generic collider sweep. Without
|
|
129
|
+
// a dedicated collider, a spawn on the bare ground (no slab, or not parented to
|
|
130
|
+
// a level that triggers the per-level fallback) has no floor to stand on and the
|
|
131
|
+
// walkthrough player falls through. Derive a thin ground slab from node data (not
|
|
132
|
+
// the rendered mesh) so it exists regardless of geometry-mount timing, sized to
|
|
133
|
+
// cover the whole scene footprint at the site's ground plane.
|
|
134
|
+
function createSiteGroundColliderGeometry(site: SiteNode, nodes: SceneNodes) {
|
|
135
|
+
if (site.visible === false) return null
|
|
136
|
+
|
|
137
|
+
const siteObject = sceneRegistry.nodes.get(site.id)
|
|
138
|
+
if (!siteObject?.visible) return null
|
|
139
|
+
|
|
140
|
+
const bounds = computeSceneBoundsXZ(nodes)
|
|
141
|
+
const [centerX, centerZ] = bounds?.center ?? [0, 0]
|
|
142
|
+
const [boundsWidth, boundsDepth] = bounds?.size ?? [0, 0]
|
|
143
|
+
const width = Math.max(
|
|
144
|
+
boundsWidth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
|
|
145
|
+
LEVEL_FALLBACK_FLOOR_MIN_SIZE,
|
|
146
|
+
)
|
|
147
|
+
const depth = Math.max(
|
|
148
|
+
boundsDepth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
|
|
149
|
+
LEVEL_FALLBACK_FLOOR_MIN_SIZE,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const geometry = createBoxColliderGeometry(width, LEVEL_FALLBACK_FLOOR_THICKNESS, depth)
|
|
153
|
+
|
|
154
|
+
siteObject.updateWorldMatrix(true, false)
|
|
155
|
+
geometry.applyMatrix4(
|
|
156
|
+
new THREE.Matrix4().makeTranslation(centerX, -LEVEL_FALLBACK_FLOOR_THICKNESS / 2, centerZ),
|
|
157
|
+
)
|
|
158
|
+
geometry.applyMatrix4(siteObject.matrixWorld)
|
|
159
|
+
return geometry
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function collectSiteGroundColliderGeometries(nodes: SceneNodes) {
|
|
163
|
+
const geometries: THREE.BufferGeometry[] = []
|
|
164
|
+
|
|
165
|
+
for (const siteId of sceneRegistry.byType.site ?? []) {
|
|
166
|
+
const node = nodes[siteId as AnyNodeId]
|
|
167
|
+
if (node?.type !== 'site') continue
|
|
168
|
+
|
|
169
|
+
const geometry = createSiteGroundColliderGeometry(node, nodes)
|
|
170
|
+
if (geometry) geometries.push(geometry)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return geometries
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Decode any attribute (interleaved, quantized/normalized integer, Float64…) into a
|
|
177
|
+
// plain, non-normalized Float32Array BufferAttribute. mergeGeometries() requires every
|
|
178
|
+
// merged geometry to share the same typed-array constructor for matching attributes, so
|
|
179
|
+
// imported item GLBs using KHR_mesh_quantization or interleaved buffers must be coerced
|
|
180
|
+
// to Float32 to match wall/slab geometry.
|
|
181
|
+
function toFloat32Attribute(source: THREE.BufferAttribute | THREE.InterleavedBufferAttribute) {
|
|
182
|
+
const itemSize = source.itemSize
|
|
183
|
+
const array = new Float32Array(source.count * itemSize)
|
|
184
|
+
for (let i = 0; i < source.count; i++) {
|
|
185
|
+
const offset = i * itemSize
|
|
186
|
+
array[offset] = source.getX(i)
|
|
187
|
+
if (itemSize > 1) array[offset + 1] = source.getY(i)
|
|
188
|
+
if (itemSize > 2) array[offset + 2] = source.getZ(i)
|
|
189
|
+
if (itemSize > 3) array[offset + 3] = source.getW(i)
|
|
190
|
+
}
|
|
191
|
+
return new THREE.BufferAttribute(array, itemSize)
|
|
192
|
+
}
|
|
193
|
+
|
|
59
194
|
function cloneWorldGeometry(mesh: THREE.Mesh) {
|
|
60
195
|
const sourceGeometry = mesh.geometry
|
|
61
196
|
const position = sourceGeometry.getAttribute('position')
|
|
@@ -65,11 +200,14 @@ function cloneWorldGeometry(mesh: THREE.Mesh) {
|
|
|
65
200
|
? sourceGeometry.toNonIndexed()
|
|
66
201
|
: sourceGeometry.clone()
|
|
67
202
|
const cleanGeometry = new THREE.BufferGeometry()
|
|
68
|
-
cleanGeometry.setAttribute(
|
|
203
|
+
cleanGeometry.setAttribute(
|
|
204
|
+
'position',
|
|
205
|
+
toFloat32Attribute(workingGeometry.getAttribute('position')),
|
|
206
|
+
)
|
|
69
207
|
|
|
70
208
|
const normal = workingGeometry.getAttribute('normal')
|
|
71
209
|
if (normal) {
|
|
72
|
-
cleanGeometry.setAttribute('normal', normal
|
|
210
|
+
cleanGeometry.setAttribute('normal', toFloat32Attribute(normal))
|
|
73
211
|
} else {
|
|
74
212
|
cleanGeometry.computeVertexNormals()
|
|
75
213
|
}
|
|
@@ -86,22 +224,18 @@ function cloneWorldGeometry(mesh: THREE.Mesh) {
|
|
|
86
224
|
return cleanGeometry
|
|
87
225
|
}
|
|
88
226
|
|
|
89
|
-
function shouldSkipColliderNode(
|
|
90
|
-
if (type === 'window') {
|
|
91
|
-
|
|
92
|
-
return node?.type === 'window' && node.openingKind === 'opening'
|
|
227
|
+
function shouldSkipColliderNode(node: AnyNode) {
|
|
228
|
+
if (node.type === 'window') {
|
|
229
|
+
return node.openingKind === 'opening'
|
|
93
230
|
}
|
|
94
231
|
|
|
95
|
-
if (type !== 'door') return false
|
|
232
|
+
if (node.type !== 'door') return false
|
|
96
233
|
|
|
97
|
-
|
|
98
|
-
if (!node || node.type !== 'door') return false
|
|
234
|
+
if (!node.segments.length) return true
|
|
99
235
|
|
|
100
236
|
if (node.openingKind === 'opening') return true
|
|
101
237
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return node.segments.every((segment) => segment.type === 'empty')
|
|
238
|
+
return node.segments.every((segment: { type: string }) => segment.type === 'empty')
|
|
105
239
|
}
|
|
106
240
|
|
|
107
241
|
function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
|
|
@@ -124,15 +258,7 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
|
|
|
124
258
|
const visibleHeight = leafH * (1 - openAmount)
|
|
125
259
|
if (visibleHeight <= 0.12) return null
|
|
126
260
|
|
|
127
|
-
const
|
|
128
|
-
leafW,
|
|
129
|
-
visibleHeight,
|
|
130
|
-
DOOR_LEAF_COLLIDER_DEPTH,
|
|
131
|
-
).toNonIndexed()
|
|
132
|
-
const geometry = new THREE.BufferGeometry()
|
|
133
|
-
geometry.setAttribute('position', sourceGeometry.getAttribute('position').clone())
|
|
134
|
-
geometry.setAttribute('normal', sourceGeometry.getAttribute('normal').clone())
|
|
135
|
-
sourceGeometry.dispose()
|
|
261
|
+
const geometry = createBoxColliderGeometry(leafW, visibleHeight, DOOR_LEAF_COLLIDER_DEPTH)
|
|
136
262
|
const visibleCenterY = leafCenterY - leafH / 2 + visibleHeight / 2
|
|
137
263
|
geometry.applyMatrix4(
|
|
138
264
|
root.matrixWorld.clone().multiply(new THREE.Matrix4().makeTranslation(0, visibleCenterY, 0)),
|
|
@@ -153,15 +279,7 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
|
|
|
153
279
|
const clampedSwingAngle = Math.max(0, Math.min(Math.PI / 2, swingAngle ?? 0))
|
|
154
280
|
const leafSwingRotation = clampedSwingAngle * swingDirectionSign * hingeDirectionSign
|
|
155
281
|
|
|
156
|
-
const
|
|
157
|
-
leafW,
|
|
158
|
-
leafH,
|
|
159
|
-
DOOR_LEAF_COLLIDER_DEPTH,
|
|
160
|
-
).toNonIndexed()
|
|
161
|
-
const geometry = new THREE.BufferGeometry()
|
|
162
|
-
geometry.setAttribute('position', sourceGeometry.getAttribute('position').clone())
|
|
163
|
-
geometry.setAttribute('normal', sourceGeometry.getAttribute('normal').clone())
|
|
164
|
-
sourceGeometry.dispose()
|
|
282
|
+
const geometry = createBoxColliderGeometry(leafW, leafH, DOOR_LEAF_COLLIDER_DEPTH)
|
|
165
283
|
const matrix = root.matrixWorld
|
|
166
284
|
.clone()
|
|
167
285
|
.multiply(new THREE.Matrix4().makeTranslation(hingeX, 0, 0))
|
|
@@ -172,16 +290,17 @@ function createDoorLeafColliderGeometry(root: THREE.Object3D, node: DoorNode) {
|
|
|
172
290
|
return geometry
|
|
173
291
|
}
|
|
174
292
|
|
|
175
|
-
function
|
|
176
|
-
const
|
|
293
|
+
function buildRegisteredColliderNodeIds(nodes: SceneNodes) {
|
|
294
|
+
const nodeIds = new Set<string>()
|
|
177
295
|
|
|
178
|
-
for (const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
296
|
+
for (const nodeId of sceneRegistry.nodes.keys()) {
|
|
297
|
+
const node = nodes[nodeId as AnyNodeId]
|
|
298
|
+
if (!node || !isGenericColliderNode(node)) continue
|
|
299
|
+
if (shouldSkipColliderNode(node)) continue
|
|
300
|
+
nodeIds.add(nodeId)
|
|
182
301
|
}
|
|
183
302
|
|
|
184
|
-
return
|
|
303
|
+
return nodeIds
|
|
185
304
|
}
|
|
186
305
|
|
|
187
306
|
function collectColliderGeometriesFromNode(
|
|
@@ -189,7 +308,7 @@ function collectColliderGeometriesFromNode(
|
|
|
189
308
|
rootNodeId: string,
|
|
190
309
|
visitedMeshes: WeakSet<THREE.Object3D>,
|
|
191
310
|
registeredObjectIds: Map<THREE.Object3D, string>,
|
|
192
|
-
|
|
311
|
+
registeredColliderNodeIds: Set<string>,
|
|
193
312
|
): THREE.BufferGeometry[] {
|
|
194
313
|
const geometries: THREE.BufferGeometry[] = []
|
|
195
314
|
|
|
@@ -211,11 +330,8 @@ function collectColliderGeometriesFromNode(
|
|
|
211
330
|
|
|
212
331
|
for (const child of object.children) {
|
|
213
332
|
const childNodeId = registeredObjectIds.get(child)
|
|
214
|
-
if (childNodeId && childNodeId !== rootNodeId) {
|
|
215
|
-
|
|
216
|
-
if (childType && COLLIDER_NODE_TYPES.includes(childType)) {
|
|
217
|
-
continue
|
|
218
|
-
}
|
|
333
|
+
if (childNodeId && childNodeId !== rootNodeId && registeredColliderNodeIds.has(childNodeId)) {
|
|
334
|
+
continue
|
|
219
335
|
}
|
|
220
336
|
|
|
221
337
|
visit(child)
|
|
@@ -228,54 +344,54 @@ function collectColliderGeometriesFromNode(
|
|
|
228
344
|
}
|
|
229
345
|
|
|
230
346
|
export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonColliderWorld | null {
|
|
347
|
+
const nodes = useScene.getState().nodes
|
|
231
348
|
const geometries: THREE.BufferGeometry[] = []
|
|
232
349
|
const visitedMeshes = new WeakSet<THREE.Object3D>()
|
|
233
|
-
const
|
|
350
|
+
const registeredColliderNodeIds = buildRegisteredColliderNodeIds(nodes)
|
|
234
351
|
const registeredObjectIds = new Map<THREE.Object3D, string>()
|
|
235
352
|
|
|
236
353
|
for (const [nodeId, object] of sceneRegistry.nodes) {
|
|
237
354
|
registeredObjectIds.set(object, nodeId)
|
|
238
355
|
}
|
|
239
356
|
|
|
240
|
-
for (const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const root = sceneRegistry.nodes.get(nodeId)
|
|
245
|
-
if (!root) continue
|
|
357
|
+
for (const nodeId of registeredColliderNodeIds) {
|
|
358
|
+
const node = nodes[nodeId as AnyNodeId]
|
|
359
|
+
if (!node) continue
|
|
246
360
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
if (node?.type !== 'door') continue
|
|
361
|
+
const root = sceneRegistry.nodes.get(nodeId)
|
|
362
|
+
if (!root) continue
|
|
250
363
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
continue
|
|
364
|
+
if (node.type === 'door') {
|
|
365
|
+
const doorGeometry = createDoorLeafColliderGeometry(root, node)
|
|
366
|
+
if (doorGeometry) {
|
|
367
|
+
geometries.push(doorGeometry)
|
|
256
368
|
}
|
|
257
|
-
|
|
258
|
-
root.updateMatrixWorld(true)
|
|
259
|
-
geometries.push(
|
|
260
|
-
...collectColliderGeometriesFromNode(
|
|
261
|
-
root,
|
|
262
|
-
nodeId,
|
|
263
|
-
visitedMeshes,
|
|
264
|
-
registeredObjectIds,
|
|
265
|
-
registeredNodeTypes,
|
|
266
|
-
),
|
|
267
|
-
)
|
|
369
|
+
continue
|
|
268
370
|
}
|
|
371
|
+
|
|
372
|
+
root.updateMatrixWorld(true)
|
|
373
|
+
geometries.push(
|
|
374
|
+
...collectColliderGeometriesFromNode(
|
|
375
|
+
root,
|
|
376
|
+
nodeId,
|
|
377
|
+
visitedMeshes,
|
|
378
|
+
registeredObjectIds,
|
|
379
|
+
registeredColliderNodeIds,
|
|
380
|
+
),
|
|
381
|
+
)
|
|
269
382
|
}
|
|
270
383
|
|
|
384
|
+
geometries.push(...collectLevelFallbackFloorGeometries(nodes))
|
|
385
|
+
geometries.push(...collectSiteGroundColliderGeometries(nodes))
|
|
386
|
+
|
|
271
387
|
if (geometries.length === 0) {
|
|
272
388
|
return null
|
|
273
389
|
}
|
|
274
390
|
|
|
275
391
|
const mergedGeometry = mergeGeometries(geometries, false)
|
|
276
|
-
|
|
392
|
+
for (const geometry of geometries) {
|
|
277
393
|
geometry.dispose()
|
|
278
|
-
}
|
|
394
|
+
}
|
|
279
395
|
|
|
280
396
|
if (!mergedGeometry || mergedGeometry.getAttribute('position') == null) {
|
|
281
397
|
mergedGeometry?.dispose()
|
|
@@ -290,7 +406,7 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider
|
|
|
290
406
|
;(bvhGeometry as any).computeBoundsTree = computeBoundsTree
|
|
291
407
|
;(bvhGeometry as any).disposeBoundsTree = disposeBoundsTree
|
|
292
408
|
bvhGeometry.computeBoundsTree?.({
|
|
293
|
-
|
|
409
|
+
maxLeafSize: 12,
|
|
294
410
|
strategy: 0,
|
|
295
411
|
} as never)
|
|
296
412
|
bvhGeometry.computeBoundingBox()
|