@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.
Files changed (99) hide show
  1. package/dist/events/bus.d.ts +39 -4
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/events/bus.js +1 -1
  4. package/dist/hooks/scene-registry/scene-registry.d.ts +1 -0
  5. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  6. package/dist/hooks/scene-registry/scene-registry.js +1 -0
  7. package/dist/hooks/spatial-grid/spatial-grid.d.ts +2 -0
  8. package/dist/hooks/spatial-grid/spatial-grid.d.ts.map +1 -1
  9. package/dist/hooks/spatial-grid/spatial-grid.js +43 -20
  10. package/dist/index.d.ts +6 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -1
  13. package/dist/lib/polygon-geometry.d.ts +3 -0
  14. package/dist/lib/polygon-geometry.d.ts.map +1 -0
  15. package/dist/lib/polygon-geometry.js +90 -0
  16. package/dist/lib/space-detection.d.ts +10 -17
  17. package/dist/lib/space-detection.d.ts.map +1 -1
  18. package/dist/lib/space-detection.js +666 -453
  19. package/dist/material-library.d.ts +18 -0
  20. package/dist/material-library.d.ts.map +1 -0
  21. package/dist/material-library.js +603 -0
  22. package/dist/schema/index.d.ts +10 -4
  23. package/dist/schema/index.d.ts.map +1 -1
  24. package/dist/schema/index.js +6 -4
  25. package/dist/schema/material.d.ts +109 -0
  26. package/dist/schema/material.d.ts.map +1 -1
  27. package/dist/schema/material.js +52 -0
  28. package/dist/schema/nodes/ceiling.d.ts +10 -0
  29. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  30. package/dist/schema/nodes/ceiling.js +6 -0
  31. package/dist/schema/nodes/door.d.ts +1 -0
  32. package/dist/schema/nodes/door.d.ts.map +1 -1
  33. package/dist/schema/nodes/fence.d.ts +85 -0
  34. package/dist/schema/nodes/fence.d.ts.map +1 -0
  35. package/dist/schema/nodes/fence.js +34 -0
  36. package/dist/schema/nodes/item.d.ts +2 -2
  37. package/dist/schema/nodes/level.d.ts +1 -1
  38. package/dist/schema/nodes/level.d.ts.map +1 -1
  39. package/dist/schema/nodes/level.js +2 -0
  40. package/dist/schema/nodes/roof-segment.d.ts +2 -0
  41. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  42. package/dist/schema/nodes/roof-segment.js +1 -0
  43. package/dist/schema/nodes/roof.d.ts +108 -0
  44. package/dist/schema/nodes/roof.d.ts.map +1 -1
  45. package/dist/schema/nodes/roof.js +58 -2
  46. package/dist/schema/nodes/site.d.ts +1 -1
  47. package/dist/schema/nodes/slab.d.ts +10 -0
  48. package/dist/schema/nodes/slab.d.ts.map +1 -1
  49. package/dist/schema/nodes/slab.js +7 -0
  50. package/dist/schema/nodes/stair-segment.d.ts +2 -0
  51. package/dist/schema/nodes/stair-segment.d.ts.map +1 -1
  52. package/dist/schema/nodes/stair-segment.js +1 -0
  53. package/dist/schema/nodes/stair.d.ts +164 -0
  54. package/dist/schema/nodes/stair.d.ts.map +1 -1
  55. package/dist/schema/nodes/stair.js +106 -5
  56. package/dist/schema/nodes/surface-hole-metadata.d.ts +10 -0
  57. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -0
  58. package/dist/schema/nodes/surface-hole-metadata.js +5 -0
  59. package/dist/schema/nodes/wall.d.ts +87 -1
  60. package/dist/schema/nodes/wall.d.ts.map +1 -1
  61. package/dist/schema/nodes/wall.js +45 -4
  62. package/dist/schema/nodes/window.d.ts +1 -0
  63. package/dist/schema/nodes/window.d.ts.map +1 -1
  64. package/dist/schema/types.d.ts +406 -4
  65. package/dist/schema/types.d.ts.map +1 -1
  66. package/dist/schema/types.js +2 -0
  67. package/dist/store/actions/node-actions.d.ts +1 -1
  68. package/dist/store/actions/node-actions.d.ts.map +1 -1
  69. package/dist/store/actions/node-actions.js +175 -0
  70. package/dist/store/history-control.d.ts +14 -0
  71. package/dist/store/history-control.d.ts.map +1 -0
  72. package/dist/store/history-control.js +22 -0
  73. package/dist/store/use-scene.d.ts.map +1 -1
  74. package/dist/store/use-scene.js +249 -3
  75. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  76. package/dist/systems/ceiling/ceiling-system.js +7 -0
  77. package/dist/systems/fence/fence-system.d.ts +2 -0
  78. package/dist/systems/fence/fence-system.d.ts.map +1 -0
  79. package/dist/systems/fence/fence-system.js +187 -0
  80. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  81. package/dist/systems/roof/roof-system.js +31 -1
  82. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  83. package/dist/systems/slab/slab-system.js +45 -8
  84. package/dist/systems/stair/stair-opening-sync.d.ts +6 -0
  85. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -0
  86. package/dist/systems/stair/stair-opening-sync.js +515 -0
  87. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  88. package/dist/systems/stair/stair-system.js +432 -10
  89. package/dist/systems/wall/wall-curve.d.ts +43 -0
  90. package/dist/systems/wall/wall-curve.d.ts.map +1 -0
  91. package/dist/systems/wall/wall-curve.js +176 -0
  92. package/dist/systems/wall/wall-footprint.d.ts.map +1 -1
  93. package/dist/systems/wall/wall-footprint.js +16 -2
  94. package/dist/systems/wall/wall-mitering.d.ts +7 -0
  95. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  96. package/dist/systems/wall/wall-mitering.js +76 -3
  97. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  98. package/dist/systems/wall/wall-system.js +202 -2
  99. 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 geometry = new THREE.ExtrudeGeometry(shape, {
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
- geometry.applyMatrix4(matrix);
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.geometry.dispose();
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
- if (merged) {
276
- mergedMesh.geometry.dispose();
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"}