@pascal-app/viewer 1.0.3 → 1.0.5
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/capture/capture-runtime.js +1 -1
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.d.ts +1 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +12 -4
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.d.ts +1 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +22 -3
- package/dist/components/viewer/glb-interactive.d.ts +25 -1
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +253 -86
- package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
- package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
- package/dist/components/viewer/glb-mechanisms.js +19 -0
- package/dist/components/viewer/glb-reference-nodes.js +1 -1
- package/dist/components/viewer/glb-replace-instances.js +1 -1
- package/dist/components/viewer/glb-scene.d.ts +45 -27
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +421 -508
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +13 -2
- package/dist/components/viewer/index.d.ts +24 -1
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +111 -37
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +113 -22
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +6 -0
- package/dist/components/viewer/perf-panel.d.ts.map +1 -1
- package/dist/components/viewer/perf-panel.js +1 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +104 -36
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +17 -3
- package/dist/components/viewer/render-error.d.ts +18 -0
- package/dist/components/viewer/render-error.d.ts.map +1 -0
- package/dist/components/viewer/render-error.js +29 -0
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +1 -1
- package/dist/components/viewer/scene-environment.d.ts.map +1 -1
- package/dist/components/viewer/scene-environment.js +31 -11
- package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
- package/dist/components/viewer/scene-ground-replacement.js +15 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +25 -7
- package/dist/components/viewer/underground-ambiance.d.ts +9 -0
- package/dist/components/viewer/underground-ambiance.d.ts.map +1 -0
- package/dist/components/viewer/underground-ambiance.js +105 -0
- package/dist/components/viewer/viewer-camera.d.ts +4 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -2
- package/dist/components/viewer/viewer-presentations.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -0
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/index.d.ts +36 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -19
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +7 -1
- package/dist/lib/backdrop.d.ts +17 -0
- package/dist/lib/backdrop.d.ts.map +1 -1
- package/dist/lib/backdrop.js +18 -1
- package/dist/lib/construction-dust.d.ts +57 -0
- package/dist/lib/construction-dust.d.ts.map +1 -0
- package/dist/lib/construction-dust.js +187 -0
- package/dist/lib/csg-utils.js +1 -1
- package/dist/lib/display-state.d.ts +25 -0
- package/dist/lib/display-state.d.ts.map +1 -0
- package/dist/lib/display-state.js +1 -0
- package/dist/lib/foundation-materials.d.ts +26 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -0
- package/dist/lib/foundation-materials.js +65 -0
- package/dist/lib/gltf-unlit.d.ts +10 -0
- package/dist/lib/gltf-unlit.d.ts.map +1 -0
- package/dist/lib/gltf-unlit.js +23 -0
- package/dist/lib/ground-window.d.ts +33 -0
- package/dist/lib/ground-window.d.ts.map +1 -0
- package/dist/lib/ground-window.js +114 -0
- package/dist/lib/hero-pose.d.ts +13 -3
- package/dist/lib/hero-pose.d.ts.map +1 -1
- package/dist/lib/hero-pose.js +72 -5
- package/dist/lib/ink-edges.d.ts +6 -3
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +17 -7
- package/dist/lib/key-light-override.d.ts +11 -0
- package/dist/lib/key-light-override.d.ts.map +1 -0
- package/dist/lib/key-light-override.js +14 -0
- package/dist/lib/ktx2-loader.d.ts +7 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +35 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layers.d.ts +11 -7
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +16 -6
- package/dist/lib/live-frame-hold.d.ts +14 -0
- package/dist/lib/live-frame-hold.d.ts.map +1 -0
- package/dist/lib/live-frame-hold.js +28 -0
- package/dist/lib/materials.d.ts +19 -7
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +65 -9
- package/dist/lib/merged-outline-node.d.ts +5 -0
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +80 -2
- package/dist/lib/node-top-surface-height.d.ts +6 -0
- package/dist/lib/node-top-surface-height.d.ts.map +1 -0
- package/dist/lib/node-top-surface-height.js +74 -0
- package/dist/lib/perf-probe-state.d.ts +39 -0
- package/dist/lib/perf-probe-state.d.ts.map +1 -0
- package/dist/lib/perf-probe-state.js +17 -0
- package/dist/lib/pointer-events.d.ts +26 -2
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +99 -12
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- package/dist/lib/procedural-emission.d.ts +7 -0
- package/dist/lib/procedural-emission.d.ts.map +1 -0
- package/dist/lib/procedural-emission.js +54 -0
- package/dist/lib/reveal-candidates.d.ts +80 -0
- package/dist/lib/reveal-candidates.d.ts.map +1 -0
- package/dist/lib/reveal-candidates.js +524 -0
- package/dist/lib/reveal-ghost.d.ts +9 -0
- package/dist/lib/reveal-ghost.d.ts.map +1 -0
- package/dist/lib/reveal-ghost.js +62 -0
- package/dist/lib/reveal-pending.d.ts +12 -0
- package/dist/lib/reveal-pending.d.ts.map +1 -0
- package/dist/lib/reveal-pending.js +12 -0
- package/dist/lib/reveal-pose.d.ts +70 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -0
- package/dist/lib/reveal-pose.js +248 -0
- package/dist/lib/reveal-schedule.d.ts +208 -0
- package/dist/lib/reveal-schedule.d.ts.map +1 -0
- package/dist/lib/reveal-schedule.js +410 -0
- package/dist/lib/reveal-texture-scale.d.ts +5 -0
- package/dist/lib/reveal-texture-scale.d.ts.map +1 -0
- package/dist/lib/reveal-texture-scale.js +113 -0
- package/dist/lib/reveal-windows.d.ts +23 -0
- package/dist/lib/reveal-windows.d.ts.map +1 -0
- package/dist/lib/reveal-windows.js +31 -0
- package/dist/lib/reveal-wipe.d.ts +71 -0
- package/dist/lib/reveal-wipe.d.ts.map +1 -0
- package/dist/lib/reveal-wipe.js +105 -0
- package/dist/lib/scene-pass-mrt.d.ts +12 -0
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -0
- package/dist/lib/scene-pass-mrt.js +23 -0
- package/dist/lib/scene-support-height.d.ts +4 -0
- package/dist/lib/scene-support-height.d.ts.map +1 -0
- package/dist/lib/scene-support-height.js +34 -0
- package/dist/lib/scene-themes.d.ts +30 -8
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +36 -0
- package/dist/lib/scene-visibility.d.ts +17 -1
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +53 -3
- package/dist/lib/scripted-opening.d.ts +11 -0
- package/dist/lib/scripted-opening.d.ts.map +1 -0
- package/dist/lib/scripted-opening.js +20 -0
- package/dist/lib/site-surfaces.d.ts +32 -0
- package/dist/lib/site-surfaces.d.ts.map +1 -0
- package/dist/lib/site-surfaces.js +70 -0
- package/dist/lib/snapshot-pipeline.d.ts +34 -3
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +247 -117
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
- package/dist/lib/terrain-perimeter-fill.js +22 -14
- package/dist/lib/tsl-compat.d.ts +2 -2
- package/dist/lib/tsl-compat.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.d.ts +168 -0
- package/dist/lib/underground-ambiance.d.ts.map +1 -0
- package/dist/lib/underground-ambiance.js +334 -0
- package/dist/lib/zone-geometry.d.ts +11 -0
- package/dist/lib/zone-geometry.d.ts.map +1 -0
- package/dist/lib/zone-geometry.js +46 -0
- package/dist/store/use-item-light-pool.d.ts +14 -10
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +52 -26
- package/dist/store/use-viewer.d.ts +194 -26
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +164 -21
- package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +88 -6
- package/dist/systems/construction-reveal/construction-dust.d.ts +16 -0
- package/dist/systems/construction-reveal/construction-dust.d.ts.map +1 -0
- package/dist/systems/construction-reveal/construction-dust.js +92 -0
- package/dist/systems/construction-reveal/construction-reveal.d.ts +276 -0
- package/dist/systems/construction-reveal/construction-reveal.d.ts.map +1 -0
- package/dist/systems/construction-reveal/construction-reveal.js +2383 -0
- package/dist/systems/construction-reveal/dust-material.d.ts +10 -0
- package/dist/systems/construction-reveal/dust-material.d.ts.map +1 -0
- package/dist/systems/construction-reveal/dust-material.js +17 -0
- package/dist/systems/construction-reveal/ghost-dress.d.ts +10 -0
- package/dist/systems/construction-reveal/ghost-dress.d.ts.map +1 -0
- package/dist/systems/construction-reveal/ghost-dress.js +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +55 -17
- package/dist/systems/elevator/elevator-interaction-system.d.ts +7 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +18 -1
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +33 -5
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +71 -11
- package/dist/systems/interactive/control-widget.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +52 -17
- package/dist/systems/interactive/procedural-controls.d.ts +13 -0
- package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
- package/dist/systems/interactive/procedural-controls.js +17 -0
- package/dist/systems/interactive/scripted-clips.d.ts +19 -0
- package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
- package/dist/systems/interactive/scripted-clips.js +134 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +9 -3
- package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
- package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
- package/dist/systems/item/object-cut-invalidation.js +68 -0
- package/dist/systems/item-light/item-light-system.d.ts +1 -1
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +48 -53
- package/dist/systems/level/level-hold.d.ts +4 -0
- package/dist/systems/level/level-hold.d.ts.map +1 -0
- package/dist/systems/level/level-hold.js +14 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -6
- package/dist/systems/level/level-utils.d.ts +19 -6
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +68 -9
- package/dist/systems/level/shadow-only-levels.d.ts +9 -0
- package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
- package/dist/systems/level/shadow-only-levels.js +7 -0
- package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts +103 -0
- package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts.map +1 -0
- package/dist/systems/roof/__fixtures__/roof-plane-cases.js +317 -0
- package/dist/systems/roof/roof-materials.d.ts +16 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +64 -31
- package/dist/systems/roof/roof-system.d.ts +21 -4
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +374 -49
- package/dist/systems/slab/slab-system.d.ts +1 -1
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +49 -19
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +133 -354
- package/dist/systems/wall/wall-cutout-cache.d.ts +39 -3
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout-cache.js +141 -19
- package/dist/systems/wall/wall-cutout.d.ts +6 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +42 -28
- package/dist/systems/wall/wall-finish-data.d.ts +22 -0
- package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
- package/dist/systems/wall/wall-finish-data.js +17 -0
- package/dist/systems/wall/wall-material-variant.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.d.ts +9 -4
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +135 -153
- package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.d.ts +33 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +621 -159
- package/dist/systems/window/corner-frame.d.ts +10 -0
- package/dist/systems/window/corner-frame.d.ts.map +1 -0
- package/dist/systems/window/corner-frame.js +17 -0
- package/dist/systems/window/window-system.d.ts +1 -1
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +71 -27
- package/package.json +10 -10
- package/dist/systems/fence/fence-system.d.ts +0 -22
- package/dist/systems/fence/fence-system.d.ts.map +0 -1
- package/dist/systems/fence/fence-system.js +0 -357
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getConicalRoofCoverage, getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getEffectiveNode, getRoofModuleFaces, getRoofPlanBounds, getRoofSegmentSurfaceY, getRoofShapeInsets, getRoofShapeRatios, getSegmentSlopeFrame, hasSegmentMaterialOverride, isBandedShedSegment, nodeRegistry, normalizeRoofSegmentTrim, pointInPolygon2D, ROOF_SHAPE_DEFAULTS, roofOverlapEntryOwns, roofPlanBoundsOverlap, sceneRegistry, unionPolygons, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
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';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
import { mergeGeometries, mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
@@ -6,6 +6,7 @@ import { ADDITION, Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
|
6
6
|
import { computeBoundsTree } from 'three-mesh-bvh';
|
|
7
7
|
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
8
8
|
import { ensureRenderableGeometryAttributes, subtractCsgBrush } from '../../lib/csg-utils';
|
|
9
|
+
import { isRevealAssembling } from '../construction-reveal/construction-reveal';
|
|
9
10
|
function csgGeometry(brush) {
|
|
10
11
|
return brush.geometry;
|
|
11
12
|
}
|
|
@@ -20,7 +21,7 @@ csgEvaluator.attributes = ['position', 'normal', 'uv', 'uv2'];
|
|
|
20
21
|
function computeGeometryBoundsTree(geometry) {
|
|
21
22
|
;
|
|
22
23
|
geometry.computeBoundsTree = computeBoundsTree;
|
|
23
|
-
geometry.computeBoundsTree({
|
|
24
|
+
geometry.computeBoundsTree({ targetLeafSize: 10 });
|
|
24
25
|
}
|
|
25
26
|
function prepareBrushForCSG(brush) {
|
|
26
27
|
ensureRenderableGeometryAttributes(brush.geometry);
|
|
@@ -108,11 +109,60 @@ function createDegenerateRoofPlaceholder() {
|
|
|
108
109
|
}
|
|
109
110
|
// Pending merged-roof updates carried across frames (for throttling)
|
|
110
111
|
const pendingRoofUpdates = new Set();
|
|
112
|
+
const pendingIndependentSegments = new Set();
|
|
111
113
|
const previousRoofPlanBounds = new Map();
|
|
112
114
|
const warnedMergedRoofNaNIds = new Set();
|
|
113
115
|
const MAX_ROOFS_PER_FRAME = 1;
|
|
114
116
|
const MAX_SEGMENTS_PER_FRAME = 3;
|
|
115
|
-
|
|
117
|
+
/** What each roof's shell was last merged from: its node and its segments, as the store held them. */
|
|
118
|
+
const mergedFrom = new Map();
|
|
119
|
+
function rememberMergedInputs(roof, nodes) {
|
|
120
|
+
const inputs = new Map();
|
|
121
|
+
for (const id of [roof.id, ...(roof.children ?? [])]) {
|
|
122
|
+
const node = nodes[id];
|
|
123
|
+
if (node)
|
|
124
|
+
inputs.set(id, node);
|
|
125
|
+
}
|
|
126
|
+
mergedFrom.set(roof.id, inputs);
|
|
127
|
+
}
|
|
128
|
+
/** Whether nothing a roof's shell is made of has changed since it was merged. */
|
|
129
|
+
function mergedFromStillHolds(roofId, nodes) {
|
|
130
|
+
const inputs = mergedFrom.get(roofId);
|
|
131
|
+
const roof = nodes[roofId];
|
|
132
|
+
if (!inputs || roof?.type !== 'roof')
|
|
133
|
+
return false;
|
|
134
|
+
const ids = [roofId, ...(roof.children ?? [])];
|
|
135
|
+
return ids.length === inputs.size && ids.every((id) => inputs.get(id) === nodes[id]);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Roofs whose merged shell is still to build: until it is, the roof shows its segments apart (or
|
|
139
|
+
* an old shell), and a capture would show that. Only roofs on screen count, a first merge (a
|
|
140
|
+
* mounted group with no shell yet) included: one never mounted stays queued and would hold every
|
|
141
|
+
* capture.
|
|
142
|
+
*/
|
|
143
|
+
export function getPendingRoofMergeCount() {
|
|
144
|
+
return countPendingRoofShells([...pendingRoofUpdates, ...pendingIndependentSegments], (id) => sceneRegistry.nodes.get(id));
|
|
145
|
+
}
|
|
146
|
+
export function countPendingRoofShells(pending, mounted) {
|
|
147
|
+
let count = 0;
|
|
148
|
+
for (const id of pending)
|
|
149
|
+
if (mounted(id))
|
|
150
|
+
count++;
|
|
151
|
+
return count;
|
|
152
|
+
}
|
|
153
|
+
function queueSiblingRoofUpdates(roofId, nodes, changedSegmentId) {
|
|
154
|
+
const queueIndependent = (roof) => {
|
|
155
|
+
for (const id of roof.children) {
|
|
156
|
+
const segment = nodes[id];
|
|
157
|
+
if (segment?.type === 'roof-segment' &&
|
|
158
|
+
segment.id !== changedSegmentId &&
|
|
159
|
+
isIndependentRoofSegment(getEffectiveNode(segment)))
|
|
160
|
+
pendingIndependentSegments.add(segment.id);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const currentRoof = nodes[roofId];
|
|
164
|
+
if (currentRoof?.type === 'roof')
|
|
165
|
+
queueIndependent(currentRoof);
|
|
116
166
|
pendingRoofUpdates.add(roofId);
|
|
117
167
|
const roof = nodes[roofId]?.type === 'roof' ? getEffectiveNode(nodes[roofId]) : undefined;
|
|
118
168
|
if (roof?.type !== 'roof' || !roof.parentId)
|
|
@@ -170,13 +220,11 @@ function queueSiblingRoofUpdates(roofId, nodes) {
|
|
|
170
220
|
if ((currentBounds && roofPlanBoundsOverlap(currentBounds, siblingBounds)) ||
|
|
171
221
|
(oldBounds && roofPlanBoundsOverlap(oldBounds, siblingBounds))) {
|
|
172
222
|
pendingRoofUpdates.add(sibling.id);
|
|
223
|
+
queueIndependent(sibling);
|
|
173
224
|
}
|
|
174
225
|
}
|
|
175
226
|
}
|
|
176
|
-
|
|
177
|
-
// ROOF SYSTEM
|
|
178
|
-
// ============================================================================
|
|
179
|
-
export const RoofSystem = () => {
|
|
227
|
+
export const RoofSystem = ({ geometryAdapter } = {}) => {
|
|
180
228
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
181
229
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
182
230
|
const rootNodeIds = useScene((state) => state.rootNodeIds);
|
|
@@ -189,23 +237,30 @@ export const RoofSystem = () => {
|
|
|
189
237
|
// Clear stale pending updates when the scene is unloaded
|
|
190
238
|
if (rootNodeIds.length === 0) {
|
|
191
239
|
pendingRoofUpdates.clear();
|
|
240
|
+
pendingIndependentSegments.clear();
|
|
192
241
|
previousRoofPlanBounds.clear();
|
|
193
242
|
warnedMergedRoofNaNIds.clear();
|
|
243
|
+
mergedFrom.clear();
|
|
194
244
|
for (const cached of mergedRoofSegmentGeometryCache.values()) {
|
|
195
245
|
disposeCachedMergedRoofSegmentGeometrySet(cached);
|
|
196
246
|
}
|
|
197
247
|
mergedRoofSegmentGeometryCache.clear();
|
|
198
248
|
return;
|
|
199
249
|
}
|
|
200
|
-
if (dirtyNodes.size === 0 &&
|
|
250
|
+
if (dirtyNodes.size === 0 &&
|
|
251
|
+
pendingRoofUpdates.size === 0 &&
|
|
252
|
+
pendingIndependentSegments.size === 0)
|
|
201
253
|
return;
|
|
202
254
|
const nodes = useScene.getState().nodes;
|
|
203
255
|
// --- Pass 1: Process dirty roof-segments (throttled) ---
|
|
204
256
|
let segmentsProcessed = 0;
|
|
205
|
-
dirtyNodes.forEach((id) => {
|
|
257
|
+
new Set([...dirtyNodes, ...pendingIndependentSegments]).forEach((id) => {
|
|
258
|
+
const independentCascade = pendingIndependentSegments.has(id) && !dirtyNodes.has(id);
|
|
206
259
|
const node = nodes[id];
|
|
207
|
-
if (!node)
|
|
260
|
+
if (!node) {
|
|
261
|
+
pendingIndependentSegments.delete(id);
|
|
208
262
|
return;
|
|
263
|
+
}
|
|
209
264
|
// Cutting roof accessories cascade their dirty mark to the host
|
|
210
265
|
// segment's parent roof so the merged shell re-CSGs with the new
|
|
211
266
|
// cut. Non-cutting accessories (vents, panels, gutters, etc.) sit on
|
|
@@ -220,6 +275,8 @@ export const RoofSystem = () => {
|
|
|
220
275
|
const seg = segId ? nodes[segId] : undefined;
|
|
221
276
|
if (roofAccessory.buildCut && seg?.parentId) {
|
|
222
277
|
pendingRoofUpdates.add(seg.parentId);
|
|
278
|
+
if (isIndependentRoofSegment(seg))
|
|
279
|
+
useScene.getState().markDirty(seg.id);
|
|
223
280
|
}
|
|
224
281
|
clearDirty(id);
|
|
225
282
|
return;
|
|
@@ -245,7 +302,7 @@ export const RoofSystem = () => {
|
|
|
245
302
|
const revealOnly = mesh.parent?.name === 'segments-wrapper' &&
|
|
246
303
|
mesh.parent?.parent?.getObjectByName('merged-roof')?.visible === true;
|
|
247
304
|
if (isVisible && !revealOnly && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
|
|
248
|
-
updateRoofSegmentGeometry(effectiveSegment, mesh, nodes);
|
|
305
|
+
updateRoofSegmentGeometry(effectiveSegment, mesh, nodes, geometryAdapter);
|
|
249
306
|
segmentsProcessed++;
|
|
250
307
|
}
|
|
251
308
|
else if (isVisible && !revealOnly) {
|
|
@@ -268,9 +325,12 @@ export const RoofSystem = () => {
|
|
|
268
325
|
else {
|
|
269
326
|
clearDirty(id);
|
|
270
327
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
328
|
+
pendingIndependentSegments.delete(id);
|
|
329
|
+
const parentId = effectiveSegment.parentId;
|
|
330
|
+
if (parentId &&
|
|
331
|
+
!independentCascade &&
|
|
332
|
+
!(isRevealAssembling(parentId) && mergedFromStillHolds(parentId, nodes))) {
|
|
333
|
+
queueSiblingRoofUpdates(parentId, nodes, id);
|
|
274
334
|
}
|
|
275
335
|
}
|
|
276
336
|
else if (node.type === 'roof') {
|
|
@@ -286,6 +346,7 @@ export const RoofSystem = () => {
|
|
|
286
346
|
const node = nodes[id];
|
|
287
347
|
if (node?.type !== 'roof') {
|
|
288
348
|
pendingRoofUpdates.delete(id);
|
|
349
|
+
mergedFrom.delete(id);
|
|
289
350
|
continue;
|
|
290
351
|
}
|
|
291
352
|
const group = sceneRegistry.nodes.get(id);
|
|
@@ -294,11 +355,11 @@ export const RoofSystem = () => {
|
|
|
294
355
|
const mergedMesh = group.getObjectByName('merged-roof');
|
|
295
356
|
if (!mergedMesh)
|
|
296
357
|
continue;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
358
|
+
// Built even while hidden: a roof assembling from its segments (construction
|
|
359
|
+
// reveal) hides its shell, and must show it whole the moment it is captured.
|
|
360
|
+
rememberMergedInputs(node, nodes);
|
|
361
|
+
updateMergedRoofGeometry(node, group, nodes);
|
|
362
|
+
roofsProcessed++;
|
|
302
363
|
pendingRoofUpdates.delete(id);
|
|
303
364
|
}
|
|
304
365
|
}, 5); // Priority 5: run after all other systems have settled
|
|
@@ -307,13 +368,14 @@ export const RoofSystem = () => {
|
|
|
307
368
|
// ============================================================================
|
|
308
369
|
// GEOMETRY GENERATION
|
|
309
370
|
// ============================================================================
|
|
310
|
-
function updateRoofSegmentGeometry(node, mesh, nodes) {
|
|
371
|
+
function updateRoofSegmentGeometry(node, mesh, nodes, adapter) {
|
|
311
372
|
const newGeo = generateRoofSegmentGeometry(node, nodes);
|
|
312
373
|
mesh.geometry.dispose();
|
|
313
374
|
mesh.geometry = newGeo;
|
|
314
375
|
computeGeometryBoundsTree(newGeo);
|
|
315
376
|
mesh.position.set(node.position[0], node.position[1], node.position[2]);
|
|
316
377
|
mesh.rotation.y = node.rotation;
|
|
378
|
+
adapter?.syncGeometry(node, mesh);
|
|
317
379
|
}
|
|
318
380
|
/**
|
|
319
381
|
* Subtract every hosted accessory cut (`capabilities.roofAccessory.
|
|
@@ -400,7 +462,7 @@ function getMergedRoofSegmentBrushes(roofNode, segment, nodes) {
|
|
|
400
462
|
if (cached?.key === cacheKey) {
|
|
401
463
|
return cloneCachedMergedRoofSegmentBrushes(cached);
|
|
402
464
|
}
|
|
403
|
-
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment));
|
|
465
|
+
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment, { fascia: wearsFascia(segment, roofNode) }));
|
|
404
466
|
if (!brushes) {
|
|
405
467
|
disposeCachedMergedRoofSegmentGeometrySet(cached);
|
|
406
468
|
mergedRoofSegmentGeometryCache.delete(segmentId);
|
|
@@ -461,7 +523,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
461
523
|
const scn = nodes[id];
|
|
462
524
|
return scn ? getEffectiveNode(scn) : undefined;
|
|
463
525
|
})
|
|
464
|
-
.filter((n) => n !== undefined && !hasSegmentMaterialOverride(n));
|
|
526
|
+
.filter((n) => n !== undefined && !hasSegmentMaterialOverride(n) && !isIndependentRoofSegment(n));
|
|
465
527
|
if (children.length === 0) {
|
|
466
528
|
mergedMesh.geometry.dispose();
|
|
467
529
|
// Not BoxGeometry: its 6 groups against the merged mesh's 4-material array
|
|
@@ -734,6 +796,8 @@ function getMergedRoofSegmentCacheKey(roofNode, segment, nodes) {
|
|
|
734
796
|
return JSON.stringify({
|
|
735
797
|
roofPosition: roofNode.position ?? [0, 0, 0],
|
|
736
798
|
roofRotation: roofNode.rotation ?? 0,
|
|
799
|
+
// a shed's fascia depends on whether its roof is a composite of pieces
|
|
800
|
+
composite: (roofNode.children?.length ?? 0) > 1,
|
|
737
801
|
segment,
|
|
738
802
|
accessories: getMergedRoofAccessoryCachePayload(segment, nodes),
|
|
739
803
|
});
|
|
@@ -819,6 +883,22 @@ const TRIM_CUT_EPSILON = 0.002;
|
|
|
819
883
|
const ROOF_EDGE_MATERIAL_INDEX = 0;
|
|
820
884
|
const ROOF_INSET_WALL_MATERIAL_INDEX = 2;
|
|
821
885
|
const DUTCH_RAKE_SIDE_MATERIAL_INDEX = 1;
|
|
886
|
+
/**
|
|
887
|
+
* A lean-to's shed segment (the lean-to assembly writes the side-infill
|
|
888
|
+
* fields) has no wall band of its own: its high side is the host wall and
|
|
889
|
+
* its ends are the inset infill panels. Every other shed — a house's
|
|
890
|
+
* mono-pitch roof, hand-placed or generated — carries the same hollow wall
|
|
891
|
+
* band a gable does (the high wall and the raked side walls above the
|
|
892
|
+
* plate), sided in the wall/trim slot like a gable's pediment.
|
|
893
|
+
*/
|
|
894
|
+
function isLeanToShedSegment(node) {
|
|
895
|
+
return (node.roofType === 'shed' &&
|
|
896
|
+
(node.shedSideInfillSpan !== undefined ||
|
|
897
|
+
node.shedSideInfillMinX !== undefined ||
|
|
898
|
+
node.shedSideInfillMaxX !== undefined ||
|
|
899
|
+
node.shedFootprintPieces !== undefined ||
|
|
900
|
+
node.shedOpenEndSides !== undefined));
|
|
901
|
+
}
|
|
822
902
|
const DUTCH_RAKE_TOP_MATERIAL_INDEX = 3;
|
|
823
903
|
const DUTCH_RAKE_SLOPE_SEAT_OFFSET = 0.0002;
|
|
824
904
|
function pushDoubleSidedFace(targetFaces, face) {
|
|
@@ -981,7 +1061,12 @@ function buildSegmentTrimCutBrushes(segment) {
|
|
|
981
1061
|
return [];
|
|
982
1062
|
}
|
|
983
1063
|
const { activeRh } = getSegmentSlopeFrame(segment);
|
|
984
|
-
const extra = segment.wallThickness +
|
|
1064
|
+
const extra = segment.wallThickness +
|
|
1065
|
+
segment.overhang +
|
|
1066
|
+
(segment.gableOverhang ?? 0) +
|
|
1067
|
+
segment.deckThickness +
|
|
1068
|
+
segment.shingleThickness +
|
|
1069
|
+
2;
|
|
985
1070
|
const minX = -segment.width / 2 - extra;
|
|
986
1071
|
const maxX = segment.width / 2 + extra;
|
|
987
1072
|
const minZ = -segment.depth / 2 - extra;
|
|
@@ -1075,6 +1160,9 @@ export function clipGeometryBySegmentTrim(geometry, segment) {
|
|
|
1075
1160
|
prepareBrushForCSG(brush);
|
|
1076
1161
|
const next = csgEvaluator.evaluate(brush, cut, SUBTRACTION);
|
|
1077
1162
|
const trimmed = csgGeometry(next).clone();
|
|
1163
|
+
const materialIndices = new Map(dummyMats.map((material, index) => [material, index]));
|
|
1164
|
+
for (const group of trimmed.groups)
|
|
1165
|
+
group.materialIndex = mapRoofGroupMaterialIndex(group.materialIndex, csgMaterials(next), materialIndices);
|
|
1078
1166
|
currentGeometry.dispose();
|
|
1079
1167
|
next.geometry.dispose();
|
|
1080
1168
|
ensureRenderableGeometryAttributes(trimmed);
|
|
@@ -1093,8 +1181,124 @@ export function clipGeometryBySegmentTrim(geometry, segment) {
|
|
|
1093
1181
|
* Generate complete hollow-shell geometry for a roof segment.
|
|
1094
1182
|
* Ports the prototype's CSG approach using three-bvh-csg.
|
|
1095
1183
|
*/
|
|
1096
|
-
|
|
1097
|
-
|
|
1184
|
+
/** A 1x8 fascia / rake board: actual 3/4 × 7-1/4 in. */
|
|
1185
|
+
const FASCIA_BOARD = { thickness: 0.019, height: 0.184 };
|
|
1186
|
+
/** A board's back sits this far inside the deck edge, so the union is one solid with no coplanar faces. */
|
|
1187
|
+
const FASCIA_EMBED = 0.002;
|
|
1188
|
+
/**
|
|
1189
|
+
* Slot 2 takes the trim finish like the rake faces, and the shell remap
|
|
1190
|
+
* leaves it alone: the remap would send a plumb eave board to the wall band
|
|
1191
|
+
* and its top edge to the shingles.
|
|
1192
|
+
*/
|
|
1193
|
+
const FASCIA_MATERIAL_INDEX = 2;
|
|
1194
|
+
/**
|
|
1195
|
+
* The fascia and rake boards a segment wears, in its own frame: a plumb 1x8
|
|
1196
|
+
* on every eave with its top on the deck's top edge, and one up every gable
|
|
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.
|
|
1201
|
+
*/
|
|
1202
|
+
function buildFasciaBoards(node, reach, eaveTopY, tanTheta) {
|
|
1203
|
+
const { roofType, width, depth } = node;
|
|
1204
|
+
const bt = node.fasciaDepth ?? FASCIA_BOARD.thickness;
|
|
1205
|
+
const bh = node.fasciaHeight ?? FASCIA_BOARD.height;
|
|
1206
|
+
// A flat roof's boards sized on purpose reach its top, over the membrane's edge: a coping band.
|
|
1207
|
+
const flatTopY = node.fasciaHeight !== undefined ? eaveTopY + node.shingleThickness : eaveTopY;
|
|
1208
|
+
const out = bt / 2 - FASCIA_EMBED;
|
|
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;
|
|
1215
|
+
const faces = [];
|
|
1216
|
+
const board = (a, b, across) => {
|
|
1217
|
+
const ax = (across[0] * bt) / 2;
|
|
1218
|
+
const az = (across[1] * bt) / 2;
|
|
1219
|
+
const p = [
|
|
1220
|
+
[a[0] - ax, a[1] - bh, a[2] - az],
|
|
1221
|
+
[b[0] - ax, b[1] - bh, b[2] - az],
|
|
1222
|
+
[b[0] + ax, b[1] - bh, b[2] + az],
|
|
1223
|
+
[a[0] + ax, a[1] - bh, a[2] + az],
|
|
1224
|
+
[a[0] - ax, a[1], a[2] - az],
|
|
1225
|
+
[b[0] - ax, b[1], b[2] - az],
|
|
1226
|
+
[b[0] + ax, b[1], b[2] + az],
|
|
1227
|
+
[a[0] + ax, a[1], a[2] + az],
|
|
1228
|
+
].map(([x, y, z]) => new THREE.Vector3(x, y, z));
|
|
1229
|
+
const centre = p.reduce((sum, v) => sum.add(v), new THREE.Vector3()).multiplyScalar(1 / 8);
|
|
1230
|
+
const quads = [
|
|
1231
|
+
[0, 1, 2, 3],
|
|
1232
|
+
[4, 5, 6, 7],
|
|
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]);
|
|
1267
|
+
}
|
|
1268
|
+
else if (roofType === 'shed') {
|
|
1269
|
+
// the low eave at +z, the slope rising to −z
|
|
1270
|
+
const highY = eaveTopY + 2 * halfD * tanTheta;
|
|
1271
|
+
board([-xL, eaveTopY, zE], [xL, eaveTopY, zE], [0, 1]);
|
|
1272
|
+
if (node.fasciaHighEdge ?? true)
|
|
1273
|
+
board([-xL, highY, -zE], [xL, highY, -zE], [0, 1]);
|
|
1274
|
+
for (const sx of [-1, 1])
|
|
1275
|
+
board([sx * xE, eaveTopY, zE], [sx * xE, highY, -zE], [1, 0]);
|
|
1276
|
+
}
|
|
1277
|
+
else
|
|
1278
|
+
return null;
|
|
1279
|
+
return createGeometryFromFaces(faces, FASCIA_MATERIAL_INDEX);
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Whether a drawn segment wears fascia boards. A lean-to carries its own
|
|
1283
|
+
* trim, and a legacy composite shed roof stacks overlapping shed pieces as
|
|
1284
|
+
* one deck (the same pieces whose wall shells are left out), so boards would
|
|
1285
|
+
* show between them.
|
|
1286
|
+
*/
|
|
1287
|
+
function wearsFascia(node, parentRoof) {
|
|
1288
|
+
if (node.fascia !== true)
|
|
1289
|
+
return false;
|
|
1290
|
+
if (node.roofType !== 'shed')
|
|
1291
|
+
return true;
|
|
1292
|
+
if (isLeanToShedSegment(node))
|
|
1293
|
+
return false;
|
|
1294
|
+
return !parentRoof || (parentRoof.children?.length ?? 0) <= 1;
|
|
1295
|
+
}
|
|
1296
|
+
/**
|
|
1297
|
+
* `fascia`: build the segment's fascia boards into its deck. Only the roof
|
|
1298
|
+
* renderer asks; a dormer, a chimney or a sibling's clip takes the bare shell.
|
|
1299
|
+
*/
|
|
1300
|
+
export function getRoofSegmentBrushes(node, options = {}) {
|
|
1301
|
+
const { roofType, width, depth, wallHeight, wallThickness, deckThickness, shingleThickness } = node;
|
|
1098
1302
|
const conicalCoverage = getConicalRoofCoverage(node);
|
|
1099
1303
|
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node);
|
|
1100
1304
|
const shapeRatios = getRoofShapeRatios({
|
|
@@ -1110,20 +1314,28 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1110
1314
|
// the 2D floorplan and the slope frame (which derives activeRh from the
|
|
1111
1315
|
// same ratio). A hardcoded 0.25 desyncs the gablet from the parameter.
|
|
1112
1316
|
const baseI = Math.min(width, depth) * node.dutchHipWidthRatio;
|
|
1113
|
-
const
|
|
1114
|
-
|
|
1317
|
+
const plateSeated = wallHeight <= 0;
|
|
1318
|
+
// `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) => {
|
|
1320
|
+
const wV = Math.max(0.01, width + 2 * xExt);
|
|
1115
1321
|
const dV = Math.max(0.01, depth + 2 * wExt);
|
|
1116
1322
|
const autoDrop = wExt * tanTheta;
|
|
1117
1323
|
// Floor every prism at 5 cm so CSG never sees a degenerate volume — by
|
|
1118
1324
|
// raising the top, never by sinking the base (the base is the wall top).
|
|
1119
1325
|
// One floor for all volumes keeps each cutter level with the shell it carves.
|
|
1120
|
-
|
|
1326
|
+
// A plate-seated roof (wallHeight 0) is the exception for its sloped deck
|
|
1327
|
+
// volumes: their eave hangs below the plate, and raising it lifted the
|
|
1328
|
+
// whole roof off its walls, so their base sinks instead.
|
|
1329
|
+
const eaveY = wallHeight - autoDrop + vOffset;
|
|
1330
|
+
const sinkBase = plateSeated && !onPlate && autoDrop !== 0;
|
|
1331
|
+
const whV = sinkBase ? eaveY : Math.max(0.05, eaveY);
|
|
1121
1332
|
let rhV = activeRh;
|
|
1122
1333
|
if (activeRh > 0) {
|
|
1123
1334
|
rhV = activeRh + autoDrop;
|
|
1124
1335
|
if (roofType === 'shed')
|
|
1125
1336
|
rhV = activeRh + 2 * autoDrop;
|
|
1126
1337
|
}
|
|
1338
|
+
const safeBaseY = sinkBase ? Math.min(baseY, whV - 0.05) : baseY;
|
|
1127
1339
|
let structuralI = baseI;
|
|
1128
1340
|
if (isVoid) {
|
|
1129
1341
|
structuralI += deckThickness;
|
|
@@ -1134,7 +1346,7 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1134
1346
|
d: dV,
|
|
1135
1347
|
wh: whV,
|
|
1136
1348
|
rh: rhV,
|
|
1137
|
-
baseY,
|
|
1349
|
+
baseY: safeBaseY,
|
|
1138
1350
|
insets: { dutchI: structuralI },
|
|
1139
1351
|
baseW: width,
|
|
1140
1352
|
baseD: depth,
|
|
@@ -1146,18 +1358,27 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1146
1358
|
}).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
|
|
1147
1359
|
return createGeometryFromFaces(faces, materialRule ?? matIndex);
|
|
1148
1360
|
};
|
|
1149
|
-
const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false);
|
|
1150
|
-
const innerGeo = getVol(-wallThickness / 2, 0, -5, 2, false);
|
|
1151
|
-
const
|
|
1152
|
-
const deckExt =
|
|
1153
|
-
|
|
1154
|
-
|
|
1361
|
+
const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false, undefined, true);
|
|
1362
|
+
const innerGeo = getVol(-wallThickness / 2, 0, -5, 2, false, undefined, true);
|
|
1363
|
+
const reach = getRoofSegmentDeckReach(node);
|
|
1364
|
+
const deckExt = reach.z;
|
|
1365
|
+
// A shed's deck and shingle edges take the wall/trim slot, unless it wears
|
|
1366
|
+
// fascia boards: then they are fascia like a gable's, in the deck slot.
|
|
1367
|
+
const shedEdgesInTrimSlot = roofType === 'shed' && !options.fascia;
|
|
1368
|
+
const shedRoofSideMaterialRule = shedEdgesInTrimSlot
|
|
1369
|
+
? (normal) => (normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX)
|
|
1155
1370
|
: undefined;
|
|
1156
|
-
const deckTopGeo = getVol(deckExt, verticalRt, 0, 1, false, shedRoofSideMaterialRule);
|
|
1157
|
-
const deckBotGeo = getVol(deckExt, 0, -5, 0, true);
|
|
1371
|
+
const deckTopGeo = getVol(deckExt, verticalRt, 0, 1, false, shedRoofSideMaterialRule, false, reach.x);
|
|
1372
|
+
const deckBotGeo = getVol(deckExt, 0, -5, 0, true, undefined, false, reach.x);
|
|
1373
|
+
// the deck's top edge at its outer plane, as `getVol` puts it
|
|
1374
|
+
const deckEaveDrop = deckExt * tanTheta;
|
|
1375
|
+
const deckEaveTopY = plateSeated && deckEaveDrop > 0
|
|
1376
|
+
? wallHeight - deckEaveDrop + verticalRt
|
|
1377
|
+
: Math.max(0.05, wallHeight - deckEaveDrop + verticalRt);
|
|
1378
|
+
const fasciaGeo = options.fascia ? buildFasciaBoards(node, reach, deckEaveTopY, tanTheta) : null;
|
|
1158
1379
|
const stSin = shingleThickness * sinTheta;
|
|
1159
1380
|
const stCos = shingleThickness * cosTheta;
|
|
1160
|
-
const shinBotW = Math.max(0.01, width + 2 *
|
|
1381
|
+
const shinBotW = Math.max(0.01, width + 2 * reach.x);
|
|
1161
1382
|
const shinBotD = Math.max(0.01, depth + 2 * deckExt);
|
|
1162
1383
|
const deckDrop = deckExt * tanTheta;
|
|
1163
1384
|
const shinBotWh = wallHeight - deckDrop + verticalRt;
|
|
@@ -1254,7 +1475,7 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1254
1475
|
if (roofType === 'dutch' && insetsTop.dutchI !== undefined) {
|
|
1255
1476
|
rakeBoards = buildDutchRakeBoards(shinTopW, shinTopD, shinTopWh, shinTopRh, insetsTop.dutchI, shapeRatios, node.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake, node.dutchTopRakeThickness ?? ROOF_SHAPE_DEFAULTS.dutchTopRakeThickness);
|
|
1256
1477
|
}
|
|
1257
|
-
const shedRoofSideMaterialIndex =
|
|
1478
|
+
const shedRoofSideMaterialIndex = shedEdgesInTrimSlot ? ROOF_EDGE_MATERIAL_INDEX : 1;
|
|
1258
1479
|
const shinBotGeo = createGeometryFromFaces(botFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : shedRoofSideMaterialIndex);
|
|
1259
1480
|
const shinTopGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : shedRoofSideMaterialIndex);
|
|
1260
1481
|
if (transZ !== 0) {
|
|
@@ -1291,13 +1512,16 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1291
1512
|
const deckTopBrush = toBrush(deckTopGeo);
|
|
1292
1513
|
const deckBotBrush = toBrush(deckBotGeo);
|
|
1293
1514
|
if (deckBotBrush) {
|
|
1294
|
-
const wV = Math.max(0.01, width + 2 *
|
|
1515
|
+
const wV = Math.max(0.01, width + 2 * reach.x);
|
|
1295
1516
|
const dV = Math.max(0.01, depth + 2 * deckExt);
|
|
1296
1517
|
deckBotBrush.scale.set(1 + eps / wV, 1, 1 + eps / dV);
|
|
1297
1518
|
deckBotBrush.updateMatrixWorld();
|
|
1298
1519
|
}
|
|
1299
1520
|
const shinTopBrush = toBrush(shinTopGeo);
|
|
1300
1521
|
const shinBotBrush = toBrush(shinBotGeo);
|
|
1522
|
+
const fasciaBrush = fasciaGeo ? toBrush(fasciaGeo) : null;
|
|
1523
|
+
if (fasciaGeo && !fasciaBrush)
|
|
1524
|
+
fasciaGeo.dispose();
|
|
1301
1525
|
if (shinBotBrush) {
|
|
1302
1526
|
const wV = shinBotW;
|
|
1303
1527
|
const dV = shinBotD;
|
|
@@ -1312,8 +1536,15 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1312
1536
|
shinBotGeo.dispose();
|
|
1313
1537
|
if (deckTopBrush && deckBotBrush && wallBrush && innerBrush && shinTopBrush && shinBotBrush) {
|
|
1314
1538
|
try {
|
|
1315
|
-
|
|
1539
|
+
let deckSlab = csgEvaluator.evaluate(deckTopBrush, deckBotBrush, SUBTRACTION);
|
|
1316
1540
|
prepareBrushForCSG(deckSlab);
|
|
1541
|
+
if (fasciaBrush) {
|
|
1542
|
+
const withFascia = csgEvaluator.evaluate(deckSlab, fasciaBrush, ADDITION);
|
|
1543
|
+
prepareBrushForCSG(withFascia);
|
|
1544
|
+
deckSlab.geometry.dispose();
|
|
1545
|
+
fasciaBrush.geometry.dispose();
|
|
1546
|
+
deckSlab = withFascia;
|
|
1547
|
+
}
|
|
1317
1548
|
const shinSlab = csgEvaluator.evaluate(shinTopBrush, shinBotBrush, SUBTRACTION);
|
|
1318
1549
|
prepareBrushForCSG(shinSlab);
|
|
1319
1550
|
deckTopBrush.geometry.dispose();
|
|
@@ -1356,16 +1587,94 @@ export function getRoofSegmentBrushes(node) {
|
|
|
1356
1587
|
wallBrush.geometry.dispose();
|
|
1357
1588
|
if (innerBrush)
|
|
1358
1589
|
innerBrush.geometry.dispose();
|
|
1590
|
+
fasciaBrush?.geometry.dispose();
|
|
1359
1591
|
rakeBoards?.dispose();
|
|
1360
1592
|
return null;
|
|
1361
1593
|
}
|
|
1594
|
+
function buildIndependentRoofSegmentGeometry(node, nodes) {
|
|
1595
|
+
if (!isIndependentRoofSegment(node))
|
|
1596
|
+
return null;
|
|
1597
|
+
const pieces = isGlazedRoofSegment(node)
|
|
1598
|
+
? getGlazedRoofPieces(node)
|
|
1599
|
+
: getBarrelRoofSolidPieces(node);
|
|
1600
|
+
const build = (pieces) => {
|
|
1601
|
+
const parts = pieces.map((piece) => createGeometryFromFaces(getGlazedRoofPieceFaces(piece).map((face) => face.map(({ x, y, z }) => new THREE.Vector3(x, y, z))), piece.slot));
|
|
1602
|
+
const geometry = (parts.length ? mergeGeometries(parts, true) : null) ?? new THREE.BufferGeometry();
|
|
1603
|
+
geometry.groups.forEach((group, index) => {
|
|
1604
|
+
group.materialIndex = parts[index]?.groups[0]?.materialIndex ?? 0;
|
|
1605
|
+
});
|
|
1606
|
+
for (const part of parts)
|
|
1607
|
+
part.dispose();
|
|
1608
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
1609
|
+
return geometry;
|
|
1610
|
+
};
|
|
1611
|
+
let covering = build(pieces.filter((piece) => piece.face.normal[1] > 0));
|
|
1612
|
+
let walls = build(pieces.filter((piece) => piece.face.normal[1] <= 0));
|
|
1613
|
+
const subtract = (geometry, cutter) => {
|
|
1614
|
+
if (!geometry.getAttribute('position')?.count)
|
|
1615
|
+
return geometry;
|
|
1616
|
+
const brush = new Brush(geometry, dummyMats);
|
|
1617
|
+
prepareBrushForCSG(brush);
|
|
1618
|
+
const result = csgEvaluator.evaluate(brush, cutter, SUBTRACTION);
|
|
1619
|
+
const next = result.geometry.clone();
|
|
1620
|
+
const indices = new Map(dummyMats.map((material, index) => [material, index]));
|
|
1621
|
+
for (const group of next.groups)
|
|
1622
|
+
group.materialIndex = mapRoofGroupMaterialIndex(group.materialIndex, csgMaterials(result), indices);
|
|
1623
|
+
result.geometry.dispose();
|
|
1624
|
+
geometry.dispose();
|
|
1625
|
+
return next;
|
|
1626
|
+
};
|
|
1627
|
+
for (const id of node.children ?? []) {
|
|
1628
|
+
const stored = nodes?.[id];
|
|
1629
|
+
if (!stored)
|
|
1630
|
+
continue;
|
|
1631
|
+
const child = getEffectiveNode(stored);
|
|
1632
|
+
if (child.metadata?.isTransient)
|
|
1633
|
+
continue;
|
|
1634
|
+
const capability = nodeRegistry.get(child.type)?.capabilities.roofAccessory;
|
|
1635
|
+
const cutGeometry = capability?.buildCut?.(child, node);
|
|
1636
|
+
if (!cutGeometry)
|
|
1637
|
+
continue;
|
|
1638
|
+
const cutter = new Brush(cutGeometry, dummyMats[0]);
|
|
1639
|
+
try {
|
|
1640
|
+
prepareBrushForCSG(cutter);
|
|
1641
|
+
if (capability?.cutScope !== 'wall')
|
|
1642
|
+
covering = subtract(covering, cutter);
|
|
1643
|
+
walls = subtract(walls, cutter);
|
|
1644
|
+
}
|
|
1645
|
+
finally {
|
|
1646
|
+
cutGeometry.dispose();
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
const parts = [covering, walls]
|
|
1650
|
+
.filter((part) => part.getAttribute('position')?.count)
|
|
1651
|
+
.map((part) => (part.index ? part.toNonIndexed() : part.clone()));
|
|
1652
|
+
let geometry = (parts.length ? mergeGeometries(parts, false) : null) ?? new THREE.BufferGeometry();
|
|
1653
|
+
let offset = 0;
|
|
1654
|
+
for (const part of parts) {
|
|
1655
|
+
for (const group of part.groups)
|
|
1656
|
+
geometry.addGroup(offset + group.start, group.count, group.materialIndex);
|
|
1657
|
+
offset += part.getAttribute('position').count;
|
|
1658
|
+
part.dispose();
|
|
1659
|
+
}
|
|
1660
|
+
covering.dispose();
|
|
1661
|
+
walls.dispose();
|
|
1662
|
+
geometry = clipGeometryBySegmentTrim(geometry, node) ?? geometry;
|
|
1663
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
1664
|
+
return geometry;
|
|
1665
|
+
}
|
|
1362
1666
|
export function generateRoofSegmentGeometry(node, nodes) {
|
|
1363
1667
|
const parentNode = node.parentId ? nodes?.[node.parentId] : undefined;
|
|
1364
1668
|
const parentRoof = parentNode?.type === 'roof' ? parentNode : undefined;
|
|
1365
1669
|
const parentRoofPosition = parentRoof?.position;
|
|
1366
1670
|
const parentRoofRotation = parentRoof?.rotation ?? 0;
|
|
1367
1671
|
const segmentWorldMatrix = composeSegmentWorldMatrix(parentRoofPosition, parentRoofRotation, node.position, node.rotation ?? 0);
|
|
1368
|
-
const directSegmentGeometry = withSegmentUvMatrix(segmentWorldMatrix, () =>
|
|
1672
|
+
const directSegmentGeometry = withSegmentUvMatrix(segmentWorldMatrix, () => {
|
|
1673
|
+
const independent = buildIndependentRoofSegmentGeometry(node, nodes);
|
|
1674
|
+
if (independent)
|
|
1675
|
+
return independent;
|
|
1676
|
+
return buildCustomShedGeometry(node, nodes) ?? buildDirectConicalSectorGeometry(node);
|
|
1677
|
+
});
|
|
1369
1678
|
if (directSegmentGeometry) {
|
|
1370
1679
|
let result = addShedInsetEndPanels(directSegmentGeometry, [node], false);
|
|
1371
1680
|
if (nodes) {
|
|
@@ -1375,7 +1684,7 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1375
1684
|
ensureRenderableGeometryAttributes(result);
|
|
1376
1685
|
return result;
|
|
1377
1686
|
}
|
|
1378
|
-
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node));
|
|
1687
|
+
const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, { fascia: wearsFascia(node, parentRoof) }));
|
|
1379
1688
|
if (!brushes) {
|
|
1380
1689
|
// Fallback: simple box
|
|
1381
1690
|
return new THREE.BoxGeometry(node.width, node.wallHeight, node.depth);
|
|
@@ -1444,7 +1753,7 @@ export function generateRoofSegmentGeometry(node, nodes) {
|
|
|
1444
1753
|
ensureRenderableGeometryAttributes(resultGeo);
|
|
1445
1754
|
return resultGeo;
|
|
1446
1755
|
}
|
|
1447
|
-
function clipDirectRoofGeometryAgainstSiblings(geometry, node, nodes, space) {
|
|
1756
|
+
export function clipDirectRoofGeometryAgainstSiblings(geometry, node, nodes, space) {
|
|
1448
1757
|
const siblingInterior = buildOccludingRoofInterior(node, nodes, space);
|
|
1449
1758
|
if (!siblingInterior)
|
|
1450
1759
|
return geometry;
|
|
@@ -2757,7 +3066,9 @@ function buildDutchRakeBoards(W, D, wh, rh, i, shapeRatios, rake, thickness) {
|
|
|
2757
3066
|
return merged;
|
|
2758
3067
|
}
|
|
2759
3068
|
function createShedInsetEndPanelGeometry(node) {
|
|
2760
|
-
|
|
3069
|
+
// a house shed has real end walls in its wall band; only a lean-to
|
|
3070
|
+
// needs the inset infill panels
|
|
3071
|
+
if (!isLeanToShedSegment(node))
|
|
2761
3072
|
return null;
|
|
2762
3073
|
const trim = normalizeRoofSegmentTrim(node);
|
|
2763
3074
|
const openEndSides = readShedOpenEndSides(node);
|
|
@@ -2996,7 +3307,21 @@ function pushRoofUv(uvs, point, normal) {
|
|
|
2996
3307
|
* to the true roof surface even on multi-slope roofs (gambrel/mansard/dutch).
|
|
2997
3308
|
*/
|
|
2998
3309
|
export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
|
|
2999
|
-
|
|
3310
|
+
if (isGlazedRoofSegment(segment) || segment.roofType === 'barrel') {
|
|
3311
|
+
const face = (isGlazedRoofSegment(segment) ? getGlazedRoofFaces(segment) : getBarrelRoofFaces(segment)).find((face) => face.normal[1] > 0 &&
|
|
3312
|
+
lx >= face.origin[0] &&
|
|
3313
|
+
lx <= face.origin[0] + face.width &&
|
|
3314
|
+
(lz - face.origin[2]) / face.v[2] >= -1e-6 &&
|
|
3315
|
+
(lz - face.origin[2]) / face.v[2] <= face.length + 1e-6);
|
|
3316
|
+
if (face) {
|
|
3317
|
+
const v = (lz - face.origin[2]) / face.v[2];
|
|
3318
|
+
return {
|
|
3319
|
+
point: new THREE.Vector3(lx, face.origin[1] + face.v[1] * v, lz),
|
|
3320
|
+
normal: new THREE.Vector3(...face.normal),
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
const { roofType, width, depth, wallHeight, deckThickness, shingleThickness } = segment;
|
|
3000
3325
|
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(segment);
|
|
3001
3326
|
if (roofType === 'flat' || activeRh === 0) {
|
|
3002
3327
|
return {
|
|
@@ -3005,11 +3330,11 @@ export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
|
|
|
3005
3330
|
};
|
|
3006
3331
|
}
|
|
3007
3332
|
const verticalRt = deckThickness / cosTheta;
|
|
3008
|
-
const
|
|
3009
|
-
const deckExt =
|
|
3333
|
+
const reach = getRoofSegmentDeckReach(segment);
|
|
3334
|
+
const deckExt = reach.z;
|
|
3010
3335
|
const stSin = shingleThickness * sinTheta;
|
|
3011
3336
|
const stCos = shingleThickness * cosTheta;
|
|
3012
|
-
const shinBotW = Math.max(0.01, width + 2 *
|
|
3337
|
+
const shinBotW = Math.max(0.01, width + 2 * reach.x);
|
|
3013
3338
|
const shinBotD = Math.max(0.01, depth + 2 * deckExt);
|
|
3014
3339
|
const deckDrop = deckExt * tanTheta;
|
|
3015
3340
|
const shinBotWh = wallHeight - deckDrop + verticalRt;
|