@pascal-app/core 0.7.0 → 0.8.0
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/dist/events/bus.d.ts +9 -6
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +1 -1
- package/dist/lib/polygon-geometry.d.ts.map +1 -1
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +10 -8
- package/dist/material-library.d.ts.map +1 -1
- package/dist/material-library.js +20 -1
- package/dist/schema/asset-url.test.js +0 -4
- package/dist/schema/material.d.ts +2 -2
- package/dist/schema/nodes/ceiling.d.ts +1 -1
- package/dist/schema/nodes/column.d.ts +1 -1
- package/dist/schema/nodes/door.d.ts +1 -1
- package/dist/schema/nodes/fence.d.ts +2 -2
- package/dist/schema/nodes/fence.js +2 -2
- package/dist/schema/nodes/item.d.ts +12 -0
- package/dist/schema/nodes/item.d.ts.map +1 -1
- package/dist/schema/nodes/item.js +12 -0
- package/dist/schema/nodes/roof-segment.d.ts +3 -3
- package/dist/schema/nodes/roof.d.ts +6 -6
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +5 -5
- package/dist/schema/nodes/site.d.ts +6 -0
- package/dist/schema/nodes/site.d.ts.map +1 -1
- package/dist/schema/nodes/slab.d.ts +1 -1
- package/dist/schema/nodes/stair-segment.d.ts +1 -1
- package/dist/schema/nodes/stair.d.ts +6 -6
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +9 -7
- package/dist/schema/nodes/wall.d.ts +3 -3
- package/dist/schema/nodes/window.d.ts +1 -1
- package/dist/schema/types.d.ts +33 -21
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +7 -5
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +11 -5
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
- package/dist/systems/stair/stair-opening-sync.js +17 -44
- package/dist/systems/stair/stair-opening-sync.test.js +0 -2
- package/dist/systems/wall/wall-curve.d.ts +1 -1
- package/dist/systems/wall/wall-curve.d.ts.map +1 -1
- package/dist/systems/wall/wall-curve.js +1 -1
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +2 -6
- package/package.json +4 -3
- package/dist/materials.d.ts +0 -10
- package/dist/materials.d.ts.map +0 -1
- package/dist/materials.js +0 -22
- package/dist/systems/ceiling/ceiling-system.d.ts +0 -8
- package/dist/systems/ceiling/ceiling-system.d.ts.map +0 -1
- package/dist/systems/ceiling/ceiling-system.js +0 -92
- package/dist/systems/door/door-system.d.ts +0 -2
- package/dist/systems/door/door-system.d.ts.map +0 -1
- package/dist/systems/door/door-system.js +0 -195
- package/dist/systems/fence/fence-system.d.ts +0 -2
- package/dist/systems/fence/fence-system.d.ts.map +0 -1
- package/dist/systems/fence/fence-system.js +0 -187
- package/dist/systems/item/item-system.d.ts +0 -2
- package/dist/systems/item/item-system.d.ts.map +0 -1
- package/dist/systems/item/item-system.js +0 -48
- package/dist/systems/roof/roof-system.d.ts +0 -16
- package/dist/systems/roof/roof-system.d.ts.map +0 -1
- package/dist/systems/roof/roof-system.js +0 -797
- package/dist/systems/slab/slab-system.d.ts +0 -8
- package/dist/systems/slab/slab-system.d.ts.map +0 -1
- package/dist/systems/slab/slab-system.js +0 -214
- package/dist/systems/stair/stair-system.d.ts +0 -2
- package/dist/systems/stair/stair-system.d.ts.map +0 -1
- package/dist/systems/stair/stair-system.js +0 -776
- package/dist/systems/wall/wall-system.d.ts +0 -12
- package/dist/systems/wall/wall-system.d.ts.map +0 -1
- package/dist/systems/wall/wall-system.js +0 -455
- package/dist/systems/window/window-system.d.ts +0 -2
- package/dist/systems/window/window-system.d.ts.map +0 -1
- package/dist/systems/window/window-system.js +0 -131
|
@@ -1,776 +0,0 @@
|
|
|
1
|
-
import { useFrame } from '@react-three/fiber';
|
|
2
|
-
import { useEffect, useRef } from 'react';
|
|
3
|
-
import * as THREE from 'three';
|
|
4
|
-
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
5
|
-
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
|
|
6
|
-
import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager';
|
|
7
|
-
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
|
|
8
|
-
import useScene from '../../store/use-scene';
|
|
9
|
-
import { syncAutoStairOpenings } from './stair-opening-sync';
|
|
10
|
-
const pendingStairUpdates = new Set();
|
|
11
|
-
const MAX_STAIRS_PER_FRAME = 2;
|
|
12
|
-
const MAX_SEGMENTS_PER_FRAME = 4;
|
|
13
|
-
const STAIR_TREAD_MATERIAL_INDEX = 0;
|
|
14
|
-
const STAIR_SIDE_MATERIAL_INDEX = 1;
|
|
15
|
-
const _uvPosition = new THREE.Vector3();
|
|
16
|
-
const _uvNormal = new THREE.Vector3();
|
|
17
|
-
// ============================================================================
|
|
18
|
-
// STAIR SYSTEM
|
|
19
|
-
// ============================================================================
|
|
20
|
-
export const StairSystem = () => {
|
|
21
|
-
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
22
|
-
const clearDirty = useScene((state) => state.clearDirty);
|
|
23
|
-
const rootNodeIds = useScene((state) => state.rootNodeIds);
|
|
24
|
-
const syncingAutoOpeningsRef = useRef(false);
|
|
25
|
-
useEffect(() => {
|
|
26
|
-
const applyUpdates = (updates) => {
|
|
27
|
-
if (updates.length === 0)
|
|
28
|
-
return;
|
|
29
|
-
syncingAutoOpeningsRef.current = true;
|
|
30
|
-
useScene.getState().updateNodes(updates);
|
|
31
|
-
queueMicrotask(() => {
|
|
32
|
-
syncingAutoOpeningsRef.current = false;
|
|
33
|
-
});
|
|
34
|
-
};
|
|
35
|
-
applyUpdates(syncAutoStairOpenings(useScene.getState().nodes));
|
|
36
|
-
return useScene.subscribe((state, prevState) => {
|
|
37
|
-
if (syncingAutoOpeningsRef.current)
|
|
38
|
-
return;
|
|
39
|
-
if (state.nodes === prevState.nodes)
|
|
40
|
-
return;
|
|
41
|
-
applyUpdates(syncAutoStairOpenings(state.nodes));
|
|
42
|
-
});
|
|
43
|
-
}, []);
|
|
44
|
-
useFrame(() => {
|
|
45
|
-
if (rootNodeIds.length === 0) {
|
|
46
|
-
pendingStairUpdates.clear();
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
if (dirtyNodes.size === 0 && pendingStairUpdates.size === 0)
|
|
50
|
-
return;
|
|
51
|
-
const nodes = useScene.getState().nodes;
|
|
52
|
-
// --- Pass 1: Process dirty stair-segments (throttled) ---
|
|
53
|
-
// Collect parent stair IDs that need segment transform recomputation
|
|
54
|
-
const parentsNeedingSegmentSync = new Set();
|
|
55
|
-
let segmentsProcessed = 0;
|
|
56
|
-
dirtyNodes.forEach((id) => {
|
|
57
|
-
const node = nodes[id];
|
|
58
|
-
if (!node)
|
|
59
|
-
return;
|
|
60
|
-
if (node.type === 'stair-segment') {
|
|
61
|
-
const mesh = sceneRegistry.nodes.get(id);
|
|
62
|
-
if (mesh) {
|
|
63
|
-
const isVisible = mesh.parent?.visible !== false;
|
|
64
|
-
if (isVisible && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
|
|
65
|
-
// Geometry will be updated; chained position is applied in the parent sync pass below
|
|
66
|
-
updateStairSegmentGeometry(node, mesh);
|
|
67
|
-
if (node.parentId)
|
|
68
|
-
parentsNeedingSegmentSync.add(node.parentId);
|
|
69
|
-
segmentsProcessed++;
|
|
70
|
-
}
|
|
71
|
-
else if (isVisible) {
|
|
72
|
-
return; // Over budget — keep dirty, process next frame
|
|
73
|
-
}
|
|
74
|
-
else if (mesh.geometry.type === 'BoxGeometry') {
|
|
75
|
-
// Replace BoxGeometry placeholder with empty geometry
|
|
76
|
-
mesh.geometry.dispose();
|
|
77
|
-
const placeholder = new THREE.BufferGeometry();
|
|
78
|
-
placeholder.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
|
|
79
|
-
mesh.geometry = placeholder;
|
|
80
|
-
}
|
|
81
|
-
clearDirty(id);
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
clearDirty(id);
|
|
85
|
-
}
|
|
86
|
-
// Queue the parent stair for a merged geometry update
|
|
87
|
-
if (node.parentId) {
|
|
88
|
-
pendingStairUpdates.add(node.parentId);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
else if (node.type === 'stair') {
|
|
92
|
-
pendingStairUpdates.add(id);
|
|
93
|
-
// Also sync individual segment positions when in edit mode
|
|
94
|
-
parentsNeedingSegmentSync.add(id);
|
|
95
|
-
clearDirty(id);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
// --- Pass 1b: Sync chained transforms to individual segment meshes (edit mode) ---
|
|
99
|
-
for (const stairId of parentsNeedingSegmentSync) {
|
|
100
|
-
const stairNode = nodes[stairId];
|
|
101
|
-
if (!stairNode || stairNode.type !== 'stair')
|
|
102
|
-
continue;
|
|
103
|
-
const group = sceneRegistry.nodes.get(stairId);
|
|
104
|
-
if (group) {
|
|
105
|
-
syncStairGroupElevation(stairNode, group, nodes);
|
|
106
|
-
}
|
|
107
|
-
syncSegmentMeshTransforms(stairNode, nodes);
|
|
108
|
-
}
|
|
109
|
-
// --- Pass 2: Process pending merged-stair updates (throttled) ---
|
|
110
|
-
let stairsProcessed = 0;
|
|
111
|
-
for (const id of pendingStairUpdates) {
|
|
112
|
-
if (stairsProcessed >= MAX_STAIRS_PER_FRAME)
|
|
113
|
-
break;
|
|
114
|
-
const node = nodes[id];
|
|
115
|
-
if (!node || node.type !== 'stair') {
|
|
116
|
-
pendingStairUpdates.delete(id);
|
|
117
|
-
continue;
|
|
118
|
-
}
|
|
119
|
-
const group = sceneRegistry.nodes.get(id);
|
|
120
|
-
if (group) {
|
|
121
|
-
const mergedMesh = group.getObjectByName('merged-stair');
|
|
122
|
-
if (mergedMesh?.visible !== false) {
|
|
123
|
-
updateMergedStairGeometry(node, group, nodes);
|
|
124
|
-
stairsProcessed++;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
pendingStairUpdates.delete(id);
|
|
128
|
-
}
|
|
129
|
-
}, 5);
|
|
130
|
-
return null;
|
|
131
|
-
};
|
|
132
|
-
// ============================================================================
|
|
133
|
-
// SEGMENT GEOMETRY
|
|
134
|
-
// ============================================================================
|
|
135
|
-
/**
|
|
136
|
-
* Generates the step/landing profile as a THREE.Shape (in the XY plane),
|
|
137
|
-
* then extrudes along Z for the segment width.
|
|
138
|
-
*/
|
|
139
|
-
function generateStairSegmentGeometry(segment, absoluteHeight) {
|
|
140
|
-
const { width, length, height, stepCount, segmentType, fillToFloor, thickness } = segment;
|
|
141
|
-
const shape = new THREE.Shape();
|
|
142
|
-
if (segmentType === 'landing') {
|
|
143
|
-
shape.moveTo(0, 0);
|
|
144
|
-
shape.lineTo(length, 0);
|
|
145
|
-
if (fillToFloor) {
|
|
146
|
-
shape.lineTo(length, -absoluteHeight);
|
|
147
|
-
shape.lineTo(0, -absoluteHeight);
|
|
148
|
-
}
|
|
149
|
-
else {
|
|
150
|
-
shape.lineTo(length, -thickness);
|
|
151
|
-
shape.lineTo(0, -thickness);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
else {
|
|
155
|
-
const riserHeight = height / stepCount;
|
|
156
|
-
const treadDepth = length / stepCount;
|
|
157
|
-
shape.moveTo(0, 0);
|
|
158
|
-
// Draw step profile
|
|
159
|
-
for (let i = 0; i < stepCount; i++) {
|
|
160
|
-
shape.lineTo(i * treadDepth, (i + 1) * riserHeight);
|
|
161
|
-
shape.lineTo((i + 1) * treadDepth, (i + 1) * riserHeight);
|
|
162
|
-
}
|
|
163
|
-
if (fillToFloor) {
|
|
164
|
-
shape.lineTo(length, -absoluteHeight);
|
|
165
|
-
shape.lineTo(0, -absoluteHeight);
|
|
166
|
-
}
|
|
167
|
-
else {
|
|
168
|
-
// Sloped bottom with consistent thickness
|
|
169
|
-
const angle = Math.atan(riserHeight / treadDepth);
|
|
170
|
-
const vOff = thickness / Math.cos(angle);
|
|
171
|
-
// Bottom-back corner
|
|
172
|
-
shape.lineTo(length, height - vOff);
|
|
173
|
-
if (absoluteHeight === 0) {
|
|
174
|
-
// Ground floor: slope hits the ground (y=0)
|
|
175
|
-
const m = riserHeight / treadDepth;
|
|
176
|
-
const xGround = length - (height - vOff) / m;
|
|
177
|
-
if (xGround > 0) {
|
|
178
|
-
shape.lineTo(xGround, 0);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
else {
|
|
182
|
-
// Floating: parallel slope
|
|
183
|
-
shape.lineTo(0, -vOff);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
shape.lineTo(0, 0);
|
|
188
|
-
const extrudedGeometry = new THREE.ExtrudeGeometry(shape, {
|
|
189
|
-
steps: 1,
|
|
190
|
-
depth: width,
|
|
191
|
-
bevelEnabled: false,
|
|
192
|
-
});
|
|
193
|
-
// Rotate so extrusion is along X (width), and the shape is in the XZ plane
|
|
194
|
-
// Shape is drawn in XY, extruded along Z → rotate -90° around Y then offset
|
|
195
|
-
const matrix = new THREE.Matrix4();
|
|
196
|
-
matrix.makeRotationY(-Math.PI / 2);
|
|
197
|
-
matrix.setPosition(width / 2, 0, 0);
|
|
198
|
-
extrudedGeometry.applyMatrix4(matrix);
|
|
199
|
-
extrudedGeometry.computeVertexNormals();
|
|
200
|
-
const geometry = extrudedGeometry.index ? extrudedGeometry.toNonIndexed() : extrudedGeometry;
|
|
201
|
-
if (geometry !== extrudedGeometry) {
|
|
202
|
-
extrudedGeometry.dispose();
|
|
203
|
-
}
|
|
204
|
-
applyStairSegmentUvs(geometry);
|
|
205
|
-
ensureUv2Attribute(geometry);
|
|
206
|
-
return geometry;
|
|
207
|
-
}
|
|
208
|
-
function updateStairSegmentGeometry(node, mesh) {
|
|
209
|
-
// Compute absolute height from parent chain
|
|
210
|
-
const absoluteHeight = computeAbsoluteHeight(node);
|
|
211
|
-
const newGeometry = generateStairSegmentGeometry(node, absoluteHeight);
|
|
212
|
-
applyStraightStairMaterialGroups(newGeometry);
|
|
213
|
-
mesh.geometry.dispose();
|
|
214
|
-
mesh.geometry = newGeometry;
|
|
215
|
-
// NOTE: position/rotation are NOT set here — they're set by syncSegmentMeshTransforms
|
|
216
|
-
// which computes the chained position based on segment order and attachmentSide.
|
|
217
|
-
}
|
|
218
|
-
/**
|
|
219
|
-
* Applies chained transforms to individual segment meshes (edit mode).
|
|
220
|
-
* Each segment's world position is determined by the chain of previous segments,
|
|
221
|
-
* not by the node's stored position field.
|
|
222
|
-
*/
|
|
223
|
-
function syncSegmentMeshTransforms(stairNode, nodes) {
|
|
224
|
-
const segments = (stairNode.children ?? [])
|
|
225
|
-
.map((childId) => nodes[childId])
|
|
226
|
-
.filter((n) => n?.type === 'stair-segment');
|
|
227
|
-
if (segments.length === 0)
|
|
228
|
-
return;
|
|
229
|
-
const transforms = computeSegmentTransforms(segments);
|
|
230
|
-
for (let i = 0; i < segments.length; i++) {
|
|
231
|
-
const segment = segments[i];
|
|
232
|
-
const transform = transforms[i];
|
|
233
|
-
const mesh = sceneRegistry.nodes.get(segment.id);
|
|
234
|
-
if (mesh) {
|
|
235
|
-
mesh.position.set(transform.position[0], transform.position[1], transform.position[2]);
|
|
236
|
-
mesh.rotation.y = transform.rotation;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function syncStairGroupElevation(stairNode, group, nodes) {
|
|
241
|
-
const levelId = resolveLevelId(stairNode, nodes);
|
|
242
|
-
const slabElevation = getStairSlabElevation(levelId, stairNode, nodes);
|
|
243
|
-
group.position.y = stairNode.position[1] + slabElevation;
|
|
244
|
-
}
|
|
245
|
-
function getStairSlabElevation(levelId, stairNode, nodes) {
|
|
246
|
-
const segments = (stairNode.children ?? [])
|
|
247
|
-
.map((childId) => nodes[childId])
|
|
248
|
-
.filter((n) => n?.type === 'stair-segment');
|
|
249
|
-
if (segments.length === 0)
|
|
250
|
-
return 0;
|
|
251
|
-
const transforms = computeSegmentTransforms(segments);
|
|
252
|
-
let maxElevation = Number.NEGATIVE_INFINITY;
|
|
253
|
-
for (let i = 0; i < segments.length; i++) {
|
|
254
|
-
const segment = segments[i];
|
|
255
|
-
const transform = transforms[i];
|
|
256
|
-
const [centerOffsetX, centerOffsetZ] = rotateXZ(0, segment.length / 2, transform.rotation);
|
|
257
|
-
const centerInGroupX = transform.position[0] + centerOffsetX;
|
|
258
|
-
const centerInGroupZ = transform.position[2] + centerOffsetZ;
|
|
259
|
-
const [centerOffsetWorldX, centerOffsetWorldZ] = rotateXZ(centerInGroupX, centerInGroupZ, stairNode.rotation);
|
|
260
|
-
const slabElevation = spatialGridManager.getSlabElevationForItem(levelId, [
|
|
261
|
-
stairNode.position[0] + centerOffsetWorldX,
|
|
262
|
-
stairNode.position[1] + transform.position[1],
|
|
263
|
-
stairNode.position[2] + centerOffsetWorldZ,
|
|
264
|
-
], [segment.width, Math.max(segment.height, segment.thickness, 0.01), segment.length], [0, stairNode.rotation + transform.rotation, 0]);
|
|
265
|
-
if (slabElevation > maxElevation) {
|
|
266
|
-
maxElevation = slabElevation;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation;
|
|
270
|
-
}
|
|
271
|
-
// ============================================================================
|
|
272
|
-
// MERGED STAIR GEOMETRY
|
|
273
|
-
// ============================================================================
|
|
274
|
-
const _matrix = new THREE.Matrix4();
|
|
275
|
-
const _position = new THREE.Vector3();
|
|
276
|
-
const _quaternion = new THREE.Quaternion();
|
|
277
|
-
const _scale = new THREE.Vector3(1, 1, 1);
|
|
278
|
-
const _yAxis = new THREE.Vector3(0, 1, 0);
|
|
279
|
-
function updateMergedStairGeometry(stairNode, group, nodes) {
|
|
280
|
-
const mergedMesh = group.getObjectByName('merged-stair');
|
|
281
|
-
if (!mergedMesh)
|
|
282
|
-
return;
|
|
283
|
-
if (stairNode.stairType === 'curved' || stairNode.stairType === 'spiral') {
|
|
284
|
-
replaceMeshGeometry(mergedMesh, createEmptyGeometry());
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
const children = stairNode.children ?? [];
|
|
288
|
-
const segments = children
|
|
289
|
-
.map((childId) => nodes[childId])
|
|
290
|
-
.filter((n) => n?.type === 'stair-segment');
|
|
291
|
-
if (segments.length === 0) {
|
|
292
|
-
replaceMeshGeometry(mergedMesh, createEmptyGeometry());
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
// Compute chained transforms for segments
|
|
296
|
-
const transforms = computeSegmentTransforms(segments);
|
|
297
|
-
const geometries = [];
|
|
298
|
-
for (let i = 0; i < segments.length; i++) {
|
|
299
|
-
const segment = segments[i];
|
|
300
|
-
const transform = transforms[i];
|
|
301
|
-
const absoluteHeight = transform.position[1];
|
|
302
|
-
const geo = generateStairSegmentGeometry(segment, absoluteHeight);
|
|
303
|
-
// Apply segment transform (position + rotation) relative to parent stair
|
|
304
|
-
_position.set(transform.position[0], transform.position[1], transform.position[2]);
|
|
305
|
-
_quaternion.setFromAxisAngle(_yAxis, transform.rotation);
|
|
306
|
-
_matrix.compose(_position, _quaternion, _scale);
|
|
307
|
-
geo.applyMatrix4(_matrix);
|
|
308
|
-
geometries.push(geo);
|
|
309
|
-
}
|
|
310
|
-
const merged = mergeGeometries(geometries, false) ?? createEmptyGeometry();
|
|
311
|
-
applyStraightStairMaterialGroups(merged);
|
|
312
|
-
replaceMeshGeometry(mergedMesh, merged);
|
|
313
|
-
// Dispose individual geometries
|
|
314
|
-
for (const geo of geometries) {
|
|
315
|
-
geo.dispose();
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
function applyStraightStairMaterialGroups(geometry) {
|
|
319
|
-
const position = geometry.getAttribute('position');
|
|
320
|
-
if (!position || position.count < 3) {
|
|
321
|
-
geometry.clearGroups();
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const index = geometry.getIndex();
|
|
325
|
-
const triangleCount = index ? index.count / 3 : position.count / 3;
|
|
326
|
-
if (!Number.isFinite(triangleCount) || triangleCount <= 0) {
|
|
327
|
-
geometry.clearGroups();
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
const triangleMaterials = new Array(triangleCount);
|
|
331
|
-
const v0 = new THREE.Vector3();
|
|
332
|
-
const v1 = new THREE.Vector3();
|
|
333
|
-
const v2 = new THREE.Vector3();
|
|
334
|
-
const edge1 = new THREE.Vector3();
|
|
335
|
-
const edge2 = new THREE.Vector3();
|
|
336
|
-
const normal = new THREE.Vector3();
|
|
337
|
-
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex++) {
|
|
338
|
-
const vertexOffset = triangleIndex * 3;
|
|
339
|
-
const a = index ? index.getX(vertexOffset) : vertexOffset;
|
|
340
|
-
const b = index ? index.getX(vertexOffset + 1) : vertexOffset + 1;
|
|
341
|
-
const c = index ? index.getX(vertexOffset + 2) : vertexOffset + 2;
|
|
342
|
-
v0.fromBufferAttribute(position, a);
|
|
343
|
-
v1.fromBufferAttribute(position, b);
|
|
344
|
-
v2.fromBufferAttribute(position, c);
|
|
345
|
-
edge1.subVectors(v1, v0);
|
|
346
|
-
edge2.subVectors(v2, v0);
|
|
347
|
-
normal.crossVectors(edge1, edge2);
|
|
348
|
-
triangleMaterials[triangleIndex] =
|
|
349
|
-
normal.lengthSq() > 0 && normal.normalize().y > 0.75
|
|
350
|
-
? STAIR_TREAD_MATERIAL_INDEX
|
|
351
|
-
: STAIR_SIDE_MATERIAL_INDEX;
|
|
352
|
-
}
|
|
353
|
-
geometry.clearGroups();
|
|
354
|
-
let currentMaterial = triangleMaterials[0];
|
|
355
|
-
let groupStart = 0;
|
|
356
|
-
for (let triangleIndex = 1; triangleIndex < triangleMaterials.length; triangleIndex++) {
|
|
357
|
-
const materialIndex = triangleMaterials[triangleIndex];
|
|
358
|
-
if (materialIndex === currentMaterial)
|
|
359
|
-
continue;
|
|
360
|
-
geometry.addGroup(groupStart * 3, (triangleIndex - groupStart) * 3, currentMaterial);
|
|
361
|
-
groupStart = triangleIndex;
|
|
362
|
-
currentMaterial = materialIndex;
|
|
363
|
-
}
|
|
364
|
-
geometry.addGroup(groupStart * 3, (triangleMaterials.length - groupStart) * 3, currentMaterial ?? STAIR_SIDE_MATERIAL_INDEX);
|
|
365
|
-
}
|
|
366
|
-
function applyStairSegmentUvs(geometry) {
|
|
367
|
-
const position = geometry.getAttribute('position');
|
|
368
|
-
const normal = geometry.getAttribute('normal');
|
|
369
|
-
if (!position || !normal || position.count === 0) {
|
|
370
|
-
geometry.deleteAttribute('uv');
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
const uv = [];
|
|
374
|
-
for (let index = 0; index < position.count; index++) {
|
|
375
|
-
_uvPosition.fromBufferAttribute(position, index);
|
|
376
|
-
_uvNormal.fromBufferAttribute(normal, index).normalize();
|
|
377
|
-
const absX = Math.abs(_uvNormal.x);
|
|
378
|
-
const absY = Math.abs(_uvNormal.y);
|
|
379
|
-
const absZ = Math.abs(_uvNormal.z);
|
|
380
|
-
if (absY >= absX && absY >= absZ) {
|
|
381
|
-
uv.push(_uvPosition.x, _uvPosition.z);
|
|
382
|
-
}
|
|
383
|
-
else if (absX >= absZ) {
|
|
384
|
-
uv.push(_uvPosition.z, _uvPosition.y);
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
uv.push(_uvPosition.x, _uvPosition.y);
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2));
|
|
391
|
-
}
|
|
392
|
-
function ensureUv2Attribute(geometry) {
|
|
393
|
-
const uv = geometry.getAttribute('uv');
|
|
394
|
-
if (!uv)
|
|
395
|
-
return;
|
|
396
|
-
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
|
|
397
|
-
}
|
|
398
|
-
/**
|
|
399
|
-
* Computes world-relative transforms for each segment by chaining
|
|
400
|
-
* based on attachmentSide. This mirrors the prototype's StairSystem logic.
|
|
401
|
-
*/
|
|
402
|
-
function computeSegmentTransforms(segments) {
|
|
403
|
-
const transforms = [];
|
|
404
|
-
let currentPos = new THREE.Vector3(0, 0, 0);
|
|
405
|
-
let currentRot = 0;
|
|
406
|
-
for (let i = 0; i < segments.length; i++) {
|
|
407
|
-
const segment = segments[i];
|
|
408
|
-
if (i === 0) {
|
|
409
|
-
transforms.push({
|
|
410
|
-
position: [currentPos.x, currentPos.y, currentPos.z],
|
|
411
|
-
rotation: currentRot,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
else {
|
|
415
|
-
const prev = segments[i - 1];
|
|
416
|
-
const localAttachPos = new THREE.Vector3();
|
|
417
|
-
let rotChange = 0;
|
|
418
|
-
switch (segment.attachmentSide) {
|
|
419
|
-
case 'front':
|
|
420
|
-
localAttachPos.set(0, prev.height, prev.length);
|
|
421
|
-
rotChange = 0;
|
|
422
|
-
break;
|
|
423
|
-
case 'left':
|
|
424
|
-
localAttachPos.set(prev.width / 2, prev.height, prev.length / 2);
|
|
425
|
-
rotChange = Math.PI / 2;
|
|
426
|
-
break;
|
|
427
|
-
case 'right':
|
|
428
|
-
localAttachPos.set(-prev.width / 2, prev.height, prev.length / 2);
|
|
429
|
-
rotChange = -Math.PI / 2;
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
// Rotate local attachment point by previous global rotation
|
|
433
|
-
localAttachPos.applyAxisAngle(new THREE.Vector3(0, 1, 0), currentRot);
|
|
434
|
-
currentPos = currentPos.clone().add(localAttachPos);
|
|
435
|
-
currentRot += rotChange;
|
|
436
|
-
transforms.push({
|
|
437
|
-
position: [currentPos.x, currentPos.y, currentPos.z],
|
|
438
|
-
rotation: currentRot,
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
return transforms;
|
|
443
|
-
}
|
|
444
|
-
function rotateXZ(x, z, angle) {
|
|
445
|
-
const cos = Math.cos(angle);
|
|
446
|
-
const sin = Math.sin(angle);
|
|
447
|
-
return [x * cos + z * sin, -x * sin + z * cos];
|
|
448
|
-
}
|
|
449
|
-
function createEmptyGeometry() {
|
|
450
|
-
const geometry = new THREE.BufferGeometry();
|
|
451
|
-
geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
|
|
452
|
-
geometry.addGroup(0, 0, STAIR_TREAD_MATERIAL_INDEX);
|
|
453
|
-
geometry.addGroup(0, 0, STAIR_SIDE_MATERIAL_INDEX);
|
|
454
|
-
return geometry;
|
|
455
|
-
}
|
|
456
|
-
function replaceMeshGeometry(mesh, geometry) {
|
|
457
|
-
mesh.geometry.dispose();
|
|
458
|
-
mesh.geometry = geometry;
|
|
459
|
-
}
|
|
460
|
-
function generateStairRailingGeometry(stairNode, segments, transforms) {
|
|
461
|
-
const railingMode = stairNode.railingMode ?? 'none';
|
|
462
|
-
if (railingMode === 'none') {
|
|
463
|
-
return createEmptyGeometry();
|
|
464
|
-
}
|
|
465
|
-
const railHeight = Math.max(0.5, stairNode.railingHeight ?? 0.92);
|
|
466
|
-
const midRailHeight = Math.max(railHeight * 0.45, 0.35);
|
|
467
|
-
const railRadius = 0.022;
|
|
468
|
-
const postRadius = 0.018;
|
|
469
|
-
const inset = 0.06;
|
|
470
|
-
const landingInset = 0.08;
|
|
471
|
-
const geometries = [];
|
|
472
|
-
const segmentRailPaths = buildStairRailPaths(segments, transforms, railingMode, inset, landingInset);
|
|
473
|
-
for (const segmentRailPath of segmentRailPaths) {
|
|
474
|
-
for (const sidePath of segmentRailPath.sidePaths) {
|
|
475
|
-
const points = sidePath.points;
|
|
476
|
-
if (points.length === 0)
|
|
477
|
-
continue;
|
|
478
|
-
geometries.push(...buildBalusterGeometries(points, railHeight, postRadius));
|
|
479
|
-
geometries.push(...buildOffsetRailSegmentGeometries(points, railHeight, railRadius));
|
|
480
|
-
geometries.push(...buildOffsetRailSegmentGeometries(points, midRailHeight, railRadius * 0.8));
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
for (let index = 1; index < segmentRailPaths.length; index++) {
|
|
484
|
-
const previousPath = segmentRailPaths[index - 1];
|
|
485
|
-
const currentPath = segmentRailPaths[index];
|
|
486
|
-
if (!(previousPath && currentPath && currentPath.connectFromPrevious))
|
|
487
|
-
continue;
|
|
488
|
-
if (previousPath.segment.segmentType === 'landing')
|
|
489
|
-
continue;
|
|
490
|
-
for (const sidePath of currentPath.sidePaths) {
|
|
491
|
-
if (currentPath.segment.segmentType === 'landing')
|
|
492
|
-
continue;
|
|
493
|
-
const currentPoint = sidePath.points[0];
|
|
494
|
-
if (!currentPoint)
|
|
495
|
-
continue;
|
|
496
|
-
const previousSidePath = [...previousPath.sidePaths]
|
|
497
|
-
.map((entry) => ({
|
|
498
|
-
entry,
|
|
499
|
-
distance: entry.points.length
|
|
500
|
-
? entry.points[entry.points.length - 1].distanceTo(currentPoint)
|
|
501
|
-
: Number.POSITIVE_INFINITY,
|
|
502
|
-
}))
|
|
503
|
-
.sort((left, right) => left.distance - right.distance)[0]?.entry;
|
|
504
|
-
const previousPoint = previousSidePath && previousSidePath.points.length > 0
|
|
505
|
-
? previousSidePath.points[previousSidePath.points.length - 1]
|
|
506
|
-
: null;
|
|
507
|
-
if (!(previousPoint && currentPoint))
|
|
508
|
-
continue;
|
|
509
|
-
const connectorPoints = [previousPoint, currentPoint];
|
|
510
|
-
geometries.push(...buildOffsetRailSegmentGeometries(connectorPoints, railHeight, railRadius));
|
|
511
|
-
geometries.push(...buildOffsetRailSegmentGeometries(connectorPoints, midRailHeight, railRadius * 0.8));
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
const merged = mergeGeometries(geometries, false) ?? createEmptyGeometry();
|
|
515
|
-
for (const geometry of geometries) {
|
|
516
|
-
geometry.dispose();
|
|
517
|
-
}
|
|
518
|
-
return merged;
|
|
519
|
-
}
|
|
520
|
-
function buildStairRailPaths(segments, transforms, railingMode, inset, landingInset) {
|
|
521
|
-
const layouts = computeStairRailLayouts(segments, transforms);
|
|
522
|
-
if (railingMode === 'both') {
|
|
523
|
-
const isStraightLineDoubleLandingLayout = segments.length === 4 &&
|
|
524
|
-
segments[0]?.segmentType === 'stair' &&
|
|
525
|
-
segments[1]?.segmentType === 'landing' &&
|
|
526
|
-
segments[2]?.segmentType === 'stair' &&
|
|
527
|
-
segments[2]?.attachmentSide === 'front' &&
|
|
528
|
-
segments[3]?.segmentType === 'landing' &&
|
|
529
|
-
segments[3]?.attachmentSide === 'front';
|
|
530
|
-
return layouts.map((layout, index) => {
|
|
531
|
-
const segment = layout.segment;
|
|
532
|
-
const previousSegment = index > 0 ? segments[index - 1] : undefined;
|
|
533
|
-
const nextSegment = index < segments.length - 1 ? segments[index + 1] : undefined;
|
|
534
|
-
const hideLandingRailing = segment.segmentType === 'landing' &&
|
|
535
|
-
previousSegment?.segmentType === 'stair' &&
|
|
536
|
-
nextSegment?.segmentType === 'stair';
|
|
537
|
-
const visualTurnSide = nextSegment?.attachmentSide;
|
|
538
|
-
const sideCandidates = hideLandingRailing
|
|
539
|
-
? visualTurnSide === 'left'
|
|
540
|
-
? ['front', 'right']
|
|
541
|
-
: visualTurnSide === 'right'
|
|
542
|
-
? ['front', 'left']
|
|
543
|
-
: ['left', 'right']
|
|
544
|
-
: segment.segmentType === 'landing'
|
|
545
|
-
? nextSegment?.segmentType === 'landing' && visualTurnSide === 'left'
|
|
546
|
-
? ['front', 'right']
|
|
547
|
-
: nextSegment?.segmentType === 'landing' && visualTurnSide === 'right'
|
|
548
|
-
? ['front', 'left']
|
|
549
|
-
: visualTurnSide === 'left'
|
|
550
|
-
? ['right']
|
|
551
|
-
: visualTurnSide === 'right'
|
|
552
|
-
? ['left']
|
|
553
|
-
: ['left', 'right']
|
|
554
|
-
: ['left', 'right'];
|
|
555
|
-
const sidePaths = sideCandidates
|
|
556
|
-
.map((side) => buildSegmentRailPath(layout, side, previousSegment, nextSegment, inset, landingInset))
|
|
557
|
-
.filter((entry) => entry !== null);
|
|
558
|
-
return {
|
|
559
|
-
segment,
|
|
560
|
-
sidePaths: isStraightLineDoubleLandingLayout && index === 1
|
|
561
|
-
? (['left', 'right']
|
|
562
|
-
.map((side) => buildSegmentRailPath(layout, side, previousSegment, nextSegment, inset, landingInset))
|
|
563
|
-
.filter((entry) => entry !== null))
|
|
564
|
-
: sidePaths,
|
|
565
|
-
connectFromPrevious: index > 0 &&
|
|
566
|
-
!(previousSegment?.segmentType === 'landing' && segment.segmentType === 'landing'),
|
|
567
|
-
};
|
|
568
|
-
});
|
|
569
|
-
}
|
|
570
|
-
const isStraightLineDoubleLandingLayout = segments.length === 4 &&
|
|
571
|
-
segments[0]?.segmentType === 'stair' &&
|
|
572
|
-
segments[1]?.segmentType === 'landing' &&
|
|
573
|
-
segments[2]?.segmentType === 'stair' &&
|
|
574
|
-
segments[2]?.attachmentSide === 'front' &&
|
|
575
|
-
segments[3]?.segmentType === 'landing' &&
|
|
576
|
-
segments[3]?.attachmentSide === 'front';
|
|
577
|
-
const resolved = [];
|
|
578
|
-
layouts.forEach((layout, index) => {
|
|
579
|
-
const segment = layout.segment;
|
|
580
|
-
const previousSegment = index > 0 ? segments[index - 1] : undefined;
|
|
581
|
-
const nextSegment = index < segments.length - 1 ? segments[index + 1] : undefined;
|
|
582
|
-
const nextAttachmentSide = nextSegment?.attachmentSide;
|
|
583
|
-
const isMiddleLandingBetweenFlights = segment.segmentType === 'landing' &&
|
|
584
|
-
previousSegment?.segmentType === 'stair' &&
|
|
585
|
-
nextSegment?.segmentType === 'stair';
|
|
586
|
-
const suppressLandingRailing = segment.segmentType === 'landing' &&
|
|
587
|
-
nextSegment?.segmentType === 'landing' &&
|
|
588
|
-
nextAttachmentSide === railingMode;
|
|
589
|
-
const landingContinuesOnPreferredSide = segment.segmentType === 'landing'
|
|
590
|
-
? nextAttachmentSide == null ||
|
|
591
|
-
nextAttachmentSide === 'front' ||
|
|
592
|
-
nextAttachmentSide === railingMode
|
|
593
|
-
: true;
|
|
594
|
-
const sidePaths = suppressLandingRailing
|
|
595
|
-
? []
|
|
596
|
-
: segment.segmentType !== 'landing'
|
|
597
|
-
? [
|
|
598
|
-
buildSegmentRailPath(layout, railingMode, previousSegment, nextSegment, inset, landingInset),
|
|
599
|
-
]
|
|
600
|
-
: isStraightLineDoubleLandingLayout
|
|
601
|
-
? [
|
|
602
|
-
buildSegmentRailPath(layout, railingMode, previousSegment, nextSegment, inset, landingInset),
|
|
603
|
-
]
|
|
604
|
-
: isMiddleLandingBetweenFlights && railingMode === 'left'
|
|
605
|
-
? nextAttachmentSide === 'right'
|
|
606
|
-
? [
|
|
607
|
-
buildSegmentRailPath(layout, 'front', previousSegment, nextSegment, inset, landingInset),
|
|
608
|
-
buildSegmentRailPath(layout, 'left', previousSegment, nextSegment, inset, landingInset),
|
|
609
|
-
]
|
|
610
|
-
: []
|
|
611
|
-
: isMiddleLandingBetweenFlights && railingMode === 'right'
|
|
612
|
-
? nextAttachmentSide === 'left'
|
|
613
|
-
? [
|
|
614
|
-
buildSegmentRailPath(layout, 'front', previousSegment, nextSegment, inset, landingInset),
|
|
615
|
-
buildSegmentRailPath(layout, 'right', previousSegment, nextSegment, inset, landingInset),
|
|
616
|
-
]
|
|
617
|
-
: []
|
|
618
|
-
: nextSegment?.segmentType === 'landing' &&
|
|
619
|
-
nextAttachmentSide != null &&
|
|
620
|
-
nextAttachmentSide !== 'front' &&
|
|
621
|
-
nextAttachmentSide !== railingMode
|
|
622
|
-
? [
|
|
623
|
-
buildSegmentRailPath(layout, 'front', previousSegment, nextSegment, inset, landingInset),
|
|
624
|
-
buildSegmentRailPath(layout, railingMode, previousSegment, nextSegment, inset, landingInset),
|
|
625
|
-
]
|
|
626
|
-
: [
|
|
627
|
-
buildSegmentRailPath(layout, railingMode, previousSegment, nextSegment, inset, landingInset),
|
|
628
|
-
];
|
|
629
|
-
resolved.push({
|
|
630
|
-
segment,
|
|
631
|
-
sidePaths: sidePaths.filter((entry) => entry !== null),
|
|
632
|
-
connectFromPrevious: index > 0 &&
|
|
633
|
-
!suppressLandingRailing &&
|
|
634
|
-
sidePaths.length > 0 &&
|
|
635
|
-
(segment.segmentType === 'landing' ? landingContinuesOnPreferredSide : true),
|
|
636
|
-
});
|
|
637
|
-
});
|
|
638
|
-
return resolved;
|
|
639
|
-
}
|
|
640
|
-
function computeStairRailLayouts(segments, transforms) {
|
|
641
|
-
return segments.map((segment, index) => {
|
|
642
|
-
const transform = transforms[index];
|
|
643
|
-
const [centerOffsetX, centerOffsetZ] = rotateXZ(0, segment.length / 2, transform.rotation);
|
|
644
|
-
return {
|
|
645
|
-
center: [transform.position[0] + centerOffsetX, transform.position[2] + centerOffsetZ],
|
|
646
|
-
elevation: transform.position[1],
|
|
647
|
-
rotation: transform.rotation,
|
|
648
|
-
segment,
|
|
649
|
-
};
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
function buildSegmentRailPath(layout, side, previousSegment, nextSegment, inset, landingInset) {
|
|
653
|
-
const segment = layout.segment;
|
|
654
|
-
const segmentSteps = Math.max(1, segment.segmentType === 'landing' ? 1 : segment.stepCount);
|
|
655
|
-
const segmentStepDepth = segment.length / segmentSteps;
|
|
656
|
-
const segmentStepHeight = segment.segmentType === 'landing' ? 0 : segment.height / segmentSteps;
|
|
657
|
-
const segmentTopThickness = getSegmentTopThickness(segment);
|
|
658
|
-
const flightSideOffset = side === 'left' ? segment.width / 2 - 0.045 : -segment.width / 2 + 0.045;
|
|
659
|
-
const flightStartX = previousSegment?.segmentType === 'landing' ? -segment.length / 2 + landingInset : -segment.length / 2;
|
|
660
|
-
const flightEndX = nextSegment?.segmentType === 'landing' ? segment.length / 2 - landingInset : segment.length / 2;
|
|
661
|
-
if (segment.segmentType === 'landing') {
|
|
662
|
-
return buildLandingRailPathFromScratch(layout, side, previousSegment, nextSegment, segmentTopThickness, landingInset);
|
|
663
|
-
}
|
|
664
|
-
return {
|
|
665
|
-
side,
|
|
666
|
-
points: [
|
|
667
|
-
...(previousSegment?.segmentType === 'landing'
|
|
668
|
-
? []
|
|
669
|
-
: [
|
|
670
|
-
toRailLayoutWorldPoint(layout, flightStartX, segmentTopThickness, flightSideOffset),
|
|
671
|
-
]),
|
|
672
|
-
...Array.from({ length: segmentSteps }).map((_, index) => toRailLayoutWorldPoint(layout, -segment.length / 2 + segmentStepDepth * index + segmentStepDepth / 2, segmentStepHeight * (index + 1), flightSideOffset)),
|
|
673
|
-
...(nextSegment?.segmentType === 'landing'
|
|
674
|
-
? []
|
|
675
|
-
: [
|
|
676
|
-
toRailLayoutWorldPoint(layout, flightEndX, segment.height, flightSideOffset),
|
|
677
|
-
]),
|
|
678
|
-
],
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
function buildLandingRailPathFromScratch(layout, side, previousSegment, nextSegment, topY, inset) {
|
|
682
|
-
const segment = layout.segment;
|
|
683
|
-
const backX = -segment.length / 2 + inset;
|
|
684
|
-
const frontX = segment.length / 2 - inset;
|
|
685
|
-
const leftZ = segment.width / 2 - inset;
|
|
686
|
-
const rightZ = -segment.width / 2 + inset;
|
|
687
|
-
const edgePoints = side === 'left'
|
|
688
|
-
? [
|
|
689
|
-
toRailLayoutWorldPoint(layout, backX, topY, leftZ),
|
|
690
|
-
toRailLayoutWorldPoint(layout, frontX, topY, leftZ),
|
|
691
|
-
]
|
|
692
|
-
: side === 'right'
|
|
693
|
-
? [
|
|
694
|
-
toRailLayoutWorldPoint(layout, backX, topY, rightZ),
|
|
695
|
-
toRailLayoutWorldPoint(layout, frontX, topY, rightZ),
|
|
696
|
-
]
|
|
697
|
-
: [
|
|
698
|
-
// When the next flight turns, rail the visible leading edge nearest the turn opening.
|
|
699
|
-
toRailLayoutWorldPoint(layout, previousSegment?.segmentType === 'stair' &&
|
|
700
|
-
nextSegment?.attachmentSide &&
|
|
701
|
-
nextSegment.attachmentSide !== 'front'
|
|
702
|
-
? backX
|
|
703
|
-
: frontX, topY, leftZ),
|
|
704
|
-
toRailLayoutWorldPoint(layout, previousSegment?.segmentType === 'stair' &&
|
|
705
|
-
nextSegment?.attachmentSide &&
|
|
706
|
-
nextSegment.attachmentSide !== 'front'
|
|
707
|
-
? backX
|
|
708
|
-
: frontX, topY, rightZ),
|
|
709
|
-
];
|
|
710
|
-
return {
|
|
711
|
-
side,
|
|
712
|
-
points: edgePoints,
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
function toRailLayoutWorldPoint(layout, localX, localY, localZ) {
|
|
716
|
-
const [offsetX, offsetZ] = rotateXZ(localZ, localX, layout.rotation);
|
|
717
|
-
return new THREE.Vector3(layout.center[0] + offsetX, layout.elevation + localY, layout.center[1] + offsetZ);
|
|
718
|
-
}
|
|
719
|
-
function buildOffsetRailSegmentGeometries(points, heightOffset, radius) {
|
|
720
|
-
const geometries = [];
|
|
721
|
-
for (let index = 0; index < points.length - 1; index++) {
|
|
722
|
-
const start = points[index];
|
|
723
|
-
const end = points[index + 1];
|
|
724
|
-
if (!(start && end))
|
|
725
|
-
continue;
|
|
726
|
-
const segmentGeometry = createCylinderBetweenPoints(start.clone().add(new THREE.Vector3(0, heightOffset, 0)), end.clone().add(new THREE.Vector3(0, heightOffset, 0)), radius, 8);
|
|
727
|
-
if (segmentGeometry) {
|
|
728
|
-
geometries.push(segmentGeometry);
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return geometries;
|
|
732
|
-
}
|
|
733
|
-
function buildBalusterGeometries(points, height, radius) {
|
|
734
|
-
const geometries = [];
|
|
735
|
-
for (const point of points) {
|
|
736
|
-
const geometry = new THREE.CylinderGeometry(radius, radius, Math.max(height, 0.05), 8);
|
|
737
|
-
geometry.translate(point.x, point.y + height / 2, point.z);
|
|
738
|
-
geometries.push(geometry);
|
|
739
|
-
}
|
|
740
|
-
return geometries;
|
|
741
|
-
}
|
|
742
|
-
function getSegmentTopThickness(segment) {
|
|
743
|
-
return Math.max(segment.thickness ?? 0.25, 0.02);
|
|
744
|
-
}
|
|
745
|
-
function createCylinderBetweenPoints(start, end, radius, radialSegments) {
|
|
746
|
-
const direction = new THREE.Vector3().subVectors(end, start);
|
|
747
|
-
const length = direction.length();
|
|
748
|
-
if (length <= 1e-5)
|
|
749
|
-
return null;
|
|
750
|
-
const midpoint = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
|
|
751
|
-
const quaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize());
|
|
752
|
-
const geometry = new THREE.CylinderGeometry(radius, radius, length, radialSegments);
|
|
753
|
-
geometry.applyQuaternion(quaternion);
|
|
754
|
-
geometry.translate(midpoint.x, midpoint.y, midpoint.z);
|
|
755
|
-
return geometry;
|
|
756
|
-
}
|
|
757
|
-
/**
|
|
758
|
-
* Computes the absolute Y height of a segment by traversing the stair's segment chain.
|
|
759
|
-
*/
|
|
760
|
-
function computeAbsoluteHeight(node) {
|
|
761
|
-
const nodes = useScene.getState().nodes;
|
|
762
|
-
if (!node.parentId)
|
|
763
|
-
return 0;
|
|
764
|
-
const parent = nodes[node.parentId];
|
|
765
|
-
if (!parent || parent.type !== 'stair')
|
|
766
|
-
return 0;
|
|
767
|
-
const stair = parent;
|
|
768
|
-
const segments = (stair.children ?? [])
|
|
769
|
-
.map((childId) => nodes[childId])
|
|
770
|
-
.filter((n) => n?.type === 'stair-segment');
|
|
771
|
-
const transforms = computeSegmentTransforms(segments);
|
|
772
|
-
const index = segments.findIndex((s) => s.id === node.id);
|
|
773
|
-
if (index < 0)
|
|
774
|
-
return 0;
|
|
775
|
-
return transforms[index]?.position[1] ?? 0;
|
|
776
|
-
}
|