@pascal-app/viewer 1.0.6 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -21
- package/dist/components/viewer/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-replace-instances.d.ts +6 -3
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
- package/dist/components/viewer/glb-replace-instances.js +5 -4
- package/dist/components/viewer/glb-scene-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 +5 -1
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +290 -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 +38 -10
- 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/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +2 -2
- 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/unsupported-gpu-fallback.d.ts.map +1 -1
- package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
- package/dist/components/viewer/use-daylight-rooms.d.ts +13 -0
- package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -0
- package/dist/components/viewer/use-daylight-rooms.js +278 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -1
- package/dist/index.d.ts +26 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -8
- package/dist/lib/asset-url.d.ts +0 -1
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +3 -3
- package/dist/lib/daylight/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 +54 -0
- package/dist/lib/daylight/room-map.d.ts +124 -0
- package/dist/lib/daylight/room-map.d.ts.map +1 -0
- package/dist/lib/daylight/room-map.js +630 -0
- package/dist/lib/daylight/room-scene.d.ts +56 -0
- package/dist/lib/daylight/room-scene.d.ts.map +1 -0
- package/dist/lib/daylight/room-scene.js +391 -0
- package/dist/lib/daylight/rooms.d.ts +292 -0
- package/dist/lib/daylight/rooms.d.ts.map +1 -0
- package/dist/lib/daylight/rooms.js +1268 -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/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +2 -1
- 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/measurement-label.d.ts +4 -0
- package/dist/lib/measurement-label.d.ts.map +1 -0
- package/dist/lib/measurement-label.js +54 -0
- package/dist/lib/measurement-presentation.d.ts +5 -0
- package/dist/lib/measurement-presentation.d.ts.map +1 -0
- package/dist/lib/measurement-presentation.js +8 -0
- package/dist/lib/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/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +3 -3
- 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 +13 -10
- 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/lib/viewer-config.d.ts +21 -0
- package/dist/lib/viewer-config.d.ts.map +1 -0
- package/dist/lib/viewer-config.js +21 -0
- package/dist/lib/viewer-preference-storage.d.ts +7 -0
- package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
- package/dist/lib/viewer-preference-storage.js +23 -0
- package/dist/store/use-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 +62 -59
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +106 -19
- 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/elevator/elevator-interaction-system.d.ts +16 -2
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +41 -8
- package/dist/systems/geometry/geometry-status.d.ts +8 -0
- package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-status.js +24 -0
- package/dist/systems/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 +582 -168
- 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 +11 -0
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +86 -25
- 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,16 @@
|
|
|
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 { setGeometryFailure, useGeometryStatus } from '../geometry/geometry-status';
|
|
13
|
+
import { ROOF_GABLE_LEFT_MATERIAL_INDEX, ROOF_GABLE_RIGHT_MATERIAL_INDEX, ROOF_HEEL_MATERIAL_INDEX, } from './roof-material-slots';
|
|
10
14
|
function csgGeometry(brush) {
|
|
11
15
|
return brush.geometry;
|
|
12
16
|
}
|
|
@@ -233,6 +237,16 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
233
237
|
// set from the render pass that received the override-publishing
|
|
234
238
|
// `markDirty` call. Mirrors WallSystem / DoorSystem.
|
|
235
239
|
useLiveNodeOverrides((s) => s.overrides);
|
|
240
|
+
// "Framing" opens the decks' structure band (`roofFramingDepth`): every segment, and so every
|
|
241
|
+
// roof shell, rebuilds when it flips.
|
|
242
|
+
useEffect(() => useViewer.subscribe((state, previous) => {
|
|
243
|
+
if (state.framingShown === previous.framingShown)
|
|
244
|
+
return;
|
|
245
|
+
const scene = useScene.getState();
|
|
246
|
+
for (const node of Object.values(scene.nodes))
|
|
247
|
+
if (node.type === 'roof-segment')
|
|
248
|
+
scene.markDirty(node.id);
|
|
249
|
+
}), []);
|
|
236
250
|
useFrame(() => {
|
|
237
251
|
// Clear stale pending updates when the scene is unloaded
|
|
238
252
|
if (rootNodeIds.length === 0) {
|
|
@@ -252,6 +266,7 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
252
266
|
pendingIndependentSegments.size === 0)
|
|
253
267
|
return;
|
|
254
268
|
const nodes = useScene.getState().nodes;
|
|
269
|
+
const framing = useViewer.getState().framingShown;
|
|
255
270
|
// --- Pass 1: Process dirty roof-segments (throttled) ---
|
|
256
271
|
let segmentsProcessed = 0;
|
|
257
272
|
new Set([...dirtyNodes, ...pendingIndependentSegments]).forEach((id) => {
|
|
@@ -302,7 +317,7 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
302
317
|
const revealOnly = mesh.parent?.name === 'segments-wrapper' &&
|
|
303
318
|
mesh.parent?.parent?.getObjectByName('merged-roof')?.visible === true;
|
|
304
319
|
if (isVisible && !revealOnly && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
|
|
305
|
-
updateRoofSegmentGeometry(effectiveSegment, mesh, nodes, geometryAdapter);
|
|
320
|
+
updateRoofSegmentGeometry(effectiveSegment, mesh, nodes, geometryAdapter, framing);
|
|
306
321
|
segmentsProcessed++;
|
|
307
322
|
}
|
|
308
323
|
else if (isVisible && !revealOnly) {
|
|
@@ -358,7 +373,13 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
358
373
|
// Built even while hidden: a roof assembling from its segments (construction
|
|
359
374
|
// reveal) hides its shell, and must show it whole the moment it is captured.
|
|
360
375
|
rememberMergedInputs(node, nodes);
|
|
361
|
-
|
|
376
|
+
try {
|
|
377
|
+
updateMergedRoofGeometry(node, group, nodes, framing);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
setGeometryFailure(node.id, 'build-failed');
|
|
381
|
+
console.error('Roof rebuild failed:', error);
|
|
382
|
+
}
|
|
362
383
|
roofsProcessed++;
|
|
363
384
|
pendingRoofUpdates.delete(id);
|
|
364
385
|
}
|
|
@@ -368,8 +389,20 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
|
368
389
|
// ============================================================================
|
|
369
390
|
// GEOMETRY GENERATION
|
|
370
391
|
// ============================================================================
|
|
371
|
-
function updateRoofSegmentGeometry(node, mesh, nodes, adapter) {
|
|
372
|
-
|
|
392
|
+
function updateRoofSegmentGeometry(node, mesh, nodes, adapter, framing = false) {
|
|
393
|
+
let newGeo;
|
|
394
|
+
try {
|
|
395
|
+
newGeo = generateRoofSegmentGeometry(node, nodes, { framing });
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
setGeometryFailure(node.id, 'build-failed');
|
|
399
|
+
console.error('Roof segment rebuild failed:', error);
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
if (useGeometryStatus.getState().failures.has(node.id)) {
|
|
403
|
+
newGeo.dispose();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
373
406
|
mesh.geometry.dispose();
|
|
374
407
|
mesh.geometry = newGeo;
|
|
375
408
|
computeGeometryBoundsTree(newGeo);
|
|
@@ -391,84 +424,95 @@ function subtractAccessoryCuts(brushes, segment, nodes) {
|
|
|
391
424
|
let workingShin = brushes.shinSlab;
|
|
392
425
|
let workingDeck = brushes.deckSlab;
|
|
393
426
|
let workingWall = brushes.wallBrush;
|
|
394
|
-
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
427
|
+
try {
|
|
428
|
+
for (const childElemId of segment.children ?? []) {
|
|
429
|
+
const storedChild = nodes[childElemId];
|
|
430
|
+
if (!storedChild)
|
|
431
|
+
continue;
|
|
432
|
+
const childElem = getEffectiveNode(storedChild);
|
|
433
|
+
const meta = typeof childElem.metadata === 'object' && childElem.metadata !== null
|
|
434
|
+
? childElem.metadata
|
|
435
|
+
: undefined;
|
|
436
|
+
if (meta?.isTransient)
|
|
437
|
+
continue;
|
|
438
|
+
const childDef = nodeRegistry.get(childElem.type);
|
|
439
|
+
const buildCut = childDef?.capabilities?.roofAccessory?.buildCut;
|
|
440
|
+
if (!buildCut)
|
|
441
|
+
continue;
|
|
442
|
+
const cutGeo = buildCut(childElem, segment);
|
|
443
|
+
if (!cutGeo)
|
|
444
|
+
continue;
|
|
445
|
+
// Wrap the kind-emitted geometry in a Brush. Kinds return raw
|
|
446
|
+
// shapes; the viewer welds (mandatory after rotations leave
|
|
447
|
+
// duplicated verts), attaches a single material group, and
|
|
448
|
+
// builds the bounds tree — keeping kind code free of
|
|
449
|
+
// three-bvh-csg / three-mesh-bvh imports.
|
|
450
|
+
const welded = mergeVertices(cutGeo, 1e-4);
|
|
451
|
+
cutGeo.dispose();
|
|
452
|
+
const idxCount = welded.getIndex()?.count ?? 0;
|
|
453
|
+
if (idxCount === 0) {
|
|
454
|
+
welded.dispose();
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
welded.clearGroups();
|
|
458
|
+
welded.addGroup(0, idxCount, 0);
|
|
459
|
+
welded.computeVertexNormals();
|
|
460
|
+
ensureRenderableGeometryAttributes(welded);
|
|
461
|
+
computeGeometryBoundsTree(welded);
|
|
462
|
+
const cut = new Brush(welded, dummyMats[0]);
|
|
463
|
+
cut.updateMatrixWorld();
|
|
464
|
+
const cutScope = childDef?.capabilities?.roofAccessory?.cutScope ?? 'all';
|
|
465
|
+
try {
|
|
466
|
+
if (cutScope !== 'wall') {
|
|
467
|
+
const nextShin = csgEvaluator.evaluate(workingShin, cut, SUBTRACTION);
|
|
468
|
+
workingShin.geometry.dispose();
|
|
469
|
+
prepareBrushForCSG(nextShin);
|
|
470
|
+
workingShin = nextShin;
|
|
471
|
+
const nextDeck = csgEvaluator.evaluate(workingDeck, cut, SUBTRACTION);
|
|
472
|
+
workingDeck.geometry.dispose();
|
|
473
|
+
prepareBrushForCSG(nextDeck);
|
|
474
|
+
workingDeck = nextDeck;
|
|
475
|
+
}
|
|
476
|
+
const nextWall = csgEvaluator.evaluate(workingWall, cut, SUBTRACTION);
|
|
477
|
+
workingWall.geometry.dispose();
|
|
478
|
+
prepareBrushForCSG(nextWall);
|
|
479
|
+
workingWall = nextWall;
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
cut.geometry.dispose();
|
|
441
483
|
}
|
|
442
|
-
const nextWall = csgEvaluator.evaluate(workingWall, cut, SUBTRACTION);
|
|
443
|
-
workingWall.geometry.dispose();
|
|
444
|
-
prepareBrushForCSG(nextWall);
|
|
445
|
-
workingWall = nextWall;
|
|
446
|
-
}
|
|
447
|
-
catch (e) {
|
|
448
|
-
console.error(`[${childElem.type}] cut CSG failed:`, e);
|
|
449
|
-
}
|
|
450
|
-
finally {
|
|
451
|
-
cut.geometry.dispose();
|
|
452
484
|
}
|
|
453
485
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
486
|
+
finally {
|
|
487
|
+
brushes.shinSlab = workingShin;
|
|
488
|
+
brushes.deckSlab = workingDeck;
|
|
489
|
+
brushes.wallBrush = workingWall;
|
|
490
|
+
}
|
|
457
491
|
}
|
|
458
|
-
function getMergedRoofSegmentBrushes(roofNode, segment, nodes) {
|
|
492
|
+
function getMergedRoofSegmentBrushes(roofNode, segment, nodes, framing) {
|
|
459
493
|
const segmentId = segment.id;
|
|
460
|
-
const
|
|
494
|
+
const framingDepth = framing ? roofFramingDepth(segment) : 0;
|
|
495
|
+
const cacheKey = getMergedRoofSegmentCacheKey(roofNode, segment, nodes, framingDepth);
|
|
461
496
|
const cached = mergedRoofSegmentGeometryCache.get(segmentId);
|
|
462
497
|
if (cached?.key === cacheKey) {
|
|
463
498
|
return cloneCachedMergedRoofSegmentBrushes(cached);
|
|
464
499
|
}
|
|
465
|
-
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment, {
|
|
500
|
+
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment, {
|
|
501
|
+
fascia: wearsFascia(segment, roofNode),
|
|
502
|
+
framing: framingDepth,
|
|
503
|
+
}));
|
|
466
504
|
if (!brushes) {
|
|
467
505
|
disposeCachedMergedRoofSegmentGeometrySet(cached);
|
|
468
506
|
mergedRoofSegmentGeometryCache.delete(segmentId);
|
|
469
507
|
return null;
|
|
470
508
|
}
|
|
471
|
-
|
|
509
|
+
try {
|
|
510
|
+
subtractAccessoryCuts(brushes, segment, nodes);
|
|
511
|
+
}
|
|
512
|
+
catch (error) {
|
|
513
|
+
disposeRoofSegmentBrushSet(brushes);
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
472
516
|
_matrix.compose(_position.set(segment.position[0], segment.position[1], segment.position[2]), _quaternion.setFromAxisAngle(_yAxis, segment.rotation), _scale);
|
|
473
517
|
const applyTransform = (brush) => {
|
|
474
518
|
csgGeometry(brush).applyMatrix4(_matrix);
|
|
@@ -505,7 +549,7 @@ function getMergedRoofSegmentBrushes(roofNode, segment, nodes) {
|
|
|
505
549
|
disposeRoofSegmentBrushSet(brushes);
|
|
506
550
|
return cloned;
|
|
507
551
|
}
|
|
508
|
-
function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
552
|
+
function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
|
|
509
553
|
const mergedMesh = group.getObjectByName('merged-roof');
|
|
510
554
|
if (!mergedMesh)
|
|
511
555
|
return;
|
|
@@ -529,6 +573,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
529
573
|
// Not BoxGeometry: its 6 groups against the merged mesh's 4-material array
|
|
530
574
|
// crash GLTFExporter (materials[4] → undefined) when the roof bakes.
|
|
531
575
|
mergedMesh.geometry = createDegenerateRoofPlaceholder();
|
|
576
|
+
setGeometryFailure(roofNode.id, null);
|
|
532
577
|
return;
|
|
533
578
|
}
|
|
534
579
|
let totalShinSlab = null;
|
|
@@ -548,7 +593,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
548
593
|
continue;
|
|
549
594
|
}
|
|
550
595
|
csgChildren.push(child);
|
|
551
|
-
const brushes = getMergedRoofSegmentBrushes(roofNode, child, nodes);
|
|
596
|
+
const brushes = getMergedRoofSegmentBrushes(roofNode, child, nodes, framing);
|
|
552
597
|
if (!brushes)
|
|
553
598
|
continue;
|
|
554
599
|
if (brushes.rakeBoards) {
|
|
@@ -625,6 +670,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
625
670
|
console.warn('[RoofSystem] Skipping merged roof geometry with invalid attributes', roofNode.id);
|
|
626
671
|
warnedMergedRoofNaNIds.add(roofNode.id);
|
|
627
672
|
}
|
|
673
|
+
setGeometryFailure(roofNode.id, 'invalid-geometry');
|
|
628
674
|
resultGeo.dispose();
|
|
629
675
|
if (combined !== shinDeck)
|
|
630
676
|
shinDeck.geometry.dispose();
|
|
@@ -648,6 +694,14 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
648
694
|
g.materialIndex = mapRoofGroupMaterialIndex(g.materialIndex, resultMaterials, matToIndex);
|
|
649
695
|
}
|
|
650
696
|
let finalGeo = addShedInsetEndPanels(resultGeo, csgChildren, true);
|
|
697
|
+
splitRoofWallBandFaces(finalGeo, csgChildren
|
|
698
|
+
.filter((child) => shouldIncludeRoofSegmentWallShell(child, roofNode))
|
|
699
|
+
.map((child) => ({
|
|
700
|
+
node: child,
|
|
701
|
+
inverse: new THREE.Matrix4()
|
|
702
|
+
.compose(_position.set(child.position[0], child.position[1], child.position[2]), _quaternion.setFromAxisAngle(_yAxis, child.rotation), _scale)
|
|
703
|
+
.invert(),
|
|
704
|
+
})), false);
|
|
651
705
|
const appendedGeometries = [...rakeBoardGeometries, ...directSegmentGeometries];
|
|
652
706
|
if (appendedGeometries.length > 0) {
|
|
653
707
|
const merged = mergeGeometriesPreservingGroups([finalGeo, ...appendedGeometries]);
|
|
@@ -665,10 +719,12 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
665
719
|
ensureRenderableGeometryAttributes(finalGeo);
|
|
666
720
|
mergedMesh.geometry.dispose();
|
|
667
721
|
mergedMesh.geometry = finalGeo;
|
|
722
|
+
setGeometryFailure(roofNode.id, null);
|
|
668
723
|
if (combined !== shinDeck)
|
|
669
724
|
shinDeck.geometry.dispose();
|
|
670
725
|
}
|
|
671
726
|
catch (e) {
|
|
727
|
+
setGeometryFailure(roofNode.id, 'build-failed');
|
|
672
728
|
console.error('Merged roof CSG failed:', e);
|
|
673
729
|
}
|
|
674
730
|
totalShinSlab.geometry.dispose();
|
|
@@ -686,6 +742,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
686
742
|
ensureRenderableGeometryAttributes(finalGeo);
|
|
687
743
|
mergedMesh.geometry.dispose();
|
|
688
744
|
mergedMesh.geometry = finalGeo;
|
|
745
|
+
setGeometryFailure(roofNode.id, null);
|
|
689
746
|
}
|
|
690
747
|
for (const geometry of directSegmentGeometries) {
|
|
691
748
|
if (geometry !== finalGeo)
|
|
@@ -792,7 +849,7 @@ function getMergedRoofAccessoryCachePayload(segment, nodes) {
|
|
|
792
849
|
}
|
|
793
850
|
return payload;
|
|
794
851
|
}
|
|
795
|
-
function getMergedRoofSegmentCacheKey(roofNode, segment, nodes) {
|
|
852
|
+
function getMergedRoofSegmentCacheKey(roofNode, segment, nodes, framing) {
|
|
796
853
|
return JSON.stringify({
|
|
797
854
|
roofPosition: roofNode.position ?? [0, 0, 0],
|
|
798
855
|
roofRotation: roofNode.rotation ?? 0,
|
|
@@ -800,6 +857,7 @@ function getMergedRoofSegmentCacheKey(roofNode, segment, nodes) {
|
|
|
800
857
|
composite: (roofNode.children?.length ?? 0) > 1,
|
|
801
858
|
segment,
|
|
802
859
|
accessories: getMergedRoofAccessoryCachePayload(segment, nodes),
|
|
860
|
+
framing,
|
|
803
861
|
});
|
|
804
862
|
}
|
|
805
863
|
export function mapRoofGroupMaterialIndex(groupMaterialIndex, csgMaterials, matToIndex) {
|
|
@@ -1181,103 +1239,215 @@ export function clipGeometryBySegmentTrim(geometry, segment) {
|
|
|
1181
1239
|
* Generate complete hollow-shell geometry for a roof segment.
|
|
1182
1240
|
* Ports the prototype's CSG approach using three-bvh-csg.
|
|
1183
1241
|
*/
|
|
1184
|
-
/** A 1x8 fascia / rake board: actual 3/4 × 7-1/4 in. */
|
|
1185
|
-
const FASCIA_BOARD = { thickness: 0.019, height: 0.184 };
|
|
1186
1242
|
/** A board's back sits this far inside the deck edge, so the union is one solid with no coplanar faces. */
|
|
1187
1243
|
const FASCIA_EMBED = 0.002;
|
|
1244
|
+
/**
|
|
1245
|
+
* Boards that meet leave this between them: drawn as one solid, overlapping or touching boards would
|
|
1246
|
+
* leave it no inside to tell, and the union with the deck tears.
|
|
1247
|
+
*/
|
|
1248
|
+
const BOARD_GAP = 0.0001;
|
|
1188
1249
|
/**
|
|
1189
1250
|
* Slot 2 takes the trim finish like the rake faces, and the shell remap
|
|
1190
1251
|
* leaves it alone: the remap would send a plumb eave board to the wall band
|
|
1191
|
-
* and its top edge to the shingles.
|
|
1252
|
+
* and its top edge to the shingles. A boxed eave's soffit and returns take it too.
|
|
1192
1253
|
*/
|
|
1193
1254
|
const FASCIA_MATERIAL_INDEX = 2;
|
|
1255
|
+
/** The forms whose eaves core's `roofEaveProfile` cuts and boards: the ones Bones frames with tails. */
|
|
1256
|
+
const EAVE_ROOF_TYPES = new Set(['gable', 'hip', 'shed', 'flat']);
|
|
1257
|
+
const EDGES_Z = [
|
|
1258
|
+
{ axis: 'z', sign: 1 },
|
|
1259
|
+
{ axis: 'z', sign: -1 },
|
|
1260
|
+
];
|
|
1261
|
+
const EDGES_X = [
|
|
1262
|
+
{ axis: 'x', sign: 1 },
|
|
1263
|
+
{ axis: 'x', sign: -1 },
|
|
1264
|
+
];
|
|
1265
|
+
/** The point of an edge's section (`s` out past its plate line, `y` over `plateY`) at `u` along it. */
|
|
1266
|
+
function edgePoint(node, edge, plateY, [s, y], u) {
|
|
1267
|
+
const out = edge.sign * ((edge.axis === 'z' ? node.depth : node.width) / 2 + s);
|
|
1268
|
+
return edge.axis === 'z'
|
|
1269
|
+
? new THREE.Vector3(u, plateY + y, out)
|
|
1270
|
+
: new THREE.Vector3(out, plateY + y, u);
|
|
1271
|
+
}
|
|
1272
|
+
/** A convex solid's faces, each turned to face away from its centre. */
|
|
1273
|
+
function outwardFaces(faces) {
|
|
1274
|
+
const points = faces.flat();
|
|
1275
|
+
const centre = points
|
|
1276
|
+
.reduce((sum, v) => sum.add(v), new THREE.Vector3())
|
|
1277
|
+
.multiplyScalar(1 / Math.max(1, points.length));
|
|
1278
|
+
return faces.map((face) => {
|
|
1279
|
+
const normal = new THREE.Vector3()
|
|
1280
|
+
.subVectors(face[1], face[0])
|
|
1281
|
+
.cross(new THREE.Vector3().subVectors(face[2], face[0]));
|
|
1282
|
+
const faceCentre = face
|
|
1283
|
+
.reduce((sum, v) => sum.add(v), new THREE.Vector3())
|
|
1284
|
+
.multiplyScalar(1 / face.length);
|
|
1285
|
+
return normal.dot(faceCentre.sub(centre)) < 0 ? [...face].reverse() : face;
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1194
1288
|
/**
|
|
1195
|
-
*
|
|
1196
|
-
*
|
|
1197
|
-
* or shed rake, from the eave line the deck itself is built on. Being part
|
|
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.
|
|
1289
|
+
* A prism over a convex `section` of an edge, run along it from `from` to `to`: each end where its
|
|
1290
|
+
* function puts it at each section point (affine in it), so an end can butt a sloped board's face.
|
|
1201
1291
|
*/
|
|
1202
|
-
function
|
|
1292
|
+
function edgePrism(node, edge, plateY, section, from, to) {
|
|
1293
|
+
const a = section.map((point) => edgePoint(node, edge, plateY, point, from(point)));
|
|
1294
|
+
const b = section.map((point) => edgePoint(node, edge, plateY, point, to(point)));
|
|
1295
|
+
const faces = [a, b];
|
|
1296
|
+
for (let i = 0; i < section.length; i++) {
|
|
1297
|
+
const j = (i + 1) % section.length;
|
|
1298
|
+
faces.push([a[i], a[j], b[j], b[i]]);
|
|
1299
|
+
}
|
|
1300
|
+
return outwardFaces(faces);
|
|
1301
|
+
}
|
|
1302
|
+
/** Where a line through two section points is at height `y`, out from the plate line. */
|
|
1303
|
+
function sAtHeight([s0, y0], [s1, y1], y) {
|
|
1304
|
+
return Math.abs(y1 - y0) < 1e-9 ? s0 : s0 + ((y - y0) * (s1 - s0)) / (y1 - y0);
|
|
1305
|
+
}
|
|
1306
|
+
/** A fascia's section with its back pulled `FASCIA_EMBED` into the deck, along its thickness. */
|
|
1307
|
+
function embeddedFascia(profile) {
|
|
1308
|
+
const [backTop, frontTop, frontBottom, backBottom] = profile.fascia.corners;
|
|
1309
|
+
const [nx, ny] = profile.fascia.square ? [profile.cos, -profile.sin] : [1, 0];
|
|
1310
|
+
const back = ([s, y]) => [s - nx * FASCIA_EMBED, y - ny * FASCIA_EMBED];
|
|
1311
|
+
return [back(backTop), frontTop, frontBottom, back(backBottom)];
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* A rake board's section in the eave's frame (`profile`): plumb, its top on the sheathing's top, as
|
|
1315
|
+
* deep as the profile's rake board, from the fascia's back at the eave (square to the slope with a
|
|
1316
|
+
* square cut; held level at the fascia's bottom with a plumb-and-level one) up to `inner`, plumb.
|
|
1317
|
+
*/
|
|
1318
|
+
function rakeSection(profile, inner) {
|
|
1319
|
+
const { top, cos, sin, tan } = profile;
|
|
1320
|
+
const depth = profile.rake.height;
|
|
1321
|
+
const bottom = (s) => top(s) - depth;
|
|
1322
|
+
// short of the fascia's back, which is pulled into the deck
|
|
1323
|
+
const pull = FASCIA_EMBED + BOARD_GAP;
|
|
1324
|
+
const [s0, y0] = profile.end[0];
|
|
1325
|
+
const corner = profile.fascia.square
|
|
1326
|
+
? [s0 - pull * cos, y0 + pull * sin]
|
|
1327
|
+
: [s0 - pull, top(s0 - pull)];
|
|
1328
|
+
const section = [corner];
|
|
1329
|
+
if (profile.cut === 'square') {
|
|
1330
|
+
const along = depth * cos;
|
|
1331
|
+
section.push([corner[0] - along * sin, corner[1] - along * cos]);
|
|
1332
|
+
}
|
|
1333
|
+
else if (profile.cut === 'plumb-level') {
|
|
1334
|
+
const level = profile.fascia.corners[3][1];
|
|
1335
|
+
const s = tan > 1e-9 ? (top(0) - depth - level) / tan : corner[0];
|
|
1336
|
+
if (bottom(corner[0]) < level && s < corner[0])
|
|
1337
|
+
section.push([corner[0], level], [s, level]);
|
|
1338
|
+
else
|
|
1339
|
+
section.push([corner[0], bottom(corner[0])]);
|
|
1340
|
+
}
|
|
1341
|
+
else
|
|
1342
|
+
section.push([corner[0], bottom(corner[0])]);
|
|
1343
|
+
section.push([inner, bottom(inner)], [inner, top(inner)]);
|
|
1344
|
+
return section;
|
|
1345
|
+
}
|
|
1346
|
+
/**
|
|
1347
|
+
* The fascia and rake boards a segment wears, in its own frame, as its eaves are cut
|
|
1348
|
+
* (`roofSegmentEaveProfile`): a 1x on every eave covering the tails' end, its top flush with the
|
|
1349
|
+
* sheathing's, and a plumb one up every gable or shed rake over the barge rafter, its top on the
|
|
1350
|
+
* sheathing's. The eave boards run through to the rake boards' faces, which butt their backs; a hip's
|
|
1351
|
+
* or flat roof's end boards butt the side boards' backs. Being part of the segment, they follow its
|
|
1352
|
+
* pitch, size, overhang, layers and type, and the trims, cuts and sibling roofs clip them like the
|
|
1353
|
+
* deck. Gable, hip, shed and flat; null for the other forms.
|
|
1354
|
+
*/
|
|
1355
|
+
function buildFasciaBoards(node, reach, eave, high) {
|
|
1203
1356
|
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;
|
|
1357
|
+
if (!EAVE_ROOF_TYPES.has(roofType))
|
|
1358
|
+
return null;
|
|
1359
|
+
const plateY = node.wallHeight;
|
|
1360
|
+
const thickness = eave.fascia.thickness;
|
|
1361
|
+
const board = embeddedFascia(eave);
|
|
1215
1362
|
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]);
|
|
1363
|
+
const rakeFace = width / 2 + reach.x + thickness;
|
|
1364
|
+
if (roofType === 'gable' || roofType === 'shed') {
|
|
1365
|
+
const sides = roofType === 'gable' ? EDGES_Z : [EDGES_Z[0]];
|
|
1366
|
+
for (const edge of sides)
|
|
1367
|
+
faces.push(...edgePrism(node, edge, plateY, board, () => -rakeFace, () => rakeFace));
|
|
1368
|
+
const highY = plateY + getSegmentSlopeFrame(node).activeRh;
|
|
1369
|
+
if (roofType === 'shed' && high && (node.fasciaHighEdge ?? true))
|
|
1370
|
+
faces.push(...edgePrism(node, EDGES_Z[1], highY, embeddedFascia(high), () => -rakeFace, () => rakeFace));
|
|
1371
|
+
// the rakes: a gable's up each slope to the ridge, a shed's from its low eave to its high edge
|
|
1372
|
+
const inner = roofType === 'gable' ? -depth / 2 + BOARD_GAP : -depth - reach.z + FASCIA_EMBED + BOARD_GAP;
|
|
1373
|
+
const section = rakeSection(eave, inner);
|
|
1374
|
+
const back = width / 2 + reach.x - FASCIA_EMBED;
|
|
1375
|
+
for (const edge of sides)
|
|
1376
|
+
for (const sx of [-1, 1]) {
|
|
1377
|
+
const [x0, x1] = sx === 1 ? [back, rakeFace] : [-rakeFace, -back];
|
|
1378
|
+
faces.push(...edgePrism(node, edge, plateY, section, () => x0, () => x1));
|
|
1379
|
+
}
|
|
1267
1380
|
}
|
|
1268
|
-
else
|
|
1269
|
-
// the
|
|
1270
|
-
const
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1381
|
+
else {
|
|
1382
|
+
// hip and flat: the boards along ±z run through to the end boards' faces, which butt their backs
|
|
1383
|
+
const [, frontTop, frontBottom] = eave.fascia.corners;
|
|
1384
|
+
const front = (y) => sAtHeight(frontTop, frontBottom, y);
|
|
1385
|
+
const back = (y) => sAtHeight(board[0], board[3], y) - BOARD_GAP;
|
|
1386
|
+
for (const edge of EDGES_Z)
|
|
1387
|
+
faces.push(...edgePrism(node, edge, plateY, board, ([, y]) => -(width / 2 + front(y)), ([, y]) => width / 2 + front(y)));
|
|
1388
|
+
for (const edge of EDGES_X)
|
|
1389
|
+
faces.push(...edgePrism(node, edge, plateY, board, ([, y]) => -(depth / 2 + back(y)), ([, y]) => depth / 2 + back(y)));
|
|
1276
1390
|
}
|
|
1277
|
-
else
|
|
1278
|
-
return null;
|
|
1279
1391
|
return createGeometryFromFaces(faces, FASCIA_MATERIAL_INDEX);
|
|
1280
1392
|
}
|
|
1393
|
+
/**
|
|
1394
|
+
* What a segment's eave cut takes off its deck slab out past the walls (`roofSegmentEaveProfile`):
|
|
1395
|
+
* the wedge outside a square cut; under a truss's top chords the heel's depth, the tails riding
|
|
1396
|
+
* that far over the deck's underside (`seal` under them, as the deck is); under a boxed eave's
|
|
1397
|
+
* soffit everything, the box standing on it. Each a convex solid, a hip's bounded by its hip lines
|
|
1398
|
+
* (a hair past them, so the two at a corner overlap). None for a plumb cut on rafters.
|
|
1399
|
+
*/
|
|
1400
|
+
function buildEaveCuts(node, reach, eave, seal) {
|
|
1401
|
+
const { roofType, width, depth } = node;
|
|
1402
|
+
if (roofType === 'flat')
|
|
1403
|
+
return [];
|
|
1404
|
+
const plateY = node.wallHeight;
|
|
1405
|
+
const sections = [];
|
|
1406
|
+
if (eave.cut === 'square') {
|
|
1407
|
+
const [corner] = eave.end;
|
|
1408
|
+
const down = (point, d) => [
|
|
1409
|
+
point[0] - d * eave.sin,
|
|
1410
|
+
point[1] - d * eave.cos,
|
|
1411
|
+
];
|
|
1412
|
+
const out = (point, d) => [
|
|
1413
|
+
point[0] + d * eave.cos,
|
|
1414
|
+
point[1] - d * eave.sin,
|
|
1415
|
+
];
|
|
1416
|
+
const reachDown = node.deckThickness + 0.1;
|
|
1417
|
+
const above = down(corner, -0.2);
|
|
1418
|
+
const below = down(corner, reachDown);
|
|
1419
|
+
sections.push([above, out(above, 0.5), out(below, 0.5), below]);
|
|
1420
|
+
}
|
|
1421
|
+
const lift = eave.under(0);
|
|
1422
|
+
const far = eave.reach + 0.3;
|
|
1423
|
+
if (lift > 1e-6) {
|
|
1424
|
+
const at = (s, d) => [s, eave.under(s) - seal - d];
|
|
1425
|
+
sections.push([at(eave.wallOut, 0), at(far, 0), at(far, 1), at(eave.wallOut, 1)]);
|
|
1426
|
+
}
|
|
1427
|
+
if (eave.soffit) {
|
|
1428
|
+
const level = eave.soffit.y - eave.soffit.thickness;
|
|
1429
|
+
sections.push([
|
|
1430
|
+
[eave.wallOut, level],
|
|
1431
|
+
[far, level],
|
|
1432
|
+
[far, level - 1],
|
|
1433
|
+
[eave.wallOut, level - 1],
|
|
1434
|
+
]);
|
|
1435
|
+
}
|
|
1436
|
+
if (sections.length === 0)
|
|
1437
|
+
return [];
|
|
1438
|
+
const solids = [];
|
|
1439
|
+
const edges = roofType === 'hip' ? [...EDGES_Z, ...EDGES_X] : roofType === 'gable' ? EDGES_Z : [EDGES_Z[0]];
|
|
1440
|
+
for (const edge of edges)
|
|
1441
|
+
for (const section of sections) {
|
|
1442
|
+
// a hip's eave runs out to its hip lines; a gable's or shed's past its rakes
|
|
1443
|
+
const half = (edge.axis === 'z' ? width : depth) / 2;
|
|
1444
|
+
const end = roofType === 'hip'
|
|
1445
|
+
? ([s]) => half + Math.max(0, s) + 0.01
|
|
1446
|
+
: () => half + reach.x + 1;
|
|
1447
|
+
solids.push(edgePrism(node, edge, plateY, section, (point) => -end(point), end));
|
|
1448
|
+
}
|
|
1449
|
+
return solids;
|
|
1450
|
+
}
|
|
1281
1451
|
/**
|
|
1282
1452
|
* Whether a drawn segment wears fascia boards. A lean-to carries its own
|
|
1283
1453
|
* trim, and a legacy composite shed roof stacks overlapping shed pieces as
|
|
@@ -1293,9 +1463,44 @@ function wearsFascia(node, parentRoof) {
|
|
|
1293
1463
|
return false;
|
|
1294
1464
|
return !parentRoof || (parentRoof.children?.length ?? 0) <= 1;
|
|
1295
1465
|
}
|
|
1466
|
+
/** What a framed roof's deck keeps over its open structure band at least: a 7/16 in sheet. */
|
|
1467
|
+
const ROOF_DECK_KEPT = 0.0111;
|
|
1468
|
+
/**
|
|
1469
|
+
* Where the deck stays whole while its structure band is open (over the walls, in the overhangs),
|
|
1470
|
+
* its underside sits this far under the rafter plane, so the rafter tails stay inside it rather
|
|
1471
|
+
* than sharing its face.
|
|
1472
|
+
*/
|
|
1473
|
+
const ROOF_DECK_SEAL = 0.005;
|
|
1474
|
+
/**
|
|
1475
|
+
* The forms whose open band is the deck's own planes raised: their planes stay put as the
|
|
1476
|
+
* footprint shrinks to the walls' inner faces. A gambrel's or a mansard's break and a dutch
|
|
1477
|
+
* gablet move with it, so those decks stay closed.
|
|
1478
|
+
*/
|
|
1479
|
+
const FRAMING_OPEN_ROOF_TYPES = new Set(['gable', 'hip', 'shed', 'flat']);
|
|
1480
|
+
/**
|
|
1481
|
+
* How deep a roof plane's deck opens under its sheathing while the viewer shows the frame
|
|
1482
|
+
* (`useViewer.framingShown`), square to the slope: the structure its layers name
|
|
1483
|
+
* (`roofStructureDepth`: its rafters, or its trusses' top chords on their heel), else Bones' default
|
|
1484
|
+
* 2x6. A deck no deeper keeps its top 7/16 in; a plane finished underneath (a cathedral ceiling)
|
|
1485
|
+
* stays closed, the frame hidden behind its finish, and so does a form `FRAMING_OPEN_ROOF_TYPES`
|
|
1486
|
+
* leaves out. 0 when nothing opens.
|
|
1487
|
+
*/
|
|
1488
|
+
export function roofFramingDepth(node) {
|
|
1489
|
+
if (!FRAMING_OPEN_ROOF_TYPES.has(node.roofType) || node.buildUp?.finish)
|
|
1490
|
+
return 0;
|
|
1491
|
+
const structure = node.buildUp
|
|
1492
|
+
? roofStructureDepth(node.buildUp.structure) || LUMBER_DEPTH['2x6']
|
|
1493
|
+
: LUMBER_DEPTH['2x6'];
|
|
1494
|
+
const depth = Math.min(structure, node.deckThickness - ROOF_DECK_KEPT);
|
|
1495
|
+
return depth > 0.005 ? depth : 0;
|
|
1496
|
+
}
|
|
1296
1497
|
/**
|
|
1297
1498
|
* `fascia`: build the segment's fascia boards into its deck. Only the roof
|
|
1298
1499
|
* renderer asks; a dormer, a chimney or a sibling's clip takes the bare shell.
|
|
1500
|
+
* `framing`: the structure band to open (`roofFramingDepth`), the roof
|
|
1501
|
+
* renderer's while the frame is shown. Inside the walls the deck keeps only the
|
|
1502
|
+
* band over it, so the rafters show from the attic; over the walls and in the
|
|
1503
|
+
* overhangs it stays whole, closing the band at the walls' inner faces.
|
|
1299
1504
|
*/
|
|
1300
1505
|
export function getRoofSegmentBrushes(node, options = {}) {
|
|
1301
1506
|
const { roofType, width, depth, wallHeight, wallThickness, deckThickness, shingleThickness } = node;
|
|
@@ -1316,7 +1521,7 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1316
1521
|
const baseI = Math.min(width, depth) * node.dutchHipWidthRatio;
|
|
1317
1522
|
const plateSeated = wallHeight <= 0;
|
|
1318
1523
|
// `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) => {
|
|
1524
|
+
const getVol = (wExt, vOffset, baseY, matIndex, isVoid, materialRule, onPlate = false, xExt = wExt, keepBase = false) => {
|
|
1320
1525
|
const wV = Math.max(0.01, width + 2 * xExt);
|
|
1321
1526
|
const dV = Math.max(0.01, depth + 2 * wExt);
|
|
1322
1527
|
const autoDrop = wExt * tanTheta;
|
|
@@ -1325,7 +1530,11 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1325
1530
|
// One floor for all volumes keeps each cutter level with the shell it carves.
|
|
1326
1531
|
// A plate-seated roof (wallHeight 0) is the exception for its sloped deck
|
|
1327
1532
|
// volumes: their eave hangs below the plate, and raising it lifted the
|
|
1328
|
-
// whole roof off its walls, so their base sinks instead
|
|
1533
|
+
// whole roof off its walls, so their base sinks instead: under the plate's
|
|
1534
|
+
// plane at their edge too, so a deck's top volume keeps the deck's whole
|
|
1535
|
+
// depth out to the eave (a base 5 cm under its top cut the eave to a 5 cm
|
|
1536
|
+
// edge with the tails hanging under it). `keepBase`: a volume whose base
|
|
1537
|
+
// is its own (a boxed eave's soffit).
|
|
1329
1538
|
const eaveY = wallHeight - autoDrop + vOffset;
|
|
1330
1539
|
const sinkBase = plateSeated && !onPlate && autoDrop !== 0;
|
|
1331
1540
|
const whV = sinkBase ? eaveY : Math.max(0.05, eaveY);
|
|
@@ -1335,7 +1544,9 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1335
1544
|
if (roofType === 'shed')
|
|
1336
1545
|
rhV = activeRh + 2 * autoDrop;
|
|
1337
1546
|
}
|
|
1338
|
-
const safeBaseY = sinkBase
|
|
1547
|
+
const safeBaseY = sinkBase && !keepBase
|
|
1548
|
+
? Math.min(baseY, Math.min(whV, wallHeight - autoDrop - ROOF_DECK_SEAL) - 0.05)
|
|
1549
|
+
: baseY;
|
|
1339
1550
|
let structuralI = baseI;
|
|
1340
1551
|
if (isVoid) {
|
|
1341
1552
|
structuralI += deckThickness;
|
|
@@ -1369,13 +1580,36 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1369
1580
|
? (normal) => (normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX)
|
|
1370
1581
|
: undefined;
|
|
1371
1582
|
const deckTopGeo = getVol(deckExt, verticalRt, 0, 1, false, shedRoofSideMaterialRule, false, reach.x);
|
|
1372
|
-
const
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
const
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1583
|
+
const framing = Math.max(0, options.framing ?? 0);
|
|
1584
|
+
const deckBotGeo = getVol(deckExt, framing > 0 ? -ROOF_DECK_SEAL : 0, -5, 0, true, undefined, false, reach.x);
|
|
1585
|
+
// the structure band inside the walls' inner faces, open while the frame is shown
|
|
1586
|
+
const framingRt = activeRh > 0 ? framing / cosTheta : framing;
|
|
1587
|
+
const half = -wallThickness / 2;
|
|
1588
|
+
const framingGeo = framing > 0 ? getVol(half, framingRt, -5, 0, true, undefined, false, half) : null;
|
|
1589
|
+
// The eaves, cut and boarded by the plane's rule, the one Bones cuts its tails by (core's
|
|
1590
|
+
// `roofSegmentEaveProfile`). Only the roof renderer asks (`options.fascia` given); a dormer, a
|
|
1591
|
+
// chimney or a sibling's clip takes the bare shell.
|
|
1592
|
+
const eave = options.fascia !== undefined && EAVE_ROOF_TYPES.has(roofType)
|
|
1593
|
+
? roofSegmentEaveProfile(node)
|
|
1594
|
+
: null;
|
|
1595
|
+
const highEave = eave && roofType === 'shed' ? roofSegmentEaveProfile(node, 'high') : null;
|
|
1596
|
+
const fasciaGeo = eave && options.fascia ? buildFasciaBoards(node, reach, eave, highEave) : null;
|
|
1597
|
+
const eaveCutGeos = eave
|
|
1598
|
+
? buildEaveCuts(node, reach, eave, framing > 0 ? ROOF_DECK_SEAL : 0).map((faces) => createGeometryFromFaces(faces, 1))
|
|
1599
|
+
: [];
|
|
1600
|
+
// A boxed eave (plumb and level): solid from the soffit, level under the tails, up into the deck,
|
|
1601
|
+
// from the walls (a hair into them) out to the fascia's back; its ends are the returns under the rakes.
|
|
1602
|
+
let boxGeo = null;
|
|
1603
|
+
let boxCut = null;
|
|
1604
|
+
if (eave?.soffit) {
|
|
1605
|
+
const soffitBottom = wallHeight + eave.soffit.y - eave.soffit.thickness;
|
|
1606
|
+
const ext = reach.z - FASCIA_EMBED;
|
|
1607
|
+
boxGeo = getVol(ext, verticalRt - 0.003, soffitBottom, FASCIA_MATERIAL_INDEX, false, undefined, false, roofType === 'hip' ? ext : reach.x - FASCIA_EMBED, true);
|
|
1608
|
+
const inside = eave.wallOut - FASCIA_EMBED;
|
|
1609
|
+
const far = width / 2 + depth / 2 + reach.x + reach.z + 2;
|
|
1610
|
+
const top = wallHeight + activeRh + deckThickness + shingleThickness + 2;
|
|
1611
|
+
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);
|
|
1612
|
+
}
|
|
1379
1613
|
const stSin = shingleThickness * sinTheta;
|
|
1380
1614
|
const stCos = shingleThickness * cosTheta;
|
|
1381
1615
|
const shinBotW = Math.max(0.01, width + 2 * reach.x);
|
|
@@ -1522,6 +1756,18 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1522
1756
|
const fasciaBrush = fasciaGeo ? toBrush(fasciaGeo) : null;
|
|
1523
1757
|
if (fasciaGeo && !fasciaBrush)
|
|
1524
1758
|
fasciaGeo.dispose();
|
|
1759
|
+
const eaveCutBrushes = eaveCutGeos.flatMap((geometry) => {
|
|
1760
|
+
const brush = toBrush(geometry);
|
|
1761
|
+
if (!brush)
|
|
1762
|
+
geometry.dispose();
|
|
1763
|
+
return brush ? [brush] : [];
|
|
1764
|
+
});
|
|
1765
|
+
const boxBrush = boxGeo ? toBrush(boxGeo) : null;
|
|
1766
|
+
if (boxGeo && !boxBrush)
|
|
1767
|
+
boxGeo.dispose();
|
|
1768
|
+
const framingBrush = framingGeo ? toBrush(framingGeo) : null;
|
|
1769
|
+
if (framingGeo && !framingBrush)
|
|
1770
|
+
framingGeo.dispose();
|
|
1525
1771
|
if (shinBotBrush) {
|
|
1526
1772
|
const wV = shinBotW;
|
|
1527
1773
|
const dV = shinBotD;
|
|
@@ -1538,6 +1784,31 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1538
1784
|
try {
|
|
1539
1785
|
let deckSlab = csgEvaluator.evaluate(deckTopBrush, deckBotBrush, SUBTRACTION);
|
|
1540
1786
|
prepareBrushForCSG(deckSlab);
|
|
1787
|
+
if (framingBrush) {
|
|
1788
|
+
const open = csgEvaluator.evaluate(deckSlab, framingBrush, SUBTRACTION);
|
|
1789
|
+
prepareBrushForCSG(open);
|
|
1790
|
+
deckSlab.geometry.dispose();
|
|
1791
|
+
framingBrush.geometry.dispose();
|
|
1792
|
+
deckSlab = open;
|
|
1793
|
+
}
|
|
1794
|
+
for (const cut of eaveCutBrushes) {
|
|
1795
|
+
const next = csgEvaluator.evaluate(deckSlab, cut, SUBTRACTION);
|
|
1796
|
+
prepareBrushForCSG(next);
|
|
1797
|
+
deckSlab.geometry.dispose();
|
|
1798
|
+
cut.geometry.dispose();
|
|
1799
|
+
deckSlab = next;
|
|
1800
|
+
}
|
|
1801
|
+
if (boxBrush && boxCut) {
|
|
1802
|
+
const box = csgEvaluator.evaluate(boxBrush, boxCut, SUBTRACTION);
|
|
1803
|
+
prepareBrushForCSG(box);
|
|
1804
|
+
const withBox = csgEvaluator.evaluate(deckSlab, box, ADDITION);
|
|
1805
|
+
prepareBrushForCSG(withBox);
|
|
1806
|
+
deckSlab.geometry.dispose();
|
|
1807
|
+
box.geometry.dispose();
|
|
1808
|
+
boxBrush.geometry.dispose();
|
|
1809
|
+
boxCut.geometry.dispose();
|
|
1810
|
+
deckSlab = withBox;
|
|
1811
|
+
}
|
|
1541
1812
|
if (fasciaBrush) {
|
|
1542
1813
|
const withFascia = csgEvaluator.evaluate(deckSlab, fasciaBrush, ADDITION);
|
|
1543
1814
|
prepareBrushForCSG(withFascia);
|
|
@@ -1588,6 +1859,11 @@ export function getRoofSegmentBrushes(node, options = {}) {
|
|
|
1588
1859
|
if (innerBrush)
|
|
1589
1860
|
innerBrush.geometry.dispose();
|
|
1590
1861
|
fasciaBrush?.geometry.dispose();
|
|
1862
|
+
framingBrush?.geometry.dispose();
|
|
1863
|
+
for (const cut of eaveCutBrushes)
|
|
1864
|
+
cut.geometry.dispose();
|
|
1865
|
+
boxBrush?.geometry.dispose();
|
|
1866
|
+
boxCut?.geometry.dispose();
|
|
1591
1867
|
rakeBoards?.dispose();
|
|
1592
1868
|
return null;
|
|
1593
1869
|
}
|
|
@@ -1663,7 +1939,12 @@ function buildIndependentRoofSegmentGeometry(node, nodes) {
|
|
|
1663
1939
|
ensureRenderableGeometryAttributes(geometry);
|
|
1664
1940
|
return geometry;
|
|
1665
1941
|
}
|
|
1666
|
-
|
|
1942
|
+
/**
|
|
1943
|
+
* `framing`: the frame is shown (`useViewer.framingShown`), so the deck opens its structure band
|
|
1944
|
+
* (`roofFramingDepth`). Only the roof system asks; a picture of the plan or a section takes the
|
|
1945
|
+
* closed deck.
|
|
1946
|
+
*/
|
|
1947
|
+
export function generateRoofSegmentGeometry(node, nodes, options = {}) {
|
|
1667
1948
|
const parentNode = node.parentId ? nodes?.[node.parentId] : undefined;
|
|
1668
1949
|
const parentRoof = parentNode?.type === 'roof' ? parentNode : undefined;
|
|
1669
1950
|
const parentRoofPosition = parentRoof?.position;
|
|
@@ -1682,15 +1963,25 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1682
1963
|
}
|
|
1683
1964
|
result.computeVertexNormals();
|
|
1684
1965
|
ensureRenderableGeometryAttributes(result);
|
|
1966
|
+
setGeometryFailure(node.id, null);
|
|
1685
1967
|
return result;
|
|
1686
1968
|
}
|
|
1687
|
-
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, {
|
|
1969
|
+
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, {
|
|
1970
|
+
fascia: wearsFascia(node, parentRoof),
|
|
1971
|
+
framing: options.framing ? roofFramingDepth(node) : 0,
|
|
1972
|
+
}));
|
|
1688
1973
|
if (!brushes) {
|
|
1689
|
-
|
|
1974
|
+
setGeometryFailure(node.id, 'build-failed');
|
|
1690
1975
|
return new THREE.BoxGeometry(node.width, node.wallHeight, node.depth);
|
|
1691
1976
|
}
|
|
1692
1977
|
if (nodes) {
|
|
1693
|
-
|
|
1978
|
+
try {
|
|
1979
|
+
subtractAccessoryCuts(brushes, node, nodes);
|
|
1980
|
+
}
|
|
1981
|
+
catch (error) {
|
|
1982
|
+
disposeRoofSegmentBrushSet(brushes);
|
|
1983
|
+
throw error;
|
|
1984
|
+
}
|
|
1694
1985
|
}
|
|
1695
1986
|
const { deckSlab, shinSlab, wallBrush, innerBrush, rakeBoards } = brushes;
|
|
1696
1987
|
let resultGeo = new THREE.BufferGeometry();
|
|
@@ -1715,8 +2006,11 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1715
2006
|
resultGeo = csgGeometry(combined);
|
|
1716
2007
|
if (geometryHasInvalidAttributes(resultGeo)) {
|
|
1717
2008
|
resultGeo.dispose();
|
|
2009
|
+
setGeometryFailure(node.id, 'invalid-geometry');
|
|
1718
2010
|
resultGeo = csgGeometry(wallBrush).clone();
|
|
1719
2011
|
}
|
|
2012
|
+
else
|
|
2013
|
+
setGeometryFailure(node.id, null);
|
|
1720
2014
|
const resultMaterials = csgMaterials(combined);
|
|
1721
2015
|
const matToIndex = new Map([
|
|
1722
2016
|
[dummyMats[0], 0],
|
|
@@ -1729,11 +2023,14 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1729
2023
|
}
|
|
1730
2024
|
remapRoofShellFaces(resultGeo, node);
|
|
1731
2025
|
resultGeo = addShedInsetEndPanels(resultGeo, [node], false);
|
|
2026
|
+
if (hollowWall)
|
|
2027
|
+
splitRoofWallBandFaces(resultGeo, [{ node, inverse: null }], true);
|
|
1732
2028
|
hollowWall?.geometry.dispose();
|
|
1733
2029
|
if (combined !== shinDeck)
|
|
1734
2030
|
shinDeck.geometry.dispose();
|
|
1735
2031
|
}
|
|
1736
2032
|
catch (e) {
|
|
2033
|
+
setGeometryFailure(node.id, 'build-failed');
|
|
1737
2034
|
console.error('Roof CSG failed:', e);
|
|
1738
2035
|
resultGeo = csgGeometry(wallBrush).clone();
|
|
1739
2036
|
}
|
|
@@ -2730,6 +3027,123 @@ function splitShellByFacePlanes(geometry, planes, materialIndex) {
|
|
|
2730
3027
|
geometry.dispose();
|
|
2731
3028
|
return createGeometryFromRawAttributes(keptPositions, keptNormals, keptUvs, keptGroups);
|
|
2732
3029
|
}
|
|
3030
|
+
/** Off a wall shell's outer plane by more than this, a face is not the wall band. */
|
|
3031
|
+
const WALL_BAND_PLANE_TOLERANCE = 0.01;
|
|
3032
|
+
const _bandA = new THREE.Vector3();
|
|
3033
|
+
const _bandB = new THREE.Vector3();
|
|
3034
|
+
const _bandC = new THREE.Vector3();
|
|
3035
|
+
const _bandCentroid = new THREE.Vector3();
|
|
3036
|
+
const _bandNormal = new THREE.Vector3();
|
|
3037
|
+
const _bandLocalCentroid = new THREE.Vector3();
|
|
3038
|
+
const _bandLocalNormal = new THREE.Vector3();
|
|
3039
|
+
const _bandNormalMatrix = new THREE.Matrix3();
|
|
3040
|
+
/**
|
|
3041
|
+
* Which wall band a slot-0 triangle is, in segment-local space, or null when
|
|
3042
|
+
* it is not on the wall shell's outer face (soffits, deck and shingle edges).
|
|
3043
|
+
* The shell's outer faces stand half the wall thickness outside the footprint;
|
|
3044
|
+
* on the ±x ends, above the eave, they are the gables, and below it, like the
|
|
3045
|
+
* ±z sides, the heel band.
|
|
3046
|
+
*/
|
|
3047
|
+
function classifyWallBandFace(node, centroid, normal) {
|
|
3048
|
+
if (Math.abs(normal.y) > 0.2)
|
|
3049
|
+
return null;
|
|
3050
|
+
const half = Math.max(0, node.wallThickness) / 2;
|
|
3051
|
+
const outerX = node.width / 2 + half;
|
|
3052
|
+
const outerZ = node.depth / 2 + half;
|
|
3053
|
+
const tol = WALL_BAND_PLANE_TOLERANCE;
|
|
3054
|
+
if (Math.abs(normal.x) > 0.9) {
|
|
3055
|
+
if (Math.sign(normal.x) !== Math.sign(centroid.x))
|
|
3056
|
+
return null;
|
|
3057
|
+
if (Math.abs(Math.abs(centroid.x) - outerX) > tol || Math.abs(centroid.z) > outerZ + tol)
|
|
3058
|
+
return null;
|
|
3059
|
+
// the shell's eave line, as `getRoofSegmentBrushes` raises it (5 cm at the least)
|
|
3060
|
+
const { tanTheta } = getSegmentSlopeFrame(node);
|
|
3061
|
+
const eaveY = Math.max(0.05, node.wallHeight - half * tanTheta);
|
|
3062
|
+
if (centroid.y <= eaveY + 1e-5)
|
|
3063
|
+
return ROOF_HEEL_MATERIAL_INDEX;
|
|
3064
|
+
return normal.x < 0 ? ROOF_GABLE_LEFT_MATERIAL_INDEX : ROOF_GABLE_RIGHT_MATERIAL_INDEX;
|
|
3065
|
+
}
|
|
3066
|
+
if (Math.abs(normal.z) > 0.9) {
|
|
3067
|
+
if (Math.sign(normal.z) !== Math.sign(centroid.z))
|
|
3068
|
+
return null;
|
|
3069
|
+
if (Math.abs(Math.abs(centroid.z) - outerZ) > tol || Math.abs(centroid.x) > outerX + tol)
|
|
3070
|
+
return null;
|
|
3071
|
+
return ROOF_HEEL_MATERIAL_INDEX;
|
|
3072
|
+
}
|
|
3073
|
+
return null;
|
|
3074
|
+
}
|
|
3075
|
+
/**
|
|
3076
|
+
* Split the wall bands off slot 0 (see `ROOF_HEEL_MATERIAL_INDEX`): the heel
|
|
3077
|
+
* band always, each gable end when `gables` (a segment's own mesh, where they
|
|
3078
|
+
* paint apart; on the merged shell they stay the edge role's). Runs on the
|
|
3079
|
+
* finished shell, after every slot remap that folds indices back into 0–3.
|
|
3080
|
+
*/
|
|
3081
|
+
export function splitRoofWallBandFaces(geometry, segments, gables) {
|
|
3082
|
+
const position = geometry.getAttribute('position');
|
|
3083
|
+
if (!position || geometry.groups.length === 0 || segments.length === 0)
|
|
3084
|
+
return;
|
|
3085
|
+
const index = geometry.getIndex();
|
|
3086
|
+
const triangleCount = Math.floor((index ? index.count : position.count) / 3);
|
|
3087
|
+
if (triangleCount === 0)
|
|
3088
|
+
return;
|
|
3089
|
+
const vertexAt = (corner) => (index ? index.getX(corner) : corner);
|
|
3090
|
+
const triangleMaterials = new Array(triangleCount).fill(0);
|
|
3091
|
+
let changed = false;
|
|
3092
|
+
for (const group of geometry.groups) {
|
|
3093
|
+
const start = Math.floor(group.start / 3);
|
|
3094
|
+
const end = Math.min(triangleCount, Math.floor((group.start + group.count) / 3));
|
|
3095
|
+
for (let triangle = start; triangle < end; triangle++) {
|
|
3096
|
+
const materialIndex = group.materialIndex ?? 0;
|
|
3097
|
+
triangleMaterials[triangle] = materialIndex;
|
|
3098
|
+
if (materialIndex !== ROOF_EDGE_MATERIAL_INDEX)
|
|
3099
|
+
continue;
|
|
3100
|
+
_bandA.fromBufferAttribute(position, vertexAt(triangle * 3));
|
|
3101
|
+
_bandB.fromBufferAttribute(position, vertexAt(triangle * 3 + 1));
|
|
3102
|
+
_bandC.fromBufferAttribute(position, vertexAt(triangle * 3 + 2));
|
|
3103
|
+
_bandCentroid
|
|
3104
|
+
.copy(_bandA)
|
|
3105
|
+
.add(_bandB)
|
|
3106
|
+
.add(_bandC)
|
|
3107
|
+
.multiplyScalar(1 / 3);
|
|
3108
|
+
_bandNormal.subVectors(_bandB, _bandA).cross(_bandC.sub(_bandA));
|
|
3109
|
+
if (_bandNormal.lengthSq() < 1e-14)
|
|
3110
|
+
continue;
|
|
3111
|
+
_bandNormal.normalize();
|
|
3112
|
+
for (const segment of segments) {
|
|
3113
|
+
_bandLocalCentroid.copy(_bandCentroid);
|
|
3114
|
+
_bandLocalNormal.copy(_bandNormal);
|
|
3115
|
+
if (segment.inverse) {
|
|
3116
|
+
_bandLocalCentroid.applyMatrix4(segment.inverse);
|
|
3117
|
+
_bandLocalNormal.applyMatrix3(_bandNormalMatrix.setFromMatrix4(segment.inverse));
|
|
3118
|
+
_bandLocalNormal.normalize();
|
|
3119
|
+
}
|
|
3120
|
+
const band = classifyWallBandFace(segment.node, _bandLocalCentroid, _bandLocalNormal);
|
|
3121
|
+
if (band === null)
|
|
3122
|
+
continue;
|
|
3123
|
+
const resolved = band === ROOF_HEEL_MATERIAL_INDEX || gables ? band : ROOF_EDGE_MATERIAL_INDEX;
|
|
3124
|
+
if (resolved !== ROOF_EDGE_MATERIAL_INDEX) {
|
|
3125
|
+
triangleMaterials[triangle] = resolved;
|
|
3126
|
+
changed = true;
|
|
3127
|
+
}
|
|
3128
|
+
break;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
if (!changed)
|
|
3133
|
+
return;
|
|
3134
|
+
geometry.clearGroups();
|
|
3135
|
+
let currentMaterial = triangleMaterials[0] ?? 0;
|
|
3136
|
+
let groupStart = 0;
|
|
3137
|
+
for (let triangle = 1; triangle < triangleCount; triangle++) {
|
|
3138
|
+
const materialIndex = triangleMaterials[triangle] ?? 0;
|
|
3139
|
+
if (materialIndex === currentMaterial)
|
|
3140
|
+
continue;
|
|
3141
|
+
geometry.addGroup(groupStart * 3, (triangle - groupStart) * 3, currentMaterial);
|
|
3142
|
+
groupStart = triangle;
|
|
3143
|
+
currentMaterial = materialIndex;
|
|
3144
|
+
}
|
|
3145
|
+
geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
|
|
3146
|
+
}
|
|
2733
3147
|
export function remapRoofShellFaces(geometry, node) {
|
|
2734
3148
|
const position = geometry.getAttribute('position');
|
|
2735
3149
|
const index = geometry.getIndex();
|