@pascal-app/viewer 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/viewer/daylight-atmosphere.d.ts +9 -0
- package/dist/components/viewer/daylight-atmosphere.d.ts.map +1 -0
- package/dist/components/viewer/daylight-atmosphere.js +315 -0
- package/dist/components/viewer/drone-controller.d.ts +15 -0
- package/dist/components/viewer/drone-controller.d.ts.map +1 -0
- package/dist/components/viewer/drone-controller.js +194 -0
- package/dist/components/viewer/glb-interactive.d.ts +4 -18
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +32 -268
- package/dist/components/viewer/glb-scene-motion.d.ts +8 -0
- package/dist/components/viewer/glb-scene-motion.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene-motion.js +57 -0
- 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 +289 -49
- package/dist/components/viewer/glb-selection-callout.d.ts +17 -0
- package/dist/components/viewer/glb-selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/glb-selection-callout.js +54 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +6 -3
- package/dist/components/viewer/index.d.ts +5 -0
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +30 -6
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +192 -5
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +150 -94
- package/dist/components/viewer/scene-environment.js +4 -5
- package/dist/components/viewer/selection-callout.d.ts +51 -0
- package/dist/components/viewer/selection-callout.d.ts.map +1 -0
- package/dist/components/viewer/selection-callout.js +197 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
- package/dist/components/viewer/use-daylight-rooms.js +236 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -1
- package/dist/index.d.ts +21 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -7
- package/dist/lib/daylight/ground.d.ts +21 -0
- package/dist/lib/daylight/ground.d.ts.map +1 -0
- package/dist/lib/daylight/ground.js +53 -0
- package/dist/lib/daylight/location.d.ts +32 -0
- package/dist/lib/daylight/location.d.ts.map +1 -0
- package/dist/lib/daylight/location.js +74 -0
- package/dist/lib/daylight/look.d.ts +33 -0
- package/dist/lib/daylight/look.d.ts.map +1 -0
- package/dist/lib/daylight/look.js +13 -0
- package/dist/lib/daylight/material-hooks.d.ts +2 -0
- package/dist/lib/daylight/material-hooks.d.ts.map +1 -0
- package/dist/lib/daylight/material-hooks.js +150 -0
- package/dist/lib/daylight/room-context.d.ts +12 -0
- package/dist/lib/daylight/room-context.d.ts.map +1 -0
- package/dist/lib/daylight/room-context.js +53 -0
- package/dist/lib/daylight/room-scene.d.ts +54 -0
- package/dist/lib/daylight/room-scene.d.ts.map +1 -0
- package/dist/lib/daylight/room-scene.js +356 -0
- package/dist/lib/daylight/rooms.d.ts +262 -0
- package/dist/lib/daylight/rooms.d.ts.map +1 -0
- package/dist/lib/daylight/rooms.js +1223 -0
- package/dist/lib/daylight/runtime.d.ts +33 -0
- package/dist/lib/daylight/runtime.d.ts.map +1 -0
- package/dist/lib/daylight/runtime.js +42 -0
- package/dist/lib/daylight/sky.d.ts +26 -0
- package/dist/lib/daylight/sky.d.ts.map +1 -0
- package/dist/lib/daylight/sky.js +217 -0
- package/dist/lib/daylight/solar.d.ts +46 -0
- package/dist/lib/daylight/solar.d.ts.map +1 -0
- package/dist/lib/daylight/solar.js +156 -0
- package/dist/lib/device-tier.d.ts +29 -0
- package/dist/lib/device-tier.d.ts.map +1 -0
- package/dist/lib/device-tier.js +71 -0
- package/dist/lib/dispose-object3d.d.ts.map +1 -1
- package/dist/lib/dispose-object3d.js +4 -2
- package/dist/lib/drone-flight.d.ts +44 -0
- package/dist/lib/drone-flight.d.ts.map +1 -0
- package/dist/lib/drone-flight.js +121 -0
- package/dist/lib/editor-highlight.d.ts +16 -0
- package/dist/lib/editor-highlight.d.ts.map +1 -0
- package/dist/lib/editor-highlight.js +33 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -1
- package/dist/lib/foundation-materials.js +2 -4
- package/dist/lib/glb-merge.d.ts +23 -0
- package/dist/lib/glb-merge.d.ts.map +1 -0
- package/dist/lib/glb-merge.js +184 -0
- package/dist/lib/hardware-geometry.d.ts +21 -0
- package/dist/lib/hardware-geometry.d.ts.map +1 -0
- package/dist/lib/hardware-geometry.js +283 -0
- package/dist/lib/layer-pass.d.ts +2 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layer-pass.js +20 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +50 -23
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -1
- package/dist/lib/occlusion-culler.d.ts +34 -0
- package/dist/lib/occlusion-culler.d.ts.map +1 -0
- package/dist/lib/occlusion-culler.js +698 -0
- package/dist/lib/post-processing-resources.d.ts +4 -0
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- package/dist/lib/post-processing-resources.js +5 -0
- package/dist/lib/readback-rows.d.ts +14 -0
- package/dist/lib/readback-rows.d.ts.map +1 -0
- package/dist/lib/readback-rows.js +31 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -1
- package/dist/lib/reveal-pose.js +3 -0
- package/dist/lib/scene-matrices.d.ts +12 -0
- package/dist/lib/scene-matrices.d.ts.map +1 -0
- package/dist/lib/scene-matrices.js +287 -0
- package/dist/lib/scene-pass-mrt.d.ts +5 -2
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -1
- package/dist/lib/scene-pass-mrt.js +7 -2
- package/dist/lib/scene-themes.d.ts +0 -2
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +0 -2
- package/dist/lib/scene-visibility.d.ts +2 -0
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +12 -3
- package/dist/lib/site-surfaces.d.ts +1 -8
- package/dist/lib/site-surfaces.d.ts.map +1 -1
- package/dist/lib/site-surfaces.js +0 -19
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +24 -40
- package/dist/lib/texture-reference.d.ts.map +1 -1
- package/dist/lib/texture-reference.js +10 -8
- package/dist/lib/underground-ambiance.d.ts +0 -2
- package/dist/lib/underground-ambiance.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.js +0 -2
- package/dist/store/use-item-light-pool.d.ts +3 -6
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +12 -0
- package/dist/store/use-viewer.d.ts +72 -15
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +103 -18
- package/dist/systems/ceiling/ceiling-system.d.ts +5 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +11 -1
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +501 -145
- package/dist/systems/door/leaf-profile.d.ts +11 -0
- package/dist/systems/door/leaf-profile.d.ts.map +1 -0
- package/dist/systems/door/leaf-profile.js +140 -0
- package/dist/systems/interactive/callout-actions.d.ts +72 -0
- package/dist/systems/interactive/callout-actions.d.ts.map +1 -0
- package/dist/systems/interactive/callout-actions.js +212 -0
- package/dist/systems/interactive/interactive-system.d.ts +5 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +44 -121
- package/dist/systems/interactive/opening-settled.d.ts +16 -0
- package/dist/systems/interactive/opening-settled.d.ts.map +1 -0
- package/dist/systems/interactive/opening-settled.js +53 -0
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +22 -214
- package/dist/systems/item-light/pooled-lights.d.ts +70 -0
- package/dist/systems/item-light/pooled-lights.d.ts.map +1 -0
- package/dist/systems/item-light/pooled-lights.js +449 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -1
- package/dist/systems/roof/roof-material-slots.d.ts +14 -0
- package/dist/systems/roof/roof-material-slots.d.ts.map +1 -0
- package/dist/systems/roof/roof-material-slots.js +13 -0
- package/dist/systems/roof/roof-materials.d.ts +25 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +85 -26
- package/dist/systems/roof/roof-system.d.ts +34 -1
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +477 -105
- package/dist/systems/wall/opening-casing-geometry.d.ts +14 -0
- package/dist/systems/wall/opening-casing-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-casing-geometry.js +102 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts +9 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -1
- package/dist/systems/wall/opening-cutout-geometry.js +7 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +28 -35
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +34 -19
- package/dist/systems/wall/wall-t-junctions.d.ts +15 -0
- package/dist/systems/wall/wall-t-junctions.d.ts.map +1 -0
- package/dist/systems/wall/wall-t-junctions.js +149 -0
- package/dist/systems/window/window-animation-system.d.ts +3 -12
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +37 -122
- package/dist/systems/window/window-pose.d.ts +31 -0
- package/dist/systems/window/window-pose.d.ts.map +1 -0
- package/dist/systems/window/window-pose.js +152 -0
- package/dist/systems/window/window-roller-shutter.d.ts +8 -0
- package/dist/systems/window/window-roller-shutter.d.ts.map +1 -0
- package/dist/systems/window/window-roller-shutter.js +64 -0
- package/dist/systems/window/window-system.d.ts +2 -11
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +470 -303
- package/package.json +6 -6
- package/dist/systems/interactive/control-widget.d.ts +0 -10
- package/dist/systems/interactive/control-widget.d.ts.map +0 -1
- package/dist/systems/interactive/control-widget.js +0 -35
- package/dist/systems/interactive/procedural-controls.d.ts +0 -13
- package/dist/systems/interactive/procedural-controls.d.ts.map +0 -1
- package/dist/systems/interactive/procedural-controls.js +0 -17
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { getBarrelRoofFaces, getBarrelRoofSolidPieces, getConicalRoofCoverage, getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getEffectiveNode, getGlazedRoofFaces, getGlazedRoofPieceFaces, getGlazedRoofPieces, getRoofModuleFaces, getRoofPlanBounds, getRoofSegmentDeckReach, getRoofSegmentSurfaceY, getRoofShapeInsets, getRoofShapeRatios, getSegmentSlopeFrame, hasSegmentMaterialOverride, isBandedShedSegment, isGlazedRoofSegment, isIndependentRoofSegment, nodeRegistry, normalizeRoofSegmentTrim, pointInPolygon2D, ROOF_SHAPE_DEFAULTS, roofOverlapEntryOwns, roofPlanBoundsOverlap, sceneRegistry, unionPolygons, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { getBarrelRoofFaces, getBarrelRoofSolidPieces, getConicalRoofCoverage, getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getEffectiveNode, getGlazedRoofFaces, getGlazedRoofPieceFaces, getGlazedRoofPieces, getRoofModuleFaces, getRoofPlanBounds, getRoofSegmentDeckReach, getRoofSegmentSurfaceY, getRoofShapeInsets, getRoofShapeRatios, getSegmentSlopeFrame, hasSegmentMaterialOverride, isBandedShedSegment, isGlazedRoofSegment, isIndependentRoofSegment, LUMBER_DEPTH, nodeRegistry, normalizeRoofSegmentTrim, pointInPolygon2D, ROOF_SHAPE_DEFAULTS, roofOverlapEntryOwns, roofPlanBoundsOverlap, roofSegmentEaveProfile, roofStructureDepth, sceneRegistry, unionPolygons, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
|
+
import { useEffect } from 'react';
|
|
3
4
|
import * as THREE from 'three';
|
|
4
5
|
import { mergeGeometries, mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
5
6
|
import { ADDITION, Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
6
7
|
import { computeBoundsTree } from 'three-mesh-bvh';
|
|
7
8
|
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
8
9
|
import { ensureRenderableGeometryAttributes, subtractCsgBrush } from '../../lib/csg-utils';
|
|
10
|
+
import useViewer from '../../store/use-viewer';
|
|
9
11
|
import { isRevealAssembling } from '../construction-reveal/construction-reveal';
|
|
12
|
+
import { ROOF_GABLE_LEFT_MATERIAL_INDEX, ROOF_GABLE_RIGHT_MATERIAL_INDEX, ROOF_HEEL_MATERIAL_INDEX, } from './roof-material-slots';
|
|
10
13
|
function csgGeometry(brush) {
|
|
11
14
|
return brush.geometry;
|
|
12
15
|
}
|
|
@@ -233,6 +236,16 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
233
236
|
// set from the render pass that received the override-publishing
|
|
234
237
|
// `markDirty` call. Mirrors WallSystem / DoorSystem.
|
|
235
238
|
useLiveNodeOverrides((s) => s.overrides);
|
|
239
|
+
// "Framing" opens the decks' structure band (`roofFramingDepth`): every segment, and so every
|
|
240
|
+
// roof shell, rebuilds when it flips.
|
|
241
|
+
useEffect(() => useViewer.subscribe((state, previous) => {
|
|
242
|
+
if (state.framingShown === previous.framingShown)
|
|
243
|
+
return;
|
|
244
|
+
const scene = useScene.getState();
|
|
245
|
+
for (const node of Object.values(scene.nodes))
|
|
246
|
+
if (node.type === 'roof-segment')
|
|
247
|
+
scene.markDirty(node.id);
|
|
248
|
+
}), []);
|
|
236
249
|
useFrame(() => {
|
|
237
250
|
// Clear stale pending updates when the scene is unloaded
|
|
238
251
|
if (rootNodeIds.length === 0) {
|
|
@@ -252,6 +265,7 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
252
265
|
pendingIndependentSegments.size === 0)
|
|
253
266
|
return;
|
|
254
267
|
const nodes = useScene.getState().nodes;
|
|
268
|
+
const framing = useViewer.getState().framingShown;
|
|
255
269
|
// --- Pass 1: Process dirty roof-segments (throttled) ---
|
|
256
270
|
let segmentsProcessed = 0;
|
|
257
271
|
new Set([...dirtyNodes, ...pendingIndependentSegments]).forEach((id) => {
|
|
@@ -302,7 +316,7 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
302
316
|
const revealOnly = mesh.parent?.name === 'segments-wrapper' &&
|
|
303
317
|
mesh.parent?.parent?.getObjectByName('merged-roof')?.visible === true;
|
|
304
318
|
if (isVisible && !revealOnly && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
|
|
305
|
-
updateRoofSegmentGeometry(effectiveSegment, mesh, nodes, geometryAdapter);
|
|
319
|
+
updateRoofSegmentGeometry(effectiveSegment, mesh, nodes, geometryAdapter, framing);
|
|
306
320
|
segmentsProcessed++;
|
|
307
321
|
}
|
|
308
322
|
else if (isVisible && !revealOnly) {
|
|
@@ -358,7 +372,7 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
358
372
|
// Built even while hidden: a roof assembling from its segments (construction
|
|
359
373
|
// reveal) hides its shell, and must show it whole the moment it is captured.
|
|
360
374
|
rememberMergedInputs(node, nodes);
|
|
361
|
-
updateMergedRoofGeometry(node, group, nodes);
|
|
375
|
+
updateMergedRoofGeometry(node, group, nodes, framing);
|
|
362
376
|
roofsProcessed++;
|
|
363
377
|
pendingRoofUpdates.delete(id);
|
|
364
378
|
}
|
|
@@ -368,8 +382,8 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
368
382
|
// ============================================================================
|
|
369
383
|
// GEOMETRY GENERATION
|
|
370
384
|
// ============================================================================
|
|
371
|
-
function updateRoofSegmentGeometry(node, mesh, nodes, adapter) {
|
|
372
|
-
const newGeo = generateRoofSegmentGeometry(node, nodes);
|
|
385
|
+
function updateRoofSegmentGeometry(node, mesh, nodes, adapter, framing = false) {
|
|
386
|
+
const newGeo = generateRoofSegmentGeometry(node, nodes, { framing });
|
|
373
387
|
mesh.geometry.dispose();
|
|
374
388
|
mesh.geometry = newGeo;
|
|
375
389
|
computeGeometryBoundsTree(newGeo);
|
|
@@ -455,14 +469,18 @@ function subtractAccessoryCuts(brushes, segment, nodes) {
|
|
|
455
469
|
brushes.deckSlab = workingDeck;
|
|
456
470
|
brushes.wallBrush = workingWall;
|
|
457
471
|
}
|
|
458
|
-
function getMergedRoofSegmentBrushes(roofNode, segment, nodes) {
|
|
472
|
+
function getMergedRoofSegmentBrushes(roofNode, segment, nodes, framing) {
|
|
459
473
|
const segmentId = segment.id;
|
|
460
|
-
const
|
|
474
|
+
const framingDepth = framing ? roofFramingDepth(segment) : 0;
|
|
475
|
+
const cacheKey = getMergedRoofSegmentCacheKey(roofNode, segment, nodes, framingDepth);
|
|
461
476
|
const cached = mergedRoofSegmentGeometryCache.get(segmentId);
|
|
462
477
|
if (cached?.key === cacheKey) {
|
|
463
478
|
return cloneCachedMergedRoofSegmentBrushes(cached);
|
|
464
479
|
}
|
|
465
|
-
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment, {
|
|
480
|
+
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment, {
|
|
481
|
+
fascia: wearsFascia(segment, roofNode),
|
|
482
|
+
framing: framingDepth,
|
|
483
|
+
}));
|
|
466
484
|
if (!brushes) {
|
|
467
485
|
disposeCachedMergedRoofSegmentGeometrySet(cached);
|
|
468
486
|
mergedRoofSegmentGeometryCache.delete(segmentId);
|
|
@@ -505,7 +523,7 @@ function getMergedRoofSegmentBrushes(roofNode, segment, nodes) {
|
|
|
505
523
|
disposeRoofSegmentBrushSet(brushes);
|
|
506
524
|
return cloned;
|
|
507
525
|
}
|
|
508
|
-
function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
526
|
+
function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
|
|
509
527
|
const mergedMesh = group.getObjectByName('merged-roof');
|
|
510
528
|
if (!mergedMesh)
|
|
511
529
|
return;
|
|
@@ -548,7 +566,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
548
566
|
continue;
|
|
549
567
|
}
|
|
550
568
|
csgChildren.push(child);
|
|
551
|
-
const brushes = getMergedRoofSegmentBrushes(roofNode, child, nodes);
|
|
569
|
+
const brushes = getMergedRoofSegmentBrushes(roofNode, child, nodes, framing);
|
|
552
570
|
if (!brushes)
|
|
553
571
|
continue;
|
|
554
572
|
if (brushes.rakeBoards) {
|
|
@@ -648,6 +666,14 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
648
666
|
g.materialIndex = mapRoofGroupMaterialIndex(g.materialIndex, resultMaterials, matToIndex);
|
|
649
667
|
}
|
|
650
668
|
let finalGeo = addShedInsetEndPanels(resultGeo, csgChildren, true);
|
|
669
|
+
splitRoofWallBandFaces(finalGeo, csgChildren
|
|
670
|
+
.filter((child) => shouldIncludeRoofSegmentWallShell(child, roofNode))
|
|
671
|
+
.map((child) => ({
|
|
672
|
+
node: child,
|
|
673
|
+
inverse: new THREE.Matrix4()
|
|
674
|
+
.compose(_position.set(child.position[0], child.position[1], child.position[2]), _quaternion.setFromAxisAngle(_yAxis, child.rotation), _scale)
|
|
675
|
+
.invert(),
|
|
676
|
+
})), false);
|
|
651
677
|
const appendedGeometries = [...rakeBoardGeometries, ...directSegmentGeometries];
|
|
652
678
|
if (appendedGeometries.length > 0) {
|
|
653
679
|
const merged = mergeGeometriesPreservingGroups([finalGeo, ...appendedGeometries]);
|
|
@@ -792,7 +818,7 @@ function getMergedRoofAccessoryCachePayload(segment, nodes) {
|
|
|
792
818
|
}
|
|
793
819
|
return payload;
|
|
794
820
|
}
|
|
795
|
-
function getMergedRoofSegmentCacheKey(roofNode, segment, nodes) {
|
|
821
|
+
function getMergedRoofSegmentCacheKey(roofNode, segment, nodes, framing) {
|
|
796
822
|
return JSON.stringify({
|
|
797
823
|
roofPosition: roofNode.position ?? [0, 0, 0],
|
|
798
824
|
roofRotation: roofNode.rotation ?? 0,
|
|
@@ -800,6 +826,7 @@ function getMergedRoofSegmentCacheKey(roofNode, segment, nodes) {
|
|
|
800
826
|
composite: (roofNode.children?.length ?? 0) > 1,
|
|
801
827
|
segment,
|
|
802
828
|
accessories: getMergedRoofAccessoryCachePayload(segment, nodes),
|
|
829
|
+
framing,
|
|
803
830
|
});
|
|
804
831
|
}
|
|
805
832
|
export function mapRoofGroupMaterialIndex(groupMaterialIndex, csgMaterials, matToIndex) {
|
|
@@ -1181,103 +1208,215 @@ export function clipGeometryBySegmentTrim(geometry, segment) {
|
|
|
1181
1208
|
* Generate complete hollow-shell geometry for a roof segment.
|
|
1182
1209
|
* Ports the prototype's CSG approach using three-bvh-csg.
|
|
1183
1210
|
*/
|
|
1184
|
-
/** A 1x8 fascia / rake board: actual 3/4 × 7-1/4 in. */
|
|
1185
|
-
const FASCIA_BOARD = { thickness: 0.019, height: 0.184 };
|
|
1186
1211
|
/** A board's back sits this far inside the deck edge, so the union is one solid with no coplanar faces. */
|
|
1187
1212
|
const FASCIA_EMBED = 0.002;
|
|
1213
|
+
/**
|
|
1214
|
+
* Boards that meet leave this between them: drawn as one solid, overlapping or touching boards would
|
|
1215
|
+
* leave it no inside to tell, and the union with the deck tears.
|
|
1216
|
+
*/
|
|
1217
|
+
const BOARD_GAP = 0.0001;
|
|
1188
1218
|
/**
|
|
1189
1219
|
* Slot 2 takes the trim finish like the rake faces, and the shell remap
|
|
1190
1220
|
* leaves it alone: the remap would send a plumb eave board to the wall band
|
|
1191
|
-
* and its top edge to the shingles.
|
|
1221
|
+
* and its top edge to the shingles. A boxed eave's soffit and returns take it too.
|
|
1192
1222
|
*/
|
|
1193
1223
|
const FASCIA_MATERIAL_INDEX = 2;
|
|
1224
|
+
/** The forms whose eaves core's `roofEaveProfile` cuts and boards: the ones Bones frames with tails. */
|
|
1225
|
+
const EAVE_ROOF_TYPES = new Set(['gable', 'hip', 'shed', 'flat']);
|
|
1226
|
+
const EDGES_Z = [
|
|
1227
|
+
{ axis: 'z', sign: 1 },
|
|
1228
|
+
{ axis: 'z', sign: -1 },
|
|
1229
|
+
];
|
|
1230
|
+
const EDGES_X = [
|
|
1231
|
+
{ axis: 'x', sign: 1 },
|
|
1232
|
+
{ axis: 'x', sign: -1 },
|
|
1233
|
+
];
|
|
1234
|
+
/** The point of an edge's section (`s` out past its plate line, `y` over `plateY`) at `u` along it. */
|
|
1235
|
+
function edgePoint(node, edge, plateY, [s, y], u) {
|
|
1236
|
+
const out = edge.sign * ((edge.axis === 'z' ? node.depth : node.width) / 2 + s);
|
|
1237
|
+
return edge.axis === 'z'
|
|
1238
|
+
? new THREE.Vector3(u, plateY + y, out)
|
|
1239
|
+
: new THREE.Vector3(out, plateY + y, u);
|
|
1240
|
+
}
|
|
1241
|
+
/** A convex solid's faces, each turned to face away from its centre. */
|
|
1242
|
+
function outwardFaces(faces) {
|
|
1243
|
+
const points = faces.flat();
|
|
1244
|
+
const centre = points
|
|
1245
|
+
.reduce((sum, v) => sum.add(v), new THREE.Vector3())
|
|
1246
|
+
.multiplyScalar(1 / Math.max(1, points.length));
|
|
1247
|
+
return faces.map((face) => {
|
|
1248
|
+
const normal = new THREE.Vector3()
|
|
1249
|
+
.subVectors(face[1], face[0])
|
|
1250
|
+
.cross(new THREE.Vector3().subVectors(face[2], face[0]));
|
|
1251
|
+
const faceCentre = face
|
|
1252
|
+
.reduce((sum, v) => sum.add(v), new THREE.Vector3())
|
|
1253
|
+
.multiplyScalar(1 / face.length);
|
|
1254
|
+
return normal.dot(faceCentre.sub(centre)) < 0 ? [...face].reverse() : face;
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* A prism over a convex `section` of an edge, run along it from `from` to `to`: each end where its
|
|
1259
|
+
* function puts it at each section point (affine in it), so an end can butt a sloped board's face.
|
|
1260
|
+
*/
|
|
1261
|
+
function edgePrism(node, edge, plateY, section, from, to) {
|
|
1262
|
+
const a = section.map((point) => edgePoint(node, edge, plateY, point, from(point)));
|
|
1263
|
+
const b = section.map((point) => edgePoint(node, edge, plateY, point, to(point)));
|
|
1264
|
+
const faces = [a, b];
|
|
1265
|
+
for (let i = 0; i < section.length; i++) {
|
|
1266
|
+
const j = (i + 1) % section.length;
|
|
1267
|
+
faces.push([a[i], a[j], b[j], b[i]]);
|
|
1268
|
+
}
|
|
1269
|
+
return outwardFaces(faces);
|
|
1270
|
+
}
|
|
1271
|
+
/** Where a line through two section points is at height `y`, out from the plate line. */
|
|
1272
|
+
function sAtHeight([s0, y0], [s1, y1], y) {
|
|
1273
|
+
return Math.abs(y1 - y0) < 1e-9 ? s0 : s0 + ((y - y0) * (s1 - s0)) / (y1 - y0);
|
|
1274
|
+
}
|
|
1275
|
+
/** A fascia's section with its back pulled `FASCIA_EMBED` into the deck, along its thickness. */
|
|
1276
|
+
function embeddedFascia(profile) {
|
|
1277
|
+
const [backTop, frontTop, frontBottom, backBottom] = profile.fascia.corners;
|
|
1278
|
+
const [nx, ny] = profile.fascia.square ? [profile.cos, -profile.sin] : [1, 0];
|
|
1279
|
+
const back = ([s, y]) => [s - nx * FASCIA_EMBED, y - ny * FASCIA_EMBED];
|
|
1280
|
+
return [back(backTop), frontTop, frontBottom, back(backBottom)];
|
|
1281
|
+
}
|
|
1194
1282
|
/**
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
*
|
|
1198
|
-
* of the segment, they follow its pitch, size, overhang and type, and the
|
|
1199
|
-
* trims, cuts and sibling roofs clip them like the deck. Gable, hip, shed
|
|
1200
|
-
* and flat; null for the other forms.
|
|
1283
|
+
* A rake board's section in the eave's frame (`profile`): plumb, its top on the sheathing's top, as
|
|
1284
|
+
* deep as the profile's rake board, from the fascia's back at the eave (square to the slope with a
|
|
1285
|
+
* square cut; held level at the fascia's bottom with a plumb-and-level one) up to `inner`, plumb.
|
|
1201
1286
|
*/
|
|
1202
|
-
function
|
|
1287
|
+
function rakeSection(profile, inner) {
|
|
1288
|
+
const { top, cos, sin, tan } = profile;
|
|
1289
|
+
const depth = profile.rake.height;
|
|
1290
|
+
const bottom = (s) => top(s) - depth;
|
|
1291
|
+
// short of the fascia's back, which is pulled into the deck
|
|
1292
|
+
const pull = FASCIA_EMBED + BOARD_GAP;
|
|
1293
|
+
const [s0, y0] = profile.end[0];
|
|
1294
|
+
const corner = profile.fascia.square
|
|
1295
|
+
? [s0 - pull * cos, y0 + pull * sin]
|
|
1296
|
+
: [s0 - pull, top(s0 - pull)];
|
|
1297
|
+
const section = [corner];
|
|
1298
|
+
if (profile.cut === 'square') {
|
|
1299
|
+
const along = depth * cos;
|
|
1300
|
+
section.push([corner[0] - along * sin, corner[1] - along * cos]);
|
|
1301
|
+
}
|
|
1302
|
+
else if (profile.cut === 'plumb-level') {
|
|
1303
|
+
const level = profile.fascia.corners[3][1];
|
|
1304
|
+
const s = tan > 1e-9 ? (top(0) - depth - level) / tan : corner[0];
|
|
1305
|
+
if (bottom(corner[0]) < level && s < corner[0])
|
|
1306
|
+
section.push([corner[0], level], [s, level]);
|
|
1307
|
+
else
|
|
1308
|
+
section.push([corner[0], bottom(corner[0])]);
|
|
1309
|
+
}
|
|
1310
|
+
else
|
|
1311
|
+
section.push([corner[0], bottom(corner[0])]);
|
|
1312
|
+
section.push([inner, bottom(inner)], [inner, top(inner)]);
|
|
1313
|
+
return section;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* The fascia and rake boards a segment wears, in its own frame, as its eaves are cut
|
|
1317
|
+
* (`roofSegmentEaveProfile`): a 1x on every eave covering the tails' end, its top flush with the
|
|
1318
|
+
* sheathing's, and a plumb one up every gable or shed rake over the barge rafter, its top on the
|
|
1319
|
+
* sheathing's. The eave boards run through to the rake boards' faces, which butt their backs; a hip's
|
|
1320
|
+
* or flat roof's end boards butt the side boards' backs. Being part of the segment, they follow its
|
|
1321
|
+
* pitch, size, overhang, layers and type, and the trims, cuts and sibling roofs clip them like the
|
|
1322
|
+
* deck. Gable, hip, shed and flat; null for the other forms.
|
|
1323
|
+
*/
|
|
1324
|
+
function buildFasciaBoards(node, reach, eave, high) {
|
|
1203
1325
|
const { roofType, width, depth } = node;
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
const
|
|
1208
|
-
const
|
|
1209
|
-
const halfD = depth / 2 + reach.z;
|
|
1210
|
-
const zE = halfD + out;
|
|
1211
|
-
const xE = width / 2 + reach.x + out;
|
|
1212
|
-
// the eave boards run through the corners to the rake / side boards'
|
|
1213
|
-
// outer faces (a lapped return, no notch at the corner)
|
|
1214
|
-
const xL = xE + bt / 2;
|
|
1326
|
+
if (!EAVE_ROOF_TYPES.has(roofType))
|
|
1327
|
+
return null;
|
|
1328
|
+
const plateY = node.wallHeight;
|
|
1329
|
+
const thickness = eave.fascia.thickness;
|
|
1330
|
+
const board = embeddedFascia(eave);
|
|
1215
1331
|
const faces = [];
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
const
|
|
1219
|
-
const
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
[
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
[0, 4, 5, 1],
|
|
1234
|
-
[1, 5, 6, 2],
|
|
1235
|
-
[2, 6, 7, 3],
|
|
1236
|
-
[3, 7, 4, 0],
|
|
1237
|
-
];
|
|
1238
|
-
for (const quad of quads) {
|
|
1239
|
-
const face = quad.map((k) => p[k].clone());
|
|
1240
|
-
const normal = new THREE.Vector3()
|
|
1241
|
-
.subVectors(face[1], face[0])
|
|
1242
|
-
.cross(new THREE.Vector3().subVectors(face[2], face[0]));
|
|
1243
|
-
const faceCentre = face
|
|
1244
|
-
.reduce((sum, v) => sum.add(v), new THREE.Vector3())
|
|
1245
|
-
.multiplyScalar(1 / 4);
|
|
1246
|
-
if (normal.dot(faceCentre.sub(centre)) < 0)
|
|
1247
|
-
face.reverse();
|
|
1248
|
-
faces.push(face);
|
|
1249
|
-
}
|
|
1250
|
-
};
|
|
1251
|
-
if (roofType === 'gable') {
|
|
1252
|
-
const ridgeY = eaveTopY + halfD * tanTheta;
|
|
1253
|
-
board([-xL, eaveTopY, zE], [xL, eaveTopY, zE], [0, 1]);
|
|
1254
|
-
board([-xL, eaveTopY, -zE], [xL, eaveTopY, -zE], [0, 1]);
|
|
1255
|
-
for (const sx of [-1, 1]) {
|
|
1256
|
-
board([sx * xE, eaveTopY, zE], [sx * xE, ridgeY, 0], [1, 0]);
|
|
1257
|
-
board([sx * xE, ridgeY, 0], [sx * xE, eaveTopY, -zE], [1, 0]);
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
else if (roofType === 'hip' || roofType === 'flat') {
|
|
1261
|
-
const y = roofType === 'flat' ? flatTopY : eaveTopY;
|
|
1262
|
-
for (const sz of [-1, 1])
|
|
1263
|
-
board([-xL, y, sz * zE], [xL, y, sz * zE], [0, 1]);
|
|
1264
|
-
// the side boards butt the eave boards' backs
|
|
1265
|
-
for (const sx of [-1, 1])
|
|
1266
|
-
board([sx * xE, y, -zE + bt / 2], [sx * xE, y, zE - bt / 2], [1, 0]);
|
|
1332
|
+
const rakeFace = width / 2 + reach.x + thickness;
|
|
1333
|
+
if (roofType === 'gable' || roofType === 'shed') {
|
|
1334
|
+
const sides = roofType === 'gable' ? EDGES_Z : [EDGES_Z[0]];
|
|
1335
|
+
for (const edge of sides)
|
|
1336
|
+
faces.push(...edgePrism(node, edge, plateY, board, () => -rakeFace, () => rakeFace));
|
|
1337
|
+
const highY = plateY + getSegmentSlopeFrame(node).activeRh;
|
|
1338
|
+
if (roofType === 'shed' && high && (node.fasciaHighEdge ?? true))
|
|
1339
|
+
faces.push(...edgePrism(node, EDGES_Z[1], highY, embeddedFascia(high), () => -rakeFace, () => rakeFace));
|
|
1340
|
+
// the rakes: a gable's up each slope to the ridge, a shed's from its low eave to its high edge
|
|
1341
|
+
const inner = roofType === 'gable' ? -depth / 2 + BOARD_GAP : -depth - reach.z + FASCIA_EMBED + BOARD_GAP;
|
|
1342
|
+
const section = rakeSection(eave, inner);
|
|
1343
|
+
const back = width / 2 + reach.x - FASCIA_EMBED;
|
|
1344
|
+
for (const edge of sides)
|
|
1345
|
+
for (const sx of [-1, 1]) {
|
|
1346
|
+
const [x0, x1] = sx === 1 ? [back, rakeFace] : [-rakeFace, -back];
|
|
1347
|
+
faces.push(...edgePrism(node, edge, plateY, section, () => x0, () => x1));
|
|
1348
|
+
}
|
|
1267
1349
|
}
|
|
1268
|
-
else
|
|
1269
|
-
// the
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1350
|
+
else {
|
|
1351
|
+
// hip and flat: the boards along ±z run through to the end boards' faces, which butt their backs
|
|
1352
|
+
const [, frontTop, frontBottom] = eave.fascia.corners;
|
|
1353
|
+
const front = (y) => sAtHeight(frontTop, frontBottom, y);
|
|
1354
|
+
const back = (y) => sAtHeight(board[0], board[3], y) - BOARD_GAP;
|
|
1355
|
+
for (const edge of EDGES_Z)
|
|
1356
|
+
faces.push(...edgePrism(node, edge, plateY, board, ([, y]) => -(width / 2 + front(y)), ([, y]) => width / 2 + front(y)));
|
|
1357
|
+
for (const edge of EDGES_X)
|
|
1358
|
+
faces.push(...edgePrism(node, edge, plateY, board, ([, y]) => -(depth / 2 + back(y)), ([, y]) => depth / 2 + back(y)));
|
|
1276
1359
|
}
|
|
1277
|
-
else
|
|
1278
|
-
return null;
|
|
1279
1360
|
return createGeometryFromFaces(faces, FASCIA_MATERIAL_INDEX);
|
|
1280
1361
|
}
|
|
1362
|
+
/**
|
|
1363
|
+
* What a segment's eave cut takes off its deck slab out past the walls (`roofSegmentEaveProfile`):
|
|
1364
|
+
* the wedge outside a square cut; under a truss's top chords the heel's depth, the tails riding
|
|
1365
|
+
* that far over the deck's underside (`seal` under them, as the deck is); under a boxed eave's
|
|
1366
|
+
* soffit everything, the box standing on it. Each a convex solid, a hip's bounded by its hip lines
|
|
1367
|
+
* (a hair past them, so the two at a corner overlap). None for a plumb cut on rafters.
|
|
1368
|
+
*/
|
|
1369
|
+
function buildEaveCuts(node, reach, eave, seal) {
|
|
1370
|
+
const { roofType, width, depth } = node;
|
|
1371
|
+
if (roofType === 'flat')
|
|
1372
|
+
return [];
|
|
1373
|
+
const plateY = node.wallHeight;
|
|
1374
|
+
const sections = [];
|
|
1375
|
+
if (eave.cut === 'square') {
|
|
1376
|
+
const [corner] = eave.end;
|
|
1377
|
+
const down = (point, d) => [
|
|
1378
|
+
point[0] - d * eave.sin,
|
|
1379
|
+
point[1] - d * eave.cos,
|
|
1380
|
+
];
|
|
1381
|
+
const out = (point, d) => [
|
|
1382
|
+
point[0] + d * eave.cos,
|
|
1383
|
+
point[1] - d * eave.sin,
|
|
1384
|
+
];
|
|
1385
|
+
const reachDown = node.deckThickness + 0.1;
|
|
1386
|
+
const above = down(corner, -0.2);
|
|
1387
|
+
const below = down(corner, reachDown);
|
|
1388
|
+
sections.push([above, out(above, 0.5), out(below, 0.5), below]);
|
|
1389
|
+
}
|
|
1390
|
+
const lift = eave.under(0);
|
|
1391
|
+
const far = eave.reach + 0.3;
|
|
1392
|
+
if (lift > 1e-6) {
|
|
1393
|
+
const at = (s, d) => [s, eave.under(s) - seal - d];
|
|
1394
|
+
sections.push([at(eave.wallOut, 0), at(far, 0), at(far, 1), at(eave.wallOut, 1)]);
|
|
1395
|
+
}
|
|
1396
|
+
if (eave.soffit) {
|
|
1397
|
+
const level = eave.soffit.y - eave.soffit.thickness;
|
|
1398
|
+
sections.push([
|
|
1399
|
+
[eave.wallOut, level],
|
|
1400
|
+
[far, level],
|
|
1401
|
+
[far, level - 1],
|
|
1402
|
+
[eave.wallOut, level - 1],
|
|
1403
|
+
]);
|
|
1404
|
+
}
|
|
1405
|
+
if (sections.length === 0)
|
|
1406
|
+
return [];
|
|
1407
|
+
const solids = [];
|
|
1408
|
+
const edges = roofType === 'hip' ? [...EDGES_Z, ...EDGES_X] : roofType === 'gable' ? EDGES_Z : [EDGES_Z[0]];
|
|
1409
|
+
for (const edge of edges)
|
|
1410
|
+
for (const section of sections) {
|
|
1411
|
+
// a hip's eave runs out to its hip lines; a gable's or shed's past its rakes
|
|
1412
|
+
const half = (edge.axis === 'z' ? width : depth) / 2;
|
|
1413
|
+
const end = roofType === 'hip'
|
|
1414
|
+
? ([s]) => half + Math.max(0, s) + 0.01
|
|
1415
|
+
: () => half + reach.x + 1;
|
|
1416
|
+
solids.push(edgePrism(node, edge, plateY, section, (point) => -end(point), end));
|
|
1417
|
+
}
|
|
1418
|
+
return solids;
|
|
1419
|
+
}
|
|
1281
1420
|
/**
|
|
1282
1421
|
* Whether a drawn segment wears fascia boards. A lean-to carries its own
|
|
1283
1422
|
* trim, and a legacy composite shed roof stacks overlapping shed pieces as
|
|
@@ -1293,9 +1432,44 @@ function wearsFascia(node, parentRoof) {
|
|
|
1293
1432
|
return false;
|
|
1294
1433
|
return !parentRoof || (parentRoof.children?.length ?? 0) <= 1;
|
|
1295
1434
|
}
|
|
1435
|
+
/** What a framed roof's deck keeps over its open structure band at least: a 7/16 in sheet. */
|
|
1436
|
+
const ROOF_DECK_KEPT = 0.0111;
|
|
1437
|
+
/**
|
|
1438
|
+
* Where the deck stays whole while its structure band is open (over the walls, in the overhangs),
|
|
1439
|
+
* its underside sits this far under the rafter plane, so the rafter tails stay inside it rather
|
|
1440
|
+
* than sharing its face.
|
|
1441
|
+
*/
|
|
1442
|
+
const ROOF_DECK_SEAL = 0.005;
|
|
1443
|
+
/**
|
|
1444
|
+
* The forms whose open band is the deck's own planes raised: their planes stay put as the
|
|
1445
|
+
* footprint shrinks to the walls' inner faces. A gambrel's or a mansard's break and a dutch
|
|
1446
|
+
* gablet move with it, so those decks stay closed.
|
|
1447
|
+
*/
|
|
1448
|
+
const FRAMING_OPEN_ROOF_TYPES = new Set(['gable', 'hip', 'shed', 'flat']);
|
|
1449
|
+
/**
|
|
1450
|
+
* How deep a roof plane's deck opens under its sheathing while the viewer shows the frame
|
|
1451
|
+
* (`useViewer.framingShown`), square to the slope: the structure its layers name
|
|
1452
|
+
* (`roofStructureDepth`: its rafters, or its trusses' top chords on their heel), else Bones' default
|
|
1453
|
+
* 2x6. A deck no deeper keeps its top 7/16 in; a plane finished underneath (a cathedral ceiling)
|
|
1454
|
+
* stays closed, the frame hidden behind its finish, and so does a form `FRAMING_OPEN_ROOF_TYPES`
|
|
1455
|
+
* leaves out. 0 when nothing opens.
|
|
1456
|
+
*/
|
|
1457
|
+
export function roofFramingDepth(node) {
|
|
1458
|
+
if (!FRAMING_OPEN_ROOF_TYPES.has(node.roofType) || node.buildUp?.finish)
|
|
1459
|
+
return 0;
|
|
1460
|
+
const structure = node.buildUp
|
|
1461
|
+
? roofStructureDepth(node.buildUp.structure) || LUMBER_DEPTH['2x6']
|
|
1462
|
+
: LUMBER_DEPTH['2x6'];
|
|
1463
|
+
const depth = Math.min(structure, node.deckThickness - ROOF_DECK_KEPT);
|
|
1464
|
+
return depth > 0.005 ? depth : 0;
|
|
1465
|
+
}
|
|
1296
1466
|
/**
|
|
1297
1467
|
* `fascia`: build the segment's fascia boards into its deck. Only the roof
|
|
1298
1468
|
* renderer asks; a dormer, a chimney or a sibling's clip takes the bare shell.
|
|
1469
|
+
* `framing`: the structure band to open (`roofFramingDepth`), the roof
|
|
1470
|
+
* renderer's while the frame is shown. Inside the walls the deck keeps only the
|
|
1471
|
+
* band over it, so the rafters show from the attic; over the walls and in the
|
|
1472
|
+
* overhangs it stays whole, closing the band at the walls' inner faces.
|
|
1299
1473
|
*/
|
|
1300
1474
|
export function getRoofSegmentBrushes(node, options = {}) {
|
|
1301
1475
|
const { roofType, width, depth, wallHeight, wallThickness, deckThickness, shingleThickness } = node;
|
|
@@ -1316,7 +1490,7 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1316
1490
|
const baseI = Math.min(width, depth) * node.dutchHipWidthRatio;
|
|
1317
1491
|
const plateSeated = wallHeight <= 0;
|
|
1318
1492
|
// `xExt`: how far the volume reaches along x, where a gable's rakes are; the eaves' `wExt` else.
|
|
1319
|
-
const getVol = (wExt, vOffset, baseY, matIndex, isVoid, materialRule, onPlate = false, xExt = wExt) => {
|
|
1493
|
+
const getVol = (wExt, vOffset, baseY, matIndex, isVoid, materialRule, onPlate = false, xExt = wExt, keepBase = false) => {
|
|
1320
1494
|
const wV = Math.max(0.01, width + 2 * xExt);
|
|
1321
1495
|
const dV = Math.max(0.01, depth + 2 * wExt);
|
|
1322
1496
|
const autoDrop = wExt * tanTheta;
|
|
@@ -1325,7 +1499,11 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1325
1499
|
// One floor for all volumes keeps each cutter level with the shell it carves.
|
|
1326
1500
|
// A plate-seated roof (wallHeight 0) is the exception for its sloped deck
|
|
1327
1501
|
// volumes: their eave hangs below the plate, and raising it lifted the
|
|
1328
|
-
// whole roof off its walls, so their base sinks instead
|
|
1502
|
+
// whole roof off its walls, so their base sinks instead: under the plate's
|
|
1503
|
+
// plane at their edge too, so a deck's top volume keeps the deck's whole
|
|
1504
|
+
// depth out to the eave (a base 5 cm under its top cut the eave to a 5 cm
|
|
1505
|
+
// edge with the tails hanging under it). `keepBase`: a volume whose base
|
|
1506
|
+
// is its own (a boxed eave's soffit).
|
|
1329
1507
|
const eaveY = wallHeight - autoDrop + vOffset;
|
|
1330
1508
|
const sinkBase = plateSeated && !onPlate && autoDrop !== 0;
|
|
1331
1509
|
const whV = sinkBase ? eaveY : Math.max(0.05, eaveY);
|
|
@@ -1335,7 +1513,9 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1335
1513
|
if (roofType === 'shed')
|
|
1336
1514
|
rhV = activeRh + 2 * autoDrop;
|
|
1337
1515
|
}
|
|
1338
|
-
const safeBaseY = sinkBase
|
|
1516
|
+
const safeBaseY = sinkBase && !keepBase
|
|
1517
|
+
? Math.min(baseY, Math.min(whV, wallHeight - autoDrop - ROOF_DECK_SEAL) - 0.05)
|
|
1518
|
+
: baseY;
|
|
1339
1519
|
let structuralI = baseI;
|
|
1340
1520
|
if (isVoid) {
|
|
1341
1521
|
structuralI += deckThickness;
|
|
@@ -1369,13 +1549,36 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1369
1549
|
? (normal) => (normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX)
|
|
1370
1550
|
: undefined;
|
|
1371
1551
|
const deckTopGeo = getVol(deckExt, verticalRt, 0, 1, false, shedRoofSideMaterialRule, false, reach.x);
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1552
|
+
const framing = Math.max(0, options.framing ?? 0);
|
|
1553
|
+
const deckBotGeo = getVol(deckExt, framing > 0 ? -ROOF_DECK_SEAL : 0, -5, 0, true, undefined, false, reach.x);
|
|
1554
|
+
// the structure band inside the walls' inner faces, open while the frame is shown
|
|
1555
|
+
const framingRt = activeRh > 0 ? framing / cosTheta : framing;
|
|
1556
|
+
const half = -wallThickness / 2;
|
|
1557
|
+
const framingGeo = framing > 0 ? getVol(half, framingRt, -5, 0, true, undefined, false, half) : null;
|
|
1558
|
+
// The eaves, cut and boarded by the plane's rule, the one Bones cuts its tails by (core's
|
|
1559
|
+
// `roofSegmentEaveProfile`). Only the roof renderer asks (`options.fascia` given); a dormer, a
|
|
1560
|
+
// chimney or a sibling's clip takes the bare shell.
|
|
1561
|
+
const eave = options.fascia !== undefined && EAVE_ROOF_TYPES.has(roofType)
|
|
1562
|
+
? roofSegmentEaveProfile(node)
|
|
1563
|
+
: null;
|
|
1564
|
+
const highEave = eave && roofType === 'shed' ? roofSegmentEaveProfile(node, 'high') : null;
|
|
1565
|
+
const fasciaGeo = eave && options.fascia ? buildFasciaBoards(node, reach, eave, highEave) : null;
|
|
1566
|
+
const eaveCutGeos = eave
|
|
1567
|
+
? buildEaveCuts(node, reach, eave, framing > 0 ? ROOF_DECK_SEAL : 0).map((faces) => createGeometryFromFaces(faces, 1))
|
|
1568
|
+
: [];
|
|
1569
|
+
// A boxed eave (plumb and level): solid from the soffit, level under the tails, up into the deck,
|
|
1570
|
+
// from the walls (a hair into them) out to the fascia's back; its ends are the returns under the rakes.
|
|
1571
|
+
let boxGeo = null;
|
|
1572
|
+
let boxCut = null;
|
|
1573
|
+
if (eave?.soffit) {
|
|
1574
|
+
const soffitBottom = wallHeight + eave.soffit.y - eave.soffit.thickness;
|
|
1575
|
+
const ext = reach.z - FASCIA_EMBED;
|
|
1576
|
+
boxGeo = getVol(ext, verticalRt - 0.003, soffitBottom, FASCIA_MATERIAL_INDEX, false, undefined, false, roofType === 'hip' ? ext : reach.x - FASCIA_EMBED, true);
|
|
1577
|
+
const inside = eave.wallOut - FASCIA_EMBED;
|
|
1578
|
+
const far = width / 2 + depth / 2 + reach.x + reach.z + 2;
|
|
1579
|
+
const top = wallHeight + activeRh + deckThickness + shingleThickness + 2;
|
|
1580
|
+
boxCut = buildTrimCutBrush(roofType === 'hip' ? -(width / 2 + inside) : -far, roofType === 'hip' ? width / 2 + inside : far, roofType === 'shed' ? -far : -(depth / 2 + inside), depth / 2 + inside, soffitBottom - 1, top);
|
|
1581
|
+
}
|
|
1379
1582
|
const stSin = shingleThickness * sinTheta;
|
|
1380
1583
|
const stCos = shingleThickness * cosTheta;
|
|
1381
1584
|
const shinBotW = Math.max(0.01, width + 2 * reach.x);
|
|
@@ -1522,6 +1725,18 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1522
1725
|
const fasciaBrush = fasciaGeo ? toBrush(fasciaGeo) : null;
|
|
1523
1726
|
if (fasciaGeo && !fasciaBrush)
|
|
1524
1727
|
fasciaGeo.dispose();
|
|
1728
|
+
const eaveCutBrushes = eaveCutGeos.flatMap((geometry) => {
|
|
1729
|
+
const brush = toBrush(geometry);
|
|
1730
|
+
if (!brush)
|
|
1731
|
+
geometry.dispose();
|
|
1732
|
+
return brush ? [brush] : [];
|
|
1733
|
+
});
|
|
1734
|
+
const boxBrush = boxGeo ? toBrush(boxGeo) : null;
|
|
1735
|
+
if (boxGeo && !boxBrush)
|
|
1736
|
+
boxGeo.dispose();
|
|
1737
|
+
const framingBrush = framingGeo ? toBrush(framingGeo) : null;
|
|
1738
|
+
if (framingGeo && !framingBrush)
|
|
1739
|
+
framingGeo.dispose();
|
|
1525
1740
|
if (shinBotBrush) {
|
|
1526
1741
|
const wV = shinBotW;
|
|
1527
1742
|
const dV = shinBotD;
|
|
@@ -1538,6 +1753,31 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1538
1753
|
try {
|
|
1539
1754
|
let deckSlab = csgEvaluator.evaluate(deckTopBrush, deckBotBrush, SUBTRACTION);
|
|
1540
1755
|
prepareBrushForCSG(deckSlab);
|
|
1756
|
+
if (framingBrush) {
|
|
1757
|
+
const open = csgEvaluator.evaluate(deckSlab, framingBrush, SUBTRACTION);
|
|
1758
|
+
prepareBrushForCSG(open);
|
|
1759
|
+
deckSlab.geometry.dispose();
|
|
1760
|
+
framingBrush.geometry.dispose();
|
|
1761
|
+
deckSlab = open;
|
|
1762
|
+
}
|
|
1763
|
+
for (const cut of eaveCutBrushes) {
|
|
1764
|
+
const next = csgEvaluator.evaluate(deckSlab, cut, SUBTRACTION);
|
|
1765
|
+
prepareBrushForCSG(next);
|
|
1766
|
+
deckSlab.geometry.dispose();
|
|
1767
|
+
cut.geometry.dispose();
|
|
1768
|
+
deckSlab = next;
|
|
1769
|
+
}
|
|
1770
|
+
if (boxBrush && boxCut) {
|
|
1771
|
+
const box = csgEvaluator.evaluate(boxBrush, boxCut, SUBTRACTION);
|
|
1772
|
+
prepareBrushForCSG(box);
|
|
1773
|
+
const withBox = csgEvaluator.evaluate(deckSlab, box, ADDITION);
|
|
1774
|
+
prepareBrushForCSG(withBox);
|
|
1775
|
+
deckSlab.geometry.dispose();
|
|
1776
|
+
box.geometry.dispose();
|
|
1777
|
+
boxBrush.geometry.dispose();
|
|
1778
|
+
boxCut.geometry.dispose();
|
|
1779
|
+
deckSlab = withBox;
|
|
1780
|
+
}
|
|
1541
1781
|
if (fasciaBrush) {
|
|
1542
1782
|
const withFascia = csgEvaluator.evaluate(deckSlab, fasciaBrush, ADDITION);
|
|
1543
1783
|
prepareBrushForCSG(withFascia);
|
|
@@ -1588,6 +1828,11 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1588
1828
|
if (innerBrush)
|
|
1589
1829
|
innerBrush.geometry.dispose();
|
|
1590
1830
|
fasciaBrush?.geometry.dispose();
|
|
1831
|
+
framingBrush?.geometry.dispose();
|
|
1832
|
+
for (const cut of eaveCutBrushes)
|
|
1833
|
+
cut.geometry.dispose();
|
|
1834
|
+
boxBrush?.geometry.dispose();
|
|
1835
|
+
boxCut?.geometry.dispose();
|
|
1591
1836
|
rakeBoards?.dispose();
|
|
1592
1837
|
return null;
|
|
1593
1838
|
}
|
|
@@ -1663,7 +1908,12 @@ function buildIndependentRoofSegmentGeometry(node, nodes) {
|
|
|
1663
1908
|
ensureRenderableGeometryAttributes(geometry);
|
|
1664
1909
|
return geometry;
|
|
1665
1910
|
}
|
|
1666
|
-
|
|
1911
|
+
/**
|
|
1912
|
+
* `framing`: the frame is shown (`useViewer.framingShown`), so the deck opens its structure band
|
|
1913
|
+
* (`roofFramingDepth`). Only the roof system asks; a picture of the plan or a section takes the
|
|
1914
|
+
* closed deck.
|
|
1915
|
+
*/
|
|
1916
|
+
export function generateRoofSegmentGeometry(node, nodes, options = {}) {
|
|
1667
1917
|
const parentNode = node.parentId ? nodes?.[node.parentId] : undefined;
|
|
1668
1918
|
const parentRoof = parentNode?.type === 'roof' ? parentNode : undefined;
|
|
1669
1919
|
const parentRoofPosition = parentRoof?.position;
|
|
@@ -1684,7 +1934,10 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1684
1934
|
ensureRenderableGeometryAttributes(result);
|
|
1685
1935
|
return result;
|
|
1686
1936
|
}
|
|
1687
|
-
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, {
|
|
1937
|
+
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, {
|
|
1938
|
+
fascia: wearsFascia(node, parentRoof),
|
|
1939
|
+
framing: options.framing ? roofFramingDepth(node) : 0,
|
|
1940
|
+
}));
|
|
1688
1941
|
if (!brushes) {
|
|
1689
1942
|
// Fallback: simple box
|
|
1690
1943
|
return new THREE.BoxGeometry(node.width, node.wallHeight, node.depth);
|
|
@@ -1729,6 +1982,8 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1729
1982
|
}
|
|
1730
1983
|
remapRoofShellFaces(resultGeo, node);
|
|
1731
1984
|
resultGeo = addShedInsetEndPanels(resultGeo, [node], false);
|
|
1985
|
+
if (hollowWall)
|
|
1986
|
+
splitRoofWallBandFaces(resultGeo, [{ node, inverse: null }], true);
|
|
1732
1987
|
hollowWall?.geometry.dispose();
|
|
1733
1988
|
if (combined !== shinDeck)
|
|
1734
1989
|
shinDeck.geometry.dispose();
|
|
@@ -2730,6 +2985,123 @@ function splitShellByFacePlanes(geometry, planes, materialIndex) {
|
|
|
2730
2985
|
geometry.dispose();
|
|
2731
2986
|
return createGeometryFromRawAttributes(keptPositions, keptNormals, keptUvs, keptGroups);
|
|
2732
2987
|
}
|
|
2988
|
+
/** Off a wall shell's outer plane by more than this, a face is not the wall band. */
|
|
2989
|
+
const WALL_BAND_PLANE_TOLERANCE = 0.01;
|
|
2990
|
+
const _bandA = new THREE.Vector3();
|
|
2991
|
+
const _bandB = new THREE.Vector3();
|
|
2992
|
+
const _bandC = new THREE.Vector3();
|
|
2993
|
+
const _bandCentroid = new THREE.Vector3();
|
|
2994
|
+
const _bandNormal = new THREE.Vector3();
|
|
2995
|
+
const _bandLocalCentroid = new THREE.Vector3();
|
|
2996
|
+
const _bandLocalNormal = new THREE.Vector3();
|
|
2997
|
+
const _bandNormalMatrix = new THREE.Matrix3();
|
|
2998
|
+
/**
|
|
2999
|
+
* Which wall band a slot-0 triangle is, in segment-local space, or null when
|
|
3000
|
+
* it is not on the wall shell's outer face (soffits, deck and shingle edges).
|
|
3001
|
+
* The shell's outer faces stand half the wall thickness outside the footprint;
|
|
3002
|
+
* on the ±x ends, above the eave, they are the gables, and below it, like the
|
|
3003
|
+
* ±z sides, the heel band.
|
|
3004
|
+
*/
|
|
3005
|
+
function classifyWallBandFace(node, centroid, normal) {
|
|
3006
|
+
if (Math.abs(normal.y) > 0.2)
|
|
3007
|
+
return null;
|
|
3008
|
+
const half = Math.max(0, node.wallThickness) / 2;
|
|
3009
|
+
const outerX = node.width / 2 + half;
|
|
3010
|
+
const outerZ = node.depth / 2 + half;
|
|
3011
|
+
const tol = WALL_BAND_PLANE_TOLERANCE;
|
|
3012
|
+
if (Math.abs(normal.x) > 0.9) {
|
|
3013
|
+
if (Math.sign(normal.x) !== Math.sign(centroid.x))
|
|
3014
|
+
return null;
|
|
3015
|
+
if (Math.abs(Math.abs(centroid.x) - outerX) > tol || Math.abs(centroid.z) > outerZ + tol)
|
|
3016
|
+
return null;
|
|
3017
|
+
// the shell's eave line, as `getRoofSegmentBrushes` raises it (5 cm at the least)
|
|
3018
|
+
const { tanTheta } = getSegmentSlopeFrame(node);
|
|
3019
|
+
const eaveY = Math.max(0.05, node.wallHeight - half * tanTheta);
|
|
3020
|
+
if (centroid.y <= eaveY + 1e-5)
|
|
3021
|
+
return ROOF_HEEL_MATERIAL_INDEX;
|
|
3022
|
+
return normal.x < 0 ? ROOF_GABLE_LEFT_MATERIAL_INDEX : ROOF_GABLE_RIGHT_MATERIAL_INDEX;
|
|
3023
|
+
}
|
|
3024
|
+
if (Math.abs(normal.z) > 0.9) {
|
|
3025
|
+
if (Math.sign(normal.z) !== Math.sign(centroid.z))
|
|
3026
|
+
return null;
|
|
3027
|
+
if (Math.abs(Math.abs(centroid.z) - outerZ) > tol || Math.abs(centroid.x) > outerX + tol)
|
|
3028
|
+
return null;
|
|
3029
|
+
return ROOF_HEEL_MATERIAL_INDEX;
|
|
3030
|
+
}
|
|
3031
|
+
return null;
|
|
3032
|
+
}
|
|
3033
|
+
/**
|
|
3034
|
+
* Split the wall bands off slot 0 (see `ROOF_HEEL_MATERIAL_INDEX`): the heel
|
|
3035
|
+
* band always, each gable end when `gables` (a segment's own mesh, where they
|
|
3036
|
+
* paint apart; on the merged shell they stay the edge role's). Runs on the
|
|
3037
|
+
* finished shell, after every slot remap that folds indices back into 0–3.
|
|
3038
|
+
*/
|
|
3039
|
+
export function splitRoofWallBandFaces(geometry, segments, gables) {
|
|
3040
|
+
const position = geometry.getAttribute('position');
|
|
3041
|
+
if (!position || geometry.groups.length === 0 || segments.length === 0)
|
|
3042
|
+
return;
|
|
3043
|
+
const index = geometry.getIndex();
|
|
3044
|
+
const triangleCount = Math.floor((index ? index.count : position.count) / 3);
|
|
3045
|
+
if (triangleCount === 0)
|
|
3046
|
+
return;
|
|
3047
|
+
const vertexAt = (corner) => (index ? index.getX(corner) : corner);
|
|
3048
|
+
const triangleMaterials = new Array(triangleCount).fill(0);
|
|
3049
|
+
let changed = false;
|
|
3050
|
+
for (const group of geometry.groups) {
|
|
3051
|
+
const start = Math.floor(group.start / 3);
|
|
3052
|
+
const end = Math.min(triangleCount, Math.floor((group.start + group.count) / 3));
|
|
3053
|
+
for (let triangle = start; triangle < end; triangle++) {
|
|
3054
|
+
const materialIndex = group.materialIndex ?? 0;
|
|
3055
|
+
triangleMaterials[triangle] = materialIndex;
|
|
3056
|
+
if (materialIndex !== ROOF_EDGE_MATERIAL_INDEX)
|
|
3057
|
+
continue;
|
|
3058
|
+
_bandA.fromBufferAttribute(position, vertexAt(triangle * 3));
|
|
3059
|
+
_bandB.fromBufferAttribute(position, vertexAt(triangle * 3 + 1));
|
|
3060
|
+
_bandC.fromBufferAttribute(position, vertexAt(triangle * 3 + 2));
|
|
3061
|
+
_bandCentroid
|
|
3062
|
+
.copy(_bandA)
|
|
3063
|
+
.add(_bandB)
|
|
3064
|
+
.add(_bandC)
|
|
3065
|
+
.multiplyScalar(1 / 3);
|
|
3066
|
+
_bandNormal.subVectors(_bandB, _bandA).cross(_bandC.sub(_bandA));
|
|
3067
|
+
if (_bandNormal.lengthSq() < 1e-14)
|
|
3068
|
+
continue;
|
|
3069
|
+
_bandNormal.normalize();
|
|
3070
|
+
for (const segment of segments) {
|
|
3071
|
+
_bandLocalCentroid.copy(_bandCentroid);
|
|
3072
|
+
_bandLocalNormal.copy(_bandNormal);
|
|
3073
|
+
if (segment.inverse) {
|
|
3074
|
+
_bandLocalCentroid.applyMatrix4(segment.inverse);
|
|
3075
|
+
_bandLocalNormal.applyMatrix3(_bandNormalMatrix.setFromMatrix4(segment.inverse));
|
|
3076
|
+
_bandLocalNormal.normalize();
|
|
3077
|
+
}
|
|
3078
|
+
const band = classifyWallBandFace(segment.node, _bandLocalCentroid, _bandLocalNormal);
|
|
3079
|
+
if (band === null)
|
|
3080
|
+
continue;
|
|
3081
|
+
const resolved = band === ROOF_HEEL_MATERIAL_INDEX || gables ? band : ROOF_EDGE_MATERIAL_INDEX;
|
|
3082
|
+
if (resolved !== ROOF_EDGE_MATERIAL_INDEX) {
|
|
3083
|
+
triangleMaterials[triangle] = resolved;
|
|
3084
|
+
changed = true;
|
|
3085
|
+
}
|
|
3086
|
+
break;
|
|
3087
|
+
}
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
if (!changed)
|
|
3091
|
+
return;
|
|
3092
|
+
geometry.clearGroups();
|
|
3093
|
+
let currentMaterial = triangleMaterials[0] ?? 0;
|
|
3094
|
+
let groupStart = 0;
|
|
3095
|
+
for (let triangle = 1; triangle < triangleCount; triangle++) {
|
|
3096
|
+
const materialIndex = triangleMaterials[triangle] ?? 0;
|
|
3097
|
+
if (materialIndex === currentMaterial)
|
|
3098
|
+
continue;
|
|
3099
|
+
geometry.addGroup(groupStart * 3, (triangle - groupStart) * 3, currentMaterial);
|
|
3100
|
+
groupStart = triangle;
|
|
3101
|
+
currentMaterial = materialIndex;
|
|
3102
|
+
}
|
|
3103
|
+
geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
|
|
3104
|
+
}
|
|
2733
3105
|
export function remapRoofShellFaces(geometry, node) {
|
|
2734
3106
|
const position = geometry.getAttribute('position');
|
|
2735
3107
|
const index = geometry.getIndex();
|