@pascal-app/editor 0.9.1 → 0.9.2
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 -9
- package/src/components/editor/alignment-3d-guide-layer.tsx +10 -2
- package/src/components/editor/bake-exporter.tsx +47 -0
- package/src/components/editor/custom-camera-controls.tsx +63 -25
- package/src/components/editor/editor-layout-mobile.tsx +5 -0
- package/src/components/editor/editor-layout-v2.tsx +18 -0
- package/src/components/editor/export-manager.tsx +71 -63
- package/src/components/editor/fence-tangent-lines-3d.tsx +87 -0
- package/src/components/editor/first-person/build-collider-world.test.ts +36 -3
- package/src/components/editor/first-person/build-collider-world.ts +32 -5
- package/src/components/editor/first-person-controls.tsx +118 -7
- package/src/components/editor/floating-action-menu.tsx +410 -33
- package/src/components/editor/floating-building-action-menu.tsx +1 -1
- package/src/components/editor/floorplan-background-selection.test.ts +58 -0
- package/src/components/editor/floorplan-background-selection.ts +3 -2
- package/src/components/editor/floorplan-panel.tsx +1804 -849
- package/src/components/editor/grid.tsx +166 -39
- package/src/components/editor/group-actions.ts +457 -0
- package/src/components/editor/group-floating-action-menu.tsx +130 -0
- package/src/components/editor/group-move-3d.ts +397 -0
- package/src/components/editor/group-rotate-handle.tsx +102 -64
- package/src/components/editor/group-selection-box-3d.tsx +173 -0
- package/src/components/editor/group-transform-shared.test.ts +192 -1
- package/src/components/editor/group-transform-shared.ts +240 -7
- package/src/components/editor/handles/handle-arrow.tsx +81 -23
- package/src/components/editor/handles/use-handle-drag.ts +37 -4
- package/src/components/editor/index.tsx +248 -27
- package/src/components/editor/measurement-pill.tsx +62 -22
- package/src/components/editor/node-action-menu.tsx +14 -1
- package/src/components/editor/node-arrow-handles.tsx +366 -173
- package/src/components/editor/opening-guides-3d-layer.tsx +144 -0
- package/src/components/editor/quick-measurement-card.tsx +70 -0
- package/src/components/editor/quick-measurement-hud.tsx +28 -0
- package/src/components/editor/riser-diagram-panel.tsx +137 -0
- package/src/components/editor/selection-manager.tsx +812 -466
- package/src/components/editor/site-edge-labels.tsx +4 -4
- package/src/components/editor/slab-hole-highlights.tsx +13 -5
- package/src/components/editor/snapshot-capture-overlay.tsx +255 -115
- package/src/components/editor/three-context-bridge.ts +22 -0
- package/src/components/editor/thumbnail-generator.tsx +136 -43
- package/src/components/editor/use-floorplan-background-placement.ts +113 -45
- package/src/components/editor/use-floorplan-scene-data.ts +5 -5
- package/src/components/editor/use-mesh-settle-epoch.ts +26 -0
- package/src/components/editor/wall-measurement-label.tsx +3 -13
- package/src/components/editor/wall-move-side-handles.tsx +36 -21
- package/src/components/editor/wall-snap-beacon-layer.tsx +160 -6
- package/src/components/editor-2d/floorplan-action-menu-layer.tsx +9 -5
- package/src/components/editor-2d/floorplan-cursor-indicator-overlay.tsx +1 -1
- package/src/components/editor-2d/floorplan-group-action-menu.tsx +87 -0
- package/src/components/editor-2d/floorplan-group-move.tsx +701 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.test.ts +113 -0
- package/src/components/editor-2d/floorplan-measurement-tool-layer.tsx +1656 -0
- package/src/components/editor-2d/floorplan-quick-measure-layer.tsx +212 -0
- package/src/components/editor-2d/floorplan-registry-action-menu.tsx +169 -4
- package/src/components/editor-2d/floorplan-registry-move-overlay.tsx +300 -59
- package/src/components/editor-2d/floorplan-snap-beacon-layer.tsx +5 -3
- package/src/components/editor-2d/renderers/floorplan-label-angle.test.ts +15 -0
- package/src/components/editor-2d/renderers/floorplan-label-angle.ts +14 -0
- package/src/components/editor-2d/renderers/floorplan-placement-preview-layer.tsx +4 -1
- package/src/components/editor-2d/renderers/floorplan-registry-layer.test.ts +308 -0
- package/src/components/editor-2d/renderers/floorplan-registry-layer.tsx +1704 -342
- package/src/components/editor-2d/renderers/floorplan-voronoi-layer.tsx +128 -0
- package/src/components/systems/ceiling/ceiling-selection-affordance-system.tsx +419 -60
- package/src/components/systems/ceiling/ceiling-system.tsx +97 -4
- package/src/components/systems/roof/roof-edit-system.tsx +1768 -13
- package/src/components/systems/selection-affordance-manager.tsx +38 -0
- package/src/components/systems/zone/zone-label-editor-system.tsx +14 -1
- package/src/components/systems/zone/zone-system.tsx +42 -13
- package/src/components/tools/elevator/elevator-tool.tsx +34 -8
- package/src/components/tools/elevator/move-elevator-tool.tsx +13 -2
- package/src/components/tools/fence/fence-drafting.ts +80 -8
- package/src/components/tools/item/move-tool.tsx +17 -13
- package/src/components/tools/item/placement-math.test.ts +13 -1
- package/src/components/tools/item/placement-math.ts +28 -2
- package/src/components/tools/item/placement-strategies.ts +246 -5
- package/src/components/tools/item/placement-types.ts +13 -1
- package/src/components/tools/item/use-draft-node.ts +31 -1
- package/src/components/tools/item/use-placement-coordinator.tsx +761 -121
- package/src/components/tools/registry/move-registry-node-tool.tsx +561 -99
- package/src/components/tools/roof/roof-tool.tsx +320 -94
- package/src/components/tools/select/box-select-state.ts +18 -7
- package/src/components/tools/select/box-select-tool.tsx +176 -40
- package/src/components/tools/select/marquee-geometry.test.ts +161 -0
- package/src/components/tools/select/marquee-geometry.ts +155 -0
- package/src/components/tools/select/plane-box-select-tool.tsx +1 -8
- package/src/components/tools/select/select-candidates.ts +1 -1
- package/src/components/tools/shared/cursor-sphere.tsx +62 -26
- package/src/components/tools/shared/drag-bounding-box.tsx +28 -4
- package/src/components/tools/shared/facing-indicator.tsx +87 -0
- package/src/components/tools/shared/facing-pose-indicator.tsx +54 -0
- package/src/components/tools/shared/placement-box.tsx +183 -1
- package/src/components/tools/shared/polygon-editor.tsx +276 -35
- package/src/components/tools/site/site-boundary-editor.tsx +88 -36
- package/src/components/tools/stair/stair-defaults.ts +1 -0
- package/src/components/tools/stair/stair-tool.tsx +82 -11
- package/src/components/tools/tool-manager.tsx +86 -25
- package/src/components/tools/wall/wall-drafting.test.ts +330 -0
- package/src/components/tools/wall/wall-drafting.ts +180 -103
- package/src/components/tools/wall/wall-snap-geometry.test.ts +44 -0
- package/src/components/tools/wall/wall-snap-geometry.ts +67 -6
- package/src/components/tools/zone/zone-boundary-editor.tsx +5 -1
- package/src/components/tools/zone/zone-tool.tsx +82 -88
- package/src/components/ui/action-menu/camera-actions.tsx +24 -17
- package/src/components/ui/action-menu/control-modes.tsx +40 -93
- package/src/components/ui/action-menu/furnish-tools.tsx +5 -5
- package/src/components/ui/action-menu/index.tsx +2 -4
- package/src/components/ui/action-menu/measurement-control.tsx +188 -0
- package/src/components/ui/action-menu/structure-tools.tsx +22 -13
- package/src/components/ui/action-menu/view-toggles.tsx +42 -90
- package/src/components/ui/command-palette/editor-commands.tsx +2 -2
- package/src/components/ui/command-palette/index.tsx +4 -3
- package/src/components/ui/controls/material-paint-panel.tsx +86 -17
- package/src/components/ui/controls/material-picker.tsx +83 -152
- package/src/components/ui/controls/material-properties-editor.tsx +108 -0
- package/src/components/ui/controls/metric-control.tsx +118 -44
- package/src/components/ui/controls/scene-material-list.tsx +247 -0
- package/src/components/ui/controls/slider-control.tsx +88 -38
- package/src/components/ui/floating-level-selector.tsx +1 -1
- package/src/components/ui/helpers/building-helper.tsx +10 -23
- package/src/components/ui/helpers/contextual-helper-panel.tsx +428 -0
- package/src/components/ui/helpers/helper-manager.tsx +227 -14
- package/src/components/ui/helpers/item-helper.tsx +28 -32
- package/src/components/ui/helpers/registered-tool-helper.tsx +45 -11
- package/src/components/ui/helpers/roof-helper.tsx +10 -12
- package/src/components/ui/icon-ref.tsx +47 -0
- package/src/components/ui/item-catalog/catalog-items.tsx +39 -1
- package/src/components/ui/item-catalog/item-catalog.tsx +13 -36
- package/src/components/ui/level-duplicate-dialog.tsx +1 -1
- package/src/components/ui/panels/node-display.ts +17 -17
- package/src/components/ui/panels/panel-manager.tsx +22 -13
- package/src/components/ui/panels/panel-wrapper.tsx +24 -3
- package/src/components/ui/panels/parametric-inspector.tsx +15 -2
- package/src/components/ui/panels/reference-panel.tsx +54 -19
- package/src/components/ui/primitives/shortcut-token.tsx +39 -3
- package/src/components/ui/primitives/sidebar.tsx +1 -0
- package/src/components/ui/sidebar/app-sidebar.tsx +5 -1
- package/src/components/ui/sidebar/icon-rail.tsx +50 -31
- package/src/components/ui/sidebar/panels/plugins-panel.tsx +218 -0
- package/src/components/ui/sidebar/panels/settings-panel/index.tsx +71 -40
- package/src/components/ui/sidebar/panels/settings-panel/keyboard-shortcuts-dialog.tsx +60 -6
- package/src/components/ui/sidebar/panels/settings-panel/load-build-dialog.tsx +12 -4
- package/src/components/ui/sidebar/panels/site-panel/ceiling-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/chimney-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/column-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/door-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/dormer-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/elevator-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/fence-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/gutter-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/index.tsx +40 -17
- package/src/components/ui/sidebar/panels/site-panel/item-tree-node.tsx +31 -17
- package/src/components/ui/sidebar/panels/site-panel/level-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/registry-tree-node.tsx +72 -25
- package/src/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +7 -3
- package/src/components/ui/sidebar/panels/site-panel/shelf-tree-node.tsx +12 -8
- package/src/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +4 -3
- package/src/components/ui/sidebar/panels/site-panel/solar-panel-tree-node.tsx +10 -7
- package/src/components/ui/sidebar/panels/site-panel/spawn-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/stair-tree-node.tsx +2 -2
- package/src/components/ui/sidebar/panels/site-panel/tree-node.tsx +44 -3
- package/src/components/ui/sidebar/panels/site-panel/tree-structure.ts +36 -0
- package/src/components/ui/sidebar/panels/site-panel/wall-tree-node.tsx +1 -1
- package/src/components/ui/sidebar/panels/site-panel/window-tree-node.tsx +7 -2
- package/src/components/ui/sidebar/panels/site-panel/zone-tree-node.tsx +3 -3
- package/src/components/ui/sidebar/tab-bar.tsx +42 -28
- package/src/components/ui/sidebar/use-plugin-panels.tsx +132 -0
- package/src/components/ui/snap-target-badge.test.tsx +40 -0
- package/src/components/ui/snap-target-badge.tsx +88 -0
- package/src/components/viewer-overlay.tsx +85 -10
- package/src/components/viewer-zone-system.tsx +6 -1
- package/src/hooks/use-auto-save.test.ts +14 -0
- package/src/hooks/use-auto-save.ts +54 -7
- package/src/hooks/use-ceiling-events.ts +3 -2
- package/src/hooks/use-drag-action.ts +4 -1
- package/src/hooks/use-keyboard.ts +385 -40
- package/src/index.tsx +219 -6
- package/src/lib/active-placement-surface.ts +35 -0
- package/src/lib/ceiling-plan-snap.ts +24 -0
- package/src/lib/contextual-help.test.ts +112 -0
- package/src/lib/contextual-help.ts +144 -0
- package/src/lib/continuation.ts +64 -0
- package/src/lib/direct-manipulation.test.ts +192 -0
- package/src/lib/direct-manipulation.ts +109 -0
- package/src/lib/editor-api.ts +23 -35
- package/src/lib/floorplan/apply-alignment.test.ts +25 -0
- package/src/lib/floorplan/apply-alignment.ts +18 -12
- package/src/lib/floorplan/floorplan-export.tsx +364 -0
- package/src/lib/floorplan/geometry.ts +53 -0
- package/src/lib/floorplan/index.ts +3 -0
- package/src/lib/floorplan/items.ts +5 -2
- package/src/lib/floorplan/plan-coords.ts +21 -0
- package/src/lib/fresh-planar-placement.test.ts +240 -3
- package/src/lib/fresh-planar-placement.ts +68 -1
- package/src/lib/glb-export.test.ts +441 -0
- package/src/lib/glb-export.ts +874 -0
- package/src/lib/history.ts +9 -0
- package/src/lib/interaction/hot-set.test.ts +133 -0
- package/src/lib/interaction/hot-set.ts +67 -0
- package/src/lib/interaction/overlay-policy.test.ts +51 -0
- package/src/lib/interaction/overlay-policy.ts +59 -0
- package/src/lib/interaction/scope.ts +211 -0
- package/src/lib/level-selection.ts +2 -2
- package/src/lib/material-paint.ts +36 -49
- package/src/lib/measurement-kind.test.ts +30 -0
- package/src/lib/measurement-kind.ts +19 -0
- package/src/lib/measurement-label.test.ts +47 -0
- package/src/lib/measurement-label.ts +68 -0
- package/src/lib/measurement-parser.ts +116 -0
- package/src/lib/measurements.test.ts +183 -0
- package/src/lib/measurements.ts +199 -0
- package/src/lib/paint-scope.test.ts +454 -0
- package/src/lib/paint-scope.ts +461 -0
- package/src/lib/placement-drag-release.ts +23 -0
- package/src/lib/planar-cursor-placement.test.ts +57 -0
- package/src/lib/plugin-panels.test.ts +19 -0
- package/src/lib/plugin-panels.ts +91 -0
- package/src/lib/quick-measurement.test.ts +77 -0
- package/src/lib/quick-measurement.ts +109 -0
- package/src/lib/roof-duplication.ts +6 -6
- package/src/lib/roof-hover-outline-proxy.ts +22 -0
- package/src/lib/roof-wall-hit.ts +164 -0
- package/src/lib/scene-clipboard.test.ts +196 -0
- package/src/lib/scene-clipboard.ts +73 -8
- package/src/lib/scene.ts +28 -5
- package/src/lib/selection-routing.test.ts +298 -0
- package/src/lib/selection-routing.ts +175 -0
- package/src/lib/sfx/index.ts +1 -0
- package/src/lib/sfx/movement-tick.test.ts +65 -0
- package/src/lib/sfx/movement-tick.ts +18 -0
- package/src/lib/sfx-bus.ts +50 -13
- package/src/lib/sfx-player.test.ts +124 -0
- package/src/lib/sfx-player.ts +111 -66
- package/src/lib/slab-plan-snap.test.ts +93 -0
- package/src/lib/slab-plan-snap.ts +108 -0
- package/src/lib/snapping-mode.test.ts +138 -0
- package/src/lib/snapping-mode.ts +179 -0
- package/src/lib/stair-duplication.ts +4 -4
- package/src/lib/surface-plan-snap.test.ts +71 -0
- package/src/lib/surface-plan-snap.ts +256 -0
- package/src/lib/use-linear-display.ts +40 -0
- package/src/lib/world-grid-snap.ts +37 -5
- package/src/store/use-direct-manipulation-feedback.ts +20 -0
- package/src/store/use-editor.tsx +484 -114
- package/src/store/use-facing-pose.ts +44 -0
- package/src/store/use-fence-curve-draft.ts +21 -0
- package/src/store/use-floorplan-draft-preview.ts +101 -0
- package/src/store/use-floorplan-marquee.ts +50 -0
- package/src/store/use-interaction-scope.test.ts +186 -0
- package/src/store/use-interaction-scope.ts +132 -0
- package/src/store/use-measurement-draft.test.ts +530 -0
- package/src/store/use-measurement-draft.ts +543 -0
- package/src/store/use-opening-guides.ts +41 -0
- package/src/store/use-placement-preview.ts +11 -3
- package/src/store/use-quick-measurement-hud.test.ts +53 -0
- package/src/store/use-quick-measurement-hud.ts +77 -0
- package/src/store/use-segment-draft-chain.ts +28 -0
- package/src/store/use-stair-build-preview.ts +43 -0
- package/src/store/use-wall-snap-indicator.ts +2 -0
- package/src/components/editor/first-person/bvh-ecctrl.tsx +0 -860
- package/src/components/editor/group-move-handle.tsx +0 -316
- package/src/components/ui/panels/paint-panel.tsx +0 -163
- package/src/lib/level-name.ts +0 -11
|
@@ -105,6 +105,36 @@ describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
|
105
105
|
world?.dispose()
|
|
106
106
|
})
|
|
107
107
|
|
|
108
|
+
test('skips meshes hidden by an invisible ancestor (stale roof segment CSG)', () => {
|
|
109
|
+
registerColliderDefinition('column', ColumnNode, 'structure')
|
|
110
|
+
|
|
111
|
+
// Mirror the roof's segments-wrapper shape: the registered mesh's own
|
|
112
|
+
// visible flag stays true while a hidden wrapper hides it at render
|
|
113
|
+
// time. The collider must match the render, not the own-flag.
|
|
114
|
+
const column = ColumnNode.parse({ id: 'column_test' })
|
|
115
|
+
const visibleColumn = ColumnNode.parse({ id: 'column_visible', position: [3, 0, 0] })
|
|
116
|
+
setSceneNodes([column, visibleColumn])
|
|
117
|
+
|
|
118
|
+
const wrapper = new Group()
|
|
119
|
+
wrapper.visible = false
|
|
120
|
+
const hiddenMesh = new Mesh(new BoxGeometry(10, 2, 10), new MeshBasicMaterial())
|
|
121
|
+
wrapper.add(hiddenMesh)
|
|
122
|
+
wrapper.updateMatrixWorld(true)
|
|
123
|
+
sceneRegistry.nodes.set(column.id, hiddenMesh)
|
|
124
|
+
sceneRegistry.byType[column.type]!.add(column.id)
|
|
125
|
+
|
|
126
|
+
mountNode(visibleColumn, [1, 2, 1], [3, 1, 0])
|
|
127
|
+
|
|
128
|
+
const world = buildFirstPersonColliderWorldFromRegistry()
|
|
129
|
+
|
|
130
|
+
expect(world).not.toBeNull()
|
|
131
|
+
// Bounds reflect only the visible 1×1 column at x = 3; the 10×10 mesh
|
|
132
|
+
// under the hidden wrapper contributed no geometry.
|
|
133
|
+
expect(world?.bounds?.min.x).toBeCloseTo(2.5)
|
|
134
|
+
expect(world?.bounds?.max.x).toBeCloseTo(3.5)
|
|
135
|
+
world?.dispose()
|
|
136
|
+
})
|
|
137
|
+
|
|
108
138
|
test('leaves elevators to their dedicated dynamic collider meshes', () => {
|
|
109
139
|
registerColliderDefinition('elevator', ElevatorNode, 'structure')
|
|
110
140
|
|
|
@@ -141,9 +171,12 @@ describe('buildFirstPersonColliderWorldFromRegistry', () => {
|
|
|
141
171
|
// Ground slab sits just below the site ground plane (y = 0).
|
|
142
172
|
expect(world?.bounds?.min.y).toBeCloseTo(-0.08)
|
|
143
173
|
expect(world?.bounds?.max.y).toBeCloseTo(0)
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
expect(world?.bounds?.
|
|
174
|
+
// The ground collider extends far past the site polygon so stepping out of
|
|
175
|
+
// the site boundary never drops the player below the ground plane.
|
|
176
|
+
expect(world?.bounds?.min.x).toBeCloseTo(-1000)
|
|
177
|
+
expect(world?.bounds?.max.x).toBeCloseTo(1000)
|
|
178
|
+
expect(world?.bounds?.min.z).toBeCloseTo(-1000)
|
|
179
|
+
expect(world?.bounds?.max.z).toBeCloseTo(1000)
|
|
147
180
|
world?.dispose()
|
|
148
181
|
})
|
|
149
182
|
})
|
|
@@ -27,6 +27,7 @@ const OPERATION_DOOR_COLLIDER_OPEN_THRESHOLD = 0.85
|
|
|
27
27
|
const LEVEL_FALLBACK_FLOOR_THICKNESS = 0.08
|
|
28
28
|
const LEVEL_FALLBACK_FLOOR_PADDING = 2
|
|
29
29
|
const LEVEL_FALLBACK_FLOOR_MIN_SIZE = 30
|
|
30
|
+
const SITE_GROUND_COLLIDER_MIN_SIZE = 2000
|
|
30
31
|
|
|
31
32
|
export const FIRST_PERSON_SPAWN_EYE_HEIGHT = SPAWN_EYE_HEIGHT
|
|
32
33
|
|
|
@@ -49,6 +50,22 @@ function isMesh(object: THREE.Object3D): object is THREE.Mesh {
|
|
|
49
50
|
return 'isMesh' in object && (object as THREE.Mesh).isMesh
|
|
50
51
|
}
|
|
51
52
|
|
|
53
|
+
// Renderer-effective visibility: an invisible ancestor hides the whole
|
|
54
|
+
// subtree at render time even when the object's own flag is true. The
|
|
55
|
+
// collider world must match what's rendered — the roof keeps stale,
|
|
56
|
+
// UNCUT per-segment CSG inside its hidden `segments-wrapper` (full-edit
|
|
57
|
+
// exit hides the wrapper without stripping geometry), and cloning those
|
|
58
|
+
// meshes would block the walkthrough player at openings the visible
|
|
59
|
+
// merged shell has cut through.
|
|
60
|
+
function isEffectivelyVisible(object: THREE.Object3D) {
|
|
61
|
+
let current: THREE.Object3D | null = object
|
|
62
|
+
while (current) {
|
|
63
|
+
if (!current.visible) return false
|
|
64
|
+
current = current.parent
|
|
65
|
+
}
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
function isColliderMaterialVisible(material: THREE.Material | THREE.Material[]) {
|
|
53
70
|
return Array.isArray(material) ? material.some((entry) => entry.visible) : material.visible
|
|
54
71
|
}
|
|
@@ -129,8 +146,10 @@ function collectLevelFallbackFloorGeometries(nodes: SceneNodes) {
|
|
|
129
146
|
// a dedicated collider, a spawn on the bare ground (no slab, or not parented to
|
|
130
147
|
// a level that triggers the per-level fallback) has no floor to stand on and the
|
|
131
148
|
// 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
|
|
133
|
-
//
|
|
149
|
+
// the rendered mesh) so it exists regardless of geometry-mount timing. The slab
|
|
150
|
+
// is effectively unbounded (not sized to the site polygon): the ground plane must
|
|
151
|
+
// keep holding the player up even after they step past the site boundary,
|
|
152
|
+
// otherwise they fall below the ground plane into the void.
|
|
134
153
|
function createSiteGroundColliderGeometry(site: SiteNode, nodes: SceneNodes) {
|
|
135
154
|
if (site.visible === false) return null
|
|
136
155
|
|
|
@@ -142,11 +161,11 @@ function createSiteGroundColliderGeometry(site: SiteNode, nodes: SceneNodes) {
|
|
|
142
161
|
const [boundsWidth, boundsDepth] = bounds?.size ?? [0, 0]
|
|
143
162
|
const width = Math.max(
|
|
144
163
|
boundsWidth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
|
|
145
|
-
|
|
164
|
+
SITE_GROUND_COLLIDER_MIN_SIZE,
|
|
146
165
|
)
|
|
147
166
|
const depth = Math.max(
|
|
148
167
|
boundsDepth + LEVEL_FALLBACK_FLOOR_PADDING * 2,
|
|
149
|
-
|
|
168
|
+
SITE_GROUND_COLLIDER_MIN_SIZE,
|
|
150
169
|
)
|
|
151
170
|
|
|
152
171
|
const geometry = createBoxColliderGeometry(width, LEVEL_FALLBACK_FLOOR_THICKNESS, depth)
|
|
@@ -316,9 +335,12 @@ function collectColliderGeometriesFromNode(
|
|
|
316
335
|
if (visitedMeshes.has(object)) return
|
|
317
336
|
visitedMeshes.add(object)
|
|
318
337
|
|
|
338
|
+
// Prune hidden subtrees — children of an invisible group never render,
|
|
339
|
+
// so they must not collide either (see isEffectivelyVisible).
|
|
340
|
+
if (!object.visible) return
|
|
341
|
+
|
|
319
342
|
if (
|
|
320
343
|
isMesh(object) &&
|
|
321
|
-
object.visible &&
|
|
322
344
|
isColliderMaterialVisible(object.material) &&
|
|
323
345
|
!SKIPPED_MESH_NAMES.has(object.name)
|
|
324
346
|
) {
|
|
@@ -361,6 +383,11 @@ export function buildFirstPersonColliderWorldFromRegistry(): FirstPersonCollider
|
|
|
361
383
|
const root = sceneRegistry.nodes.get(nodeId)
|
|
362
384
|
if (!root) continue
|
|
363
385
|
|
|
386
|
+
// Registered objects can sit inside a hidden wrapper (roof segments
|
|
387
|
+
// under `segments-wrapper`) — the per-node traversal starts AT the
|
|
388
|
+
// object, so the ancestor chain must be checked here.
|
|
389
|
+
if (!isEffectivelyVisible(root)) continue
|
|
390
|
+
|
|
364
391
|
if (node.type === 'door') {
|
|
365
392
|
const doorGeometry = createDoorLeafColliderGeometry(root, node)
|
|
366
393
|
if (doorGeometry) {
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from 'three'
|
|
46
46
|
import { acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh'
|
|
47
47
|
import '../../three-types'
|
|
48
|
+
import { BVHEcctrl, type BVHEcctrlApi, type MovementInput } from '@pascal-app/viewer'
|
|
48
49
|
import {
|
|
49
50
|
closeDoorOpenState,
|
|
50
51
|
DOOR_SWING_OPEN_ANGLE,
|
|
@@ -64,8 +65,6 @@ import {
|
|
|
64
65
|
type FirstPersonColliderWorld,
|
|
65
66
|
type FirstPersonSpawn,
|
|
66
67
|
} from './first-person/build-collider-world'
|
|
67
|
-
import type { BVHEcctrlApi } from './first-person/bvh-ecctrl'
|
|
68
|
-
import BVHEcctrl from './first-person/bvh-ecctrl'
|
|
69
68
|
|
|
70
69
|
const CAMERA_EYE_OFFSET = 0.45
|
|
71
70
|
const LOOK_SENSITIVITY = 0.002
|
|
@@ -78,7 +77,11 @@ const ELEVATOR_COLLIDER_FLOOR_THICKNESS = 0.08
|
|
|
78
77
|
const ELEVATOR_COLLIDER_DOOR_DEPTH = 0.12
|
|
79
78
|
const ELEVATOR_ENTRY_DOOR_OPEN_THRESHOLD = 0.72
|
|
80
79
|
const DEFAULT_ELEVATOR_LEVEL_HEIGHT = 2.5
|
|
81
|
-
const
|
|
80
|
+
const VOID_FALL_RESPAWN_DEPTH = 12
|
|
81
|
+
|
|
82
|
+
type MovementKeyName = Exclude<keyof MovementInput, 'joystick'>
|
|
83
|
+
|
|
84
|
+
const movementKeyboardBindings: Array<{ name: MovementKeyName; keys: string[] }> = [
|
|
82
85
|
{ name: 'forward', keys: ['ArrowUp', 'KeyW'] },
|
|
83
86
|
{ name: 'backward', keys: ['ArrowDown', 'KeyS'] },
|
|
84
87
|
{ name: 'leftward', keys: ['ArrowLeft', 'KeyA'] },
|
|
@@ -86,6 +89,36 @@ const keyboardMap = [
|
|
|
86
89
|
{ name: 'jump', keys: ['Space'] },
|
|
87
90
|
{ name: 'run', keys: ['ShiftLeft', 'ShiftRight'] },
|
|
88
91
|
]
|
|
92
|
+
const keyboardMap = movementKeyboardBindings
|
|
93
|
+
const movementKeyToName = new Map<string, MovementKeyName>(
|
|
94
|
+
movementKeyboardBindings.flatMap(({ name, keys }) => keys.map((key) => [key, name] as const)),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
const inactiveMovementInput: MovementInput = {
|
|
98
|
+
backward: false,
|
|
99
|
+
forward: false,
|
|
100
|
+
jump: false,
|
|
101
|
+
leftward: false,
|
|
102
|
+
rightward: false,
|
|
103
|
+
run: false,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getMovementInputForKey(code: string, active: boolean): MovementInput | null {
|
|
107
|
+
const name = movementKeyToName.get(code)
|
|
108
|
+
return name ? ({ [name]: active } as MovementInput) : null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function focusFirstPersonCanvas(canvas: HTMLCanvasElement) {
|
|
112
|
+
const activeElement = document.activeElement
|
|
113
|
+
if (activeElement instanceof HTMLElement && !canvas.contains(activeElement)) {
|
|
114
|
+
activeElement.blur()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!canvas.hasAttribute('tabindex')) {
|
|
118
|
+
canvas.tabIndex = -1
|
|
119
|
+
}
|
|
120
|
+
canvas.focus({ preventScroll: true })
|
|
121
|
+
}
|
|
89
122
|
|
|
90
123
|
const cameraOffset = new Vector3(0, CAMERA_EYE_OFFSET, 0)
|
|
91
124
|
const cameraEuler = new Euler(0, 0, 0, 'YXZ')
|
|
@@ -535,6 +568,8 @@ export const FirstPersonControls = () => {
|
|
|
535
568
|
const selectedLevelId = useViewer((state) => state.selection.levelId)
|
|
536
569
|
const placedSpawnNode = useScene((state) => resolvePlacedSpawnNode(state.nodes, selectedLevelId))
|
|
537
570
|
const controllerRef = useRef<BVHEcctrlApi | null>(null)
|
|
571
|
+
const movementInputRef = useRef<MovementInput>({ ...inactiveMovementInput })
|
|
572
|
+
const hadPointerLockRef = useRef(false)
|
|
538
573
|
const yawRef = useRef(0)
|
|
539
574
|
const pitchRef = useRef(0)
|
|
540
575
|
const interactableTargetRef = useRef<FirstPersonInteractableTarget | null>(null)
|
|
@@ -577,6 +612,13 @@ export const FirstPersonControls = () => {
|
|
|
577
612
|
setIsElevatorRideLocked(locked)
|
|
578
613
|
}, [])
|
|
579
614
|
|
|
615
|
+
const setControllerApi = useCallback((api: BVHEcctrlApi | null) => {
|
|
616
|
+
controllerRef.current = api
|
|
617
|
+
if (api) {
|
|
618
|
+
api.setMovement(movementInputRef.current)
|
|
619
|
+
}
|
|
620
|
+
}, [])
|
|
621
|
+
|
|
580
622
|
const resolveInteractableDoorId = useCallback((): AnyNodeId | null => {
|
|
581
623
|
const nodes = useScene.getState().nodes
|
|
582
624
|
camera.updateMatrixWorld(true)
|
|
@@ -915,6 +957,14 @@ export const FirstPersonControls = () => {
|
|
|
915
957
|
})
|
|
916
958
|
}, [camera, controllerStart, placedSpawn, world])
|
|
917
959
|
|
|
960
|
+
useEffect(() => {
|
|
961
|
+
const canvas = gl.domElement
|
|
962
|
+
focusFirstPersonCanvas(canvas)
|
|
963
|
+
|
|
964
|
+
const frame = window.requestAnimationFrame(() => focusFirstPersonCanvas(canvas))
|
|
965
|
+
return () => window.cancelAnimationFrame(frame)
|
|
966
|
+
}, [gl])
|
|
967
|
+
|
|
918
968
|
useEffect(() => {
|
|
919
969
|
const canvas = gl.domElement
|
|
920
970
|
const handleMouseMove = (e: MouseEvent) => {
|
|
@@ -945,14 +995,29 @@ export const FirstPersonControls = () => {
|
|
|
945
995
|
toggleInteractableTarget()
|
|
946
996
|
}
|
|
947
997
|
|
|
998
|
+
const handlePointerLockChange = () => {
|
|
999
|
+
const isLocked = document.pointerLockElement === canvas
|
|
1000
|
+
if (isLocked) {
|
|
1001
|
+
hadPointerLockRef.current = true
|
|
1002
|
+
return
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
if (hadPointerLockRef.current && useEditor.getState().isFirstPersonMode) {
|
|
1006
|
+
useEditor.getState().setFirstPersonMode(false)
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
handlePointerLockChange()
|
|
948
1011
|
document.addEventListener('mousemove', handleMouseMove)
|
|
949
1012
|
document.addEventListener('click', handleClick)
|
|
950
1013
|
document.addEventListener('mousedown', handleMouseDown, true)
|
|
1014
|
+
document.addEventListener('pointerlockchange', handlePointerLockChange)
|
|
951
1015
|
|
|
952
1016
|
return () => {
|
|
953
1017
|
document.removeEventListener('mousemove', handleMouseMove)
|
|
954
1018
|
document.removeEventListener('click', handleClick)
|
|
955
1019
|
document.removeEventListener('mousedown', handleMouseDown, true)
|
|
1020
|
+
document.removeEventListener('pointerlockchange', handlePointerLockChange)
|
|
956
1021
|
if (document.pointerLockElement === canvas) {
|
|
957
1022
|
document.exitPointerLock()
|
|
958
1023
|
}
|
|
@@ -962,7 +1027,24 @@ export const FirstPersonControls = () => {
|
|
|
962
1027
|
useEffect(() => {
|
|
963
1028
|
const canvas = gl.domElement
|
|
964
1029
|
|
|
1030
|
+
const applyMovementKey = (event: KeyboardEvent, active: boolean) => {
|
|
1031
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
1032
|
+
return false
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const movement = getMovementInputForKey(event.code, active)
|
|
1036
|
+
if (!movement) return false
|
|
1037
|
+
|
|
1038
|
+
event.preventDefault()
|
|
1039
|
+
Object.assign(movementInputRef.current, movement)
|
|
1040
|
+
controllerRef.current?.setMovement(movement)
|
|
1041
|
+
return true
|
|
1042
|
+
}
|
|
1043
|
+
|
|
965
1044
|
const handleKeyDown = (event: KeyboardEvent) => {
|
|
1045
|
+
const handledMovement = applyMovementKey(event, true)
|
|
1046
|
+
if (handledMovement) return
|
|
1047
|
+
|
|
966
1048
|
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
967
1049
|
return
|
|
968
1050
|
}
|
|
@@ -985,9 +1067,15 @@ export const FirstPersonControls = () => {
|
|
|
985
1067
|
}
|
|
986
1068
|
}
|
|
987
1069
|
|
|
1070
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
1071
|
+
applyMovementKey(event, false)
|
|
1072
|
+
}
|
|
1073
|
+
|
|
988
1074
|
document.addEventListener('keydown', handleKeyDown, true)
|
|
1075
|
+
document.addEventListener('keyup', handleKeyUp, true)
|
|
989
1076
|
return () => {
|
|
990
1077
|
document.removeEventListener('keydown', handleKeyDown, true)
|
|
1078
|
+
document.removeEventListener('keyup', handleKeyUp, true)
|
|
991
1079
|
}
|
|
992
1080
|
}, [closeInteractableTarget, gl, toggleInteractableTarget])
|
|
993
1081
|
|
|
@@ -1217,6 +1305,29 @@ export const FirstPersonControls = () => {
|
|
|
1217
1305
|
if (!controllerRef.current?.group) return
|
|
1218
1306
|
|
|
1219
1307
|
const group = controllerRef.current.group
|
|
1308
|
+
|
|
1309
|
+
// The site ground collider is effectively unbounded, but scenes without a
|
|
1310
|
+
// site node only have finite fallback floors — if the controller still ends
|
|
1311
|
+
// up below every collider it can never land, so put it back at the spawn.
|
|
1312
|
+
// Prefer the live spawn node over the mount-time start position so a spawn
|
|
1313
|
+
// moved mid-walkthrough doesn't respawn the player at stale coordinates.
|
|
1314
|
+
const worldBounds = worldRef.current?.bounds
|
|
1315
|
+
if (worldBounds && group.position.y < worldBounds.min.y - VOID_FALL_RESPAWN_DEPTH) {
|
|
1316
|
+
const respawnPosition = placedSpawn
|
|
1317
|
+
? [
|
|
1318
|
+
placedSpawn.position[0],
|
|
1319
|
+
placedSpawn.position[1] - CONTROLLER_CENTER_FROM_EYE,
|
|
1320
|
+
placedSpawn.position[2],
|
|
1321
|
+
]
|
|
1322
|
+
: controllerStart?.position
|
|
1323
|
+
if (respawnPosition) {
|
|
1324
|
+
group.position.set(respawnPosition[0]!, respawnPosition[1]!, respawnPosition[2]!)
|
|
1325
|
+
controllerRef.current.resetLinVel()
|
|
1326
|
+
ridingElevatorRef.current = null
|
|
1327
|
+
setElevatorRideLocked(false)
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1220
1331
|
group.rotation.y = 0
|
|
1221
1332
|
camera.position.copy(group.position).add(cameraOffset)
|
|
1222
1333
|
cameraEuler.set(pitchRef.current, yawRef.current, 0, 'YXZ')
|
|
@@ -1277,14 +1388,14 @@ export const FirstPersonControls = () => {
|
|
|
1277
1388
|
floatSensorRadius={0.15}
|
|
1278
1389
|
floatSpringK={1200}
|
|
1279
1390
|
gravity={9.81}
|
|
1280
|
-
jumpVel={
|
|
1391
|
+
jumpVel={5}
|
|
1281
1392
|
key="first-person-controller"
|
|
1282
|
-
maxRunSpeed={5
|
|
1393
|
+
maxRunSpeed={5}
|
|
1283
1394
|
maxSlope={1.2}
|
|
1284
|
-
maxWalkSpeed={
|
|
1395
|
+
maxWalkSpeed={2}
|
|
1285
1396
|
paused={isElevatorRideLocked}
|
|
1286
1397
|
position={controllerStart.position}
|
|
1287
|
-
ref={
|
|
1398
|
+
ref={setControllerApi}
|
|
1288
1399
|
/>
|
|
1289
1400
|
</KeyboardControls>
|
|
1290
1401
|
)}
|