@pascal-app/viewer 1.0.0-beta.5 → 1.0.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/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/capture/asset-url.d.ts +2 -0
- package/dist/capture/asset-url.d.ts.map +1 -0
- package/dist/capture/asset-url.js +13 -0
- package/dist/capture/capture-runtime.d.ts +43 -0
- package/dist/capture/capture-runtime.d.ts.map +1 -0
- package/dist/capture/capture-runtime.js +228 -0
- package/dist/capture/frame.d.ts +4 -0
- package/dist/capture/frame.d.ts.map +1 -0
- package/dist/capture/frame.js +21 -0
- package/dist/capture/index.d.ts +12 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +11 -0
- package/dist/capture/layer-visibility.d.ts +5 -0
- package/dist/capture/layer-visibility.d.ts.map +1 -0
- package/dist/capture/layer-visibility.js +11 -0
- package/dist/capture/layers/clay-matcap.d.ts +3 -0
- package/dist/capture/layers/clay-matcap.d.ts.map +1 -0
- package/dist/capture/layers/clay-matcap.js +26 -0
- package/dist/capture/layers/device-motion-layer.d.ts +7 -0
- package/dist/capture/layers/device-motion-layer.d.ts.map +1 -0
- package/dist/capture/layers/device-motion-layer.js +78 -0
- package/dist/capture/layers/point-cloud-layer.d.ts +15 -0
- package/dist/capture/layers/point-cloud-layer.d.ts.map +1 -0
- package/dist/capture/layers/point-cloud-layer.js +106 -0
- package/dist/capture/layers/room-model-layer.d.ts +9 -0
- package/dist/capture/layers/room-model-layer.d.ts.map +1 -0
- package/dist/capture/layers/room-model-layer.js +71 -0
- package/dist/capture/layers/surface-mesh-data.d.ts +9 -0
- package/dist/capture/layers/surface-mesh-data.d.ts.map +1 -0
- package/dist/capture/layers/surface-mesh-data.js +74 -0
- package/dist/capture/layers/surface-mesh-layer.d.ts +7 -0
- package/dist/capture/layers/surface-mesh-layer.d.ts.map +1 -0
- package/dist/capture/layers/surface-mesh-layer.js +16 -0
- package/dist/capture/preview.d.ts +3 -0
- package/dist/capture/preview.d.ts.map +1 -0
- package/dist/capture/preview.js +2 -0
- package/dist/capture/source-state.d.ts +22 -0
- package/dist/capture/source-state.d.ts.map +1 -0
- package/dist/capture/source-state.js +182 -0
- package/dist/capture/stream-rendering.d.ts +14 -0
- package/dist/capture/stream-rendering.d.ts.map +1 -0
- package/dist/capture/stream-rendering.js +69 -0
- package/dist/capture/trajectory.d.ts +20 -0
- package/dist/capture/trajectory.d.ts.map +1 -0
- package/dist/capture/trajectory.js +77 -0
- package/dist/components/viewer/batched-mesh-spike.d.ts +14 -0
- package/dist/components/viewer/batched-mesh-spike.d.ts.map +1 -0
- package/dist/components/viewer/batched-mesh-spike.js +64 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +4 -3
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +70 -62
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +123 -89
- package/dist/components/viewer/perf-monitor.d.ts +6 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +307 -116
- package/dist/components/viewer/perf-panel.d.ts +3 -0
- package/dist/components/viewer/perf-panel.d.ts.map +1 -0
- package/dist/components/viewer/perf-panel.js +178 -0
- package/dist/components/viewer/pointer-raycast-layers.d.ts +15 -0
- package/dist/components/viewer/pointer-raycast-layers.d.ts.map +1 -0
- package/dist/components/viewer/pointer-raycast-layers.js +28 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +124 -83
- package/dist/components/viewer/registered-systems.d.ts +14 -5
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +22 -10
- package/dist/components/viewer/scene-atmosphere.d.ts +30 -0
- package/dist/components/viewer/scene-atmosphere.d.ts.map +1 -0
- package/dist/components/viewer/scene-atmosphere.js +89 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -1
- package/dist/components/viewer/scene-environment.js +19 -5
- package/dist/components/viewer/scene-ground-replacement.d.ts +5 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -0
- package/dist/components/viewer/scene-ground-replacement.js +35 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +4 -1
- package/dist/components/viewer/viewer-presentations.d.ts +67 -0
- package/dist/components/viewer/viewer-presentations.d.ts.map +1 -0
- package/dist/components/viewer/viewer-presentations.js +120 -0
- package/dist/hooks/use-library-materials-version.d.ts +7 -0
- package/dist/hooks/use-library-materials-version.d.ts.map +1 -0
- package/dist/hooks/use-library-materials-version.js +10 -0
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +1 -0
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -6
- package/dist/lib/csg-utils.d.ts +1 -0
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +6 -1
- package/dist/lib/gpu-perf.d.ts +0 -2
- package/dist/lib/gpu-perf.d.ts.map +1 -1
- package/dist/lib/gpu-perf.js +7 -21
- package/dist/lib/layer-pass.d.ts +28 -0
- package/dist/lib/layer-pass.d.ts.map +1 -0
- package/dist/lib/layer-pass.js +254 -0
- package/dist/lib/materials.d.ts +12 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +51 -17
- package/dist/lib/merged-outline-node.d.ts +17 -8
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +194 -52
- package/dist/lib/perf-actions.d.ts +55 -0
- package/dist/lib/perf-actions.d.ts.map +1 -0
- package/dist/lib/perf-actions.js +211 -0
- package/dist/lib/perf-observers.d.ts +2 -0
- package/dist/lib/perf-observers.d.ts.map +1 -0
- package/dist/lib/perf-observers.js +17 -0
- package/dist/lib/perf-panel-store.d.ts +71 -0
- package/dist/lib/perf-panel-store.d.ts.map +1 -0
- package/dist/lib/perf-panel-store.js +29 -0
- package/dist/lib/perf-tracks.d.ts +43 -0
- package/dist/lib/perf-tracks.d.ts.map +1 -0
- package/dist/lib/perf-tracks.js +113 -0
- package/dist/lib/pointer-events.d.ts +7 -0
- package/dist/lib/pointer-events.d.ts.map +1 -0
- package/dist/lib/pointer-events.js +680 -0
- package/dist/lib/post-processing-resources.d.ts +12 -0
- package/dist/lib/post-processing-resources.d.ts.map +1 -0
- package/dist/lib/post-processing-resources.js +19 -0
- package/dist/lib/renderer-capability.d.ts.map +1 -1
- package/dist/lib/renderer-capability.js +4 -2
- package/dist/lib/scene-visibility.d.ts +2 -1
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +22 -1
- package/dist/store/use-viewer.d.ts +2 -2
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +1 -1
- package/dist/systems/door/door-animation-system.d.ts.map +1 -1
- package/dist/systems/door/door-animation-system.js +6 -9
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +13 -2
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +3 -2
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +1 -1
- package/dist/systems/level/level-utils.d.ts +9 -0
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +14 -0
- package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts +8 -0
- package/dist/systems/perf-action-settle/perf-action-settle-system.d.ts.map +1 -0
- package/dist/systems/perf-action-settle/perf-action-settle-system.js +32 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1196 -58
- package/dist/systems/scan/scan-system.js +6 -5
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +1 -2
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +9 -8
- package/dist/systems/surface-hole-geometry.d.ts +1 -1
- package/dist/systems/surface-hole-geometry.d.ts.map +1 -1
- package/dist/systems/surface-hole-geometry.js +1 -1
- package/dist/systems/wall/level-miter-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-build-lifecycle.d.ts +9 -0
- package/dist/systems/wall/wall-build-lifecycle.d.ts.map +1 -0
- package/dist/systems/wall/wall-build-lifecycle.js +74 -0
- package/dist/systems/wall/wall-cutout-cache.d.ts +57 -0
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -0
- package/dist/systems/wall/wall-cutout-cache.js +304 -0
- package/dist/systems/wall/wall-cutout.d.ts +12 -1
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +24 -161
- package/dist/systems/wall/wall-material-variant.d.ts +31 -0
- package/dist/systems/wall/wall-material-variant.d.ts.map +1 -0
- package/dist/systems/wall/wall-material-variant.js +10 -0
- package/dist/systems/wall/wall-materials.d.ts +6 -0
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +29 -13
- package/dist/systems/wall/wall-placeholder-sweep.d.ts +50 -0
- package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -0
- package/dist/systems/wall/wall-placeholder-sweep.js +51 -0
- package/dist/systems/wall/wall-rebuild-notifications.d.ts +5 -0
- package/dist/systems/wall/wall-rebuild-notifications.d.ts.map +1 -0
- package/dist/systems/wall/wall-rebuild-notifications.js +23 -0
- package/dist/systems/wall/wall-system.d.ts +11 -4
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +405 -153
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +9 -8
- package/dist/systems/window/window-system.d.ts +1 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +16 -2
- package/package.json +17 -4
- package/dist/lib/polygon-union.d.ts +0 -4
- package/dist/lib/polygon-union.d.ts.map +0 -1
- package/dist/lib/polygon-union.js +0 -289
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getEffectiveNode, getRoofModuleFaces, getRoofShapeInsets, getRoofShapeRatios, getSegmentSlopeFrame, hasSegmentMaterialOverride, nodeRegistry, normalizeRoofSegmentTrim, ROOF_SHAPE_DEFAULTS, sceneRegistry, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { getConicalRoofCoverage, getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getEffectiveNode, getRoofModuleFaces, getRoofPlanBounds, getRoofSegmentSurfaceY, getRoofShapeInsets, getRoofShapeRatios, getSegmentSlopeFrame, hasSegmentMaterialOverride, isBandedShedSegment, nodeRegistry, normalizeRoofSegmentTrim, pointInPolygon2D, ROOF_SHAPE_DEFAULTS, roofOverlapEntryOwns, roofPlanBoundsOverlap, sceneRegistry, unionPolygons, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
import { mergeGeometries, mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
5
5
|
import { ADDITION, Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
6
6
|
import { computeBoundsTree } from 'three-mesh-bvh';
|
|
7
7
|
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
8
|
-
import { ensureRenderableGeometryAttributes } from '../../lib/csg-utils';
|
|
8
|
+
import { ensureRenderableGeometryAttributes, subtractCsgBrush } from '../../lib/csg-utils';
|
|
9
9
|
function csgGeometry(brush) {
|
|
10
10
|
return brush.geometry;
|
|
11
11
|
}
|
|
@@ -108,9 +108,71 @@ function createDegenerateRoofPlaceholder() {
|
|
|
108
108
|
}
|
|
109
109
|
// Pending merged-roof updates carried across frames (for throttling)
|
|
110
110
|
const pendingRoofUpdates = new Set();
|
|
111
|
+
const previousRoofPlanBounds = new Map();
|
|
111
112
|
const warnedMergedRoofNaNIds = new Set();
|
|
112
113
|
const MAX_ROOFS_PER_FRAME = 1;
|
|
113
114
|
const MAX_SEGMENTS_PER_FRAME = 3;
|
|
115
|
+
function queueSiblingRoofUpdates(roofId, nodes) {
|
|
116
|
+
pendingRoofUpdates.add(roofId);
|
|
117
|
+
const roof = nodes[roofId]?.type === 'roof' ? getEffectiveNode(nodes[roofId]) : undefined;
|
|
118
|
+
if (roof?.type !== 'roof' || !roof.parentId)
|
|
119
|
+
return;
|
|
120
|
+
const currentBounds = getRoofPlanBounds({
|
|
121
|
+
position: roof.position,
|
|
122
|
+
rotation: roof.rotation,
|
|
123
|
+
segments: (roof.children ?? []).flatMap((id) => {
|
|
124
|
+
const segment = nodes[id];
|
|
125
|
+
if (segment?.type !== 'roof-segment')
|
|
126
|
+
return [];
|
|
127
|
+
const effective = getEffectiveNode(segment);
|
|
128
|
+
return [
|
|
129
|
+
{
|
|
130
|
+
position: effective.position,
|
|
131
|
+
rotation: effective.rotation,
|
|
132
|
+
width: effective.width,
|
|
133
|
+
depth: effective.depth,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
const oldBounds = previousRoofPlanBounds.get(roofId);
|
|
139
|
+
if (currentBounds)
|
|
140
|
+
previousRoofPlanBounds.set(roofId, currentBounds);
|
|
141
|
+
const parent = nodes[roof.parentId];
|
|
142
|
+
if (!parent || !('children' in parent) || !Array.isArray(parent.children))
|
|
143
|
+
return;
|
|
144
|
+
for (const siblingId of parent.children) {
|
|
145
|
+
const sibling = nodes[siblingId];
|
|
146
|
+
if (sibling?.type !== 'roof' || sibling.id === roofId)
|
|
147
|
+
continue;
|
|
148
|
+
const effectiveSibling = getEffectiveNode(sibling);
|
|
149
|
+
const siblingBounds = getRoofPlanBounds({
|
|
150
|
+
position: effectiveSibling.position,
|
|
151
|
+
rotation: effectiveSibling.rotation,
|
|
152
|
+
segments: (effectiveSibling.children ?? []).flatMap((id) => {
|
|
153
|
+
const segment = nodes[id];
|
|
154
|
+
if (segment?.type !== 'roof-segment')
|
|
155
|
+
return [];
|
|
156
|
+
const effective = getEffectiveNode(segment);
|
|
157
|
+
return [
|
|
158
|
+
{
|
|
159
|
+
position: effective.position,
|
|
160
|
+
rotation: effective.rotation,
|
|
161
|
+
width: effective.width,
|
|
162
|
+
depth: effective.depth,
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
}),
|
|
166
|
+
});
|
|
167
|
+
if (!siblingBounds)
|
|
168
|
+
continue;
|
|
169
|
+
previousRoofPlanBounds.set(sibling.id, siblingBounds);
|
|
170
|
+
if ((currentBounds && roofPlanBoundsOverlap(currentBounds, siblingBounds)) ||
|
|
171
|
+
(oldBounds && roofPlanBoundsOverlap(oldBounds, siblingBounds))) {
|
|
172
|
+
pendingRoofUpdates.add(sibling.id);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
114
176
|
// ============================================================================
|
|
115
177
|
// ROOF SYSTEM
|
|
116
178
|
// ============================================================================
|
|
@@ -127,6 +189,7 @@ export const RoofSystem = () => {
|
|
|
127
189
|
// Clear stale pending updates when the scene is unloaded
|
|
128
190
|
if (rootNodeIds.length === 0) {
|
|
129
191
|
pendingRoofUpdates.clear();
|
|
192
|
+
previousRoofPlanBounds.clear();
|
|
130
193
|
warnedMergedRoofNaNIds.clear();
|
|
131
194
|
for (const cached of mergedRoofSegmentGeometryCache.values()) {
|
|
132
195
|
disposeCachedMergedRoofSegmentGeometrySet(cached);
|
|
@@ -207,11 +270,11 @@ export const RoofSystem = () => {
|
|
|
207
270
|
}
|
|
208
271
|
// Queue the parent roof for a merged geometry update
|
|
209
272
|
if (effectiveSegment.parentId) {
|
|
210
|
-
|
|
273
|
+
queueSiblingRoofUpdates(effectiveSegment.parentId, nodes);
|
|
211
274
|
}
|
|
212
275
|
}
|
|
213
276
|
else if (node.type === 'roof') {
|
|
214
|
-
|
|
277
|
+
queueSiblingRoofUpdates(id, nodes);
|
|
215
278
|
clearDirty(id);
|
|
216
279
|
}
|
|
217
280
|
});
|
|
@@ -408,16 +471,36 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
408
471
|
}
|
|
409
472
|
let totalShinSlab = null;
|
|
410
473
|
let totalDeckSlab = null;
|
|
411
|
-
let
|
|
412
|
-
let totalInner = null;
|
|
474
|
+
let totalWallShell = null;
|
|
413
475
|
const rakeBoardGeometries = [];
|
|
476
|
+
const directSegmentGeometries = [];
|
|
477
|
+
const csgChildren = [];
|
|
414
478
|
for (const child of children) {
|
|
479
|
+
const directGeometry = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, child.position, child.rotation ?? 0), () => buildCustomShedGeometry(child, nodes) ?? buildDirectConicalSectorGeometry(child));
|
|
480
|
+
if (directGeometry) {
|
|
481
|
+
let withPanels = addShedInsetEndPanels(directGeometry, [child], false);
|
|
482
|
+
_matrix.compose(_position.set(child.position[0], child.position[1], child.position[2]), _quaternion.setFromAxisAngle(_yAxis, child.rotation), _scale);
|
|
483
|
+
withPanels.applyMatrix4(_matrix);
|
|
484
|
+
withPanels = clipDirectRoofGeometryAgainstSiblings(withPanels, child, nodes, 'roof');
|
|
485
|
+
directSegmentGeometries.push(withPanels);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
csgChildren.push(child);
|
|
415
489
|
const brushes = getMergedRoofSegmentBrushes(roofNode, child, nodes);
|
|
416
490
|
if (!brushes)
|
|
417
491
|
continue;
|
|
418
492
|
if (brushes.rakeBoards) {
|
|
419
493
|
rakeBoardGeometries.push(brushes.rakeBoards);
|
|
420
494
|
}
|
|
495
|
+
const occludingInterior = buildOccludingRoofInterior(child, nodes, 'roof');
|
|
496
|
+
if (occludingInterior) {
|
|
497
|
+
const exposedShingles = subtractCsgBrush(brushes.shinSlab, occludingInterior, csgEvaluator);
|
|
498
|
+
brushes.shinSlab.geometry.dispose();
|
|
499
|
+
brushes.shinSlab = exposedShingles;
|
|
500
|
+
const exposedDeck = subtractCsgBrush(brushes.deckSlab, occludingInterior, csgEvaluator);
|
|
501
|
+
brushes.deckSlab.geometry.dispose();
|
|
502
|
+
brushes.deckSlab = exposedDeck;
|
|
503
|
+
}
|
|
421
504
|
if (totalShinSlab) {
|
|
422
505
|
const next = csgEvaluator.evaluate(totalShinSlab, brushes.shinSlab, ADDITION);
|
|
423
506
|
totalShinSlab.geometry.dispose();
|
|
@@ -438,34 +521,41 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
438
521
|
else {
|
|
439
522
|
totalDeckSlab = brushes.deckSlab;
|
|
440
523
|
}
|
|
441
|
-
if (
|
|
442
|
-
const next = csgEvaluator.evaluate(totalWall, brushes.wallBrush, ADDITION);
|
|
443
|
-
totalWall.geometry.dispose();
|
|
524
|
+
if (!shouldIncludeRoofSegmentWallShell(child, roofNode)) {
|
|
444
525
|
brushes.wallBrush.geometry.dispose();
|
|
445
|
-
prepareBrushForCSG(next);
|
|
446
|
-
totalWall = next;
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
totalWall = brushes.wallBrush;
|
|
450
|
-
}
|
|
451
|
-
if (totalInner) {
|
|
452
|
-
const next = csgEvaluator.evaluate(totalInner, brushes.innerBrush, ADDITION);
|
|
453
|
-
totalInner.geometry.dispose();
|
|
454
526
|
brushes.innerBrush.geometry.dispose();
|
|
455
|
-
prepareBrushForCSG(next);
|
|
456
|
-
totalInner = next;
|
|
457
527
|
}
|
|
458
528
|
else {
|
|
459
|
-
|
|
529
|
+
let wallShell = csgEvaluator.evaluate(brushes.wallBrush, brushes.innerBrush, SUBTRACTION);
|
|
530
|
+
brushes.wallBrush.geometry.dispose();
|
|
531
|
+
brushes.innerBrush.geometry.dispose();
|
|
532
|
+
prepareBrushForCSG(wallShell);
|
|
533
|
+
if (occludingInterior) {
|
|
534
|
+
const exposedWall = subtractCsgBrush(wallShell, occludingInterior, csgEvaluator);
|
|
535
|
+
wallShell.geometry.dispose();
|
|
536
|
+
wallShell = exposedWall;
|
|
537
|
+
}
|
|
538
|
+
if (totalWallShell) {
|
|
539
|
+
const next = csgEvaluator.evaluate(totalWallShell, wallShell, ADDITION);
|
|
540
|
+
totalWallShell.geometry.dispose();
|
|
541
|
+
wallShell.geometry.dispose();
|
|
542
|
+
prepareBrushForCSG(next);
|
|
543
|
+
totalWallShell = next;
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
totalWallShell = wallShell;
|
|
547
|
+
}
|
|
460
548
|
}
|
|
549
|
+
occludingInterior?.geometry.dispose();
|
|
461
550
|
}
|
|
462
|
-
if (totalShinSlab && totalDeckSlab
|
|
551
|
+
if (totalShinSlab && totalDeckSlab) {
|
|
463
552
|
try {
|
|
464
|
-
const finalWallTrimmed = csgEvaluator.evaluate(totalWall, totalInner, SUBTRACTION);
|
|
465
|
-
prepareBrushForCSG(finalWallTrimmed);
|
|
466
553
|
const shinDeck = csgEvaluator.evaluate(totalShinSlab, totalDeckSlab, ADDITION);
|
|
467
554
|
prepareBrushForCSG(shinDeck);
|
|
468
|
-
|
|
555
|
+
let combined = shinDeck;
|
|
556
|
+
if (totalWallShell) {
|
|
557
|
+
combined = csgEvaluator.evaluate(shinDeck, totalWallShell, ADDITION);
|
|
558
|
+
}
|
|
469
559
|
prepareBrushForCSG(combined);
|
|
470
560
|
const resultGeo = csgGeometry(combined);
|
|
471
561
|
if (geometryHasInvalidAttributes(resultGeo)) {
|
|
@@ -474,14 +564,15 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
474
564
|
warnedMergedRoofNaNIds.add(roofNode.id);
|
|
475
565
|
}
|
|
476
566
|
resultGeo.dispose();
|
|
477
|
-
|
|
478
|
-
|
|
567
|
+
if (combined !== shinDeck)
|
|
568
|
+
shinDeck.geometry.dispose();
|
|
479
569
|
totalShinSlab.geometry.dispose();
|
|
480
570
|
totalDeckSlab.geometry.dispose();
|
|
481
|
-
|
|
482
|
-
totalInner.geometry.dispose();
|
|
571
|
+
totalWallShell?.geometry.dispose();
|
|
483
572
|
for (const geometry of rakeBoardGeometries)
|
|
484
573
|
geometry.dispose();
|
|
574
|
+
for (const geometry of directSegmentGeometries)
|
|
575
|
+
geometry.dispose();
|
|
485
576
|
return;
|
|
486
577
|
}
|
|
487
578
|
const resultMaterials = csgMaterials(combined);
|
|
@@ -494,9 +585,10 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
494
585
|
for (const g of resultGeo.groups) {
|
|
495
586
|
g.materialIndex = mapRoofGroupMaterialIndex(g.materialIndex, resultMaterials, matToIndex);
|
|
496
587
|
}
|
|
497
|
-
let finalGeo = resultGeo;
|
|
498
|
-
|
|
499
|
-
|
|
588
|
+
let finalGeo = addShedInsetEndPanels(resultGeo, csgChildren, true);
|
|
589
|
+
const appendedGeometries = [...rakeBoardGeometries, ...directSegmentGeometries];
|
|
590
|
+
if (appendedGeometries.length > 0) {
|
|
591
|
+
const merged = mergeGeometriesPreservingGroups([finalGeo, ...appendedGeometries]);
|
|
500
592
|
if (merged) {
|
|
501
593
|
finalGeo.dispose();
|
|
502
594
|
finalGeo = merged;
|
|
@@ -504,23 +596,40 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
504
596
|
}
|
|
505
597
|
for (const geometry of rakeBoardGeometries)
|
|
506
598
|
geometry.dispose();
|
|
599
|
+
for (const geometry of directSegmentGeometries)
|
|
600
|
+
geometry.dispose();
|
|
601
|
+
directSegmentGeometries.length = 0;
|
|
507
602
|
finalGeo.computeVertexNormals();
|
|
508
603
|
ensureRenderableGeometryAttributes(finalGeo);
|
|
509
604
|
mergedMesh.geometry.dispose();
|
|
510
605
|
mergedMesh.geometry = finalGeo;
|
|
511
|
-
|
|
512
|
-
|
|
606
|
+
if (combined !== shinDeck)
|
|
607
|
+
shinDeck.geometry.dispose();
|
|
513
608
|
}
|
|
514
609
|
catch (e) {
|
|
515
610
|
console.error('Merged roof CSG failed:', e);
|
|
516
611
|
}
|
|
517
612
|
totalShinSlab.geometry.dispose();
|
|
518
613
|
totalDeckSlab.geometry.dispose();
|
|
519
|
-
|
|
520
|
-
totalInner.geometry.dispose();
|
|
614
|
+
totalWallShell?.geometry.dispose();
|
|
521
615
|
for (const geometry of rakeBoardGeometries)
|
|
522
616
|
geometry.dispose();
|
|
523
617
|
}
|
|
618
|
+
if (directSegmentGeometries.length > 0) {
|
|
619
|
+
const finalGeo = directSegmentGeometries.length === 1
|
|
620
|
+
? directSegmentGeometries[0]
|
|
621
|
+
: mergeGeometriesPreservingGroups(directSegmentGeometries);
|
|
622
|
+
if (finalGeo) {
|
|
623
|
+
finalGeo.computeVertexNormals();
|
|
624
|
+
ensureRenderableGeometryAttributes(finalGeo);
|
|
625
|
+
mergedMesh.geometry.dispose();
|
|
626
|
+
mergedMesh.geometry = finalGeo;
|
|
627
|
+
}
|
|
628
|
+
for (const geometry of directSegmentGeometries) {
|
|
629
|
+
if (geometry !== finalGeo)
|
|
630
|
+
geometry.dispose();
|
|
631
|
+
}
|
|
632
|
+
}
|
|
524
633
|
}
|
|
525
634
|
function geometryHasInvalidAttributes(geometry) {
|
|
526
635
|
const position = geometry.getAttribute('position');
|
|
@@ -707,6 +816,8 @@ const SHINGLE_SURFACE_EPSILON = 0.02;
|
|
|
707
816
|
const RAKE_FACE_NORMAL_EPSILON = 0.3;
|
|
708
817
|
const RAKE_FACE_ALIGNMENT_EPSILON = 0.35;
|
|
709
818
|
const TRIM_CUT_EPSILON = 0.002;
|
|
819
|
+
const ROOF_EDGE_MATERIAL_INDEX = 0;
|
|
820
|
+
const ROOF_INSET_WALL_MATERIAL_INDEX = 2;
|
|
710
821
|
const DUTCH_RAKE_SIDE_MATERIAL_INDEX = 1;
|
|
711
822
|
const DUTCH_RAKE_TOP_MATERIAL_INDEX = 3;
|
|
712
823
|
const DUTCH_RAKE_SLOPE_SEAT_OFFSET = 0.0002;
|
|
@@ -714,6 +825,41 @@ function pushDoubleSidedFace(targetFaces, face) {
|
|
|
714
825
|
targetFaces.push(face);
|
|
715
826
|
targetFaces.push(face.map((point) => point.clone()).reverse());
|
|
716
827
|
}
|
|
828
|
+
function readShedFootprintPieces(node) {
|
|
829
|
+
const value = node.shedFootprintPieces;
|
|
830
|
+
if (!Array.isArray(value))
|
|
831
|
+
return [];
|
|
832
|
+
return value.flatMap((polygon) => {
|
|
833
|
+
if (!Array.isArray(polygon))
|
|
834
|
+
return [];
|
|
835
|
+
const points = polygon.flatMap((point) => {
|
|
836
|
+
if (!Array.isArray(point) || point.length < 2)
|
|
837
|
+
return [];
|
|
838
|
+
const x = readFiniteNumber(point[0]);
|
|
839
|
+
const z = readFiniteNumber(point[1]);
|
|
840
|
+
return x === null || z === null ? [] : [[x, z]];
|
|
841
|
+
});
|
|
842
|
+
return points.length >= 3 && points.length === polygon.length ? [points] : [];
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function readShedOpenEndSides(node) {
|
|
846
|
+
const value = node.shedOpenEndSides;
|
|
847
|
+
if (!Array.isArray(value))
|
|
848
|
+
return new Set();
|
|
849
|
+
return new Set(value.filter((side) => side === 'left' || side === 'right'));
|
|
850
|
+
}
|
|
851
|
+
function shouldIncludeRoofSegmentWallShell(node, parentRoof) {
|
|
852
|
+
if (node.wallShell === 'include')
|
|
853
|
+
return true;
|
|
854
|
+
if (node.wallShell === 'omit')
|
|
855
|
+
return false;
|
|
856
|
+
if (node.roofType !== 'shed')
|
|
857
|
+
return true;
|
|
858
|
+
// Older composite roofs use overlapping shed segments as deck pieces. Their
|
|
859
|
+
// wall volumes were never part of the rendered shell and make CSG grow
|
|
860
|
+
// exponentially when unioned together.
|
|
861
|
+
return !parentRoof || (parentRoof.children?.length ?? 0) <= 1;
|
|
862
|
+
}
|
|
717
863
|
function hasSegmentTrim(node) {
|
|
718
864
|
const trim = normalizeRoofSegmentTrim(node);
|
|
719
865
|
return (trim.left > 0 ||
|
|
@@ -949,6 +1095,7 @@ export function clipGeometryBySegmentTrim(geometry, segment) {
|
|
|
949
1095
|
*/
|
|
950
1096
|
export function getRoofSegmentBrushes(node) {
|
|
951
1097
|
const { roofType, width, depth, wallHeight, wallThickness, deckThickness, overhang, shingleThickness, } = node;
|
|
1098
|
+
const conicalCoverage = getConicalRoofCoverage(node);
|
|
952
1099
|
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node);
|
|
953
1100
|
const shapeRatios = getRoofShapeRatios({
|
|
954
1101
|
gambrelLowerWidthRatio: node.gambrelLowerWidthRatio,
|
|
@@ -963,18 +1110,20 @@ export function getRoofSegmentBrushes(node) {
|
|
|
963
1110
|
// the 2D floorplan and the slope frame (which derives activeRh from the
|
|
964
1111
|
// same ratio). A hardcoded 0.25 desyncs the gablet from the parameter.
|
|
965
1112
|
const baseI = Math.min(width, depth) * node.dutchHipWidthRatio;
|
|
966
|
-
const getVol = (wExt, vOffset, baseY, matIndex, isVoid) => {
|
|
1113
|
+
const getVol = (wExt, vOffset, baseY, matIndex, isVoid, materialRule) => {
|
|
967
1114
|
const wV = Math.max(0.01, width + 2 * wExt);
|
|
968
1115
|
const dV = Math.max(0.01, depth + 2 * wExt);
|
|
969
1116
|
const autoDrop = wExt * tanTheta;
|
|
970
|
-
|
|
1117
|
+
// Floor every prism at 5 cm so CSG never sees a degenerate volume — by
|
|
1118
|
+
// raising the top, never by sinking the base (the base is the wall top).
|
|
1119
|
+
// One floor for all volumes keeps each cutter level with the shell it carves.
|
|
1120
|
+
const whV = Math.max(0.05, wallHeight - autoDrop + vOffset);
|
|
971
1121
|
let rhV = activeRh;
|
|
972
1122
|
if (activeRh > 0) {
|
|
973
1123
|
rhV = activeRh + autoDrop;
|
|
974
1124
|
if (roofType === 'shed')
|
|
975
1125
|
rhV = activeRh + 2 * autoDrop;
|
|
976
1126
|
}
|
|
977
|
-
const safeBaseY = Math.min(baseY, whV - 0.05);
|
|
978
1127
|
let structuralI = baseI;
|
|
979
1128
|
if (isVoid) {
|
|
980
1129
|
structuralI += deckThickness;
|
|
@@ -985,21 +1134,26 @@ export function getRoofSegmentBrushes(node) {
|
|
|
985
1134
|
d: dV,
|
|
986
1135
|
wh: whV,
|
|
987
1136
|
rh: rhV,
|
|
988
|
-
baseY
|
|
1137
|
+
baseY,
|
|
989
1138
|
insets: { dutchI: structuralI },
|
|
990
1139
|
baseW: width,
|
|
991
1140
|
baseD: depth,
|
|
992
1141
|
tanTheta,
|
|
993
1142
|
shapeRatios,
|
|
994
1143
|
dutchTopRakeThickness: node.dutchTopRakeThickness,
|
|
1144
|
+
conicalStartAngle: conicalCoverage.startAngle,
|
|
1145
|
+
conicalSweepAngle: conicalCoverage.sweepAngle,
|
|
995
1146
|
}).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
|
|
996
|
-
return createGeometryFromFaces(faces, matIndex);
|
|
1147
|
+
return createGeometryFromFaces(faces, materialRule ?? matIndex);
|
|
997
1148
|
};
|
|
998
1149
|
const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false);
|
|
999
1150
|
const innerGeo = getVol(-wallThickness / 2, 0, -5, 2, false);
|
|
1000
1151
|
const horizontalOverhang = overhang * cosTheta;
|
|
1001
1152
|
const deckExt = wallThickness / 2 + horizontalOverhang;
|
|
1002
|
-
const
|
|
1153
|
+
const shedRoofSideMaterialRule = roofType === 'shed'
|
|
1154
|
+
? (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX
|
|
1155
|
+
: undefined;
|
|
1156
|
+
const deckTopGeo = getVol(deckExt, verticalRt, 0, 1, false, shedRoofSideMaterialRule);
|
|
1003
1157
|
const deckBotGeo = getVol(deckExt, 0, -5, 0, true);
|
|
1004
1158
|
const stSin = shingleThickness * sinTheta;
|
|
1005
1159
|
const stCos = shingleThickness * cosTheta;
|
|
@@ -1016,7 +1170,7 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1016
1170
|
let shinTopW = shinBotW;
|
|
1017
1171
|
let shinTopD = shinBotD;
|
|
1018
1172
|
let transZ = 0;
|
|
1019
|
-
if (['hip', 'mansard', 'dutch'].includes(roofType)) {
|
|
1173
|
+
if (['hip', 'mansard', 'dutch', 'conical'].includes(roofType)) {
|
|
1020
1174
|
shinTopW += 2 * stSin;
|
|
1021
1175
|
shinTopD += 2 * stSin;
|
|
1022
1176
|
}
|
|
@@ -1077,6 +1231,8 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1077
1231
|
tanTheta,
|
|
1078
1232
|
shapeRatios,
|
|
1079
1233
|
dutchTopRakeThickness: node.dutchTopRakeThickness,
|
|
1234
|
+
conicalStartAngle: conicalCoverage.startAngle,
|
|
1235
|
+
conicalSweepAngle: conicalCoverage.sweepAngle,
|
|
1080
1236
|
}).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
|
|
1081
1237
|
const topFaces = getRoofModuleFaces({
|
|
1082
1238
|
type: roofType,
|
|
@@ -1091,13 +1247,16 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1091
1247
|
tanTheta,
|
|
1092
1248
|
shapeRatios,
|
|
1093
1249
|
dutchTopRakeThickness: node.dutchTopRakeThickness,
|
|
1250
|
+
conicalStartAngle: conicalCoverage.startAngle,
|
|
1251
|
+
conicalSweepAngle: conicalCoverage.sweepAngle,
|
|
1094
1252
|
}).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
|
|
1095
1253
|
let rakeBoards = null;
|
|
1096
1254
|
if (roofType === 'dutch' && insetsTop.dutchI !== undefined) {
|
|
1097
1255
|
rakeBoards = buildDutchRakeBoards(shinTopW, shinTopD, shinTopWh, shinTopRh, insetsTop.dutchI, shapeRatios, node.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake, node.dutchTopRakeThickness ?? ROOF_SHAPE_DEFAULTS.dutchTopRakeThickness);
|
|
1098
1256
|
}
|
|
1099
|
-
const
|
|
1100
|
-
const
|
|
1257
|
+
const shedRoofSideMaterialIndex = roofType === 'shed' ? ROOF_EDGE_MATERIAL_INDEX : 1;
|
|
1258
|
+
const shinBotGeo = createGeometryFromFaces(botFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : shedRoofSideMaterialIndex);
|
|
1259
|
+
const shinTopGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : shedRoofSideMaterialIndex);
|
|
1101
1260
|
if (transZ !== 0) {
|
|
1102
1261
|
shinTopGeo.translate(0, 0, transZ);
|
|
1103
1262
|
rakeBoards?.translate(0, 0, transZ);
|
|
@@ -1201,12 +1360,22 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1201
1360
|
return null;
|
|
1202
1361
|
}
|
|
1203
1362
|
export function generateRoofSegmentGeometry(node, nodes) {
|
|
1204
|
-
const
|
|
1205
|
-
const
|
|
1206
|
-
const
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
const
|
|
1363
|
+
const parentNode = node.parentId ? nodes?.[node.parentId] : undefined;
|
|
1364
|
+
const parentRoof = parentNode?.type === 'roof' ? parentNode : undefined;
|
|
1365
|
+
const parentRoofPosition = parentRoof?.position;
|
|
1366
|
+
const parentRoofRotation = parentRoof?.rotation ?? 0;
|
|
1367
|
+
const segmentWorldMatrix = composeSegmentWorldMatrix(parentRoofPosition, parentRoofRotation, node.position, node.rotation ?? 0);
|
|
1368
|
+
const directSegmentGeometry = withSegmentUvMatrix(segmentWorldMatrix, () => buildCustomShedGeometry(node, nodes) ?? buildDirectConicalSectorGeometry(node));
|
|
1369
|
+
if (directSegmentGeometry) {
|
|
1370
|
+
let result = addShedInsetEndPanels(directSegmentGeometry, [node], false);
|
|
1371
|
+
if (nodes) {
|
|
1372
|
+
result = clipDirectRoofGeometryAgainstSiblings(result, node, nodes, 'segment');
|
|
1373
|
+
}
|
|
1374
|
+
result.computeVertexNormals();
|
|
1375
|
+
ensureRenderableGeometryAttributes(result);
|
|
1376
|
+
return result;
|
|
1377
|
+
}
|
|
1378
|
+
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node));
|
|
1210
1379
|
if (!brushes) {
|
|
1211
1380
|
// Fallback: simple box
|
|
1212
1381
|
return new THREE.BoxGeometry(node.width, node.wallHeight, node.depth);
|
|
@@ -1217,12 +1386,23 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1217
1386
|
const { deckSlab, shinSlab, wallBrush, innerBrush, rakeBoards } = brushes;
|
|
1218
1387
|
let resultGeo = new THREE.BufferGeometry();
|
|
1219
1388
|
try {
|
|
1220
|
-
const hollowWall = csgEvaluator.evaluate(wallBrush, innerBrush, SUBTRACTION);
|
|
1221
|
-
prepareBrushForCSG(hollowWall);
|
|
1222
1389
|
const shinDeck = csgEvaluator.evaluate(shinSlab, deckSlab, ADDITION);
|
|
1223
1390
|
prepareBrushForCSG(shinDeck);
|
|
1224
|
-
|
|
1391
|
+
let combined = shinDeck;
|
|
1392
|
+
let hollowWall = null;
|
|
1393
|
+
if (shouldIncludeRoofSegmentWallShell(node, parentRoof)) {
|
|
1394
|
+
hollowWall = csgEvaluator.evaluate(wallBrush, innerBrush, SUBTRACTION);
|
|
1395
|
+
prepareBrushForCSG(hollowWall);
|
|
1396
|
+
combined = csgEvaluator.evaluate(shinDeck, hollowWall, ADDITION);
|
|
1397
|
+
}
|
|
1225
1398
|
prepareBrushForCSG(combined);
|
|
1399
|
+
const siblingInterior = nodes ? buildOccludingRoofInterior(node, nodes, 'segment') : null;
|
|
1400
|
+
if (siblingInterior) {
|
|
1401
|
+
const unclipped = combined;
|
|
1402
|
+
combined = subtractCsgBrush(unclipped, siblingInterior, csgEvaluator);
|
|
1403
|
+
unclipped.geometry.dispose();
|
|
1404
|
+
siblingInterior.geometry.dispose();
|
|
1405
|
+
}
|
|
1226
1406
|
resultGeo = csgGeometry(combined);
|
|
1227
1407
|
if (geometryHasInvalidAttributes(resultGeo)) {
|
|
1228
1408
|
resultGeo.dispose();
|
|
@@ -1239,8 +1419,10 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1239
1419
|
group.materialIndex = mapRoofGroupMaterialIndex(group.materialIndex, resultMaterials, matToIndex);
|
|
1240
1420
|
}
|
|
1241
1421
|
remapRoofShellFaces(resultGeo, node);
|
|
1242
|
-
|
|
1243
|
-
|
|
1422
|
+
resultGeo = addShedInsetEndPanels(resultGeo, [node], false);
|
|
1423
|
+
hollowWall?.geometry.dispose();
|
|
1424
|
+
if (combined !== shinDeck)
|
|
1425
|
+
shinDeck.geometry.dispose();
|
|
1244
1426
|
}
|
|
1245
1427
|
catch (e) {
|
|
1246
1428
|
console.error('Roof CSG failed:', e);
|
|
@@ -1262,6 +1444,124 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1262
1444
|
ensureRenderableGeometryAttributes(resultGeo);
|
|
1263
1445
|
return resultGeo;
|
|
1264
1446
|
}
|
|
1447
|
+
function clipDirectRoofGeometryAgainstSiblings(geometry, node, nodes, space) {
|
|
1448
|
+
const siblingInterior = buildOccludingRoofInterior(node, nodes, space);
|
|
1449
|
+
if (!siblingInterior)
|
|
1450
|
+
return geometry;
|
|
1451
|
+
const brush = new Brush(geometry, dummyMats);
|
|
1452
|
+
prepareBrushForCSG(brush);
|
|
1453
|
+
try {
|
|
1454
|
+
const clipped = subtractCsgBrush(brush, siblingInterior, csgEvaluator);
|
|
1455
|
+
const clippedGeometry = csgGeometry(clipped);
|
|
1456
|
+
const clippedMaterials = csgMaterials(clipped);
|
|
1457
|
+
const materialIndices = new Map([
|
|
1458
|
+
[dummyMats[0], 0],
|
|
1459
|
+
[dummyMats[1], 1],
|
|
1460
|
+
[dummyMats[2], 2],
|
|
1461
|
+
[dummyMats[3], 3],
|
|
1462
|
+
]);
|
|
1463
|
+
for (const group of clippedGeometry.groups) {
|
|
1464
|
+
group.materialIndex = mapRoofGroupMaterialIndex(group.materialIndex, clippedMaterials, materialIndices);
|
|
1465
|
+
}
|
|
1466
|
+
geometry.dispose();
|
|
1467
|
+
clippedGeometry.computeVertexNormals();
|
|
1468
|
+
ensureRenderableGeometryAttributes(clippedGeometry);
|
|
1469
|
+
return clippedGeometry;
|
|
1470
|
+
}
|
|
1471
|
+
catch (error) {
|
|
1472
|
+
console.error('Direct roof intersection CSG failed:', error);
|
|
1473
|
+
return geometry;
|
|
1474
|
+
}
|
|
1475
|
+
finally {
|
|
1476
|
+
siblingInterior.geometry.dispose();
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function buildOccludingRoofInterior(node, nodes, space) {
|
|
1480
|
+
if (!node.parentId)
|
|
1481
|
+
return null;
|
|
1482
|
+
const parent = nodes[node.parentId];
|
|
1483
|
+
if (parent?.type !== 'roof')
|
|
1484
|
+
return null;
|
|
1485
|
+
const roofEntries = collectSiblingRoofEntries(parent, nodes);
|
|
1486
|
+
const currentEntry = roofEntries.find(({ segment }) => segment.id === node.id);
|
|
1487
|
+
if (!currentEntry)
|
|
1488
|
+
return null;
|
|
1489
|
+
const targetRoofInverse = composeRoofTransform(parent).invert();
|
|
1490
|
+
const targetSegmentInverse = composeSegmentTransform(node).invert();
|
|
1491
|
+
let combinedInterior = null;
|
|
1492
|
+
for (let siblingIndex = 0; siblingIndex < roofEntries.length; siblingIndex++) {
|
|
1493
|
+
const entry = roofEntries[siblingIndex];
|
|
1494
|
+
const sibling = entry.segment;
|
|
1495
|
+
if (sibling.id === node.id)
|
|
1496
|
+
continue;
|
|
1497
|
+
if (sibling.roofType === 'shed')
|
|
1498
|
+
continue;
|
|
1499
|
+
const siblingOwnsOverlap = roofOverlapEntryOwns(roofOverlapEntry(entry.roof, sibling, nodes), roofOverlapEntry(currentEntry.roof, node, nodes));
|
|
1500
|
+
if (!siblingOwnsOverlap)
|
|
1501
|
+
continue;
|
|
1502
|
+
const siblingBrushes = getRoofSegmentBrushes(sibling);
|
|
1503
|
+
if (!siblingBrushes)
|
|
1504
|
+
continue;
|
|
1505
|
+
const siblingInTargetRoof = new THREE.Matrix4()
|
|
1506
|
+
.multiplyMatrices(targetRoofInverse, composeRoofTransform(entry.roof))
|
|
1507
|
+
.multiply(composeSegmentTransform(sibling));
|
|
1508
|
+
const relativeMatrix = space === 'segment'
|
|
1509
|
+
? new THREE.Matrix4().multiplyMatrices(targetSegmentInverse, siblingInTargetRoof)
|
|
1510
|
+
: siblingInTargetRoof;
|
|
1511
|
+
csgGeometry(siblingBrushes.innerBrush).applyMatrix4(relativeMatrix);
|
|
1512
|
+
siblingBrushes.innerBrush.updateMatrixWorld();
|
|
1513
|
+
siblingBrushes.shinSlab.geometry.dispose();
|
|
1514
|
+
siblingBrushes.deckSlab.geometry.dispose();
|
|
1515
|
+
siblingBrushes.wallBrush.geometry.dispose();
|
|
1516
|
+
siblingBrushes.rakeBoards?.dispose();
|
|
1517
|
+
if (combinedInterior) {
|
|
1518
|
+
const next = csgEvaluator.evaluate(combinedInterior, siblingBrushes.innerBrush, ADDITION);
|
|
1519
|
+
combinedInterior.geometry.dispose();
|
|
1520
|
+
siblingBrushes.innerBrush.geometry.dispose();
|
|
1521
|
+
prepareBrushForCSG(next);
|
|
1522
|
+
combinedInterior = next;
|
|
1523
|
+
}
|
|
1524
|
+
else {
|
|
1525
|
+
combinedInterior = siblingBrushes.innerBrush;
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
return combinedInterior;
|
|
1529
|
+
}
|
|
1530
|
+
function roofOverlapEntry(roof, segment, nodes) {
|
|
1531
|
+
const supportSegment = roof.support?.kind === 'roof' ? nodes[roof.support.roofSegmentId] : undefined;
|
|
1532
|
+
return {
|
|
1533
|
+
roofId: String(roof.id),
|
|
1534
|
+
segmentId: String(segment.id),
|
|
1535
|
+
supportRoofId: supportSegment?.type === 'roof-segment' && supportSegment.parentId
|
|
1536
|
+
? String(supportSegment.parentId)
|
|
1537
|
+
: undefined,
|
|
1538
|
+
supportRoofSegmentId: roof.support?.kind === 'roof' ? String(roof.support.roofSegmentId) : undefined,
|
|
1539
|
+
roofType: segment.roofType,
|
|
1540
|
+
width: segment.width,
|
|
1541
|
+
depth: segment.depth,
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
function collectSiblingRoofEntries(targetRoof, nodes) {
|
|
1545
|
+
const parent = targetRoof.parentId ? nodes[targetRoof.parentId] : undefined;
|
|
1546
|
+
const orderedRoofIds = parent && 'children' in parent && Array.isArray(parent.children)
|
|
1547
|
+
? parent.children.filter((id) => nodes[id]?.type === 'roof')
|
|
1548
|
+
: [targetRoof.id];
|
|
1549
|
+
if (!orderedRoofIds.includes(targetRoof.id))
|
|
1550
|
+
orderedRoofIds.push(targetRoof.id);
|
|
1551
|
+
return orderedRoofIds.flatMap((roofId) => {
|
|
1552
|
+
const roof = getEffectiveNode(nodes[roofId]);
|
|
1553
|
+
return (roof.children ?? []).flatMap((segmentId) => {
|
|
1554
|
+
const segment = nodes[segmentId];
|
|
1555
|
+
return segment?.type === 'roof-segment' ? [{ roof, segment: getEffectiveNode(segment) }] : [];
|
|
1556
|
+
});
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
function composeRoofTransform(roof) {
|
|
1560
|
+
return new THREE.Matrix4().compose(new THREE.Vector3(...roof.position), new THREE.Quaternion().setFromAxisAngle(_yAxis, roof.rotation ?? 0), new THREE.Vector3(1, 1, 1));
|
|
1561
|
+
}
|
|
1562
|
+
function composeSegmentTransform(segment) {
|
|
1563
|
+
return new THREE.Matrix4().compose(new THREE.Vector3(...segment.position), new THREE.Quaternion().setFromAxisAngle(_yAxis, segment.rotation ?? 0), new THREE.Vector3(1, 1, 1));
|
|
1564
|
+
}
|
|
1265
1565
|
function createGeometryFromRawAttributes(positions, normals, uvs, groups) {
|
|
1266
1566
|
const geometry = new THREE.BufferGeometry();
|
|
1267
1567
|
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
@@ -1298,6 +1598,742 @@ function mergeGeometriesPreservingGroups(geometries) {
|
|
|
1298
1598
|
}
|
|
1299
1599
|
return merged;
|
|
1300
1600
|
}
|
|
1601
|
+
// Signed bend reference for a banded segment: the divisor that turns a flat
|
|
1602
|
+
// along-width coordinate into an arc angle.
|
|
1603
|
+
function bandSignedRef(arc) {
|
|
1604
|
+
return (Math.sign(arc.centerZ) || 1) * arc.radius;
|
|
1605
|
+
}
|
|
1606
|
+
// Concentric map: rotate a flat segment-local (x, z) about the stored arc center by
|
|
1607
|
+
// the angle its along-width coordinate subtends. The back (wall) edge lands at the
|
|
1608
|
+
// wall's radius, the front edge at radius ± depth — a thin annular band, never a disc.
|
|
1609
|
+
function bendBandPoint(arc, signedRef, x, z) {
|
|
1610
|
+
const phi = (x - arc.centerX) / signedRef;
|
|
1611
|
+
const radial = z - arc.centerZ;
|
|
1612
|
+
return {
|
|
1613
|
+
x: arc.centerX - radial * Math.sin(phi),
|
|
1614
|
+
z: arc.centerZ + radial * Math.cos(phi),
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
// Remap every vertex of a flat segment-local geometry onto the concentric band,
|
|
1618
|
+
// keeping Y. Used for the side-infill end panels so they follow the arc ends.
|
|
1619
|
+
function applyBandBendToGeometry(geometry, arc) {
|
|
1620
|
+
const position = geometry.getAttribute('position');
|
|
1621
|
+
if (!position)
|
|
1622
|
+
return;
|
|
1623
|
+
const signedRef = bandSignedRef(arc);
|
|
1624
|
+
for (let index = 0; index < position.count; index++) {
|
|
1625
|
+
const bent = bendBandPoint(arc, signedRef, position.getX(index), position.getZ(index));
|
|
1626
|
+
position.setX(index, bent.x);
|
|
1627
|
+
position.setZ(index, bent.z);
|
|
1628
|
+
}
|
|
1629
|
+
position.needsUpdate = true;
|
|
1630
|
+
}
|
|
1631
|
+
// Faceted annular-band deck for a shed segment bent across its width. The slope runs
|
|
1632
|
+
// unchanged along depth (Z); the width axis (X) sweeps the stored concentric arc, so
|
|
1633
|
+
// the deck hugs the host wall as a thin band and can never balloon into a disc.
|
|
1634
|
+
function buildConcentricBandDeckGeometry(node) {
|
|
1635
|
+
const arc = node.arc;
|
|
1636
|
+
if (!arc)
|
|
1637
|
+
return null;
|
|
1638
|
+
const width = node.width;
|
|
1639
|
+
const halfWidth = width / 2;
|
|
1640
|
+
const halfDepth = node.depth / 2;
|
|
1641
|
+
const signedRef = bandSignedRef(arc);
|
|
1642
|
+
const { cosTheta } = getSegmentSlopeFrame(node);
|
|
1643
|
+
const verticalThickness = node.deckThickness / Math.max(0.1, cosTheta) + node.shingleThickness * cosTheta;
|
|
1644
|
+
const facetCount = Math.max(4, Math.min(32, Math.ceil(width / 0.4)));
|
|
1645
|
+
const bend = (localX, localZ) => {
|
|
1646
|
+
const bent = bendBandPoint(arc, signedRef, localX, localZ);
|
|
1647
|
+
return new THREE.Vector3(bent.x, getRoofSegmentSurfaceY(node, localX, localZ), bent.z);
|
|
1648
|
+
};
|
|
1649
|
+
const backBottom = [];
|
|
1650
|
+
const frontBottom = [];
|
|
1651
|
+
for (let index = 0; index <= facetCount; index++) {
|
|
1652
|
+
const localX = -halfWidth + (index / facetCount) * width;
|
|
1653
|
+
backBottom.push(bend(localX, -halfDepth));
|
|
1654
|
+
frontBottom.push(bend(localX, halfDepth));
|
|
1655
|
+
}
|
|
1656
|
+
const raise = (point) => new THREE.Vector3(point.x, point.y + verticalThickness, point.z);
|
|
1657
|
+
const backTop = backBottom.map(raise);
|
|
1658
|
+
const frontTop = frontBottom.map(raise);
|
|
1659
|
+
const faces = [];
|
|
1660
|
+
for (let index = 0; index < facetCount; index++) {
|
|
1661
|
+
const next = index + 1;
|
|
1662
|
+
// Each angular interval is its own convex quad. A single polygon around
|
|
1663
|
+
// the complete annular boundary is concave, so fan triangulation sends
|
|
1664
|
+
// diagonals through the open center and fills the roof as a solid sector.
|
|
1665
|
+
faces.push([
|
|
1666
|
+
backBottom[index].clone(),
|
|
1667
|
+
backBottom[next].clone(),
|
|
1668
|
+
frontBottom[next].clone(),
|
|
1669
|
+
frontBottom[index].clone(),
|
|
1670
|
+
], [
|
|
1671
|
+
frontTop[index].clone(),
|
|
1672
|
+
frontTop[next].clone(),
|
|
1673
|
+
backTop[next].clone(),
|
|
1674
|
+
backTop[index].clone(),
|
|
1675
|
+
], [
|
|
1676
|
+
backBottom[next].clone(),
|
|
1677
|
+
backBottom[index].clone(),
|
|
1678
|
+
backTop[index].clone(),
|
|
1679
|
+
backTop[next].clone(),
|
|
1680
|
+
], [
|
|
1681
|
+
frontBottom[index].clone(),
|
|
1682
|
+
frontBottom[next].clone(),
|
|
1683
|
+
frontTop[next].clone(),
|
|
1684
|
+
frontTop[index].clone(),
|
|
1685
|
+
]);
|
|
1686
|
+
}
|
|
1687
|
+
const last = facetCount;
|
|
1688
|
+
faces.push([backBottom[0].clone(), frontBottom[0].clone(), frontTop[0].clone(), backTop[0].clone()], [
|
|
1689
|
+
frontBottom[last].clone(),
|
|
1690
|
+
backBottom[last].clone(),
|
|
1691
|
+
backTop[last].clone(),
|
|
1692
|
+
frontTop[last].clone(),
|
|
1693
|
+
]);
|
|
1694
|
+
const merged = createGeometryFromFaces(faces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX);
|
|
1695
|
+
merged.computeVertexNormals();
|
|
1696
|
+
ensureRenderableGeometryAttributes(merged);
|
|
1697
|
+
return merged;
|
|
1698
|
+
}
|
|
1699
|
+
function buildDirectConicalSectorGeometry(node) {
|
|
1700
|
+
if (node.roofType !== 'conical' || hasSegmentTrim(node))
|
|
1701
|
+
return null;
|
|
1702
|
+
const coverage = getConicalRoofCoverage(node);
|
|
1703
|
+
if (coverage.fullCircle)
|
|
1704
|
+
return null;
|
|
1705
|
+
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node);
|
|
1706
|
+
const deckRadius = node.width / 2 + node.wallThickness / 2 + node.overhang * Math.max(0, cosTheta);
|
|
1707
|
+
const deckDrop = (deckRadius - node.width / 2) * tanTheta;
|
|
1708
|
+
const deckVerticalThickness = node.deckThickness / Math.max(0.1, cosTheta);
|
|
1709
|
+
const shingleRadialThickness = node.shingleThickness * sinTheta;
|
|
1710
|
+
const shingleVerticalThickness = node.shingleThickness * cosTheta;
|
|
1711
|
+
const deckBottom = {
|
|
1712
|
+
radius: deckRadius,
|
|
1713
|
+
eaveY: node.wallHeight - deckDrop,
|
|
1714
|
+
peakY: node.wallHeight + activeRh,
|
|
1715
|
+
};
|
|
1716
|
+
const deckTop = {
|
|
1717
|
+
radius: deckRadius,
|
|
1718
|
+
eaveY: deckBottom.eaveY + deckVerticalThickness,
|
|
1719
|
+
peakY: deckBottom.peakY + deckVerticalThickness,
|
|
1720
|
+
};
|
|
1721
|
+
const shingleTop = {
|
|
1722
|
+
radius: deckRadius + shingleRadialThickness,
|
|
1723
|
+
eaveY: deckTop.eaveY + shingleVerticalThickness,
|
|
1724
|
+
peakY: deckTop.peakY + shingleVerticalThickness + shingleRadialThickness * tanTheta,
|
|
1725
|
+
};
|
|
1726
|
+
const radialSegments = Math.max(1, Math.ceil((48 * Math.abs(coverage.sweepAngle)) / (Math.PI * 2)));
|
|
1727
|
+
const angles = Array.from({ length: radialSegments + 1 }, (_, index) => coverage.startAngle + (index / radialSegments) * coverage.sweepAngle);
|
|
1728
|
+
const ring = (profile) => angles.map((angle) => new THREE.Vector3(Math.cos(angle) * profile.radius, profile.eaveY, Math.sin(angle) * profile.radius));
|
|
1729
|
+
const deckBottomRing = ring(deckBottom);
|
|
1730
|
+
const deckTopRing = ring(deckTop);
|
|
1731
|
+
const shingleTopRing = ring(shingleTop);
|
|
1732
|
+
const deckBottomApex = new THREE.Vector3(0, deckBottom.peakY, 0);
|
|
1733
|
+
const deckTopApex = new THREE.Vector3(0, deckTop.peakY, 0);
|
|
1734
|
+
const shingleTopApex = new THREE.Vector3(0, shingleTop.peakY, 0);
|
|
1735
|
+
const orient = (face, upward) => {
|
|
1736
|
+
const normalY = new THREE.Vector3()
|
|
1737
|
+
.subVectors(face[1], face[0])
|
|
1738
|
+
.cross(new THREE.Vector3().subVectors(face[2], face[0])).y;
|
|
1739
|
+
return normalY >= 0 === upward ? face : [...face].reverse();
|
|
1740
|
+
};
|
|
1741
|
+
const layerFaces = (bottomRing, bottomApex, topRing, topApex) => {
|
|
1742
|
+
const bottomFaces = [];
|
|
1743
|
+
const topFaces = [];
|
|
1744
|
+
const edgeFaces = [];
|
|
1745
|
+
for (let index = 0; index < radialSegments; index += 1) {
|
|
1746
|
+
const next = index + 1;
|
|
1747
|
+
bottomFaces.push(orient([bottomRing[index], bottomRing[next], bottomApex].map((point) => point.clone()), false));
|
|
1748
|
+
topFaces.push(orient([topRing[index], topRing[next], topApex].map((point) => point.clone()), true));
|
|
1749
|
+
edgeFaces.push([
|
|
1750
|
+
bottomRing[index].clone(),
|
|
1751
|
+
bottomRing[next].clone(),
|
|
1752
|
+
topRing[next].clone(),
|
|
1753
|
+
topRing[index].clone(),
|
|
1754
|
+
]);
|
|
1755
|
+
}
|
|
1756
|
+
const last = radialSegments;
|
|
1757
|
+
edgeFaces.push([bottomApex.clone(), bottomRing[0].clone(), topRing[0].clone(), topApex.clone()], [bottomApex.clone(), topApex.clone(), topRing[last].clone(), bottomRing[last].clone()]);
|
|
1758
|
+
return { bottomFaces, topFaces, edgeFaces };
|
|
1759
|
+
};
|
|
1760
|
+
const deck = layerFaces(deckBottomRing, deckBottomApex, deckTopRing, deckTopApex);
|
|
1761
|
+
const shingles = layerFaces(deckTopRing, deckTopApex, shingleTopRing, shingleTopApex);
|
|
1762
|
+
const geometries = [
|
|
1763
|
+
createGeometryFromFaces([...deck.bottomFaces, ...deck.edgeFaces], ROOF_EDGE_MATERIAL_INDEX),
|
|
1764
|
+
createGeometryFromFaces(shingles.topFaces, 3),
|
|
1765
|
+
createGeometryFromFaces(shingles.edgeFaces, ROOF_EDGE_MATERIAL_INDEX),
|
|
1766
|
+
];
|
|
1767
|
+
if (node.wallHeight > 0.001) {
|
|
1768
|
+
const outerRadius = node.width / 2 + node.wallThickness / 2;
|
|
1769
|
+
const innerRadius = Math.max(0.005, node.width / 2 - node.wallThickness / 2);
|
|
1770
|
+
const outerBottom = angles.map((angle) => new THREE.Vector3(Math.cos(angle) * outerRadius, 0, Math.sin(angle) * outerRadius));
|
|
1771
|
+
const outerTop = outerBottom.map((point) => new THREE.Vector3(point.x, node.wallHeight, point.z));
|
|
1772
|
+
const innerBottom = angles.map((angle) => new THREE.Vector3(Math.cos(angle) * innerRadius, 0, Math.sin(angle) * innerRadius));
|
|
1773
|
+
const innerTop = innerBottom.map((point) => new THREE.Vector3(point.x, node.wallHeight, point.z));
|
|
1774
|
+
const wallFaces = [];
|
|
1775
|
+
for (let index = 0; index < radialSegments; index += 1) {
|
|
1776
|
+
const next = index + 1;
|
|
1777
|
+
pushDoubleSidedFace(wallFaces, [
|
|
1778
|
+
outerBottom[next],
|
|
1779
|
+
outerBottom[index],
|
|
1780
|
+
outerTop[index],
|
|
1781
|
+
outerTop[next],
|
|
1782
|
+
]);
|
|
1783
|
+
pushDoubleSidedFace(wallFaces, [
|
|
1784
|
+
innerBottom[index],
|
|
1785
|
+
innerBottom[next],
|
|
1786
|
+
innerTop[next],
|
|
1787
|
+
innerTop[index],
|
|
1788
|
+
]);
|
|
1789
|
+
}
|
|
1790
|
+
const last = radialSegments;
|
|
1791
|
+
pushDoubleSidedFace(wallFaces, [outerBottom[0], innerBottom[0], innerTop[0], outerTop[0]]);
|
|
1792
|
+
pushDoubleSidedFace(wallFaces, [
|
|
1793
|
+
innerBottom[last],
|
|
1794
|
+
outerBottom[last],
|
|
1795
|
+
outerTop[last],
|
|
1796
|
+
innerTop[last],
|
|
1797
|
+
]);
|
|
1798
|
+
geometries.push(createGeometryFromFaces(wallFaces, ROOF_EDGE_MATERIAL_INDEX));
|
|
1799
|
+
}
|
|
1800
|
+
const merged = mergeGeometriesPreservingGroups(geometries);
|
|
1801
|
+
for (const geometry of geometries)
|
|
1802
|
+
geometry.dispose();
|
|
1803
|
+
if (!merged)
|
|
1804
|
+
return null;
|
|
1805
|
+
merged.computeVertexNormals();
|
|
1806
|
+
ensureRenderableGeometryAttributes(merged);
|
|
1807
|
+
return merged;
|
|
1808
|
+
}
|
|
1809
|
+
function clipRoofPolygonAtX(polygon, boundaryX, keepGreater) {
|
|
1810
|
+
const clipped = [];
|
|
1811
|
+
for (let index = 0; index < polygon.length; index++) {
|
|
1812
|
+
const current = polygon[index];
|
|
1813
|
+
const next = polygon[(index + 1) % polygon.length];
|
|
1814
|
+
const currentInside = keepGreater ? current[0] >= boundaryX : current[0] <= boundaryX;
|
|
1815
|
+
const nextInside = keepGreater ? next[0] >= boundaryX : next[0] <= boundaryX;
|
|
1816
|
+
if (currentInside)
|
|
1817
|
+
clipped.push([current[0], current[1]]);
|
|
1818
|
+
if (currentInside === nextInside)
|
|
1819
|
+
continue;
|
|
1820
|
+
const ratio = (boundaryX - current[0]) / (next[0] - current[0]);
|
|
1821
|
+
clipped.push([boundaryX, current[1] + (next[1] - current[1]) * ratio]);
|
|
1822
|
+
}
|
|
1823
|
+
return clipped;
|
|
1824
|
+
}
|
|
1825
|
+
function sanitizeRoofPlanPolygon(polygon) {
|
|
1826
|
+
const tolerance = 1e-8;
|
|
1827
|
+
const points = polygon.filter((point, index) => {
|
|
1828
|
+
const previous = polygon[(index + polygon.length - 1) % polygon.length];
|
|
1829
|
+
return Math.hypot(point[0] - previous[0], point[1] - previous[1]) > tolerance;
|
|
1830
|
+
});
|
|
1831
|
+
let changed = true;
|
|
1832
|
+
while (changed && points.length >= 3) {
|
|
1833
|
+
changed = false;
|
|
1834
|
+
for (let index = 0; index < points.length; index++) {
|
|
1835
|
+
const previous = points[(index + points.length - 1) % points.length];
|
|
1836
|
+
const point = points[index];
|
|
1837
|
+
const next = points[(index + 1) % points.length];
|
|
1838
|
+
const cross = (point[0] - previous[0]) * (next[1] - point[1]) -
|
|
1839
|
+
(point[1] - previous[1]) * (next[0] - point[0]);
|
|
1840
|
+
if (Math.abs(cross) > tolerance)
|
|
1841
|
+
continue;
|
|
1842
|
+
points.splice(index, 1);
|
|
1843
|
+
changed = true;
|
|
1844
|
+
break;
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
return points;
|
|
1848
|
+
}
|
|
1849
|
+
function facetBandedRoofPieces(pieces, width) {
|
|
1850
|
+
const facetCount = Math.max(4, Math.min(32, Math.ceil(width / 0.4)));
|
|
1851
|
+
const halfWidth = width / 2;
|
|
1852
|
+
const facetWidth = width / facetCount;
|
|
1853
|
+
const faceted = [];
|
|
1854
|
+
for (const piece of pieces) {
|
|
1855
|
+
for (let index = 0; index < facetCount; index++) {
|
|
1856
|
+
const minX = -halfWidth + index * facetWidth;
|
|
1857
|
+
const maxX = index === facetCount - 1 ? halfWidth : minX + facetWidth;
|
|
1858
|
+
const clipped = clipRoofPolygonAtX(clipRoofPolygonAtX(piece, minX, true), maxX, false);
|
|
1859
|
+
const area = clipped.reduce((sum, point, pointIndex) => {
|
|
1860
|
+
const next = clipped[(pointIndex + 1) % clipped.length];
|
|
1861
|
+
return sum + point[0] * next[1] - next[0] * point[1];
|
|
1862
|
+
}, 0);
|
|
1863
|
+
if (clipped.length >= 3 && Math.abs(area) > 1e-8)
|
|
1864
|
+
faceted.push(clipped);
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
return faceted;
|
|
1868
|
+
}
|
|
1869
|
+
function facetBandedRoofBoundary(polygon, width) {
|
|
1870
|
+
const facetCount = Math.max(4, Math.min(32, Math.ceil(width / 0.4)));
|
|
1871
|
+
const halfWidth = width / 2;
|
|
1872
|
+
const facetWidth = width / facetCount;
|
|
1873
|
+
const boundaries = Array.from({ length: facetCount - 1 }, (_, index) => -halfWidth + (index + 1) * facetWidth);
|
|
1874
|
+
const segments = [];
|
|
1875
|
+
for (let index = 0; index < polygon.length; index++) {
|
|
1876
|
+
const start = polygon[index];
|
|
1877
|
+
const end = polygon[(index + 1) % polygon.length];
|
|
1878
|
+
const deltaX = end[0] - start[0];
|
|
1879
|
+
const splits = boundaries
|
|
1880
|
+
.flatMap((boundaryX) => {
|
|
1881
|
+
if (Math.abs(deltaX) <= 1e-8)
|
|
1882
|
+
return [];
|
|
1883
|
+
const ratio = (boundaryX - start[0]) / deltaX;
|
|
1884
|
+
return ratio > 1e-8 && ratio < 1 - 1e-8 ? [ratio] : [];
|
|
1885
|
+
})
|
|
1886
|
+
.sort((left, right) => left - right);
|
|
1887
|
+
const points = [0, ...splits, 1].map((ratio) => [
|
|
1888
|
+
start[0] + (end[0] - start[0]) * ratio,
|
|
1889
|
+
start[1] + (end[1] - start[1]) * ratio,
|
|
1890
|
+
]);
|
|
1891
|
+
for (let pointIndex = 0; pointIndex + 1 < points.length; pointIndex++) {
|
|
1892
|
+
segments.push([points[pointIndex], points[pointIndex + 1]]);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return segments;
|
|
1896
|
+
}
|
|
1897
|
+
function transformShedPlanPoint(node, point) {
|
|
1898
|
+
// `shedFootprintPieces` are persisted in the flat segment frame, while a
|
|
1899
|
+
// curved host's rendered deck is bent onto its annular band below. Keep the
|
|
1900
|
+
// overlap/joint queries in that same rendered frame; otherwise a curved run
|
|
1901
|
+
// can be matched against a neighbour at the opposite end of the wall.
|
|
1902
|
+
const frame = node.shedJointFrame;
|
|
1903
|
+
const bent = node.arc && frame
|
|
1904
|
+
? bendBandPoint(node.arc, bandSignedRef(node.arc), point[0], point[1])
|
|
1905
|
+
: { x: point[0], z: point[1] };
|
|
1906
|
+
const rotation = node.rotation ?? 0;
|
|
1907
|
+
const cos = Math.cos(rotation);
|
|
1908
|
+
const sin = Math.sin(rotation);
|
|
1909
|
+
const segmentPoint = [
|
|
1910
|
+
(node.position[0] ?? 0) + bent.x * cos + bent.z * sin,
|
|
1911
|
+
(node.position[2] ?? 0) - bent.x * sin + bent.z * cos,
|
|
1912
|
+
];
|
|
1913
|
+
if (!frame)
|
|
1914
|
+
return segmentPoint;
|
|
1915
|
+
const ownerCos = Math.cos(frame.rotation);
|
|
1916
|
+
const ownerSin = Math.sin(frame.rotation);
|
|
1917
|
+
return [
|
|
1918
|
+
frame.position[0] + segmentPoint[0] * ownerCos + segmentPoint[1] * ownerSin,
|
|
1919
|
+
frame.position[2] - segmentPoint[0] * ownerSin + segmentPoint[1] * ownerCos,
|
|
1920
|
+
];
|
|
1921
|
+
}
|
|
1922
|
+
function inverseTransformShedPlanPoint(node, point) {
|
|
1923
|
+
const frame = node.shedJointFrame;
|
|
1924
|
+
let source = point;
|
|
1925
|
+
if (frame) {
|
|
1926
|
+
const ownerCos = Math.cos(frame.rotation);
|
|
1927
|
+
const ownerSin = Math.sin(frame.rotation);
|
|
1928
|
+
const dx = point[0] - frame.position[0];
|
|
1929
|
+
const dz = point[1] - frame.position[2];
|
|
1930
|
+
source = [dx * ownerCos - dz * ownerSin, dx * ownerSin + dz * ownerCos];
|
|
1931
|
+
}
|
|
1932
|
+
const rotation = node.rotation ?? 0;
|
|
1933
|
+
const cos = Math.cos(rotation);
|
|
1934
|
+
const sin = Math.sin(rotation);
|
|
1935
|
+
const dx = source[0] - (node.position[0] ?? 0);
|
|
1936
|
+
const dz = source[1] - (node.position[2] ?? 0);
|
|
1937
|
+
const segmentPoint = [dx * cos - dz * sin, dx * sin + dz * cos];
|
|
1938
|
+
if (!node.arc || !frame)
|
|
1939
|
+
return segmentPoint;
|
|
1940
|
+
const signedRef = bandSignedRef(node.arc);
|
|
1941
|
+
const radialX = segmentPoint[0] - node.arc.centerX;
|
|
1942
|
+
const radialZ = segmentPoint[1] - node.arc.centerZ;
|
|
1943
|
+
const radialSign = -(Math.sign(node.arc.centerZ) || 1);
|
|
1944
|
+
const phi = Math.atan2(-radialX * radialSign, radialZ * radialSign);
|
|
1945
|
+
const radial = Math.hypot(radialX, radialZ) * radialSign;
|
|
1946
|
+
return [node.arc.centerX + phi * signedRef, node.arc.centerZ + radial];
|
|
1947
|
+
}
|
|
1948
|
+
function pointInOrNearShedPolygon(point, polygon, tolerance) {
|
|
1949
|
+
if (pointInPolygon2D([point[0], point[1]], polygon, { includeBoundary: false }))
|
|
1950
|
+
return true;
|
|
1951
|
+
if (!(tolerance > 0))
|
|
1952
|
+
return false;
|
|
1953
|
+
const toleranceSquared = tolerance * tolerance;
|
|
1954
|
+
for (let index = 0; index < polygon.length; index++) {
|
|
1955
|
+
const start = polygon[index];
|
|
1956
|
+
const end = polygon[(index + 1) % polygon.length];
|
|
1957
|
+
const dx = end[0] - start[0];
|
|
1958
|
+
const dz = end[1] - start[1];
|
|
1959
|
+
const lengthSquared = dx * dx + dz * dz;
|
|
1960
|
+
const ratio = lengthSquared > 1e-12
|
|
1961
|
+
? Math.max(0, Math.min(1, ((point[0] - start[0]) * dx + (point[1] - start[1]) * dz) / lengthSquared))
|
|
1962
|
+
: 0;
|
|
1963
|
+
const closestX = start[0] + dx * ratio;
|
|
1964
|
+
const closestZ = start[1] + dz * ratio;
|
|
1965
|
+
const distanceSquared = (point[0] - closestX) * (point[0] - closestX) + (point[1] - closestZ) * (point[1] - closestZ);
|
|
1966
|
+
if (distanceSquared <= toleranceSquared)
|
|
1967
|
+
return true;
|
|
1968
|
+
}
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
// Managed shed segments record the ids of their joined owners. Restricting sibling detection
|
|
1972
|
+
// to these keeps a shed from mitering against a run it merely passes near in
|
|
1973
|
+
// world space (e.g. the two free ends of a J that overlap across its mouth).
|
|
1974
|
+
function isJoinedShedSibling(node, candidate) {
|
|
1975
|
+
const neighbors = node.shedJointNeighborIds;
|
|
1976
|
+
if (!neighbors || neighbors.length === 0)
|
|
1977
|
+
return true;
|
|
1978
|
+
return candidate.shedJointOwnerId !== undefined && neighbors.includes(candidate.shedJointOwnerId);
|
|
1979
|
+
}
|
|
1980
|
+
function shedJoinTolerance(node, sibling) {
|
|
1981
|
+
const sideOverhang = (segment) => {
|
|
1982
|
+
const structuralSpan = readFiniteNumber(segment.shedSideInfillSpan);
|
|
1983
|
+
return structuralSpan === null ? 0 : Math.max(0, (segment.width - structuralSpan) / 2);
|
|
1984
|
+
};
|
|
1985
|
+
return Math.max(0.02, Math.min(0.2, Math.max(node.overhang, sibling.overhang, node.wallThickness, sibling.wallThickness, sideOverhang(node), sideOverhang(sibling))));
|
|
1986
|
+
}
|
|
1987
|
+
function siblingShedSegments(node, nodes) {
|
|
1988
|
+
return Object.values(nodes).filter((candidate) => candidate.type === 'roof-segment' &&
|
|
1989
|
+
candidate.id !== node.id &&
|
|
1990
|
+
(Boolean(node.parentId && candidate.parentId === node.parentId) ||
|
|
1991
|
+
Boolean(node.shedJointScopeId && candidate.shedJointScopeId === node.shedJointScopeId)) &&
|
|
1992
|
+
candidate.roofType === 'shed' &&
|
|
1993
|
+
isJoinedShedSibling(node, candidate));
|
|
1994
|
+
}
|
|
1995
|
+
function edgeTouchesSiblingShed(node, start, end, nodes) {
|
|
1996
|
+
if (!nodes)
|
|
1997
|
+
return false;
|
|
1998
|
+
const siblings = siblingShedSegments(node, nodes);
|
|
1999
|
+
if (siblings.length === 0)
|
|
2000
|
+
return false;
|
|
2001
|
+
const dx = end[0] - start[0];
|
|
2002
|
+
const dz = end[1] - start[1];
|
|
2003
|
+
const length = Math.hypot(dx, dz);
|
|
2004
|
+
if (length <= 1e-6)
|
|
2005
|
+
return false;
|
|
2006
|
+
const nx = -dz / length;
|
|
2007
|
+
const nz = dx / length;
|
|
2008
|
+
for (const ratio of [0.25, 0.5, 0.75]) {
|
|
2009
|
+
const x = start[0] + dx * ratio;
|
|
2010
|
+
const z = start[1] + dz * ratio;
|
|
2011
|
+
const world = transformShedPlanPoint(node, [x, z]);
|
|
2012
|
+
const samples = [
|
|
2013
|
+
world,
|
|
2014
|
+
transformShedPlanPoint(node, [x + nx * 1e-4, z + nz * 1e-4]),
|
|
2015
|
+
transformShedPlanPoint(node, [x - nx * 1e-4, z - nz * 1e-4]),
|
|
2016
|
+
];
|
|
2017
|
+
for (const sample of samples) {
|
|
2018
|
+
if (siblings.some((sibling) => {
|
|
2019
|
+
const local = inverseTransformShedPlanPoint(sibling, sample);
|
|
2020
|
+
const footprints = readShedFootprintPieces(sibling);
|
|
2021
|
+
const polygons = footprints.length > 0
|
|
2022
|
+
? footprints
|
|
2023
|
+
: sibling.managedByParent
|
|
2024
|
+
? [managedShedFootprint(sibling)]
|
|
2025
|
+
: [];
|
|
2026
|
+
return polygons.some((polygon) => pointInOrNearShedPolygon(local, polygon, shedJoinTolerance(node, sibling)));
|
|
2027
|
+
})) {
|
|
2028
|
+
return true;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
return false;
|
|
2033
|
+
}
|
|
2034
|
+
function findSiblingShedAcrossEdge(node, start, end, nodes) {
|
|
2035
|
+
if (!nodes)
|
|
2036
|
+
return undefined;
|
|
2037
|
+
const siblings = siblingShedSegments(node, nodes);
|
|
2038
|
+
const dx = end[0] - start[0];
|
|
2039
|
+
const dz = end[1] - start[1];
|
|
2040
|
+
const length = Math.hypot(dx, dz);
|
|
2041
|
+
if (length <= 1e-6)
|
|
2042
|
+
return undefined;
|
|
2043
|
+
const nx = -dz / length;
|
|
2044
|
+
const nz = dx / length;
|
|
2045
|
+
return siblings.find((sibling) => [0.25, 0.5, 0.75].every((ratio) => {
|
|
2046
|
+
const x = start[0] + dx * ratio;
|
|
2047
|
+
const z = start[1] + dz * ratio;
|
|
2048
|
+
const samples = [
|
|
2049
|
+
transformShedPlanPoint(node, [x, z]),
|
|
2050
|
+
transformShedPlanPoint(node, [x + nx * 1e-4, z + nz * 1e-4]),
|
|
2051
|
+
transformShedPlanPoint(node, [x - nx * 1e-4, z - nz * 1e-4]),
|
|
2052
|
+
];
|
|
2053
|
+
return samples.some((sample) => {
|
|
2054
|
+
const local = inverseTransformShedPlanPoint(sibling, sample);
|
|
2055
|
+
const footprints = readShedFootprintPieces(sibling);
|
|
2056
|
+
const polygons = footprints.length > 0
|
|
2057
|
+
? footprints
|
|
2058
|
+
: sibling.managedByParent
|
|
2059
|
+
? [managedShedFootprint(sibling)]
|
|
2060
|
+
: [];
|
|
2061
|
+
return polygons.some((polygon) => pointInOrNearShedPolygon(local, polygon, shedJoinTolerance(node, sibling)));
|
|
2062
|
+
});
|
|
2063
|
+
}));
|
|
2064
|
+
}
|
|
2065
|
+
function shedWorldYOrigin(node, nodes) {
|
|
2066
|
+
if (node.shedJointFrame)
|
|
2067
|
+
return node.shedJointFrame.position[1] + (node.position[1] ?? 0);
|
|
2068
|
+
const parent = node.parentId ? nodes[node.parentId] : undefined;
|
|
2069
|
+
return (parent?.type === 'roof' ? (parent.position[1] ?? 0) : 0) + (node.position[1] ?? 0);
|
|
2070
|
+
}
|
|
2071
|
+
function shedVerticalThickness(node) {
|
|
2072
|
+
const { cosTheta } = getSegmentSlopeFrame(node);
|
|
2073
|
+
return node.deckThickness / Math.max(0.1, cosTheta) + node.shingleThickness * cosTheta;
|
|
2074
|
+
}
|
|
2075
|
+
function siblingShedTopYInNodeFrame(node, sibling, point, nodes) {
|
|
2076
|
+
const worldPoint = transformShedPlanPoint(node, point);
|
|
2077
|
+
const siblingPoint = inverseTransformShedPlanPoint(sibling, worldPoint);
|
|
2078
|
+
return (shedWorldYOrigin(sibling, nodes) +
|
|
2079
|
+
getRoofSegmentSurfaceY(sibling, siblingPoint[0], siblingPoint[1]) +
|
|
2080
|
+
shedVerticalThickness(sibling) -
|
|
2081
|
+
shedWorldYOrigin(node, nodes));
|
|
2082
|
+
}
|
|
2083
|
+
function buildShedJointTransitionFaces(node, sibling, start, end, nodes, verticalThickness) {
|
|
2084
|
+
const heights = (point) => {
|
|
2085
|
+
const ownTop = getRoofSegmentSurfaceY(node, point[0], point[1]) + verticalThickness;
|
|
2086
|
+
const siblingTop = siblingShedTopYInNodeFrame(node, sibling, point, nodes);
|
|
2087
|
+
return { ownTop, siblingTop };
|
|
2088
|
+
};
|
|
2089
|
+
const startHeights = heights(start);
|
|
2090
|
+
const endHeights = heights(end);
|
|
2091
|
+
const startDelta = startHeights.siblingTop - startHeights.ownTop;
|
|
2092
|
+
const endDelta = endHeights.siblingTop - endHeights.ownTop;
|
|
2093
|
+
const tolerance = 1e-5;
|
|
2094
|
+
const face = (firstPoint, firstHeights, secondPoint, secondHeights) => {
|
|
2095
|
+
const firstOwn = new THREE.Vector3(firstPoint[0], firstHeights.ownTop, firstPoint[1]);
|
|
2096
|
+
const secondOwn = new THREE.Vector3(secondPoint[0], secondHeights.ownTop, secondPoint[1]);
|
|
2097
|
+
const secondSibling = new THREE.Vector3(secondPoint[0], secondHeights.siblingTop, secondPoint[1]);
|
|
2098
|
+
const firstSibling = new THREE.Vector3(firstPoint[0], firstHeights.siblingTop, firstPoint[1]);
|
|
2099
|
+
if (Math.abs(firstHeights.siblingTop - firstHeights.ownTop) <= tolerance) {
|
|
2100
|
+
return [firstOwn, secondOwn, secondSibling];
|
|
2101
|
+
}
|
|
2102
|
+
if (Math.abs(secondHeights.siblingTop - secondHeights.ownTop) <= tolerance) {
|
|
2103
|
+
return [firstOwn, secondOwn, firstSibling];
|
|
2104
|
+
}
|
|
2105
|
+
return [firstOwn, secondOwn, secondSibling, firstSibling];
|
|
2106
|
+
};
|
|
2107
|
+
// Geometric ownership: a run closes the step only along the stretch where its
|
|
2108
|
+
// own roof top sits BELOW the sibling's (delta > 0), raising a vertical wall up
|
|
2109
|
+
// to the sibling's top. `delta` is antisymmetric between the two runs, so every
|
|
2110
|
+
// point of a seam is owned by exactly one run — the lower one — independent of
|
|
2111
|
+
// run count, chain vs. loop topology, or random node ids. Flush stretches
|
|
2112
|
+
// (|delta| <= tol) have no step and draw nothing, so a reversed/continuous fold
|
|
2113
|
+
// stays open. This replaces the old id-order tiebreak and the "skip when joined
|
|
2114
|
+
// on both ends" rule, which together left every seam of a closed loop unclosed.
|
|
2115
|
+
const faces = [];
|
|
2116
|
+
if (startDelta > tolerance && endDelta > tolerance) {
|
|
2117
|
+
pushDoubleSidedFace(faces, face(start, startHeights, end, endHeights));
|
|
2118
|
+
}
|
|
2119
|
+
else if (startDelta > tolerance || endDelta > tolerance) {
|
|
2120
|
+
const ratio = startDelta / (startDelta - endDelta);
|
|
2121
|
+
const crossingPoint = [
|
|
2122
|
+
start[0] + (end[0] - start[0]) * ratio,
|
|
2123
|
+
start[1] + (end[1] - start[1]) * ratio,
|
|
2124
|
+
];
|
|
2125
|
+
const crossingHeights = heights(crossingPoint);
|
|
2126
|
+
if (startDelta > tolerance) {
|
|
2127
|
+
pushDoubleSidedFace(faces, face(start, startHeights, crossingPoint, crossingHeights));
|
|
2128
|
+
}
|
|
2129
|
+
else {
|
|
2130
|
+
pushDoubleSidedFace(faces, face(crossingPoint, crossingHeights, end, endHeights));
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
return faces;
|
|
2134
|
+
}
|
|
2135
|
+
function clipShedPolygonByLine(polygon, lineStart, lineEnd, outsidePoint) {
|
|
2136
|
+
const lineDx = lineEnd[0] - lineStart[0];
|
|
2137
|
+
const lineDz = lineEnd[1] - lineStart[1];
|
|
2138
|
+
const outsideSide = Math.sign(lineDx * (outsidePoint[1] - lineStart[1]) - lineDz * (outsidePoint[0] - lineStart[0])) || 1;
|
|
2139
|
+
const side = (point) => outsideSide * (lineDx * (point[1] - lineStart[1]) - lineDz * (point[0] - lineStart[0]));
|
|
2140
|
+
const clipped = [];
|
|
2141
|
+
for (let index = 0; index < polygon.length; index++) {
|
|
2142
|
+
const current = polygon[index];
|
|
2143
|
+
const next = polygon[(index + 1) % polygon.length];
|
|
2144
|
+
const currentSide = side(current);
|
|
2145
|
+
const nextSide = side(next);
|
|
2146
|
+
const currentInside = currentSide <= 1e-7;
|
|
2147
|
+
const nextInside = nextSide <= 1e-7;
|
|
2148
|
+
if (currentInside)
|
|
2149
|
+
clipped.push([current[0], current[1]]);
|
|
2150
|
+
if (currentInside !== nextInside) {
|
|
2151
|
+
const ratio = currentSide / (currentSide - nextSide);
|
|
2152
|
+
clipped.push([
|
|
2153
|
+
current[0] + (next[0] - current[0]) * ratio,
|
|
2154
|
+
current[1] + (next[1] - current[1]) * ratio,
|
|
2155
|
+
]);
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
return clipped;
|
|
2159
|
+
}
|
|
2160
|
+
function managedShedFootprint(node) {
|
|
2161
|
+
const trim = normalizeRoofSegmentTrim(node);
|
|
2162
|
+
let polygon = [
|
|
2163
|
+
[-node.width / 2 + trim.left, -node.depth / 2 + trim.back],
|
|
2164
|
+
[node.width / 2 - trim.right, -node.depth / 2 + trim.back],
|
|
2165
|
+
[node.width / 2 - trim.right, node.depth / 2 - trim.front],
|
|
2166
|
+
[-node.width / 2 + trim.left, node.depth / 2 - trim.front],
|
|
2167
|
+
];
|
|
2168
|
+
const diagonalTrims = [
|
|
2169
|
+
{
|
|
2170
|
+
x: trim.frontLeftX,
|
|
2171
|
+
z: trim.frontLeftZ,
|
|
2172
|
+
start: [-node.width / 2 + trim.left + trim.frontLeftX, node.depth / 2 - trim.front],
|
|
2173
|
+
end: [-node.width / 2 + trim.left, node.depth / 2 - trim.front - trim.frontLeftZ],
|
|
2174
|
+
outside: [-node.width / 2 - 1, node.depth / 2 + 1],
|
|
2175
|
+
},
|
|
2176
|
+
{
|
|
2177
|
+
x: trim.frontRightX,
|
|
2178
|
+
z: trim.frontRightZ,
|
|
2179
|
+
start: [node.width / 2 - trim.right, node.depth / 2 - trim.front - trim.frontRightZ],
|
|
2180
|
+
end: [node.width / 2 - trim.right - trim.frontRightX, node.depth / 2 - trim.front],
|
|
2181
|
+
outside: [node.width / 2 + 1, node.depth / 2 + 1],
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
x: trim.backLeftX,
|
|
2185
|
+
z: trim.backLeftZ,
|
|
2186
|
+
start: [-node.width / 2 + trim.left, -node.depth / 2 + trim.back + trim.backLeftZ],
|
|
2187
|
+
end: [-node.width / 2 + trim.left + trim.backLeftX, -node.depth / 2 + trim.back],
|
|
2188
|
+
outside: [-node.width / 2 - 1, -node.depth / 2 - 1],
|
|
2189
|
+
},
|
|
2190
|
+
{
|
|
2191
|
+
x: trim.backRightX,
|
|
2192
|
+
z: trim.backRightZ,
|
|
2193
|
+
start: [node.width / 2 - trim.right - trim.backRightX, -node.depth / 2 + trim.back],
|
|
2194
|
+
end: [node.width / 2 - trim.right, -node.depth / 2 + trim.back + trim.backRightZ],
|
|
2195
|
+
outside: [node.width / 2 + 1, -node.depth / 2 - 1],
|
|
2196
|
+
},
|
|
2197
|
+
];
|
|
2198
|
+
for (const diagonal of diagonalTrims) {
|
|
2199
|
+
if (diagonal.x <= 0 || diagonal.z <= 0 || polygon.length < 3)
|
|
2200
|
+
continue;
|
|
2201
|
+
polygon = clipShedPolygonByLine(polygon, diagonal.start, diagonal.end, diagonal.outside);
|
|
2202
|
+
}
|
|
2203
|
+
return polygon;
|
|
2204
|
+
}
|
|
2205
|
+
function buildCustomShedGeometry(node, nodes) {
|
|
2206
|
+
if (node.roofType !== 'shed')
|
|
2207
|
+
return null;
|
|
2208
|
+
const storedPieces = readShedFootprintPieces(node);
|
|
2209
|
+
const pieces = storedPieces.length > 0
|
|
2210
|
+
? storedPieces
|
|
2211
|
+
: node.managedByParent
|
|
2212
|
+
? [managedShedFootprint(node)]
|
|
2213
|
+
: [];
|
|
2214
|
+
const banded = isBandedShedSegment(node) && node.arc;
|
|
2215
|
+
if (pieces.length === 0)
|
|
2216
|
+
return banded ? buildConcentricBandDeckGeometry(node) : null;
|
|
2217
|
+
const renderPieces = banded
|
|
2218
|
+
? [...facetBandedRoofPieces(pieces.slice(0, 1), node.width), ...pieces.slice(1)]
|
|
2219
|
+
: pieces;
|
|
2220
|
+
const { cosTheta } = getSegmentSlopeFrame(node);
|
|
2221
|
+
const verticalThickness = node.deckThickness / Math.max(0.1, cosTheta) + node.shingleThickness * cosTheta;
|
|
2222
|
+
const geometries = [];
|
|
2223
|
+
for (const polygon of renderPieces) {
|
|
2224
|
+
const sanitized = sanitizeRoofPlanPolygon(polygon);
|
|
2225
|
+
const signedArea = sanitized.reduce((area, point, index) => {
|
|
2226
|
+
const next = sanitized[(index + 1) % sanitized.length];
|
|
2227
|
+
return area + point[0] * next[1] - next[0] * point[1];
|
|
2228
|
+
}, 0);
|
|
2229
|
+
if (Math.abs(signedArea) <= 1e-9)
|
|
2230
|
+
continue;
|
|
2231
|
+
const outline = signedArea > 0 ? sanitized : [...sanitized].reverse();
|
|
2232
|
+
const edgeSiblings = banded
|
|
2233
|
+
? outline.map(() => undefined)
|
|
2234
|
+
: outline.map((point, index) => {
|
|
2235
|
+
const next = outline[(index + 1) % outline.length];
|
|
2236
|
+
return edgeTouchesSiblingShed(node, point, next, nodes)
|
|
2237
|
+
? findSiblingShedAcrossEdge(node, point, next, nodes)
|
|
2238
|
+
: undefined;
|
|
2239
|
+
});
|
|
2240
|
+
const topY = outline.map(([x, z]) => getRoofSegmentSurfaceY(node, x, z) + verticalThickness);
|
|
2241
|
+
// A convex curved/straight joint places its triangular infill on the
|
|
2242
|
+
// straight run. Pin that patch's shared edge to the curved surface so the
|
|
2243
|
+
// infill meets both roofs instead of floating above the curved seam.
|
|
2244
|
+
if (!banded && outline.length === 3 && nodes) {
|
|
2245
|
+
for (let index = 0; index < outline.length; index++) {
|
|
2246
|
+
const sibling = edgeSiblings[index];
|
|
2247
|
+
if (!sibling?.arc)
|
|
2248
|
+
continue;
|
|
2249
|
+
const next = (index + 1) % outline.length;
|
|
2250
|
+
topY[index] = siblingShedTopYInNodeFrame(node, sibling, outline[index], nodes);
|
|
2251
|
+
topY[next] = siblingShedTopYInNodeFrame(node, sibling, outline[next], nodes);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
const bottom = outline.map(([x, z], index) => new THREE.Vector3(x, topY[index] - verticalThickness, z));
|
|
2255
|
+
const triangles = THREE.ShapeUtils.triangulateShape(outline.map(([x, z]) => new THREE.Vector2(x, z)), []);
|
|
2256
|
+
const jointTransitionFaces = [];
|
|
2257
|
+
const faces = triangles.flatMap((triangle) => {
|
|
2258
|
+
const bottomFace = triangle.map((index) => bottom[index].clone());
|
|
2259
|
+
const normalY = new THREE.Vector3()
|
|
2260
|
+
.subVectors(bottomFace[1], bottomFace[0])
|
|
2261
|
+
.cross(new THREE.Vector3().subVectors(bottomFace[2], bottomFace[0])).y;
|
|
2262
|
+
if (normalY > 0)
|
|
2263
|
+
bottomFace.reverse();
|
|
2264
|
+
const topFace = [...bottomFace]
|
|
2265
|
+
.reverse()
|
|
2266
|
+
.map((point) => new THREE.Vector3(point.x, point.y + verticalThickness, point.z));
|
|
2267
|
+
return [bottomFace, topFace];
|
|
2268
|
+
});
|
|
2269
|
+
if (!banded) {
|
|
2270
|
+
for (let index = 0; index < bottom.length; index++) {
|
|
2271
|
+
const next = (index + 1) % bottom.length;
|
|
2272
|
+
const sideFace = [
|
|
2273
|
+
bottom[next].clone(),
|
|
2274
|
+
bottom[index].clone(),
|
|
2275
|
+
new THREE.Vector3(bottom[index].x, bottom[index].y + verticalThickness, bottom[index].z),
|
|
2276
|
+
new THREE.Vector3(bottom[next].x, bottom[next].y + verticalThickness, bottom[next].z),
|
|
2277
|
+
];
|
|
2278
|
+
const sibling = edgeSiblings[index];
|
|
2279
|
+
if (!sibling) {
|
|
2280
|
+
faces.push(sideFace);
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
if (nodes && Boolean(node.arc) === Boolean(sibling.arc)) {
|
|
2284
|
+
jointTransitionFaces.push(...buildShedJointTransitionFaces(node, sibling, outline[index], outline[next], nodes, verticalThickness));
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
geometries.push(createGeometryFromFaces(faces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX));
|
|
2289
|
+
if (jointTransitionFaces.length > 0) {
|
|
2290
|
+
geometries.push(createGeometryFromFaces(jointTransitionFaces, 3));
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
if (banded) {
|
|
2294
|
+
const boundaryFaces = [];
|
|
2295
|
+
const jointTransitionFaces = [];
|
|
2296
|
+
for (const polygon of unionPolygons(pieces.map((piece) => [...piece]))) {
|
|
2297
|
+
for (const [start, end] of facetBandedRoofBoundary(sanitizeRoofPlanPolygon(polygon), node.width)) {
|
|
2298
|
+
const startBottom = new THREE.Vector3(start[0], getRoofSegmentSurfaceY(node, start[0], start[1]), start[1]);
|
|
2299
|
+
const endBottom = new THREE.Vector3(end[0], getRoofSegmentSurfaceY(node, end[0], end[1]), end[1]);
|
|
2300
|
+
const sibling = edgeTouchesSiblingShed(node, start, end, nodes)
|
|
2301
|
+
? findSiblingShedAcrossEdge(node, start, end, nodes)
|
|
2302
|
+
: undefined;
|
|
2303
|
+
if (sibling && nodes && Boolean(node.arc) === Boolean(sibling.arc)) {
|
|
2304
|
+
jointTransitionFaces.push(...buildShedJointTransitionFaces(node, sibling, start, end, nodes, verticalThickness));
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
if (sibling)
|
|
2308
|
+
continue;
|
|
2309
|
+
boundaryFaces.push([
|
|
2310
|
+
endBottom,
|
|
2311
|
+
startBottom,
|
|
2312
|
+
new THREE.Vector3(startBottom.x, startBottom.y + verticalThickness, startBottom.z),
|
|
2313
|
+
new THREE.Vector3(endBottom.x, endBottom.y + verticalThickness, endBottom.z),
|
|
2314
|
+
]);
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
if (boundaryFaces.length > 0) {
|
|
2318
|
+
geometries.push(createGeometryFromFaces(boundaryFaces, ROOF_EDGE_MATERIAL_INDEX));
|
|
2319
|
+
}
|
|
2320
|
+
if (jointTransitionFaces.length > 0) {
|
|
2321
|
+
geometries.push(createGeometryFromFaces(jointTransitionFaces, 3));
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
if (geometries.length === 0)
|
|
2325
|
+
return null;
|
|
2326
|
+
const merged = mergeGeometriesPreservingGroups(geometries);
|
|
2327
|
+
for (const geometry of geometries)
|
|
2328
|
+
geometry.dispose();
|
|
2329
|
+
if (!merged)
|
|
2330
|
+
return null;
|
|
2331
|
+
if (banded)
|
|
2332
|
+
applyBandBendToGeometry(merged, banded);
|
|
2333
|
+
merged.computeVertexNormals();
|
|
2334
|
+
ensureRenderableGeometryAttributes(merged);
|
|
2335
|
+
return merged;
|
|
2336
|
+
}
|
|
1301
2337
|
function collectGeometryPlanes(geometry) {
|
|
1302
2338
|
const source = geometry.index ? geometry.toNonIndexed() : geometry;
|
|
1303
2339
|
const position = source.getAttribute('position');
|
|
@@ -1720,6 +2756,108 @@ function buildDutchRakeBoards(W, D, wh, rh, i, shapeRatios, rake, thickness) {
|
|
|
1720
2756
|
geometry.dispose();
|
|
1721
2757
|
return merged;
|
|
1722
2758
|
}
|
|
2759
|
+
function createShedInsetEndPanelGeometry(node) {
|
|
2760
|
+
if (node.roofType !== 'shed')
|
|
2761
|
+
return null;
|
|
2762
|
+
const trim = normalizeRoofSegmentTrim(node);
|
|
2763
|
+
const openEndSides = readShedOpenEndSides(node);
|
|
2764
|
+
const hasCornerSide = (side) => side < 0
|
|
2765
|
+
? openEndSides.has('left') ||
|
|
2766
|
+
(trim.frontLeftX > 0 && trim.frontLeftZ > 0) ||
|
|
2767
|
+
(trim.backLeftX > 0 && trim.backLeftZ > 0)
|
|
2768
|
+
: (trim.frontRightX > 0 && trim.frontRightZ > 0) ||
|
|
2769
|
+
(trim.backRightX > 0 && trim.backRightZ > 0) ||
|
|
2770
|
+
openEndSides.has('right');
|
|
2771
|
+
const sideInset = Math.min(Math.max(node.wallThickness, 0.05), node.overhang * 0.5, 0.12);
|
|
2772
|
+
const fallbackPanelHalfWidth = Math.max(0.01, node.width / 2 - sideInset);
|
|
2773
|
+
const sidePanelX = (side) => resolveShedSideInfillX(node, side, fallbackPanelHalfWidth);
|
|
2774
|
+
const { activeRh, tanTheta } = getSegmentSlopeFrame(node);
|
|
2775
|
+
const shapeRatios = getRoofShapeRatios({
|
|
2776
|
+
gambrelLowerWidthRatio: node.gambrelLowerWidthRatio,
|
|
2777
|
+
mansardSteepWidthRatio: node.mansardSteepWidthRatio,
|
|
2778
|
+
dutchHipWidthRatio: node.dutchHipWidthRatio,
|
|
2779
|
+
dutchHipHeightRatio: node.dutchHipHeightRatio,
|
|
2780
|
+
dutchWaistLengthRatio: node.dutchWaistLengthRatio,
|
|
2781
|
+
dutchGabletRake: node.dutchGabletRake,
|
|
2782
|
+
});
|
|
2783
|
+
const wallOuterOffset = node.wallThickness / 2;
|
|
2784
|
+
const autoDrop = wallOuterOffset * tanTheta;
|
|
2785
|
+
const wh = Math.max(0.05, node.wallHeight - autoDrop);
|
|
2786
|
+
const rh = activeRh > 0 ? activeRh + 2 * autoDrop : activeRh;
|
|
2787
|
+
const faces = getRoofModuleFaces({
|
|
2788
|
+
type: 'shed',
|
|
2789
|
+
w: node.width + node.wallThickness,
|
|
2790
|
+
d: node.depth + node.wallThickness,
|
|
2791
|
+
wh,
|
|
2792
|
+
rh,
|
|
2793
|
+
baseY: 0,
|
|
2794
|
+
insets: {},
|
|
2795
|
+
baseW: node.width,
|
|
2796
|
+
baseD: node.depth,
|
|
2797
|
+
tanTheta,
|
|
2798
|
+
shapeRatios,
|
|
2799
|
+
dutchTopRakeThickness: node.dutchTopRakeThickness,
|
|
2800
|
+
}).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
|
|
2801
|
+
const wallFaces = [];
|
|
2802
|
+
for (const faceIndex of [6, 8]) {
|
|
2803
|
+
const face = faces[faceIndex];
|
|
2804
|
+
if (!face)
|
|
2805
|
+
continue;
|
|
2806
|
+
const faceSide = face.some((point) => point.x < 0) ? -1 : 1;
|
|
2807
|
+
if (hasCornerSide(faceSide))
|
|
2808
|
+
continue;
|
|
2809
|
+
wallFaces.push(face.map((point) => {
|
|
2810
|
+
const side = point.x < 0 ? -1 : 1;
|
|
2811
|
+
return new THREE.Vector3(sidePanelX(side), point.y, point.z);
|
|
2812
|
+
}));
|
|
2813
|
+
}
|
|
2814
|
+
if (wallFaces.length === 0)
|
|
2815
|
+
return null;
|
|
2816
|
+
return createGeometryFromFaces(wallFaces, ROOF_INSET_WALL_MATERIAL_INDEX);
|
|
2817
|
+
}
|
|
2818
|
+
function resolveShedSideInfillX(node, side, fallbackPanelHalfWidth) {
|
|
2819
|
+
const sideX = readFiniteNumber(side < 0 ? node.shedSideInfillMinX : node.shedSideInfillMaxX);
|
|
2820
|
+
if (sideX !== null)
|
|
2821
|
+
return THREE.MathUtils.clamp(sideX, -node.width / 2, node.width / 2);
|
|
2822
|
+
const span = readFiniteNumber(node.shedSideInfillSpan);
|
|
2823
|
+
if (span !== null && span > 0) {
|
|
2824
|
+
return side * Math.min(span / 2, node.width / 2);
|
|
2825
|
+
}
|
|
2826
|
+
return side * fallbackPanelHalfWidth;
|
|
2827
|
+
}
|
|
2828
|
+
function readFiniteNumber(value) {
|
|
2829
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
2830
|
+
}
|
|
2831
|
+
function addShedInsetEndPanels(geometry, segments, applySegmentTransform) {
|
|
2832
|
+
const shedSegments = segments.filter((segment) => segment.roofType === 'shed' && segment.shedInsetEndPanels);
|
|
2833
|
+
if (shedSegments.length === 0)
|
|
2834
|
+
return geometry;
|
|
2835
|
+
const panelGeometries = [];
|
|
2836
|
+
for (const segment of shedSegments) {
|
|
2837
|
+
const panel = createShedInsetEndPanelGeometry(segment);
|
|
2838
|
+
if (!panel)
|
|
2839
|
+
continue;
|
|
2840
|
+
// A banded (curved) deck rotates each span end about the arc center; the flat
|
|
2841
|
+
// end panel is at a fixed X, so the bend is a rigid rotation that seats it on
|
|
2842
|
+
// the arc end. Applied before any segment transform so it stays segment-local.
|
|
2843
|
+
if (isBandedShedSegment(segment) && segment.arc)
|
|
2844
|
+
applyBandBendToGeometry(panel, segment.arc);
|
|
2845
|
+
if (applySegmentTransform) {
|
|
2846
|
+
_matrix.compose(_position.set(segment.position[0], segment.position[1], segment.position[2]), _quaternion.setFromAxisAngle(_yAxis, segment.rotation), _scale);
|
|
2847
|
+
panel.applyMatrix4(_matrix);
|
|
2848
|
+
}
|
|
2849
|
+
panelGeometries.push(panel);
|
|
2850
|
+
}
|
|
2851
|
+
if (panelGeometries.length === 0)
|
|
2852
|
+
return geometry;
|
|
2853
|
+
const merged = mergeGeometriesPreservingGroups([geometry, ...panelGeometries]);
|
|
2854
|
+
for (const panel of panelGeometries)
|
|
2855
|
+
panel.dispose();
|
|
2856
|
+
if (!merged)
|
|
2857
|
+
return geometry;
|
|
2858
|
+
geometry.dispose();
|
|
2859
|
+
return merged;
|
|
2860
|
+
}
|
|
1723
2861
|
/**
|
|
1724
2862
|
* Converts an array of face polygons into a BufferGeometry.
|
|
1725
2863
|
* Each face is triangulated via fan triangulation.
|
|
@@ -1884,7 +3022,7 @@ export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
|
|
|
1884
3022
|
let shinTopW = shinBotW;
|
|
1885
3023
|
let shinTopD = shinBotD;
|
|
1886
3024
|
let transZ = 0;
|
|
1887
|
-
if (['hip', 'mansard', 'dutch'].includes(roofType)) {
|
|
3025
|
+
if (['hip', 'mansard', 'dutch', 'conical'].includes(roofType)) {
|
|
1888
3026
|
shinTopW += 2 * stSin;
|
|
1889
3027
|
shinTopD += 2 * stSin;
|
|
1890
3028
|
}
|
|
@@ -1906,7 +3044,7 @@ export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
|
|
|
1906
3044
|
let iB = 0;
|
|
1907
3045
|
let iL = 0;
|
|
1908
3046
|
let iR = 0;
|
|
1909
|
-
if (roofType === 'hip') {
|
|
3047
|
+
if (roofType === 'hip' || roofType === 'conical') {
|
|
1910
3048
|
iF = inset;
|
|
1911
3049
|
iB = inset;
|
|
1912
3050
|
iL = inset;
|