@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.
- package/README.md +54 -21
- package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
- package/dist/components/viewer/glb-replace-instances.js +5 -4
- package/dist/components/viewer/glb-scene.d.ts +3 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +2 -1
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +8 -4
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +2 -2
- package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
- package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -1
- package/dist/components/viewer/use-daylight-rooms.js +75 -33
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/lib/asset-url.d.ts +0 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +3 -3
- package/dist/lib/daylight/room-context.d.ts.map +1 -1
- package/dist/lib/daylight/room-context.js +3 -2
- package/dist/lib/daylight/room-map.d.ts +124 -0
- package/dist/lib/daylight/room-map.d.ts.map +1 -0
- package/dist/lib/daylight/room-map.js +630 -0
- package/dist/lib/daylight/room-scene.d.ts +4 -2
- package/dist/lib/daylight/room-scene.d.ts.map +1 -1
- package/dist/lib/daylight/room-scene.js +72 -37
- package/dist/lib/daylight/rooms.d.ts +60 -30
- package/dist/lib/daylight/rooms.d.ts.map +1 -1
- package/dist/lib/daylight/rooms.js +331 -286
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +2 -1
- package/dist/lib/measurement-label.d.ts +4 -0
- package/dist/lib/measurement-label.d.ts.map +1 -0
- package/dist/lib/measurement-label.js +54 -0
- package/dist/lib/measurement-presentation.d.ts +5 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -0
- package/dist/lib/measurement-presentation.js +8 -0
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +3 -3
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +4 -3
- package/dist/lib/viewer-config.d.ts +21 -0
- package/dist/lib/viewer-config.d.ts.map +1 -0
- package/dist/lib/viewer-config.js +21 -0
- package/dist/lib/viewer-preference-storage.d.ts +7 -0
- package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
- package/dist/lib/viewer-preference-storage.js +23 -0
- package/dist/store/use-viewer.d.ts +2 -56
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +3 -1
- package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +41 -8
- package/dist/systems/geometry/geometry-status.d.ts +8 -0
- package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-status.js +24 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +107 -65
- package/dist/systems/wall/wall-system.d.ts +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +56 -10
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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:
|
|
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:
|
|
1693
|
-
right:
|
|
1736
|
+
left: left - (cornerEnd === 'start' ? reach : 0),
|
|
1737
|
+
right: right + (cornerEnd === 'end' ? reach : 0),
|
|
1694
1738
|
bottom: bottom - bottomPadding,
|
|
1695
|
-
top:
|
|
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
|
|
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
|
|
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
|
|
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",
|