@pascal-app/core 0.5.0 → 0.6.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 +39 -4
- package/dist/events/bus.d.ts.map +1 -1
- package/dist/events/bus.js +1 -1
- package/dist/hooks/scene-registry/scene-registry.d.ts +1 -0
- package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
- package/dist/hooks/scene-registry/scene-registry.js +1 -0
- package/dist/hooks/spatial-grid/spatial-grid.d.ts +2 -0
- package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -1
- package/dist/hooks/spatial-grid/spatial-grid.js +43 -20
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/lib/polygon-geometry.d.ts +3 -0
- package/dist/lib/polygon-geometry.d.ts.map +1 -0
- package/dist/lib/polygon-geometry.js +90 -0
- package/dist/lib/space-detection.d.ts +10 -17
- package/dist/lib/space-detection.d.ts.map +1 -1
- package/dist/lib/space-detection.js +666 -453
- package/dist/material-library.d.ts +18 -0
- package/dist/material-library.d.ts.map +1 -0
- package/dist/material-library.js +603 -0
- package/dist/schema/index.d.ts +10 -4
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +6 -4
- package/dist/schema/material.d.ts +109 -0
- package/dist/schema/material.d.ts.map +1 -1
- package/dist/schema/material.js +52 -0
- package/dist/schema/nodes/ceiling.d.ts +10 -0
- package/dist/schema/nodes/ceiling.d.ts.map +1 -1
- package/dist/schema/nodes/ceiling.js +6 -0
- package/dist/schema/nodes/door.d.ts +1 -0
- package/dist/schema/nodes/door.d.ts.map +1 -1
- package/dist/schema/nodes/fence.d.ts +85 -0
- package/dist/schema/nodes/fence.d.ts.map +1 -0
- package/dist/schema/nodes/fence.js +34 -0
- package/dist/schema/nodes/item.d.ts +2 -2
- package/dist/schema/nodes/level.d.ts +1 -1
- package/dist/schema/nodes/level.d.ts.map +1 -1
- package/dist/schema/nodes/level.js +2 -0
- package/dist/schema/nodes/roof-segment.d.ts +2 -0
- package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
- package/dist/schema/nodes/roof-segment.js +1 -0
- package/dist/schema/nodes/roof.d.ts +108 -0
- package/dist/schema/nodes/roof.d.ts.map +1 -1
- package/dist/schema/nodes/roof.js +58 -2
- package/dist/schema/nodes/site.d.ts +1 -1
- package/dist/schema/nodes/slab.d.ts +10 -0
- package/dist/schema/nodes/slab.d.ts.map +1 -1
- package/dist/schema/nodes/slab.js +7 -0
- package/dist/schema/nodes/stair-segment.d.ts +2 -0
- package/dist/schema/nodes/stair-segment.d.ts.map +1 -1
- package/dist/schema/nodes/stair-segment.js +1 -0
- package/dist/schema/nodes/stair.d.ts +164 -0
- package/dist/schema/nodes/stair.d.ts.map +1 -1
- package/dist/schema/nodes/stair.js +106 -5
- package/dist/schema/nodes/surface-hole-metadata.d.ts +10 -0
- package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -0
- package/dist/schema/nodes/surface-hole-metadata.js +5 -0
- package/dist/schema/nodes/wall.d.ts +87 -1
- package/dist/schema/nodes/wall.d.ts.map +1 -1
- package/dist/schema/nodes/wall.js +45 -4
- package/dist/schema/nodes/window.d.ts +1 -0
- package/dist/schema/nodes/window.d.ts.map +1 -1
- package/dist/schema/types.d.ts +406 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/types.js +2 -0
- package/dist/store/actions/node-actions.d.ts +1 -1
- package/dist/store/actions/node-actions.d.ts.map +1 -1
- package/dist/store/actions/node-actions.js +175 -0
- package/dist/store/history-control.d.ts +14 -0
- package/dist/store/history-control.d.ts.map +1 -0
- package/dist/store/history-control.js +22 -0
- package/dist/store/use-scene.d.ts.map +1 -1
- package/dist/store/use-scene.js +249 -3
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +7 -0
- package/dist/systems/fence/fence-system.d.ts +2 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -0
- package/dist/systems/fence/fence-system.js +187 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +31 -1
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +45 -8
- package/dist/systems/stair/stair-opening-sync.d.ts +6 -0
- package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -0
- package/dist/systems/stair/stair-opening-sync.js +515 -0
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +432 -10
- package/dist/systems/wall/wall-curve.d.ts +43 -0
- package/dist/systems/wall/wall-curve.d.ts.map +1 -0
- package/dist/systems/wall/wall-curve.js +176 -0
- package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
- package/dist/systems/wall/wall-footprint.js +16 -2
- package/dist/systems/wall/wall-mitering.d.ts +7 -0
- package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
- package/dist/systems/wall/wall-mitering.js +76 -3
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +202 -2
- package/package.json +3 -3
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { useFrame } from '@react-three/fiber';
|
|
2
|
+
import { useEffect, useRef } from 'react';
|
|
2
3
|
import * as THREE from 'three';
|
|
3
4
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
4
5
|
import { sceneRegistry } from '../../hooks/scene-registry/scene-registry';
|
|
5
6
|
import { spatialGridManager } from '../../hooks/spatial-grid/spatial-grid-manager';
|
|
6
7
|
import { resolveLevelId } from '../../hooks/spatial-grid/spatial-grid-sync';
|
|
7
8
|
import useScene from '../../store/use-scene';
|
|
9
|
+
import { syncAutoStairOpenings } from './stair-opening-sync';
|
|
8
10
|
const pendingStairUpdates = new Set();
|
|
9
11
|
const MAX_STAIRS_PER_FRAME = 2;
|
|
10
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();
|
|
11
17
|
// ============================================================================
|
|
12
18
|
// STAIR SYSTEM
|
|
13
19
|
// ============================================================================
|
|
@@ -15,6 +21,26 @@ export const StairSystem = () => {
|
|
|
15
21
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
16
22
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
17
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
|
+
}, []);
|
|
18
44
|
useFrame(() => {
|
|
19
45
|
if (rootNodeIds.length === 0) {
|
|
20
46
|
pendingStairUpdates.clear();
|
|
@@ -159,7 +185,7 @@ function generateStairSegmentGeometry(segment, absoluteHeight) {
|
|
|
159
185
|
}
|
|
160
186
|
}
|
|
161
187
|
shape.lineTo(0, 0);
|
|
162
|
-
const
|
|
188
|
+
const extrudedGeometry = new THREE.ExtrudeGeometry(shape, {
|
|
163
189
|
steps: 1,
|
|
164
190
|
depth: width,
|
|
165
191
|
bevelEnabled: false,
|
|
@@ -169,13 +195,21 @@ function generateStairSegmentGeometry(segment, absoluteHeight) {
|
|
|
169
195
|
const matrix = new THREE.Matrix4();
|
|
170
196
|
matrix.makeRotationY(-Math.PI / 2);
|
|
171
197
|
matrix.setPosition(width / 2, 0, 0);
|
|
172
|
-
|
|
198
|
+
extrudedGeometry.applyMatrix4(matrix);
|
|
199
|
+
extrudedGeometry.computeVertexNormals();
|
|
200
|
+
const geometry = extrudedGeometry.toNonIndexed() ?? extrudedGeometry;
|
|
201
|
+
if (geometry !== extrudedGeometry) {
|
|
202
|
+
extrudedGeometry.dispose();
|
|
203
|
+
}
|
|
204
|
+
applyStairSegmentUvs(geometry);
|
|
205
|
+
ensureUv2Attribute(geometry);
|
|
173
206
|
return geometry;
|
|
174
207
|
}
|
|
175
208
|
function updateStairSegmentGeometry(node, mesh) {
|
|
176
209
|
// Compute absolute height from parent chain
|
|
177
210
|
const absoluteHeight = computeAbsoluteHeight(node);
|
|
178
211
|
const newGeometry = generateStairSegmentGeometry(node, absoluteHeight);
|
|
212
|
+
applyStraightStairMaterialGroups(newGeometry);
|
|
179
213
|
mesh.geometry.dispose();
|
|
180
214
|
mesh.geometry = newGeometry;
|
|
181
215
|
// NOTE: position/rotation are NOT set here — they're set by syncSegmentMeshTransforms
|
|
@@ -246,14 +280,16 @@ function updateMergedStairGeometry(stairNode, group, nodes) {
|
|
|
246
280
|
const mergedMesh = group.getObjectByName('merged-stair');
|
|
247
281
|
if (!mergedMesh)
|
|
248
282
|
return;
|
|
283
|
+
if (stairNode.stairType === 'curved' || stairNode.stairType === 'spiral') {
|
|
284
|
+
replaceMeshGeometry(mergedMesh, createEmptyGeometry());
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
249
287
|
const children = stairNode.children ?? [];
|
|
250
288
|
const segments = children
|
|
251
289
|
.map((childId) => nodes[childId])
|
|
252
290
|
.filter((n) => n?.type === 'stair-segment');
|
|
253
291
|
if (segments.length === 0) {
|
|
254
|
-
mergedMesh
|
|
255
|
-
mergedMesh.geometry = new THREE.BufferGeometry();
|
|
256
|
-
mergedMesh.geometry.setAttribute('position', new THREE.Float32BufferAttribute([], 3));
|
|
292
|
+
replaceMeshGeometry(mergedMesh, createEmptyGeometry());
|
|
257
293
|
return;
|
|
258
294
|
}
|
|
259
295
|
// Compute chained transforms for segments
|
|
@@ -271,16 +307,94 @@ function updateMergedStairGeometry(stairNode, group, nodes) {
|
|
|
271
307
|
geo.applyMatrix4(_matrix);
|
|
272
308
|
geometries.push(geo);
|
|
273
309
|
}
|
|
274
|
-
const merged = mergeGeometries(geometries, false);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
mergedMesh.geometry = merged;
|
|
278
|
-
}
|
|
310
|
+
const merged = mergeGeometries(geometries, false) ?? createEmptyGeometry();
|
|
311
|
+
applyStraightStairMaterialGroups(merged);
|
|
312
|
+
replaceMeshGeometry(mergedMesh, merged);
|
|
279
313
|
// Dispose individual geometries
|
|
280
314
|
for (const geo of geometries) {
|
|
281
315
|
geo.dispose();
|
|
282
316
|
}
|
|
283
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
|
+
}
|
|
284
398
|
/**
|
|
285
399
|
* Computes world-relative transforms for each segment by chaining
|
|
286
400
|
* based on attachmentSide. This mirrors the prototype's StairSystem logic.
|
|
@@ -332,6 +446,314 @@ function rotateXZ(x, z, angle) {
|
|
|
332
446
|
const sin = Math.sin(angle);
|
|
333
447
|
return [x * cos + z * sin, -x * sin + z * cos];
|
|
334
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
|
+
}
|
|
335
757
|
/**
|
|
336
758
|
* Computes the absolute Y height of a segment by traversing the stair's segment chain.
|
|
337
759
|
*/
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Point2D } from './wall-mitering';
|
|
2
|
+
import type { FenceNode, WallNode } from '../../schema';
|
|
3
|
+
type WallCurveLike = Pick<WallNode | FenceNode, 'start' | 'end' | 'curveOffset'>;
|
|
4
|
+
type CurveFrame = {
|
|
5
|
+
point: Point2D;
|
|
6
|
+
tangent: Point2D;
|
|
7
|
+
normal: Point2D;
|
|
8
|
+
};
|
|
9
|
+
type WallSurfaceMiterOverrides = {
|
|
10
|
+
startLeft?: Point2D;
|
|
11
|
+
startRight?: Point2D;
|
|
12
|
+
endLeft?: Point2D;
|
|
13
|
+
endRight?: Point2D;
|
|
14
|
+
};
|
|
15
|
+
export declare function getWallStartPoint(wall: WallCurveLike): Point2D;
|
|
16
|
+
export declare function getWallEndPoint(wall: WallCurveLike): Point2D;
|
|
17
|
+
export declare function getWallChordLength(wall: WallCurveLike): number;
|
|
18
|
+
export declare function getMaxWallCurveOffset(wall: WallCurveLike): number;
|
|
19
|
+
export declare function getWallStraightSnapOffset(wall: WallCurveLike): number;
|
|
20
|
+
export declare function normalizeWallCurveOffset(wall: WallCurveLike, offset: number): number;
|
|
21
|
+
export declare function getClampedWallCurveOffset(wall: WallCurveLike): number;
|
|
22
|
+
export declare function isCurvedWall(wall: WallCurveLike): boolean;
|
|
23
|
+
export declare function getWallChordFrame(wall: WallCurveLike): {
|
|
24
|
+
start: Point2D;
|
|
25
|
+
end: Point2D;
|
|
26
|
+
midpoint: Point2D;
|
|
27
|
+
tangent: {
|
|
28
|
+
x: number;
|
|
29
|
+
y: number;
|
|
30
|
+
};
|
|
31
|
+
normal: {
|
|
32
|
+
x: number;
|
|
33
|
+
y: number;
|
|
34
|
+
};
|
|
35
|
+
length: number;
|
|
36
|
+
};
|
|
37
|
+
export declare function getWallCurveFrameAt(wall: WallCurveLike, t: number): CurveFrame;
|
|
38
|
+
export declare function getWallMidpointHandlePoint(wall: WallCurveLike): Point2D;
|
|
39
|
+
export declare function sampleWallCenterline(wall: WallCurveLike, segments?: number): Point2D[];
|
|
40
|
+
export declare function getWallCurveLength(wall: WallCurveLike, segments?: number): number;
|
|
41
|
+
export declare function getWallSurfacePolygon(wall: Pick<WallNode | FenceNode, 'start' | 'end' | 'curveOffset' | 'thickness'>, segments?: number, miterOverrides?: WallSurfaceMiterOverrides): Point2D[];
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=wall-curve.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wall-curve.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-curve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAA;AAKvD,KAAK,aAAa,GAAG,IAAI,CAAC,QAAQ,GAAG,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,aAAa,CAAC,CAAA;AAEhF,KAAK,UAAU,GAAG;IAChB,KAAK,EAAE,OAAO,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;IAChB,MAAM,EAAE,OAAO,CAAA;CAChB,CAAA;AAED,KAAK,yBAAyB,GAAG;IAC/B,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAA;CACnB,CAAA;AAcD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE9D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAE5D;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,UAErD;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,UAExD;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,aAAa,UAE5D;AAWD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,UAG3E;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,aAAa,UAI5D;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,WAE/C;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa;;;;;;;;;;;;;EA6BpD;AA+BD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,GAAG,UAAU,CAwC9E;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,aAAa,WAE7D;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,SAA0B,aAG3F;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,aAAa,EAAE,QAAQ,SAA0B,UASzF;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,aAAa,GAAG,WAAW,CAAC,EAC/E,QAAQ,SAA0B,EAClC,cAAc,CAAC,EAAE,yBAAyB,aA2B3C"}
|