@pascal-app/viewer 1.0.7 → 1.1.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 (67) hide show
  1. package/README.md +54 -21
  2. package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
  3. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
  4. package/dist/components/viewer/glb-replace-instances.js +5 -4
  5. package/dist/components/viewer/glb-scene.d.ts +3 -1
  6. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  7. package/dist/components/viewer/glb-scene.js +2 -1
  8. package/dist/components/viewer/index.d.ts.map +1 -1
  9. package/dist/components/viewer/index.js +8 -4
  10. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  11. package/dist/components/viewer/registered-systems.js +2 -2
  12. package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
  13. package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
  14. package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -1
  15. package/dist/components/viewer/use-daylight-rooms.js +75 -33
  16. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  17. package/dist/hooks/use-gltf-ktx2.js +2 -1
  18. package/dist/index.d.ts +5 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +5 -1
  21. package/dist/lib/asset-url.d.ts +0 -1
  22. package/dist/lib/asset-url.d.ts.map +1 -1
  23. package/dist/lib/asset-url.js +3 -3
  24. package/dist/lib/daylight/room-context.d.ts.map +1 -1
  25. package/dist/lib/daylight/room-context.js +3 -2
  26. package/dist/lib/daylight/room-map.d.ts +124 -0
  27. package/dist/lib/daylight/room-map.d.ts.map +1 -0
  28. package/dist/lib/daylight/room-map.js +630 -0
  29. package/dist/lib/daylight/room-scene.d.ts +4 -2
  30. package/dist/lib/daylight/room-scene.d.ts.map +1 -1
  31. package/dist/lib/daylight/room-scene.js +72 -37
  32. package/dist/lib/daylight/rooms.d.ts +60 -30
  33. package/dist/lib/daylight/rooms.d.ts.map +1 -1
  34. package/dist/lib/daylight/rooms.js +331 -286
  35. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  36. package/dist/lib/ktx2-loader.js +2 -1
  37. package/dist/lib/measurement-label.d.ts +4 -0
  38. package/dist/lib/measurement-label.d.ts.map +1 -0
  39. package/dist/lib/measurement-label.js +54 -0
  40. package/dist/lib/measurement-presentation.d.ts +5 -0
  41. package/dist/lib/measurement-presentation.d.ts.map +1 -0
  42. package/dist/lib/measurement-presentation.js +8 -0
  43. package/dist/lib/pointer-events.d.ts.map +1 -1
  44. package/dist/lib/pointer-events.js +3 -3
  45. package/dist/lib/texture-reference.d.ts.map +1 -1
  46. package/dist/lib/texture-reference.js +4 -3
  47. package/dist/lib/viewer-config.d.ts +21 -0
  48. package/dist/lib/viewer-config.d.ts.map +1 -0
  49. package/dist/lib/viewer-config.js +21 -0
  50. package/dist/lib/viewer-preference-storage.d.ts +7 -0
  51. package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
  52. package/dist/lib/viewer-preference-storage.js +23 -0
  53. package/dist/store/use-viewer.d.ts +2 -56
  54. package/dist/store/use-viewer.d.ts.map +1 -1
  55. package/dist/store/use-viewer.js +3 -1
  56. package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
  57. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  58. package/dist/systems/elevator/elevator-interaction-system.js +41 -8
  59. package/dist/systems/geometry/geometry-status.d.ts +8 -0
  60. package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
  61. package/dist/systems/geometry/geometry-status.js +24 -0
  62. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  63. package/dist/systems/roof/roof-system.js +107 -65
  64. package/dist/systems/wall/wall-system.d.ts +11 -0
  65. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  66. package/dist/systems/wall/wall-system.js +56 -10
  67. package/package.json +3 -3
@@ -9,6 +9,7 @@ import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
9
9
  import { ensureRenderableGeometryAttributes, subtractCsgBrush } from '../../lib/csg-utils';
10
10
  import useViewer from '../../store/use-viewer';
11
11
  import { isRevealAssembling } from '../construction-reveal/construction-reveal';
12
+ import { setGeometryFailure, useGeometryStatus } from '../geometry/geometry-status';
12
13
  import { ROOF_GABLE_LEFT_MATERIAL_INDEX, ROOF_GABLE_RIGHT_MATERIAL_INDEX, ROOF_HEEL_MATERIAL_INDEX, } from './roof-material-slots';
13
14
  function csgGeometry(brush) {
14
15
  return brush.geometry;
@@ -372,7 +373,13 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
372
373
  // Built even while hidden: a roof assembling from its segments (construction
373
374
  // reveal) hides its shell, and must show it whole the moment it is captured.
374
375
  rememberMergedInputs(node, nodes);
375
- updateMergedRoofGeometry(node, group, nodes, framing);
376
+ try {
377
+ updateMergedRoofGeometry(node, group, nodes, framing);
378
+ }
379
+ catch (error) {
380
+ setGeometryFailure(node.id, 'build-failed');
381
+ console.error('Roof rebuild failed:', error);
382
+ }
376
383
  roofsProcessed++;
377
384
  pendingRoofUpdates.delete(id);
378
385
  }
@@ -383,7 +390,19 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
383
390
  // GEOMETRY GENERATION
384
391
  // ============================================================================
385
392
  function updateRoofSegmentGeometry(node, mesh, nodes, adapter, framing = false) {
386
- const newGeo = generateRoofSegmentGeometry(node, nodes, { framing });
393
+ let newGeo;
394
+ try {
395
+ newGeo = generateRoofSegmentGeometry(node, nodes, { framing });
396
+ }
397
+ catch (error) {
398
+ setGeometryFailure(node.id, 'build-failed');
399
+ console.error('Roof segment rebuild failed:', error);
400
+ return;
401
+ }
402
+ if (useGeometryStatus.getState().failures.has(node.id)) {
403
+ newGeo.dispose();
404
+ return;
405
+ }
387
406
  mesh.geometry.dispose();
388
407
  mesh.geometry = newGeo;
389
408
  computeGeometryBoundsTree(newGeo);
@@ -405,69 +424,70 @@ function subtractAccessoryCuts(brushes, segment, nodes) {
405
424
  let workingShin = brushes.shinSlab;
406
425
  let workingDeck = brushes.deckSlab;
407
426
  let workingWall = brushes.wallBrush;
408
- for (const childElemId of segment.children ?? []) {
409
- const storedChild = nodes[childElemId];
410
- if (!storedChild)
411
- continue;
412
- const childElem = getEffectiveNode(storedChild);
413
- const meta = typeof childElem.metadata === 'object' && childElem.metadata !== null
414
- ? childElem.metadata
415
- : undefined;
416
- if (meta?.isTransient)
417
- continue;
418
- const childDef = nodeRegistry.get(childElem.type);
419
- const buildCut = childDef?.capabilities?.roofAccessory?.buildCut;
420
- if (!buildCut)
421
- continue;
422
- const cutGeo = buildCut(childElem, segment);
423
- if (!cutGeo)
424
- continue;
425
- // Wrap the kind-emitted geometry in a Brush. Kinds return raw
426
- // shapes; the viewer welds (mandatory after rotations leave
427
- // duplicated verts), attaches a single material group, and
428
- // builds the bounds tree — keeping kind code free of
429
- // three-bvh-csg / three-mesh-bvh imports.
430
- const welded = mergeVertices(cutGeo, 1e-4);
431
- cutGeo.dispose();
432
- const idxCount = welded.getIndex()?.count ?? 0;
433
- if (idxCount === 0) {
434
- welded.dispose();
435
- continue;
436
- }
437
- welded.clearGroups();
438
- welded.addGroup(0, idxCount, 0);
439
- welded.computeVertexNormals();
440
- ensureRenderableGeometryAttributes(welded);
441
- computeGeometryBoundsTree(welded);
442
- const cut = new Brush(welded, dummyMats[0]);
443
- cut.updateMatrixWorld();
444
- const cutScope = childDef?.capabilities?.roofAccessory?.cutScope ?? 'all';
445
- try {
446
- if (cutScope !== 'wall') {
447
- const nextShin = csgEvaluator.evaluate(workingShin, cut, SUBTRACTION);
448
- workingShin.geometry.dispose();
449
- prepareBrushForCSG(nextShin);
450
- workingShin = nextShin;
451
- const nextDeck = csgEvaluator.evaluate(workingDeck, cut, SUBTRACTION);
452
- workingDeck.geometry.dispose();
453
- prepareBrushForCSG(nextDeck);
454
- workingDeck = nextDeck;
427
+ try {
428
+ for (const childElemId of segment.children ?? []) {
429
+ const storedChild = nodes[childElemId];
430
+ if (!storedChild)
431
+ continue;
432
+ const childElem = getEffectiveNode(storedChild);
433
+ const meta = typeof childElem.metadata === 'object' && childElem.metadata !== null
434
+ ? childElem.metadata
435
+ : undefined;
436
+ if (meta?.isTransient)
437
+ continue;
438
+ const childDef = nodeRegistry.get(childElem.type);
439
+ const buildCut = childDef?.capabilities?.roofAccessory?.buildCut;
440
+ if (!buildCut)
441
+ continue;
442
+ const cutGeo = buildCut(childElem, segment);
443
+ if (!cutGeo)
444
+ continue;
445
+ // Wrap the kind-emitted geometry in a Brush. Kinds return raw
446
+ // shapes; the viewer welds (mandatory after rotations leave
447
+ // duplicated verts), attaches a single material group, and
448
+ // builds the bounds tree — keeping kind code free of
449
+ // three-bvh-csg / three-mesh-bvh imports.
450
+ const welded = mergeVertices(cutGeo, 1e-4);
451
+ cutGeo.dispose();
452
+ const idxCount = welded.getIndex()?.count ?? 0;
453
+ if (idxCount === 0) {
454
+ welded.dispose();
455
+ continue;
456
+ }
457
+ welded.clearGroups();
458
+ welded.addGroup(0, idxCount, 0);
459
+ welded.computeVertexNormals();
460
+ ensureRenderableGeometryAttributes(welded);
461
+ computeGeometryBoundsTree(welded);
462
+ const cut = new Brush(welded, dummyMats[0]);
463
+ cut.updateMatrixWorld();
464
+ const cutScope = childDef?.capabilities?.roofAccessory?.cutScope ?? 'all';
465
+ try {
466
+ if (cutScope !== 'wall') {
467
+ const nextShin = csgEvaluator.evaluate(workingShin, cut, SUBTRACTION);
468
+ workingShin.geometry.dispose();
469
+ prepareBrushForCSG(nextShin);
470
+ workingShin = nextShin;
471
+ const nextDeck = csgEvaluator.evaluate(workingDeck, cut, SUBTRACTION);
472
+ workingDeck.geometry.dispose();
473
+ prepareBrushForCSG(nextDeck);
474
+ workingDeck = nextDeck;
475
+ }
476
+ const nextWall = csgEvaluator.evaluate(workingWall, cut, SUBTRACTION);
477
+ workingWall.geometry.dispose();
478
+ prepareBrushForCSG(nextWall);
479
+ workingWall = nextWall;
480
+ }
481
+ finally {
482
+ cut.geometry.dispose();
455
483
  }
456
- const nextWall = csgEvaluator.evaluate(workingWall, cut, SUBTRACTION);
457
- workingWall.geometry.dispose();
458
- prepareBrushForCSG(nextWall);
459
- workingWall = nextWall;
460
- }
461
- catch (e) {
462
- console.error(`[${childElem.type}] cut CSG failed:`, e);
463
- }
464
- finally {
465
- cut.geometry.dispose();
466
484
  }
467
485
  }
468
- brushes.shinSlab = workingShin;
469
- brushes.deckSlab = workingDeck;
470
- brushes.wallBrush = workingWall;
486
+ finally {
487
+ brushes.shinSlab = workingShin;
488
+ brushes.deckSlab = workingDeck;
489
+ brushes.wallBrush = workingWall;
490
+ }
471
491
  }
472
492
  function getMergedRoofSegmentBrushes(roofNode, segment, nodes, framing) {
473
493
  const segmentId = segment.id;
@@ -486,7 +506,13 @@ function getMergedRoofSegmentBrushes(roofNode, segment, nodes, framing) {
486
506
  mergedRoofSegmentGeometryCache.delete(segmentId);
487
507
  return null;
488
508
  }
489
- subtractAccessoryCuts(brushes, segment, nodes);
509
+ try {
510
+ subtractAccessoryCuts(brushes, segment, nodes);
511
+ }
512
+ catch (error) {
513
+ disposeRoofSegmentBrushSet(brushes);
514
+ throw error;
515
+ }
490
516
  _matrix.compose(_position.set(segment.position[0], segment.position[1], segment.position[2]), _quaternion.setFromAxisAngle(_yAxis, segment.rotation), _scale);
491
517
  const applyTransform = (brush) => {
492
518
  csgGeometry(brush).applyMatrix4(_matrix);
@@ -547,6 +573,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
547
573
  // Not BoxGeometry: its 6 groups against the merged mesh's 4-material array
548
574
  // crash GLTFExporter (materials[4] → undefined) when the roof bakes.
549
575
  mergedMesh.geometry = createDegenerateRoofPlaceholder();
576
+ setGeometryFailure(roofNode.id, null);
550
577
  return;
551
578
  }
552
579
  let totalShinSlab = null;
@@ -643,6 +670,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
643
670
  console.warn('[RoofSystem] Skipping merged roof geometry with invalid attributes', roofNode.id);
644
671
  warnedMergedRoofNaNIds.add(roofNode.id);
645
672
  }
673
+ setGeometryFailure(roofNode.id, 'invalid-geometry');
646
674
  resultGeo.dispose();
647
675
  if (combined !== shinDeck)
648
676
  shinDeck.geometry.dispose();
@@ -691,10 +719,12 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
691
719
  ensureRenderableGeometryAttributes(finalGeo);
692
720
  mergedMesh.geometry.dispose();
693
721
  mergedMesh.geometry = finalGeo;
722
+ setGeometryFailure(roofNode.id, null);
694
723
  if (combined !== shinDeck)
695
724
  shinDeck.geometry.dispose();
696
725
  }
697
726
  catch (e) {
727
+ setGeometryFailure(roofNode.id, 'build-failed');
698
728
  console.error('Merged roof CSG failed:', e);
699
729
  }
700
730
  totalShinSlab.geometry.dispose();
@@ -712,6 +742,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
712
742
  ensureRenderableGeometryAttributes(finalGeo);
713
743
  mergedMesh.geometry.dispose();
714
744
  mergedMesh.geometry = finalGeo;
745
+ setGeometryFailure(roofNode.id, null);
715
746
  }
716
747
  for (const geometry of directSegmentGeometries) {
717
748
  if (geometry !== finalGeo)
@@ -1932,6 +1963,7 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
1932
1963
  }
1933
1964
  result.computeVertexNormals();
1934
1965
  ensureRenderableGeometryAttributes(result);
1966
+ setGeometryFailure(node.id, null);
1935
1967
  return result;
1936
1968
  }
1937
1969
  const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, {
@@ -1939,11 +1971,17 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
1939
1971
  framing: options.framing ? roofFramingDepth(node) : 0,
1940
1972
  }));
1941
1973
  if (!brushes) {
1942
- // Fallback: simple box
1974
+ setGeometryFailure(node.id, 'build-failed');
1943
1975
  return new THREE.BoxGeometry(node.width, node.wallHeight, node.depth);
1944
1976
  }
1945
1977
  if (nodes) {
1946
- subtractAccessoryCuts(brushes, node, nodes);
1978
+ try {
1979
+ subtractAccessoryCuts(brushes, node, nodes);
1980
+ }
1981
+ catch (error) {
1982
+ disposeRoofSegmentBrushSet(brushes);
1983
+ throw error;
1984
+ }
1947
1985
  }
1948
1986
  const { deckSlab, shinSlab, wallBrush, innerBrush, rakeBoards } = brushes;
1949
1987
  let resultGeo = new THREE.BufferGeometry();
@@ -1968,8 +2006,11 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
1968
2006
  resultGeo = csgGeometry(combined);
1969
2007
  if (geometryHasInvalidAttributes(resultGeo)) {
1970
2008
  resultGeo.dispose();
2009
+ setGeometryFailure(node.id, 'invalid-geometry');
1971
2010
  resultGeo = csgGeometry(wallBrush).clone();
1972
2011
  }
2012
+ else
2013
+ setGeometryFailure(node.id, null);
1973
2014
  const resultMaterials = csgMaterials(combined);
1974
2015
  const matToIndex = new Map([
1975
2016
  [dummyMats[0], 0],
@@ -1989,6 +2030,7 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
1989
2030
  shinDeck.geometry.dispose();
1990
2031
  }
1991
2032
  catch (e) {
2033
+ setGeometryFailure(node.id, 'build-failed');
1992
2034
  console.error('Roof CSG failed:', e);
1993
2035
  resultGeo = csgGeometry(wallBrush).clone();
1994
2036
  }
@@ -55,6 +55,17 @@ export declare function markWallsForZoneFinishChanges(): void;
55
55
  */
56
56
  export declare function wallGeometryChildren(node: WallNode, nodes: Readonly<Record<string, AnyNode>>): AnyNode[];
57
57
  type WallTerrainBottomSampler = (x: number, z: number) => number | null;
58
+ /** The wall's top in wall-local coordinates: `height` at the start, rising `slope` per metre of x. */
59
+ type WallTopLine = {
60
+ height: number;
61
+ slope: number;
62
+ };
63
+ /**
64
+ * Wall-local Y of the sloped top at `x`. Mitered corners sample `x` outside
65
+ * [0, L], where the line can fall below the wall base; the top never drops
66
+ * under {@link MIN_WALL_END_HEIGHT} (or the flat height, if lower).
67
+ */
68
+ export declare function wallTopYAt(wallTop: WallTopLine, x: number): number;
58
69
  export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number, baseElevation?: number, baseSegments?: readonly WallSlabSupportSegment[], storeyHeight?: number, terrainBottomAt?: WallTerrainBottomSampler, faceBase?: {
59
70
  a: readonly WallSlabSupportSegment[];
60
71
  b: readonly WallSlabSupportSegment[];
@@ -1 +1 @@
1
- {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EACZ,KAAK,SAAS,EAqCd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,QAAQ,EAEb,KAAK,sBAAsB,EAK5B,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAAwC,MAAM,eAAe,CAAA;AAqBrF,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,oBAAoB,CAAA;AAK3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAyChE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,GAAG;IACjE,MAAM,EAAE,KAAK,GAAG,IAAI,CAAA;IACpB,eAAe,EAAE,KAAK,EAAE,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;CACrB,CA4FA;AAiJD;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,gBAAgB,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAC,GAAG,GAAG,GAAG,EAAE,eAAe,EAAE,CAAC,GAAG,IAAI,CAAA;IACrD,6FAA6F;IAC7F,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,wFAAwF;IACxF,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAClD,CAAA;AAoUD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,gBAAgB,EAAE,MAAM,EACxB,SAAS,EAAE,MAAM,GAChB,OAAO,CAET;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAID,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,OAAO,EAAE,0BAA0B,KAChC;QAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC;QAAC,cAAc,EAAE,OAAO,EAAE,CAAA;KAAE,CAAA;IAC/D,aAAa,CAAC,EAAE,CACd,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,KAAK,CAAC,cAAc,EAC9B,QAAQ,EAAE,SAAS,OAAO,EAAE,KACzB,KAAK,CAAC,cAAc,CAAA;IACzB,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,cAAc,KAAK,IAAI,CAAA;CACnG,CAAA;AAED,eAAO,MAAM,UAAU,yBAAyB;IAAE,eAAe,CAAC,EAAE,mBAAmB,CAAA;CAAE,SAexF,CAAA;AAED,wBAAgB,iBAAiB,CAAC,eAAe,CAAC,EAAE,mBAAmB,QAatE;AAyCD;;;;GAIG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAoCpD;AAmZD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,EAAE,CAUX;AA8ED,KAAK,wBAAwB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;AA4OvE,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,EACjB,aAAa,SAAgB,EAC7B,YAAY,GAAE,SAAS,sBAAsB,EAE5C,EACD,YAAY,SAAuB,EACnC,eAAe,CAAC,EAAE,wBAAwB,EAC1C,QAAQ,CAAC,EAAE;IAAE,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAAC,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAA;CAAE,EACzF,YAAY,GAAE,gBAAsD,EACpE,UAAU,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAA6B,GACxE,KAAK,CAAC,cAAc,CAkUtB"}
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { buildWallFinishLayout, containsPoint, cornerPair, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getDoorOpeningSize, getEffectiveNode, getOpeningWallCut, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallLevelZones, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, isCutterName, openingHostFacing, pointToKey, resolveCutterHost, resolveLevelId, resolveWallFaceBottom, resolveWallFinish, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, WALL_SURFACE_SLOT_DEFAULTS, wallFinishMaterialIndex, zoneHasWallFinish, } from '@pascal-app/core';
1
+ import { buildWallFinishLayout, clampWallEndHeightOffset, containsPoint, cornerPair, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getDoorOpeningSize, getEffectiveNode, getOpeningWallCut, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallLevelZones, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, isCutterName, MIN_WALL_END_HEIGHT, openingHostFacing, pointToKey, resolveCutterHost, resolveLevelId, resolveWallFaceBottom, resolveWallFinish, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, WALL_SURFACE_SLOT_DEFAULTS, wallFinishMaterialIndex, zoneHasWallFinish, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import { useEffect } from 'react';
4
4
  import * as THREE from 'three';
@@ -1227,6 +1227,41 @@ function buildWallFinishContext(wall, layout, faceBase, slabElevation) {
1227
1227
  .sort((left, right) => left.start - right.start);
1228
1228
  return { layout, faceBase: { a: runs(faceBase.a), b: runs(faceBase.b) }, refs, foundation };
1229
1229
  }
1230
+ /**
1231
+ * Wall-local Y of the sloped top at `x`. Mitered corners sample `x` outside
1232
+ * [0, L], where the line can fall below the wall base; the top never drops
1233
+ * under {@link MIN_WALL_END_HEIGHT} (or the flat height, if lower).
1234
+ */
1235
+ export function wallTopYAt(wallTop, x) {
1236
+ const { height, slope } = wallTop;
1237
+ if (slope === 0)
1238
+ return height;
1239
+ return Math.max(height + slope * x, Math.min(height, MIN_WALL_END_HEIGHT));
1240
+ }
1241
+ /**
1242
+ * Tilts a wall's top edge along its length so the `end` side sits taller (or
1243
+ * shorter) than the `start` side — e.g. a knee wall following a single-pitch
1244
+ * roof slope — instead of requiring a non-rectangular footprint. Only
1245
+ * vertices sitting exactly at the flat extruded top (`topY`) move.
1246
+ *
1247
+ * Evaluates the linear plane equation `slope * localX` continuously across
1248
+ * all top vertices (including mitered corner vertices extending beyond [0, L])
1249
+ * so the extruded top face remains a single coplanar surface without corner
1250
+ * creases or triangulation folds — except where {@link wallTopYAt} holds an
1251
+ * extended miter vertex above the base.
1252
+ */
1253
+ function applyWallEndHeightSlope(geometry, wallTop) {
1254
+ const { height: topY, slope } = wallTop;
1255
+ if (slope === 0)
1256
+ return;
1257
+ const position = geometry.getAttribute('position');
1258
+ for (let i = 0; i < position.count; i++) {
1259
+ if (Math.abs(position.getY(i) - topY) > 1e-4)
1260
+ continue;
1261
+ position.setY(i, wallTopYAt(wallTop, position.getX(i)));
1262
+ }
1263
+ position.needsUpdate = true;
1264
+ }
1230
1265
  export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0, baseElevation = slabElevation, baseSegments = [
1231
1266
  { start: 0, end: 1, elevation: baseElevation },
1232
1267
  ], storeyHeight = DEFAULT_LEVEL_HEIGHT, terrainBottomAt, faceBase, finishLayout = buildWallFinishLayout(wallNode, []), sceneNodes = useScene.getState().nodes) {
@@ -1236,6 +1271,7 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1236
1271
  const level = sceneNodes[levelId];
1237
1272
  const authoredStoreyHeight = level?.type === 'level' ? (level.height ?? null) : null;
1238
1273
  const topElevation = resolveWallTop(wallNode, storeyHeight, slabElevation, authoredStoreyHeight);
1274
+ const effectiveWallHeight = topElevation - slabElevation;
1239
1275
  const faceDatum = faceBase;
1240
1276
  if (faceBase)
1241
1277
  faceBase = {
@@ -1266,6 +1302,10 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1266
1302
  if (L < 1e-9) {
1267
1303
  return new THREE.BufferGeometry();
1268
1304
  }
1305
+ const wallTop = {
1306
+ height: effectiveWallHeight,
1307
+ slope: clampWallEndHeightOffset(wallNode.endHeightOffset, effectiveWallHeight) / L,
1308
+ };
1269
1309
  const boundaryPoints = getWallMiterBoundaryPoints(wallNode, miterData);
1270
1310
  const polyPoints = isCurvedWall(wallNode)
1271
1311
  ? getWallSurfacePolygon(wallNode, 24, insetCurvedWallBoundaryPointsFor3D(wallNode, boundaryPoints, miterData) ?? undefined)
@@ -1312,10 +1352,10 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1312
1352
  depth: height,
1313
1353
  bevelEnabled: false,
1314
1354
  });
1315
- // Rotate so extrusion direction (Z) becomes height direction (Y)
1316
1355
  geometry.rotateX(-Math.PI / 2);
1317
1356
  if (Math.abs(localBottom) > 1e-9)
1318
1357
  geometry.translate(0, localBottom, 0);
1358
+ applyWallEndHeightSlope(geometry, wallTop);
1319
1359
  geometry.computeVertexNormals();
1320
1360
  assignWallMaterialGroups(geometry, wallNode, boundaryEdges, finish);
1321
1361
  ensureRenderableGeometryAttributes(geometry);
@@ -1431,7 +1471,7 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
1431
1471
  }
1432
1472
  const cutoutBrushes = [
1433
1473
  ...baseProfileCutouts,
1434
- ...collectCutoutBrushes(wallNode, childrenNodes, thickness, faceDatum, slabElevation, sceneNodes, baseSegments),
1474
+ ...collectCutoutBrushes(wallNode, childrenNodes, thickness, wallTop, faceDatum, slabElevation, sceneNodes, baseSegments),
1435
1475
  ];
1436
1476
  if (cutoutBrushes.length === 0) {
1437
1477
  const splitGeometry = finishWallFaces(splitGeometryAtPlanes(geometry, finishPlanes.y, finishPlanes.x), wallNode, boundaryEdges, finish);
@@ -1527,7 +1567,7 @@ function authoredCutoutBrush(cutoutMesh, wallMatrixInverse, wallThickness, wallN
1527
1567
  computeGeometryBoundsTree(geometry);
1528
1568
  return new Brush(geometry);
1529
1569
  }
1530
- function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase, elevation = 0, sceneNodes = useScene.getState().nodes, baseSegments = [{ start: 0, end: 1, elevation }]) {
1570
+ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, wallTop, faceBase, elevation = 0, sceneNodes = useScene.getState().nodes, baseSegments = [{ start: 0, end: 1, elevation }]) {
1531
1571
  const brushes = [];
1532
1572
  const wallMesh = sceneRegistry.nodes.get(wallNode.id);
1533
1573
  if (!wallMesh)
@@ -1584,7 +1624,7 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1584
1624
  child.position[1] + cut.datum - elevation,
1585
1625
  child.position[2],
1586
1626
  ],
1587
- }, wallThickness, getWallBodyCenterOffset(wallNode), cut.bottom - elevation, child.type === 'window' ? cornerPair(sceneNodes, child)?.end : undefined));
1627
+ }, wallThickness, getWallBodyCenterOffset(wallNode), cut.bottom - elevation, child.type === 'window' ? cornerPair(sceneNodes, child)?.end : undefined, wallTop));
1588
1628
  continue;
1589
1629
  }
1590
1630
  if (cut.band.length < 3)
@@ -1595,8 +1635,9 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1595
1635
  return new THREE.Vector2(dx * Math.cos(angle) + dz * Math.sin(angle), dx * Math.sin(angle) - dz * Math.cos(angle));
1596
1636
  });
1597
1637
  const padding = getOpeningCutoutBottomPadding(child, cut.bottom - elevation);
1638
+ const top = Math.min(cut.top - elevation, Math.min(...points.map((point) => wallTopYAt(wallTop, point.x))));
1598
1639
  const geometry = new THREE.ExtrudeGeometry(new THREE.Shape(points), {
1599
- depth: cut.top - cut.bottom + padding,
1640
+ depth: top - (cut.bottom - elevation) + padding,
1600
1641
  bevelEnabled: false,
1601
1642
  });
1602
1643
  geometry.rotateX(-Math.PI / 2);
@@ -1681,18 +1722,23 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase,
1681
1722
  * cut only takes this wall's own geometry, so running past it touches nothing else.
1682
1723
  */
1683
1724
  const CORNER_CUT_REACH = 6;
1684
- function createOpeningCutoutBrush(opening, wallThickness, centerOffset, cutBottom, cornerEnd) {
1725
+ function createOpeningCutoutBrush(opening, wallThickness, centerOffset, cutBottom, cornerEnd, wallTop) {
1685
1726
  const size = getDoorOpeningSize(opening);
1686
1727
  const centerX = opening.position[0] - size.leafOffsetX * openingHostFacing(opening);
1687
1728
  const halfWidth = size.width / 2;
1688
1729
  const bottom = cutBottom ?? opening.position[1] - opening.height / 2;
1689
1730
  const bottomPadding = getOpeningCutoutBottomPadding(opening, bottom);
1690
1731
  const reach = CORNER_CUT_REACH * wallThickness;
1732
+ const left = centerX - halfWidth;
1733
+ const right = centerX + halfWidth;
1734
+ const openingTop = opening.position[1] - opening.height / 2 + size.height;
1691
1735
  const geometry = buildOpeningCutoutGeometry(opening, {
1692
- left: centerX - halfWidth - (cornerEnd === 'start' ? reach : 0),
1693
- right: centerX + halfWidth + (cornerEnd === 'end' ? reach : 0),
1736
+ left: left - (cornerEnd === 'start' ? reach : 0),
1737
+ right: right + (cornerEnd === 'end' ? reach : 0),
1694
1738
  bottom: bottom - bottomPadding,
1695
- top: opening.position[1] - opening.height / 2 + size.height,
1739
+ top: wallTop
1740
+ ? Math.min(openingTop, wallTopYAt(wallTop, wallTop.slope > 0 ? left : right))
1741
+ : openingTop,
1696
1742
  }, wallThickness * 2, wallThickness);
1697
1743
  if (centerOffset !== 0)
1698
1744
  geometry.translate(0, 0, centerOffset);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pascal-app/viewer",
3
- "version": "1.0.7",
3
+ "version": "1.1.0",
4
4
  "description": "3D viewer component for Pascal building editor",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -33,7 +33,7 @@
33
33
  "prepublishOnly": "npm run build"
34
34
  },
35
35
  "peerDependencies": {
36
- "@pascal-app/core": "^1.0.7",
36
+ "@pascal-app/core": "^1.1.0",
37
37
  "@react-three/drei": "^10",
38
38
  "@react-three/fiber": "^9",
39
39
  "react": "^18 || ^19",
@@ -46,7 +46,7 @@
46
46
  "zustand": "^5"
47
47
  },
48
48
  "devDependencies": {
49
- "@pascal-app/core": "^1.0.7",
49
+ "@pascal-app/core": "^1.1.0",
50
50
  "@pascal/typescript-config": "*",
51
51
  "@react-three/test-renderer": "^9.1.1",
52
52
  "@types/node": "^26",