@pascal-app/viewer 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +5 -2
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/parametric-node-renderer.js +7 -4
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +39 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
- package/dist/components/viewer/index.d.ts +19 -2
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +104 -12
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +36 -5
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +17 -2
- package/dist/components/viewer/post-processing.d.ts +7 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +179 -24
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +6 -4
- package/dist/components/viewer/scene-bvh.d.ts +2 -1
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +3 -0
- package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
- package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.test.js +13 -0
- package/dist/components/viewer/scene-environment.d.ts +2 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -0
- package/dist/components/viewer/scene-environment.js +79 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +18 -10
- package/dist/components/viewer/walkthrough-controls.d.ts +1 -0
- package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.js +20 -0
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +3 -22
- package/dist/hooks/use-node-events.d.ts +1 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +22 -14
- package/dist/index.d.ts +19 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -8
- package/dist/lib/backdrop.d.ts +33 -0
- package/dist/lib/backdrop.d.ts.map +1 -0
- package/dist/lib/backdrop.js +92 -0
- package/dist/lib/box-uv.d.ts +13 -0
- package/dist/lib/box-uv.d.ts.map +1 -0
- package/dist/lib/box-uv.js +32 -0
- package/dist/lib/csg-utils.d.ts +3 -9
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +65 -5
- package/dist/lib/csg-utils.test.d.ts +2 -0
- package/dist/lib/csg-utils.test.d.ts.map +1 -0
- package/dist/lib/csg-utils.test.js +45 -0
- package/dist/lib/edge-style.d.ts +1 -0
- package/dist/lib/edge-style.d.ts.map +1 -1
- package/dist/lib/edge-style.js +26 -6
- package/dist/lib/ink-edges.d.ts +1 -1
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +13 -7
- package/dist/lib/ktx2-loader.d.ts +43 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +136 -0
- package/dist/lib/layers.d.ts +10 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +10 -0
- package/dist/lib/materials.d.ts +14 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +103 -26
- package/dist/lib/scene-themes.d.ts +1 -0
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +25 -16
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +60 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +175 -3
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +42 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +4 -1
- package/dist/systems/door/door-animation.test.d.ts +2 -0
- package/dist/systems/door/door-animation.test.d.ts.map +1 -0
- package/dist/systems/door/door-animation.test.js +51 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
- package/dist/systems/door/door-floor-alignment.test.js +50 -0
- package/dist/systems/door/door-system.d.ts +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +494 -161
- package/dist/systems/fence/fence-system.d.ts +17 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +221 -53
- package/dist/systems/geometry/geometry-system.d.ts +7 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +95 -26
- package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
- package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.test.js +30 -0
- package/dist/systems/interactive/control-widget.d.ts +10 -0
- package/dist/systems/interactive/control-widget.d.ts.map +1 -0
- package/dist/systems/interactive/control-widget.js +35 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +55 -56
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +20 -6
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +28 -4
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +2 -41
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +29 -8
- package/dist/systems/roof/roof-system.d.ts +21 -8
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1079 -221
- package/dist/systems/slab/slab-system.d.ts +5 -3
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +41 -23
- package/dist/systems/slab/slab-system.test.js +3 -2
- package/dist/systems/stair/stair-system.js +6 -3
- package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.js +167 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +46 -17
- package/dist/systems/wall/wall-materials.d.ts +11 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +244 -35
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +48 -0
- package/dist/systems/wall/wall-system.d.ts +2 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +329 -174
- package/dist/systems/window/window-animation-system.d.ts +12 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +16 -5
- package/dist/systems/window/window-system.d.ts +7 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +240 -14
- package/package.json +4 -4
- package/dist/components/viewer/ground-occluder.d.ts +0 -2
- package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
- package/dist/components/viewer/ground-occluder.js +0 -67
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { getEffectiveNode, getSegmentSlopeFrame, hasSegmentMaterialOverride, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getEffectiveNode, getRoofModuleFaces, getRoofShapeInsets, getRoofShapeRatios, getSegmentSlopeFrame, hasSegmentMaterialOverride, nodeRegistry, normalizeRoofSegmentTrim, ROOF_SHAPE_DEFAULTS, sceneRegistry, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
|
-
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
4
|
+
import { mergeGeometries, mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
5
5
|
import { ADDITION, Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
6
6
|
import { computeBoundsTree } from 'three-mesh-bvh';
|
|
7
|
+
import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
|
|
8
|
+
import { ensureRenderableGeometryAttributes } from '../../lib/csg-utils';
|
|
7
9
|
function csgGeometry(brush) {
|
|
8
10
|
return brush.geometry;
|
|
9
11
|
}
|
|
@@ -14,13 +16,14 @@ function csgMaterials(brush) {
|
|
|
14
16
|
const csgEvaluator = new Evaluator();
|
|
15
17
|
csgEvaluator.useGroups = true;
|
|
16
18
|
csgEvaluator.consolidateGroups = false; // shared dummyMats across brushes causes consolidation to misalign groupIndices vs groupOrder indices → crash
|
|
17
|
-
csgEvaluator.attributes = ['position', 'normal', 'uv'];
|
|
19
|
+
csgEvaluator.attributes = ['position', 'normal', 'uv', 'uv2'];
|
|
18
20
|
function computeGeometryBoundsTree(geometry) {
|
|
19
21
|
;
|
|
20
22
|
geometry.computeBoundsTree = computeBoundsTree;
|
|
21
23
|
geometry.computeBoundsTree({ maxLeafSize: 10 });
|
|
22
24
|
}
|
|
23
25
|
function prepareBrushForCSG(brush) {
|
|
26
|
+
ensureRenderableGeometryAttributes(brush.geometry);
|
|
24
27
|
computeGeometryBoundsTree(brush.geometry);
|
|
25
28
|
brush.updateMatrixWorld();
|
|
26
29
|
}
|
|
@@ -34,6 +37,44 @@ const _uvFaceNormal = new THREE.Vector3();
|
|
|
34
37
|
const _uvWorldDown = new THREE.Vector3(0, -1, 0);
|
|
35
38
|
const _uvDownSlope = new THREE.Vector3();
|
|
36
39
|
const _uvAcrossSlope = new THREE.Vector3();
|
|
40
|
+
// World transform of the segment whose geometry is currently being built, so
|
|
41
|
+
// vertical (gable wall) faces project their UVs in WORLD space — identical to
|
|
42
|
+
// the wall kind's `applyWorldPlanarWallUVs` (U = ±worldX/Z, V = 1 - worldY) so
|
|
43
|
+
// the gable band tiles continuously into the walls below. Identity = local
|
|
44
|
+
// (the default outside a segment build). Set via `withSegmentUvMatrix`.
|
|
45
|
+
const _segmentUvMatrix = new THREE.Matrix4();
|
|
46
|
+
const _segmentUvNormalMatrix = new THREE.Matrix3();
|
|
47
|
+
const _uvWorldPoint = new THREE.Vector3();
|
|
48
|
+
const _uvWorldNormal = new THREE.Vector3();
|
|
49
|
+
const _prevSegmentUvMatrix = new THREE.Matrix4();
|
|
50
|
+
const _prevSegmentUvNormalMatrix = new THREE.Matrix3();
|
|
51
|
+
function withSegmentUvMatrix(matrix, build) {
|
|
52
|
+
_prevSegmentUvMatrix.copy(_segmentUvMatrix);
|
|
53
|
+
_prevSegmentUvNormalMatrix.copy(_segmentUvNormalMatrix);
|
|
54
|
+
_segmentUvMatrix.copy(matrix);
|
|
55
|
+
_segmentUvNormalMatrix.getNormalMatrix(matrix);
|
|
56
|
+
try {
|
|
57
|
+
return build();
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
_segmentUvMatrix.copy(_prevSegmentUvMatrix);
|
|
61
|
+
_segmentUvNormalMatrix.copy(_prevSegmentUvNormalMatrix);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Scratch matrices for composing a segment's world transform (roof group ∘
|
|
65
|
+
// segment) at each build, without per-call allocation.
|
|
66
|
+
const _segWorldMatrix = new THREE.Matrix4();
|
|
67
|
+
const _roofGroupMatrix = new THREE.Matrix4();
|
|
68
|
+
const _segLocalMatrix = new THREE.Matrix4();
|
|
69
|
+
const _uvTmpPos = new THREE.Vector3();
|
|
70
|
+
const _uvTmpQuat = new THREE.Quaternion();
|
|
71
|
+
const _uvUnitScale = new THREE.Vector3(1, 1, 1);
|
|
72
|
+
/** Compose the world transform `T(roofPos)·Ry(roofRot) · T(segPos)·Ry(segRot)`. */
|
|
73
|
+
function composeSegmentWorldMatrix(roofPosition, roofRotation, segPosition, segRotation) {
|
|
74
|
+
_roofGroupMatrix.compose(_uvTmpPos.set(roofPosition?.[0] ?? 0, roofPosition?.[1] ?? 0, roofPosition?.[2] ?? 0), _uvTmpQuat.setFromAxisAngle(_yAxis, roofRotation), _uvUnitScale);
|
|
75
|
+
_segLocalMatrix.compose(_uvTmpPos.set(segPosition[0] ?? 0, segPosition[1] ?? 0, segPosition[2] ?? 0), _uvTmpQuat.setFromAxisAngle(_yAxis, segRotation), _uvUnitScale);
|
|
76
|
+
return _segWorldMatrix.multiplyMatrices(_roofGroupMatrix, _segLocalMatrix);
|
|
77
|
+
}
|
|
37
78
|
const _tmpVec3A = new THREE.Vector3();
|
|
38
79
|
const _tmpVec3B = new THREE.Vector3();
|
|
39
80
|
const _surfaceRay = new THREE.Ray();
|
|
@@ -44,6 +85,27 @@ const _surfaceV0 = new THREE.Vector3();
|
|
|
44
85
|
const _surfaceV1 = new THREE.Vector3();
|
|
45
86
|
const _surfaceV2 = new THREE.Vector3();
|
|
46
87
|
const _surfaceFaceNormal = new THREE.Vector3();
|
|
88
|
+
/**
|
|
89
|
+
* Degenerate placeholder for a roof mesh with nothing to draw (initial
|
|
90
|
+
* BoxGeometry swap-out, or a roof whose segments were all deleted/painted).
|
|
91
|
+
* Three zero-vertices (one invisible triangle), not an empty attribute: an
|
|
92
|
+
* empty position (count 0) leaves WebGPU vertex buffer slot 0 unbound if the
|
|
93
|
+
* mesh is ever drawn, and computeBoundsTree needs a real position buffer to
|
|
94
|
+
* index. Deliberately NO groups: count-0 groups crash MeshBVH's packed-tree
|
|
95
|
+
* build (it partitions roots by group), and a BoxGeometry's 6 groups against
|
|
96
|
+
* the 4 roof materials crash raycasts and GLTFExporter. Group-less + a
|
|
97
|
+
* zero-area triangle is safe everywhere — it draws nothing under an array
|
|
98
|
+
* material and can never be ray-hit.
|
|
99
|
+
*/
|
|
100
|
+
function createDegenerateRoofPlaceholder() {
|
|
101
|
+
const placeholder = new THREE.BufferGeometry();
|
|
102
|
+
placeholder.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(9), 3));
|
|
103
|
+
placeholder.setAttribute('normal', new THREE.Float32BufferAttribute(new Float32Array(9), 3));
|
|
104
|
+
placeholder.setAttribute('uv', new THREE.Float32BufferAttribute(new Float32Array(6), 2));
|
|
105
|
+
placeholder.setAttribute('uv2', new THREE.Float32BufferAttribute(new Float32Array(6), 2));
|
|
106
|
+
computeGeometryBoundsTree(placeholder);
|
|
107
|
+
return placeholder;
|
|
108
|
+
}
|
|
47
109
|
// Pending merged-roof updates carried across frames (for throttling)
|
|
48
110
|
const pendingRoofUpdates = new Set();
|
|
49
111
|
const warnedMergedRoofNaNIds = new Set();
|
|
@@ -66,6 +128,10 @@ export const RoofSystem = () => {
|
|
|
66
128
|
if (rootNodeIds.length === 0) {
|
|
67
129
|
pendingRoofUpdates.clear();
|
|
68
130
|
warnedMergedRoofNaNIds.clear();
|
|
131
|
+
for (const cached of mergedRoofSegmentGeometryCache.values()) {
|
|
132
|
+
disposeCachedMergedRoofSegmentGeometrySet(cached);
|
|
133
|
+
}
|
|
134
|
+
mergedRoofSegmentGeometryCache.clear();
|
|
69
135
|
return;
|
|
70
136
|
}
|
|
71
137
|
if (dirtyNodes.size === 0 && pendingRoofUpdates.size === 0)
|
|
@@ -77,19 +143,19 @@ export const RoofSystem = () => {
|
|
|
77
143
|
const node = nodes[id];
|
|
78
144
|
if (!node)
|
|
79
145
|
return;
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// merged shell re-CSGs with the new cut. Without this, moving /
|
|
85
|
-
// resizing an accessory leaves the merged roof showing the
|
|
86
|
-
// previous cut shape (stale CSG) once the user exits segment
|
|
87
|
-
// edit mode. Registry-driven so the viewer stays kind-agnostic.
|
|
146
|
+
// Cutting roof accessories cascade their dirty mark to the host
|
|
147
|
+
// segment's parent roof so the merged shell re-CSGs with the new
|
|
148
|
+
// cut. Non-cutting accessories (vents, panels, gutters, etc.) sit on
|
|
149
|
+
// top of the shell and should not force a full roof merge.
|
|
88
150
|
const def = nodeRegistry.get(node.type);
|
|
89
|
-
|
|
151
|
+
// Kinds with `dirtyHandledByOwnSystem` (door / window) reach the roof
|
|
152
|
+
// through their own geometry system's parentId cascade instead —
|
|
153
|
+
// their dirty marks belong to that system, not to this loop.
|
|
154
|
+
const roofAccessory = def?.capabilities?.roofAccessory;
|
|
155
|
+
if (roofAccessory && !roofAccessory.dirtyHandledByOwnSystem) {
|
|
90
156
|
const segId = node.roofSegmentId;
|
|
91
157
|
const seg = segId ? nodes[segId] : undefined;
|
|
92
|
-
if (seg?.parentId) {
|
|
158
|
+
if (roofAccessory.buildCut && seg?.parentId) {
|
|
93
159
|
pendingRoofUpdates.add(seg.parentId);
|
|
94
160
|
}
|
|
95
161
|
clearDirty(id);
|
|
@@ -106,11 +172,20 @@ export const RoofSystem = () => {
|
|
|
106
172
|
// Only compute expensive individual CSG when the segment is actually rendered
|
|
107
173
|
// (its parent group is visible = the roof is selected for editing)
|
|
108
174
|
const isVisible = mesh.parent?.visible !== false;
|
|
109
|
-
|
|
110
|
-
|
|
175
|
+
// Accessory-reveal mode (RoofEditSystem): the wrapper is shown so
|
|
176
|
+
// portaled handles render, but the merged shell stays visible and
|
|
177
|
+
// the segment meshes are stripped to empty placeholders. Rebuilding
|
|
178
|
+
// per-segment CSG here would draw UNCUT geometry on top of the
|
|
179
|
+
// merged shell — hiding a freshly cut opening (door / window /
|
|
180
|
+
// skylight) until the next deselect. Full edit mode hides the
|
|
181
|
+
// merged mesh, so gate the rebuild on its visibility.
|
|
182
|
+
const revealOnly = mesh.parent?.name === 'segments-wrapper' &&
|
|
183
|
+
mesh.parent?.parent?.getObjectByName('merged-roof')?.visible === true;
|
|
184
|
+
if (isVisible && !revealOnly && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
|
|
185
|
+
updateRoofSegmentGeometry(effectiveSegment, mesh, nodes);
|
|
111
186
|
segmentsProcessed++;
|
|
112
187
|
}
|
|
113
|
-
else if (isVisible) {
|
|
188
|
+
else if (isVisible && !revealOnly) {
|
|
114
189
|
return; // Over budget — keep dirty, process next frame
|
|
115
190
|
}
|
|
116
191
|
else {
|
|
@@ -120,14 +195,7 @@ export const RoofSystem = () => {
|
|
|
120
195
|
// so MeshBVH hits groups[4].materialIndex → undefined.side → crash.
|
|
121
196
|
if (mesh.geometry.type === 'BoxGeometry') {
|
|
122
197
|
mesh.geometry.dispose();
|
|
123
|
-
|
|
124
|
-
// Three zero-vertices (one degenerate, invisible triangle), not an
|
|
125
|
-
// empty attribute: an empty position (count 0) leaves WebGPU vertex
|
|
126
|
-
// buffer slot 0 unbound if the mesh is ever drawn, and computeBoundsTree
|
|
127
|
-
// needs a real position buffer to index.
|
|
128
|
-
placeholder.setAttribute('position', new THREE.Float32BufferAttribute(new Float32Array(9), 3));
|
|
129
|
-
computeGeometryBoundsTree(placeholder);
|
|
130
|
-
mesh.geometry = placeholder;
|
|
198
|
+
mesh.geometry = createDegenerateRoofPlaceholder();
|
|
131
199
|
}
|
|
132
200
|
mesh.position.set(effectiveSegment.position[0], effectiveSegment.position[1], effectiveSegment.position[2]);
|
|
133
201
|
mesh.rotation.y = effectiveSegment.rotation;
|
|
@@ -153,7 +221,7 @@ export const RoofSystem = () => {
|
|
|
153
221
|
if (roofsProcessed >= MAX_ROOFS_PER_FRAME)
|
|
154
222
|
break;
|
|
155
223
|
const node = nodes[id];
|
|
156
|
-
if (
|
|
224
|
+
if (node?.type !== 'roof') {
|
|
157
225
|
pendingRoofUpdates.delete(id);
|
|
158
226
|
continue;
|
|
159
227
|
}
|
|
@@ -176,14 +244,142 @@ export const RoofSystem = () => {
|
|
|
176
244
|
// ============================================================================
|
|
177
245
|
// GEOMETRY GENERATION
|
|
178
246
|
// ============================================================================
|
|
179
|
-
function updateRoofSegmentGeometry(node, mesh) {
|
|
180
|
-
const newGeo = generateRoofSegmentGeometry(node);
|
|
247
|
+
function updateRoofSegmentGeometry(node, mesh, nodes) {
|
|
248
|
+
const newGeo = generateRoofSegmentGeometry(node, nodes);
|
|
181
249
|
mesh.geometry.dispose();
|
|
182
250
|
mesh.geometry = newGeo;
|
|
183
251
|
computeGeometryBoundsTree(newGeo);
|
|
184
252
|
mesh.position.set(node.position[0], node.position[1], node.position[2]);
|
|
185
253
|
mesh.rotation.y = node.rotation;
|
|
186
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Subtract every hosted accessory cut (`capabilities.roofAccessory.
|
|
257
|
+
* buildCut`) from a segment's brushes, in SEGMENT-LOCAL space. Shared by
|
|
258
|
+
* the merged-shell path AND the per-segment path (full edit mode /
|
|
259
|
+
* painted segments) — without the latter, selecting a segment used to
|
|
260
|
+
* swap the merged shell for uncut per-segment meshes and every door /
|
|
261
|
+
* window / skylight hole vanished until deselect. Children are read
|
|
262
|
+
* live-effective so an in-flight handle drag carves the live hole.
|
|
263
|
+
* Registry-driven so the viewer never names a kind.
|
|
264
|
+
*/
|
|
265
|
+
function subtractAccessoryCuts(brushes, segment, nodes) {
|
|
266
|
+
let workingShin = brushes.shinSlab;
|
|
267
|
+
let workingDeck = brushes.deckSlab;
|
|
268
|
+
let workingWall = brushes.wallBrush;
|
|
269
|
+
for (const childElemId of segment.children ?? []) {
|
|
270
|
+
const storedChild = nodes[childElemId];
|
|
271
|
+
if (!storedChild)
|
|
272
|
+
continue;
|
|
273
|
+
const childElem = getEffectiveNode(storedChild);
|
|
274
|
+
const meta = typeof childElem.metadata === 'object' && childElem.metadata !== null
|
|
275
|
+
? childElem.metadata
|
|
276
|
+
: undefined;
|
|
277
|
+
if (meta?.isTransient)
|
|
278
|
+
continue;
|
|
279
|
+
const childDef = nodeRegistry.get(childElem.type);
|
|
280
|
+
const buildCut = childDef?.capabilities?.roofAccessory?.buildCut;
|
|
281
|
+
if (!buildCut)
|
|
282
|
+
continue;
|
|
283
|
+
const cutGeo = buildCut(childElem, segment);
|
|
284
|
+
if (!cutGeo)
|
|
285
|
+
continue;
|
|
286
|
+
// Wrap the kind-emitted geometry in a Brush. Kinds return raw
|
|
287
|
+
// shapes; the viewer welds (mandatory after rotations leave
|
|
288
|
+
// duplicated verts), attaches a single material group, and
|
|
289
|
+
// builds the bounds tree — keeping kind code free of
|
|
290
|
+
// three-bvh-csg / three-mesh-bvh imports.
|
|
291
|
+
const welded = mergeVertices(cutGeo, 1e-4);
|
|
292
|
+
cutGeo.dispose();
|
|
293
|
+
const idxCount = welded.getIndex()?.count ?? 0;
|
|
294
|
+
if (idxCount === 0) {
|
|
295
|
+
welded.dispose();
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
welded.clearGroups();
|
|
299
|
+
welded.addGroup(0, idxCount, 0);
|
|
300
|
+
welded.computeVertexNormals();
|
|
301
|
+
ensureRenderableGeometryAttributes(welded);
|
|
302
|
+
computeGeometryBoundsTree(welded);
|
|
303
|
+
const cut = new Brush(welded, dummyMats[0]);
|
|
304
|
+
cut.updateMatrixWorld();
|
|
305
|
+
const cutScope = childDef?.capabilities?.roofAccessory?.cutScope ?? 'all';
|
|
306
|
+
try {
|
|
307
|
+
if (cutScope !== 'wall') {
|
|
308
|
+
const nextShin = csgEvaluator.evaluate(workingShin, cut, SUBTRACTION);
|
|
309
|
+
workingShin.geometry.dispose();
|
|
310
|
+
prepareBrushForCSG(nextShin);
|
|
311
|
+
workingShin = nextShin;
|
|
312
|
+
const nextDeck = csgEvaluator.evaluate(workingDeck, cut, SUBTRACTION);
|
|
313
|
+
workingDeck.geometry.dispose();
|
|
314
|
+
prepareBrushForCSG(nextDeck);
|
|
315
|
+
workingDeck = nextDeck;
|
|
316
|
+
}
|
|
317
|
+
const nextWall = csgEvaluator.evaluate(workingWall, cut, SUBTRACTION);
|
|
318
|
+
workingWall.geometry.dispose();
|
|
319
|
+
prepareBrushForCSG(nextWall);
|
|
320
|
+
workingWall = nextWall;
|
|
321
|
+
}
|
|
322
|
+
catch (e) {
|
|
323
|
+
console.error(`[${childElem.type}] cut CSG failed:`, e);
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
cut.geometry.dispose();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
brushes.shinSlab = workingShin;
|
|
330
|
+
brushes.deckSlab = workingDeck;
|
|
331
|
+
brushes.wallBrush = workingWall;
|
|
332
|
+
}
|
|
333
|
+
function getMergedRoofSegmentBrushes(roofNode, segment, nodes) {
|
|
334
|
+
const segmentId = segment.id;
|
|
335
|
+
const cacheKey = getMergedRoofSegmentCacheKey(roofNode, segment, nodes);
|
|
336
|
+
const cached = mergedRoofSegmentGeometryCache.get(segmentId);
|
|
337
|
+
if (cached?.key === cacheKey) {
|
|
338
|
+
return cloneCachedMergedRoofSegmentBrushes(cached);
|
|
339
|
+
}
|
|
340
|
+
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment));
|
|
341
|
+
if (!brushes) {
|
|
342
|
+
disposeCachedMergedRoofSegmentGeometrySet(cached);
|
|
343
|
+
mergedRoofSegmentGeometryCache.delete(segmentId);
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
subtractAccessoryCuts(brushes, segment, nodes);
|
|
347
|
+
_matrix.compose(_position.set(segment.position[0], segment.position[1], segment.position[2]), _quaternion.setFromAxisAngle(_yAxis, segment.rotation), _scale);
|
|
348
|
+
const applyTransform = (brush) => {
|
|
349
|
+
csgGeometry(brush).applyMatrix4(_matrix);
|
|
350
|
+
brush.updateMatrixWorld();
|
|
351
|
+
};
|
|
352
|
+
applyTransform(brushes.shinSlab);
|
|
353
|
+
applyTransform(brushes.deckSlab);
|
|
354
|
+
applyTransform(brushes.wallBrush);
|
|
355
|
+
applyTransform(brushes.innerBrush);
|
|
356
|
+
brushes.rakeBoards?.applyMatrix4(_matrix);
|
|
357
|
+
const nextCached = {
|
|
358
|
+
key: cacheKey,
|
|
359
|
+
deckSlab: {
|
|
360
|
+
geometry: csgGeometry(brushes.deckSlab).clone(),
|
|
361
|
+
materials: csgMaterials(brushes.deckSlab),
|
|
362
|
+
},
|
|
363
|
+
shinSlab: {
|
|
364
|
+
geometry: csgGeometry(brushes.shinSlab).clone(),
|
|
365
|
+
materials: csgMaterials(brushes.shinSlab),
|
|
366
|
+
},
|
|
367
|
+
wallBrush: {
|
|
368
|
+
geometry: csgGeometry(brushes.wallBrush).clone(),
|
|
369
|
+
materials: csgMaterials(brushes.wallBrush),
|
|
370
|
+
},
|
|
371
|
+
innerBrush: {
|
|
372
|
+
geometry: csgGeometry(brushes.innerBrush).clone(),
|
|
373
|
+
materials: csgMaterials(brushes.innerBrush),
|
|
374
|
+
},
|
|
375
|
+
rakeBoards: brushes.rakeBoards?.clone() ?? null,
|
|
376
|
+
};
|
|
377
|
+
disposeCachedMergedRoofSegmentGeometrySet(cached);
|
|
378
|
+
mergedRoofSegmentGeometryCache.set(segmentId, nextCached);
|
|
379
|
+
const cloned = cloneCachedMergedRoofSegmentBrushes(nextCached);
|
|
380
|
+
disposeRoofSegmentBrushSet(brushes);
|
|
381
|
+
return cloned;
|
|
382
|
+
}
|
|
187
383
|
function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
188
384
|
const mergedMesh = group.getObjectByName('merged-roof');
|
|
189
385
|
if (!mergedMesh)
|
|
@@ -205,94 +401,23 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
205
401
|
.filter((n) => n !== undefined && !hasSegmentMaterialOverride(n));
|
|
206
402
|
if (children.length === 0) {
|
|
207
403
|
mergedMesh.geometry.dispose();
|
|
208
|
-
//
|
|
209
|
-
|
|
404
|
+
// Not BoxGeometry: its 6 groups against the merged mesh's 4-material array
|
|
405
|
+
// crash GLTFExporter (materials[4] → undefined) when the roof bakes.
|
|
406
|
+
mergedMesh.geometry = createDegenerateRoofPlaceholder();
|
|
210
407
|
return;
|
|
211
408
|
}
|
|
212
409
|
let totalShinSlab = null;
|
|
213
410
|
let totalDeckSlab = null;
|
|
214
411
|
let totalWall = null;
|
|
215
412
|
let totalInner = null;
|
|
413
|
+
const rakeBoardGeometries = [];
|
|
216
414
|
for (const child of children) {
|
|
217
|
-
const brushes =
|
|
415
|
+
const brushes = getMergedRoofSegmentBrushes(roofNode, child, nodes);
|
|
218
416
|
if (!brushes)
|
|
219
417
|
continue;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// `capabilities.roofAccessory.buildCut`) from shin / deck / wall
|
|
223
|
-
// before we accumulate. Mirrors roof-system v1 — the cut is built
|
|
224
|
-
// in segment-local, then carved out before the segment transform
|
|
225
|
-
// stacks on. Registry-driven so the viewer never names a kind.
|
|
226
|
-
let workingShin = brushes.shinSlab;
|
|
227
|
-
let workingDeck = brushes.deckSlab;
|
|
228
|
-
let workingWall = brushes.wallBrush;
|
|
229
|
-
for (const childElemId of child.children ?? []) {
|
|
230
|
-
const childElem = nodes[childElemId];
|
|
231
|
-
if (!childElem)
|
|
232
|
-
continue;
|
|
233
|
-
const meta = typeof childElem.metadata === 'object' && childElem.metadata !== null
|
|
234
|
-
? childElem.metadata
|
|
235
|
-
: undefined;
|
|
236
|
-
if (meta?.isTransient)
|
|
237
|
-
continue;
|
|
238
|
-
const childDef = nodeRegistry.get(childElem.type);
|
|
239
|
-
const buildCut = childDef?.capabilities?.roofAccessory?.buildCut;
|
|
240
|
-
if (!buildCut)
|
|
241
|
-
continue;
|
|
242
|
-
const cutGeo = buildCut(childElem, child);
|
|
243
|
-
if (!cutGeo)
|
|
244
|
-
continue;
|
|
245
|
-
// Wrap the kind-emitted geometry in a Brush. Kinds return raw
|
|
246
|
-
// shapes; the viewer welds (mandatory after rotations leave
|
|
247
|
-
// duplicated verts), attaches a single material group, and
|
|
248
|
-
// builds the bounds tree — keeping kind code free of
|
|
249
|
-
// three-bvh-csg / three-mesh-bvh imports.
|
|
250
|
-
const welded = mergeVertices(cutGeo, 1e-4);
|
|
251
|
-
cutGeo.dispose();
|
|
252
|
-
const idxCount = welded.getIndex()?.count ?? 0;
|
|
253
|
-
if (idxCount === 0) {
|
|
254
|
-
welded.dispose();
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
welded.clearGroups();
|
|
258
|
-
welded.addGroup(0, idxCount, 0);
|
|
259
|
-
welded.computeVertexNormals();
|
|
260
|
-
computeGeometryBoundsTree(welded);
|
|
261
|
-
const cut = new Brush(welded, dummyMats[0]);
|
|
262
|
-
cut.updateMatrixWorld();
|
|
263
|
-
try {
|
|
264
|
-
const nextShin = csgEvaluator.evaluate(workingShin, cut, SUBTRACTION);
|
|
265
|
-
workingShin.geometry.dispose();
|
|
266
|
-
prepareBrushForCSG(nextShin);
|
|
267
|
-
workingShin = nextShin;
|
|
268
|
-
const nextDeck = csgEvaluator.evaluate(workingDeck, cut, SUBTRACTION);
|
|
269
|
-
workingDeck.geometry.dispose();
|
|
270
|
-
prepareBrushForCSG(nextDeck);
|
|
271
|
-
workingDeck = nextDeck;
|
|
272
|
-
const nextWall = csgEvaluator.evaluate(workingWall, cut, SUBTRACTION);
|
|
273
|
-
workingWall.geometry.dispose();
|
|
274
|
-
prepareBrushForCSG(nextWall);
|
|
275
|
-
workingWall = nextWall;
|
|
276
|
-
}
|
|
277
|
-
catch (e) {
|
|
278
|
-
console.error(`[${childElem.type}] cut CSG failed:`, e);
|
|
279
|
-
}
|
|
280
|
-
finally {
|
|
281
|
-
cut.geometry.dispose();
|
|
282
|
-
}
|
|
418
|
+
if (brushes.rakeBoards) {
|
|
419
|
+
rakeBoardGeometries.push(brushes.rakeBoards);
|
|
283
420
|
}
|
|
284
|
-
brushes.shinSlab = workingShin;
|
|
285
|
-
brushes.deckSlab = workingDeck;
|
|
286
|
-
brushes.wallBrush = workingWall;
|
|
287
|
-
_matrix.compose(_position.set(child.position[0], child.position[1], child.position[2]), _quaternion.setFromAxisAngle(_yAxis, child.rotation), _scale);
|
|
288
|
-
const applyTransform = (brush) => {
|
|
289
|
-
csgGeometry(brush).applyMatrix4(_matrix);
|
|
290
|
-
brush.updateMatrixWorld();
|
|
291
|
-
};
|
|
292
|
-
applyTransform(brushes.shinSlab);
|
|
293
|
-
applyTransform(brushes.deckSlab);
|
|
294
|
-
applyTransform(brushes.wallBrush);
|
|
295
|
-
applyTransform(brushes.innerBrush);
|
|
296
421
|
if (totalShinSlab) {
|
|
297
422
|
const next = csgEvaluator.evaluate(totalShinSlab, brushes.shinSlab, ADDITION);
|
|
298
423
|
totalShinSlab.geometry.dispose();
|
|
@@ -336,26 +461,27 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
336
461
|
}
|
|
337
462
|
if (totalShinSlab && totalDeckSlab && totalWall && totalInner) {
|
|
338
463
|
try {
|
|
339
|
-
const finalShinTrimmed = csgEvaluator.evaluate(totalShinSlab, totalInner, SUBTRACTION);
|
|
340
|
-
const finalDeckTrimmed = csgEvaluator.evaluate(totalDeckSlab, totalInner, SUBTRACTION);
|
|
341
464
|
const finalWallTrimmed = csgEvaluator.evaluate(totalWall, totalInner, SUBTRACTION);
|
|
342
|
-
|
|
465
|
+
prepareBrushForCSG(finalWallTrimmed);
|
|
466
|
+
const shinDeck = csgEvaluator.evaluate(totalShinSlab, totalDeckSlab, ADDITION);
|
|
467
|
+
prepareBrushForCSG(shinDeck);
|
|
343
468
|
const combined = csgEvaluator.evaluate(shinDeck, finalWallTrimmed, ADDITION);
|
|
469
|
+
prepareBrushForCSG(combined);
|
|
344
470
|
const resultGeo = csgGeometry(combined);
|
|
345
|
-
if (
|
|
471
|
+
if (geometryHasInvalidAttributes(resultGeo)) {
|
|
346
472
|
if (!warnedMergedRoofNaNIds.has(roofNode.id)) {
|
|
347
|
-
console.warn('[RoofSystem] Skipping merged roof geometry with
|
|
473
|
+
console.warn('[RoofSystem] Skipping merged roof geometry with invalid attributes', roofNode.id);
|
|
348
474
|
warnedMergedRoofNaNIds.add(roofNode.id);
|
|
349
475
|
}
|
|
350
476
|
resultGeo.dispose();
|
|
351
|
-
finalShinTrimmed.geometry.dispose();
|
|
352
|
-
finalDeckTrimmed.geometry.dispose();
|
|
353
477
|
finalWallTrimmed.geometry.dispose();
|
|
354
478
|
shinDeck.geometry.dispose();
|
|
355
479
|
totalShinSlab.geometry.dispose();
|
|
356
480
|
totalDeckSlab.geometry.dispose();
|
|
357
481
|
totalWall.geometry.dispose();
|
|
358
482
|
totalInner.geometry.dispose();
|
|
483
|
+
for (const geometry of rakeBoardGeometries)
|
|
484
|
+
geometry.dispose();
|
|
359
485
|
return;
|
|
360
486
|
}
|
|
361
487
|
const resultMaterials = csgMaterials(combined);
|
|
@@ -368,12 +494,20 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
368
494
|
for (const g of resultGeo.groups) {
|
|
369
495
|
g.materialIndex = mapRoofGroupMaterialIndex(g.materialIndex, resultMaterials, matToIndex);
|
|
370
496
|
}
|
|
371
|
-
resultGeo
|
|
372
|
-
|
|
497
|
+
let finalGeo = resultGeo;
|
|
498
|
+
if (rakeBoardGeometries.length > 0) {
|
|
499
|
+
const merged = mergeGeometriesPreservingGroups([finalGeo, ...rakeBoardGeometries]);
|
|
500
|
+
if (merged) {
|
|
501
|
+
finalGeo.dispose();
|
|
502
|
+
finalGeo = merged;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
for (const geometry of rakeBoardGeometries)
|
|
506
|
+
geometry.dispose();
|
|
507
|
+
finalGeo.computeVertexNormals();
|
|
508
|
+
ensureRenderableGeometryAttributes(finalGeo);
|
|
373
509
|
mergedMesh.geometry.dispose();
|
|
374
|
-
mergedMesh.geometry =
|
|
375
|
-
finalShinTrimmed.geometry.dispose();
|
|
376
|
-
finalDeckTrimmed.geometry.dispose();
|
|
510
|
+
mergedMesh.geometry = finalGeo;
|
|
377
511
|
finalWallTrimmed.geometry.dispose();
|
|
378
512
|
shinDeck.geometry.dispose();
|
|
379
513
|
}
|
|
@@ -384,16 +518,40 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
384
518
|
totalDeckSlab.geometry.dispose();
|
|
385
519
|
totalWall.geometry.dispose();
|
|
386
520
|
totalInner.geometry.dispose();
|
|
521
|
+
for (const geometry of rakeBoardGeometries)
|
|
522
|
+
geometry.dispose();
|
|
387
523
|
}
|
|
388
524
|
}
|
|
389
|
-
function
|
|
525
|
+
function geometryHasInvalidAttributes(geometry) {
|
|
390
526
|
const position = geometry.getAttribute('position');
|
|
391
|
-
if (!position)
|
|
392
|
-
return
|
|
393
|
-
for (
|
|
394
|
-
|
|
527
|
+
if (!(position && position.count > 0))
|
|
528
|
+
return true;
|
|
529
|
+
for (const name of ['position', 'normal', 'uv', 'uv2']) {
|
|
530
|
+
const attribute = geometry.getAttribute(name);
|
|
531
|
+
if (!attribute)
|
|
532
|
+
continue;
|
|
533
|
+
for (let i = 0; i < attribute.array.length; i++) {
|
|
534
|
+
if (!Number.isFinite(attribute.array[i]))
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const index = geometry.getIndex();
|
|
539
|
+
if (!index || index.count === 0)
|
|
540
|
+
return true;
|
|
541
|
+
for (let i = 0; i < index.count; i++) {
|
|
542
|
+
const value = index.getX(i);
|
|
543
|
+
if (!Number.isInteger(value) || value < 0 || value >= position.count)
|
|
395
544
|
return true;
|
|
396
545
|
}
|
|
546
|
+
for (const group of geometry.groups) {
|
|
547
|
+
if (!Number.isInteger(group.start) ||
|
|
548
|
+
!Number.isInteger(group.count) ||
|
|
549
|
+
group.start < 0 ||
|
|
550
|
+
group.count <= 0 ||
|
|
551
|
+
group.start + group.count > index.count) {
|
|
552
|
+
return true;
|
|
553
|
+
}
|
|
554
|
+
}
|
|
397
555
|
return false;
|
|
398
556
|
}
|
|
399
557
|
/**
|
|
@@ -413,6 +571,64 @@ export const roofCsgDummyMats = [
|
|
|
413
571
|
// Internal alias kept so the surrounding file's many call sites don't churn.
|
|
414
572
|
const dummyMats = roofCsgDummyMats;
|
|
415
573
|
export const ROOF_MATERIAL_SLOT_COUNT = 4;
|
|
574
|
+
const mergedRoofSegmentGeometryCache = new Map();
|
|
575
|
+
function disposeCachedMergedRoofSegmentGeometrySet(cached) {
|
|
576
|
+
if (!cached)
|
|
577
|
+
return;
|
|
578
|
+
cached.deckSlab.geometry.dispose();
|
|
579
|
+
cached.shinSlab.geometry.dispose();
|
|
580
|
+
cached.wallBrush.geometry.dispose();
|
|
581
|
+
cached.innerBrush.geometry.dispose();
|
|
582
|
+
cached.rakeBoards?.dispose();
|
|
583
|
+
}
|
|
584
|
+
function disposeRoofSegmentBrushSet(brushes) {
|
|
585
|
+
brushes.deckSlab.geometry.dispose();
|
|
586
|
+
brushes.shinSlab.geometry.dispose();
|
|
587
|
+
brushes.wallBrush.geometry.dispose();
|
|
588
|
+
brushes.innerBrush.geometry.dispose();
|
|
589
|
+
brushes.rakeBoards?.dispose();
|
|
590
|
+
}
|
|
591
|
+
function cloneCachedBrush(cached) {
|
|
592
|
+
const brush = new Brush(cached.geometry.clone(), cached.materials);
|
|
593
|
+
prepareBrushForCSG(brush);
|
|
594
|
+
return brush;
|
|
595
|
+
}
|
|
596
|
+
function cloneCachedMergedRoofSegmentBrushes(cached) {
|
|
597
|
+
return {
|
|
598
|
+
deckSlab: cloneCachedBrush(cached.deckSlab),
|
|
599
|
+
shinSlab: cloneCachedBrush(cached.shinSlab),
|
|
600
|
+
wallBrush: cloneCachedBrush(cached.wallBrush),
|
|
601
|
+
innerBrush: cloneCachedBrush(cached.innerBrush),
|
|
602
|
+
rakeBoards: cached.rakeBoards?.clone() ?? null,
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
function getMergedRoofAccessoryCachePayload(segment, nodes) {
|
|
606
|
+
const payload = [];
|
|
607
|
+
for (const childElemId of segment.children ?? []) {
|
|
608
|
+
const storedChild = nodes[childElemId];
|
|
609
|
+
if (!storedChild)
|
|
610
|
+
continue;
|
|
611
|
+
const childElem = getEffectiveNode(storedChild);
|
|
612
|
+
const meta = typeof childElem.metadata === 'object' && childElem.metadata !== null
|
|
613
|
+
? childElem.metadata
|
|
614
|
+
: undefined;
|
|
615
|
+
if (meta?.isTransient)
|
|
616
|
+
continue;
|
|
617
|
+
const childDef = nodeRegistry.get(childElem.type);
|
|
618
|
+
if (!childDef?.capabilities?.roofAccessory?.buildCut)
|
|
619
|
+
continue;
|
|
620
|
+
payload.push(childElem);
|
|
621
|
+
}
|
|
622
|
+
return payload;
|
|
623
|
+
}
|
|
624
|
+
function getMergedRoofSegmentCacheKey(roofNode, segment, nodes) {
|
|
625
|
+
return JSON.stringify({
|
|
626
|
+
roofPosition: roofNode.position ?? [0, 0, 0],
|
|
627
|
+
roofRotation: roofNode.rotation ?? 0,
|
|
628
|
+
segment,
|
|
629
|
+
accessories: getMergedRoofAccessoryCachePayload(segment, nodes),
|
|
630
|
+
});
|
|
631
|
+
}
|
|
416
632
|
export function mapRoofGroupMaterialIndex(groupMaterialIndex, csgMaterials, matToIndex) {
|
|
417
633
|
if (groupMaterialIndex === undefined)
|
|
418
634
|
return 0;
|
|
@@ -443,9 +659,290 @@ function normalizeRoofMaterialIndex(materialIndex) {
|
|
|
443
659
|
return 0;
|
|
444
660
|
return normalized;
|
|
445
661
|
}
|
|
662
|
+
function remapDutchRakeBoardMaterials(geometry) {
|
|
663
|
+
const position = geometry.getAttribute('position');
|
|
664
|
+
if (!position)
|
|
665
|
+
return;
|
|
666
|
+
const index = geometry.getIndex();
|
|
667
|
+
const triangleCount = (index?.count ?? position.count) / 3;
|
|
668
|
+
if (!Number.isFinite(triangleCount) || triangleCount <= 0)
|
|
669
|
+
return;
|
|
670
|
+
const a = new THREE.Vector3();
|
|
671
|
+
const b = new THREE.Vector3();
|
|
672
|
+
const c = new THREE.Vector3();
|
|
673
|
+
const ab = new THREE.Vector3();
|
|
674
|
+
const ac = new THREE.Vector3();
|
|
675
|
+
const normal = new THREE.Vector3();
|
|
676
|
+
const triangleMaterials = new Array(triangleCount).fill(DUTCH_RAKE_SIDE_MATERIAL_INDEX);
|
|
677
|
+
for (let triangleIndex = 0; triangleIndex < triangleCount; triangleIndex += 1) {
|
|
678
|
+
const offset = triangleIndex * 3;
|
|
679
|
+
const ia = index ? index.getX(offset) : offset;
|
|
680
|
+
const ib = index ? index.getX(offset + 1) : offset + 1;
|
|
681
|
+
const ic = index ? index.getX(offset + 2) : offset + 2;
|
|
682
|
+
a.fromBufferAttribute(position, ia);
|
|
683
|
+
b.fromBufferAttribute(position, ib);
|
|
684
|
+
c.fromBufferAttribute(position, ic);
|
|
685
|
+
ab.subVectors(b, a);
|
|
686
|
+
ac.subVectors(c, a);
|
|
687
|
+
normal.crossVectors(ab, ac).normalize();
|
|
688
|
+
triangleMaterials[triangleIndex] =
|
|
689
|
+
Math.abs(normal.y) > SHINGLE_SURFACE_EPSILON
|
|
690
|
+
? DUTCH_RAKE_TOP_MATERIAL_INDEX
|
|
691
|
+
: DUTCH_RAKE_SIDE_MATERIAL_INDEX;
|
|
692
|
+
}
|
|
693
|
+
geometry.clearGroups();
|
|
694
|
+
let currentMaterial = triangleMaterials[0] ?? DUTCH_RAKE_SIDE_MATERIAL_INDEX;
|
|
695
|
+
let groupStart = 0;
|
|
696
|
+
for (let triangleIndex = 1; triangleIndex < triangleCount; triangleIndex += 1) {
|
|
697
|
+
const materialIndex = triangleMaterials[triangleIndex] ?? DUTCH_RAKE_SIDE_MATERIAL_INDEX;
|
|
698
|
+
if (materialIndex === currentMaterial)
|
|
699
|
+
continue;
|
|
700
|
+
geometry.addGroup(groupStart * 3, (triangleIndex - groupStart) * 3, currentMaterial);
|
|
701
|
+
groupStart = triangleIndex;
|
|
702
|
+
currentMaterial = materialIndex;
|
|
703
|
+
}
|
|
704
|
+
geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
|
|
705
|
+
}
|
|
446
706
|
const SHINGLE_SURFACE_EPSILON = 0.02;
|
|
447
707
|
const RAKE_FACE_NORMAL_EPSILON = 0.3;
|
|
448
708
|
const RAKE_FACE_ALIGNMENT_EPSILON = 0.35;
|
|
709
|
+
const TRIM_CUT_EPSILON = 0.002;
|
|
710
|
+
const DUTCH_RAKE_SIDE_MATERIAL_INDEX = 1;
|
|
711
|
+
const DUTCH_RAKE_TOP_MATERIAL_INDEX = 3;
|
|
712
|
+
const DUTCH_RAKE_SLOPE_SEAT_OFFSET = 0.0002;
|
|
713
|
+
function pushDoubleSidedFace(targetFaces, face) {
|
|
714
|
+
targetFaces.push(face);
|
|
715
|
+
targetFaces.push(face.map((point) => point.clone()).reverse());
|
|
716
|
+
}
|
|
717
|
+
function hasSegmentTrim(node) {
|
|
718
|
+
const trim = normalizeRoofSegmentTrim(node);
|
|
719
|
+
return (trim.left > 0 ||
|
|
720
|
+
trim.right > 0 ||
|
|
721
|
+
trim.front > 0 ||
|
|
722
|
+
trim.back > 0 ||
|
|
723
|
+
trim.frontLeft > 0 ||
|
|
724
|
+
trim.frontRight > 0 ||
|
|
725
|
+
trim.backLeft > 0 ||
|
|
726
|
+
trim.backRight > 0 ||
|
|
727
|
+
trim.frontLeftX > 0 ||
|
|
728
|
+
trim.frontLeftZ > 0 ||
|
|
729
|
+
trim.frontRightX > 0 ||
|
|
730
|
+
trim.frontRightZ > 0 ||
|
|
731
|
+
trim.backLeftX > 0 ||
|
|
732
|
+
trim.backLeftZ > 0 ||
|
|
733
|
+
trim.backRightX > 0 ||
|
|
734
|
+
trim.backRightZ > 0);
|
|
735
|
+
}
|
|
736
|
+
// Material slot the freshly exposed trim plane is assigned to. Slot 0 is the
|
|
737
|
+
// wall/trim band — the same finish the gable walls render with (concrete-drywall
|
|
738
|
+
// by default, continuous with the walls below). `remapRoofShellFaces` does not
|
|
739
|
+
// reclassify slot 0, so any new interior face CSG carves out of a BoxGeometry
|
|
740
|
+
// cutter lands on the wall band regardless of which box face it came from.
|
|
741
|
+
// Without this the box's default 6 groups (materialIndex 0..5) collapse via
|
|
742
|
+
// mod-4 and `remapRoofShellFaces` would reshuffle the vertical ones across
|
|
743
|
+
// slots. Accessories still clamp the slot via `useSegmentTrimClippedGeometry`
|
|
744
|
+
// when they expose fewer material slots.
|
|
745
|
+
const TRIM_CUT_MATERIAL_SLOT = 0;
|
|
746
|
+
function assignTrimCutterSlot(geometry) {
|
|
747
|
+
geometry.clearGroups();
|
|
748
|
+
const count = geometry.index ? geometry.index.count : geometry.getAttribute('position').count;
|
|
749
|
+
geometry.addGroup(0, count, TRIM_CUT_MATERIAL_SLOT);
|
|
750
|
+
}
|
|
751
|
+
function buildTrimCutBrush(minX, maxX, minZ, maxZ, minY, maxY) {
|
|
752
|
+
const width = maxX - minX;
|
|
753
|
+
const height = maxY - minY;
|
|
754
|
+
const depth = maxZ - minZ;
|
|
755
|
+
if (!(width > 0 && height > 0 && depth > 0))
|
|
756
|
+
return null;
|
|
757
|
+
const geometry = new THREE.BoxGeometry(width, height, depth);
|
|
758
|
+
geometry.translate((minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2);
|
|
759
|
+
// World-metre UVs on the cutter so the newly exposed interior faces produced
|
|
760
|
+
// by CSG inherit tiled UVs (1 uv unit per metre) instead of the box's default
|
|
761
|
+
// 0→1-per-face, which would stretch the finish to fit each cut face.
|
|
762
|
+
applyWorldScaleBoxUVs(geometry, width, height, depth);
|
|
763
|
+
assignTrimCutterSlot(geometry);
|
|
764
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
765
|
+
computeGeometryBoundsTree(geometry);
|
|
766
|
+
const cut = new Brush(geometry, dummyMats);
|
|
767
|
+
cut.updateMatrixWorld();
|
|
768
|
+
return cut;
|
|
769
|
+
}
|
|
770
|
+
function buildDiagonalTrimCutBrush(bounds, lineA, lineB, outsidePoint) {
|
|
771
|
+
const dx = lineB[0] - lineA[0];
|
|
772
|
+
const dz = lineB[1] - lineA[1];
|
|
773
|
+
const lineLength = Math.hypot(dx, dz);
|
|
774
|
+
const height = bounds.maxY - bounds.minY;
|
|
775
|
+
if (!(lineLength > 0 && height > 0))
|
|
776
|
+
return null;
|
|
777
|
+
const ux = dx / lineLength;
|
|
778
|
+
const uz = dz / lineLength;
|
|
779
|
+
let nx = -uz;
|
|
780
|
+
let nz = ux;
|
|
781
|
+
const midX = (lineA[0] + lineB[0]) / 2;
|
|
782
|
+
const midZ = (lineA[1] + lineB[1]) / 2;
|
|
783
|
+
const toOutsideX = outsidePoint[0] - midX;
|
|
784
|
+
const toOutsideZ = outsidePoint[1] - midZ;
|
|
785
|
+
let normalFlipped = false;
|
|
786
|
+
if (nx * toOutsideX + nz * toOutsideZ < 0) {
|
|
787
|
+
nx *= -1;
|
|
788
|
+
nz *= -1;
|
|
789
|
+
normalFlipped = true;
|
|
790
|
+
}
|
|
791
|
+
const boundsDiagonal = Math.hypot(bounds.maxX - bounds.minX, bounds.maxZ - bounds.minZ);
|
|
792
|
+
const cutterLength = Math.max(lineLength, boundsDiagonal) * 2;
|
|
793
|
+
const cutterDepth = boundsDiagonal * 2;
|
|
794
|
+
const centerX = midX + nx * (cutterDepth / 2 - TRIM_CUT_EPSILON);
|
|
795
|
+
const centerZ = midZ + nz * (cutterDepth / 2 - TRIM_CUT_EPSILON);
|
|
796
|
+
const yaw = Math.atan2(-uz, ux) + (normalFlipped ? Math.PI : 0);
|
|
797
|
+
const geometry = new THREE.BoxGeometry(cutterLength, height, cutterDepth);
|
|
798
|
+
geometry.rotateY(yaw);
|
|
799
|
+
geometry.translate(centerX, (bounds.minY + bounds.maxY) / 2, centerZ);
|
|
800
|
+
// World-metre UVs so the diagonal cut's interior face inherits tiled UVs.
|
|
801
|
+
applyWorldScaleBoxUVs(geometry, cutterLength, height, cutterDepth);
|
|
802
|
+
assignTrimCutterSlot(geometry);
|
|
803
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
804
|
+
computeGeometryBoundsTree(geometry);
|
|
805
|
+
const cut = new Brush(geometry, dummyMats);
|
|
806
|
+
cut.updateMatrixWorld();
|
|
807
|
+
return cut;
|
|
808
|
+
}
|
|
809
|
+
function subtractCutFromSegmentBrushes(brushes, cut) {
|
|
810
|
+
for (const key of ['shinSlab', 'deckSlab', 'wallBrush', 'innerBrush']) {
|
|
811
|
+
const next = csgEvaluator.evaluate(brushes[key], cut, SUBTRACTION);
|
|
812
|
+
brushes[key].geometry.dispose();
|
|
813
|
+
prepareBrushForCSG(next);
|
|
814
|
+
brushes[key] = next;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
function buildSegmentTrimCutBrushes(segment) {
|
|
818
|
+
const trim = normalizeRoofSegmentTrim(segment);
|
|
819
|
+
if (trim.left === 0 &&
|
|
820
|
+
trim.right === 0 &&
|
|
821
|
+
trim.front === 0 &&
|
|
822
|
+
trim.back === 0 &&
|
|
823
|
+
trim.frontLeft === 0 &&
|
|
824
|
+
trim.frontRight === 0 &&
|
|
825
|
+
trim.backLeft === 0 &&
|
|
826
|
+
trim.backRight === 0 &&
|
|
827
|
+
trim.frontLeftX === 0 &&
|
|
828
|
+
trim.frontLeftZ === 0 &&
|
|
829
|
+
trim.frontRightX === 0 &&
|
|
830
|
+
trim.frontRightZ === 0 &&
|
|
831
|
+
trim.backLeftX === 0 &&
|
|
832
|
+
trim.backLeftZ === 0 &&
|
|
833
|
+
trim.backRightX === 0 &&
|
|
834
|
+
trim.backRightZ === 0) {
|
|
835
|
+
return [];
|
|
836
|
+
}
|
|
837
|
+
const { activeRh } = getSegmentSlopeFrame(segment);
|
|
838
|
+
const extra = segment.wallThickness + segment.overhang + segment.deckThickness + segment.shingleThickness + 2;
|
|
839
|
+
const minX = -segment.width / 2 - extra;
|
|
840
|
+
const maxX = segment.width / 2 + extra;
|
|
841
|
+
const minZ = -segment.depth / 2 - extra;
|
|
842
|
+
const maxZ = segment.depth / 2 + extra;
|
|
843
|
+
const minY = -2;
|
|
844
|
+
const maxY = segment.wallHeight + activeRh + segment.deckThickness + segment.shingleThickness + 2;
|
|
845
|
+
const cuts = [];
|
|
846
|
+
if (trim.left > 0) {
|
|
847
|
+
const planeX = -segment.width / 2 + trim.left;
|
|
848
|
+
const cut = buildTrimCutBrush(minX, planeX + TRIM_CUT_EPSILON, minZ, maxZ, minY, maxY);
|
|
849
|
+
if (cut)
|
|
850
|
+
cuts.push(cut);
|
|
851
|
+
}
|
|
852
|
+
if (trim.right > 0) {
|
|
853
|
+
const planeX = segment.width / 2 - trim.right;
|
|
854
|
+
const cut = buildTrimCutBrush(planeX - TRIM_CUT_EPSILON, maxX, minZ, maxZ, minY, maxY);
|
|
855
|
+
if (cut)
|
|
856
|
+
cuts.push(cut);
|
|
857
|
+
}
|
|
858
|
+
if (trim.front > 0) {
|
|
859
|
+
const planeZ = segment.depth / 2 - trim.front;
|
|
860
|
+
const cut = buildTrimCutBrush(minX, maxX, planeZ - TRIM_CUT_EPSILON, maxZ, minY, maxY);
|
|
861
|
+
if (cut)
|
|
862
|
+
cuts.push(cut);
|
|
863
|
+
}
|
|
864
|
+
if (trim.back > 0) {
|
|
865
|
+
const planeZ = -segment.depth / 2 + trim.back;
|
|
866
|
+
const cut = buildTrimCutBrush(minX, maxX, minZ, planeZ + TRIM_CUT_EPSILON, minY, maxY);
|
|
867
|
+
if (cut)
|
|
868
|
+
cuts.push(cut);
|
|
869
|
+
}
|
|
870
|
+
const leftX = -segment.width / 2 + trim.left;
|
|
871
|
+
const rightX = segment.width / 2 - trim.right;
|
|
872
|
+
const frontZ = segment.depth / 2 - trim.front;
|
|
873
|
+
const backZ = -segment.depth / 2 + trim.back;
|
|
874
|
+
if (trim.frontLeftX > 0 && trim.frontLeftZ > 0) {
|
|
875
|
+
const cut = buildDiagonalTrimCutBrush({ minX, maxX, minZ, maxZ, minY, maxY }, [leftX + trim.frontLeftX + TRIM_CUT_EPSILON, frontZ], [leftX, frontZ - trim.frontLeftZ - TRIM_CUT_EPSILON], [minX, maxZ]);
|
|
876
|
+
if (cut)
|
|
877
|
+
cuts.push(cut);
|
|
878
|
+
}
|
|
879
|
+
if (trim.frontRightX > 0 && trim.frontRightZ > 0) {
|
|
880
|
+
const cut = buildDiagonalTrimCutBrush({ minX, maxX, minZ, maxZ, minY, maxY }, [rightX, frontZ - trim.frontRightZ - TRIM_CUT_EPSILON], [rightX - trim.frontRightX - TRIM_CUT_EPSILON, frontZ], [maxX, maxZ]);
|
|
881
|
+
if (cut)
|
|
882
|
+
cuts.push(cut);
|
|
883
|
+
}
|
|
884
|
+
if (trim.backLeftX > 0 && trim.backLeftZ > 0) {
|
|
885
|
+
const cut = buildDiagonalTrimCutBrush({ minX, maxX, minZ, maxZ, minY, maxY }, [leftX, backZ + trim.backLeftZ + TRIM_CUT_EPSILON], [leftX + trim.backLeftX + TRIM_CUT_EPSILON, backZ], [minX, minZ]);
|
|
886
|
+
if (cut)
|
|
887
|
+
cuts.push(cut);
|
|
888
|
+
}
|
|
889
|
+
if (trim.backRightX > 0 && trim.backRightZ > 0) {
|
|
890
|
+
const cut = buildDiagonalTrimCutBrush({ minX, maxX, minZ, maxZ, minY, maxY }, [rightX - trim.backRightX - TRIM_CUT_EPSILON, backZ], [rightX, backZ + trim.backRightZ + TRIM_CUT_EPSILON], [maxX, minZ]);
|
|
891
|
+
if (cut)
|
|
892
|
+
cuts.push(cut);
|
|
893
|
+
}
|
|
894
|
+
return cuts;
|
|
895
|
+
}
|
|
896
|
+
function subtractSegmentTrimCuts(brushes, segment) {
|
|
897
|
+
const cuts = buildSegmentTrimCutBrushes(segment);
|
|
898
|
+
for (const cut of cuts) {
|
|
899
|
+
try {
|
|
900
|
+
subtractCutFromSegmentBrushes(brushes, cut);
|
|
901
|
+
}
|
|
902
|
+
catch (e) {
|
|
903
|
+
console.error('Roof trim CSG failed:', e);
|
|
904
|
+
}
|
|
905
|
+
finally {
|
|
906
|
+
cut.geometry.dispose();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* Subtract a segment's trim cuts from an arbitrary segment-local geometry,
|
|
912
|
+
* returning the clipped result. The input geometry is consumed (disposed) on
|
|
913
|
+
* each successful CSG pass — callers that need to keep the original must pass a
|
|
914
|
+
* clone. Returns the input untouched when the segment has no trim. Used
|
|
915
|
+
* internally for rake-board / end-slope attachments and exported so roof
|
|
916
|
+
* accessories (chimney, vents, skylight, …) can clip their own meshes by the
|
|
917
|
+
* same trim, in the same segment-local frame.
|
|
918
|
+
*/
|
|
919
|
+
export function clipGeometryBySegmentTrim(geometry, segment) {
|
|
920
|
+
if (!geometry)
|
|
921
|
+
return null;
|
|
922
|
+
const cuts = buildSegmentTrimCutBrushes(segment);
|
|
923
|
+
if (cuts.length === 0)
|
|
924
|
+
return geometry;
|
|
925
|
+
let currentGeometry = geometry;
|
|
926
|
+
for (const cut of cuts) {
|
|
927
|
+
try {
|
|
928
|
+
const brush = new Brush(currentGeometry, dummyMats);
|
|
929
|
+
prepareBrushForCSG(brush);
|
|
930
|
+
const next = csgEvaluator.evaluate(brush, cut, SUBTRACTION);
|
|
931
|
+
const trimmed = csgGeometry(next).clone();
|
|
932
|
+
currentGeometry.dispose();
|
|
933
|
+
next.geometry.dispose();
|
|
934
|
+
ensureRenderableGeometryAttributes(trimmed);
|
|
935
|
+
currentGeometry = trimmed;
|
|
936
|
+
}
|
|
937
|
+
catch (e) {
|
|
938
|
+
console.error('Roof trim CSG failed for attachment geometry:', e);
|
|
939
|
+
}
|
|
940
|
+
finally {
|
|
941
|
+
cut.geometry.dispose();
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return currentGeometry;
|
|
945
|
+
}
|
|
449
946
|
/**
|
|
450
947
|
* Generate complete hollow-shell geometry for a roof segment.
|
|
451
948
|
* Ports the prototype's CSG approach using three-bvh-csg.
|
|
@@ -453,13 +950,19 @@ const RAKE_FACE_ALIGNMENT_EPSILON = 0.35;
|
|
|
453
950
|
export function getRoofSegmentBrushes(node) {
|
|
454
951
|
const { roofType, width, depth, wallHeight, wallThickness, deckThickness, overhang, shingleThickness, } = node;
|
|
455
952
|
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node);
|
|
456
|
-
const shapeRatios = {
|
|
953
|
+
const shapeRatios = getRoofShapeRatios({
|
|
457
954
|
gambrelLowerWidthRatio: node.gambrelLowerWidthRatio,
|
|
458
955
|
mansardSteepWidthRatio: node.mansardSteepWidthRatio,
|
|
459
956
|
dutchHipWidthRatio: node.dutchHipWidthRatio,
|
|
460
|
-
|
|
957
|
+
dutchHipHeightRatio: node.dutchHipHeightRatio,
|
|
958
|
+
dutchWaistLengthRatio: node.dutchWaistLengthRatio,
|
|
959
|
+
dutchGabletRake: node.dutchGabletRake,
|
|
960
|
+
});
|
|
461
961
|
const verticalRt = activeRh > 0 ? deckThickness / cosTheta : deckThickness;
|
|
462
|
-
|
|
962
|
+
// Gablet inset must track dutchHipWidthRatio so the 3D waist matches both
|
|
963
|
+
// the 2D floorplan and the slope frame (which derives activeRh from the
|
|
964
|
+
// same ratio). A hardcoded 0.25 desyncs the gablet from the parameter.
|
|
965
|
+
const baseI = Math.min(width, depth) * node.dutchHipWidthRatio;
|
|
463
966
|
const getVol = (wExt, vOffset, baseY, matIndex, isVoid) => {
|
|
464
967
|
const wV = Math.max(0.01, width + 2 * wExt);
|
|
465
968
|
const dV = Math.max(0.01, depth + 2 * wExt);
|
|
@@ -476,7 +979,20 @@ export function getRoofSegmentBrushes(node) {
|
|
|
476
979
|
if (isVoid) {
|
|
477
980
|
structuralI += deckThickness;
|
|
478
981
|
}
|
|
479
|
-
const faces =
|
|
982
|
+
const faces = getRoofModuleFaces({
|
|
983
|
+
type: roofType,
|
|
984
|
+
w: wV,
|
|
985
|
+
d: dV,
|
|
986
|
+
wh: whV,
|
|
987
|
+
rh: rhV,
|
|
988
|
+
baseY: safeBaseY,
|
|
989
|
+
insets: { dutchI: structuralI },
|
|
990
|
+
baseW: width,
|
|
991
|
+
baseD: depth,
|
|
992
|
+
tanTheta,
|
|
993
|
+
shapeRatios,
|
|
994
|
+
dutchTopRakeThickness: node.dutchTopRakeThickness,
|
|
995
|
+
}).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
|
|
480
996
|
return createGeometryFromFaces(faces, matIndex);
|
|
481
997
|
};
|
|
482
998
|
const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false);
|
|
@@ -522,40 +1038,69 @@ export function getRoofSegmentBrushes(node) {
|
|
|
522
1038
|
const dropBot = Math.min(2.0, maxDrop * 0.8);
|
|
523
1039
|
const topBaseY = shinBotWh - dropTop;
|
|
524
1040
|
const botBaseY = shinBotWh - dropBot;
|
|
525
|
-
const
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
};
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
1041
|
+
const insetsBot = getRoofShapeInsets({
|
|
1042
|
+
roofType,
|
|
1043
|
+
width,
|
|
1044
|
+
depth,
|
|
1045
|
+
wh: shinBotWh,
|
|
1046
|
+
baseY: botBaseY,
|
|
1047
|
+
isVoid: true,
|
|
1048
|
+
brushW: shinBotW,
|
|
1049
|
+
brushD: shinBotD,
|
|
1050
|
+
tanTheta,
|
|
1051
|
+
shingleThickness,
|
|
1052
|
+
dutchHipWidthRatio: node.dutchHipWidthRatio,
|
|
1053
|
+
});
|
|
1054
|
+
const insetsTop = getRoofShapeInsets({
|
|
1055
|
+
roofType,
|
|
1056
|
+
width,
|
|
1057
|
+
depth,
|
|
1058
|
+
wh: shinTopWh,
|
|
1059
|
+
baseY: topBaseY,
|
|
1060
|
+
isVoid: false,
|
|
1061
|
+
brushW: shinTopW,
|
|
1062
|
+
brushD: shinTopD,
|
|
1063
|
+
tanTheta,
|
|
1064
|
+
shingleThickness,
|
|
1065
|
+
dutchHipWidthRatio: node.dutchHipWidthRatio,
|
|
1066
|
+
});
|
|
1067
|
+
const botFaces = getRoofModuleFaces({
|
|
1068
|
+
type: roofType,
|
|
1069
|
+
w: shinBotW,
|
|
1070
|
+
d: shinBotD,
|
|
1071
|
+
wh: shinBotWh,
|
|
1072
|
+
rh: shinBotRh,
|
|
1073
|
+
baseY: botBaseY,
|
|
1074
|
+
insets: insetsBot,
|
|
1075
|
+
baseW: width,
|
|
1076
|
+
baseD: depth,
|
|
1077
|
+
tanTheta,
|
|
1078
|
+
shapeRatios,
|
|
1079
|
+
dutchTopRakeThickness: node.dutchTopRakeThickness,
|
|
1080
|
+
}).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
|
|
1081
|
+
const topFaces = getRoofModuleFaces({
|
|
1082
|
+
type: roofType,
|
|
1083
|
+
w: shinTopW,
|
|
1084
|
+
d: shinTopD,
|
|
1085
|
+
wh: shinTopWh,
|
|
1086
|
+
rh: shinTopRh,
|
|
1087
|
+
baseY: topBaseY,
|
|
1088
|
+
insets: insetsTop,
|
|
1089
|
+
baseW: width,
|
|
1090
|
+
baseD: depth,
|
|
1091
|
+
tanTheta,
|
|
1092
|
+
shapeRatios,
|
|
1093
|
+
dutchTopRakeThickness: node.dutchTopRakeThickness,
|
|
1094
|
+
}).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
|
|
1095
|
+
let rakeBoards = null;
|
|
1096
|
+
if (roofType === 'dutch' && insetsTop.dutchI !== undefined) {
|
|
1097
|
+
rakeBoards = buildDutchRakeBoards(shinTopW, shinTopD, shinTopWh, shinTopRh, insetsTop.dutchI, shapeRatios, node.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake, node.dutchTopRakeThickness ?? ROOF_SHAPE_DEFAULTS.dutchTopRakeThickness);
|
|
1098
|
+
}
|
|
1099
|
+
const shinBotGeo = createGeometryFromFaces(botFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : 1);
|
|
556
1100
|
const shinTopGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : 1);
|
|
557
1101
|
if (transZ !== 0) {
|
|
558
1102
|
shinTopGeo.translate(0, 0, transZ);
|
|
1103
|
+
rakeBoards?.translate(0, 0, transZ);
|
|
559
1104
|
}
|
|
560
1105
|
const toBrush = (geo) => {
|
|
561
1106
|
if (!geo?.attributes.position || geo.attributes.position.count === 0)
|
|
@@ -567,6 +1112,9 @@ export function getRoofSegmentBrushes(node) {
|
|
|
567
1112
|
geo.groups = geo.groups.filter((g) => g.count > 0);
|
|
568
1113
|
if (geo.groups.length === 0)
|
|
569
1114
|
return null;
|
|
1115
|
+
ensureRenderableGeometryAttributes(geo);
|
|
1116
|
+
if (geometryHasInvalidAttributes(geo))
|
|
1117
|
+
return null;
|
|
570
1118
|
computeGeometryBoundsTree(geo);
|
|
571
1119
|
const brush = new Brush(geo, dummyMats);
|
|
572
1120
|
brush.updateMatrixWorld();
|
|
@@ -606,12 +1154,32 @@ export function getRoofSegmentBrushes(node) {
|
|
|
606
1154
|
if (deckTopBrush && deckBotBrush && wallBrush && innerBrush && shinTopBrush && shinBotBrush) {
|
|
607
1155
|
try {
|
|
608
1156
|
const deckSlab = csgEvaluator.evaluate(deckTopBrush, deckBotBrush, SUBTRACTION);
|
|
1157
|
+
prepareBrushForCSG(deckSlab);
|
|
609
1158
|
const shinSlab = csgEvaluator.evaluate(shinTopBrush, shinBotBrush, SUBTRACTION);
|
|
1159
|
+
prepareBrushForCSG(shinSlab);
|
|
610
1160
|
deckTopBrush.geometry.dispose();
|
|
611
1161
|
deckBotBrush.geometry.dispose();
|
|
612
1162
|
shinTopBrush.geometry.dispose();
|
|
613
1163
|
shinBotBrush.geometry.dispose();
|
|
614
|
-
|
|
1164
|
+
const brushes = {
|
|
1165
|
+
deckSlab,
|
|
1166
|
+
shinSlab,
|
|
1167
|
+
wallBrush,
|
|
1168
|
+
innerBrush,
|
|
1169
|
+
rakeBoards,
|
|
1170
|
+
};
|
|
1171
|
+
if (hasSegmentTrim(node)) {
|
|
1172
|
+
subtractSegmentTrimCuts(brushes, node);
|
|
1173
|
+
brushes.rakeBoards = clipGeometryBySegmentTrim(brushes.rakeBoards, node);
|
|
1174
|
+
// The clip is a CSG subtraction: rake faces can come back as
|
|
1175
|
+
// `slot + 4n` because the cutter contributes its own material array.
|
|
1176
|
+
// Preserve top roof-material faces (slot 3) and force only cutter /
|
|
1177
|
+
// side faces back to the rake side material.
|
|
1178
|
+
if (brushes.rakeBoards) {
|
|
1179
|
+
remapDutchRakeBoardMaterials(brushes.rakeBoards);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return brushes;
|
|
615
1183
|
}
|
|
616
1184
|
catch (e) {
|
|
617
1185
|
console.error('CSG prep failed:', e);
|
|
@@ -629,21 +1197,37 @@ export function getRoofSegmentBrushes(node) {
|
|
|
629
1197
|
wallBrush.geometry.dispose();
|
|
630
1198
|
if (innerBrush)
|
|
631
1199
|
innerBrush.geometry.dispose();
|
|
1200
|
+
rakeBoards?.dispose();
|
|
632
1201
|
return null;
|
|
633
1202
|
}
|
|
634
|
-
export function generateRoofSegmentGeometry(node) {
|
|
635
|
-
const
|
|
1203
|
+
export function generateRoofSegmentGeometry(node, nodes) {
|
|
1204
|
+
const parentRoof = node.parentId ? nodes?.[node.parentId] : undefined;
|
|
1205
|
+
const parentRoofPosition = parentRoof && 'position' in parentRoof ? parentRoof.position : undefined;
|
|
1206
|
+
const parentRoofRotation = parentRoof && 'rotation' in parentRoof
|
|
1207
|
+
? (parentRoof.rotation ?? 0)
|
|
1208
|
+
: 0;
|
|
1209
|
+
const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(parentRoofPosition, parentRoofRotation, node.position, node.rotation ?? 0), () => getRoofSegmentBrushes(node));
|
|
636
1210
|
if (!brushes) {
|
|
637
1211
|
// Fallback: simple box
|
|
638
1212
|
return new THREE.BoxGeometry(node.width, node.wallHeight, node.depth);
|
|
639
1213
|
}
|
|
640
|
-
|
|
1214
|
+
if (nodes) {
|
|
1215
|
+
subtractAccessoryCuts(brushes, node, nodes);
|
|
1216
|
+
}
|
|
1217
|
+
const { deckSlab, shinSlab, wallBrush, innerBrush, rakeBoards } = brushes;
|
|
641
1218
|
let resultGeo = new THREE.BufferGeometry();
|
|
642
1219
|
try {
|
|
643
1220
|
const hollowWall = csgEvaluator.evaluate(wallBrush, innerBrush, SUBTRACTION);
|
|
1221
|
+
prepareBrushForCSG(hollowWall);
|
|
644
1222
|
const shinDeck = csgEvaluator.evaluate(shinSlab, deckSlab, ADDITION);
|
|
1223
|
+
prepareBrushForCSG(shinDeck);
|
|
645
1224
|
const combined = csgEvaluator.evaluate(shinDeck, hollowWall, ADDITION);
|
|
1225
|
+
prepareBrushForCSG(combined);
|
|
646
1226
|
resultGeo = csgGeometry(combined);
|
|
1227
|
+
if (geometryHasInvalidAttributes(resultGeo)) {
|
|
1228
|
+
resultGeo.dispose();
|
|
1229
|
+
resultGeo = csgGeometry(wallBrush).clone();
|
|
1230
|
+
}
|
|
647
1231
|
const resultMaterials = csgMaterials(combined);
|
|
648
1232
|
const matToIndex = new Map([
|
|
649
1233
|
[dummyMats[0], 0],
|
|
@@ -666,10 +1250,141 @@ export function generateRoofSegmentGeometry(node) {
|
|
|
666
1250
|
shinSlab.geometry.dispose();
|
|
667
1251
|
wallBrush.geometry.dispose();
|
|
668
1252
|
innerBrush.geometry.dispose();
|
|
1253
|
+
if (rakeBoards) {
|
|
1254
|
+
const merged = mergeGeometriesPreservingGroups([resultGeo, rakeBoards]);
|
|
1255
|
+
rakeBoards.dispose();
|
|
1256
|
+
if (merged) {
|
|
1257
|
+
resultGeo.dispose();
|
|
1258
|
+
resultGeo = merged;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
669
1261
|
resultGeo.computeVertexNormals();
|
|
670
|
-
|
|
1262
|
+
ensureRenderableGeometryAttributes(resultGeo);
|
|
671
1263
|
return resultGeo;
|
|
672
1264
|
}
|
|
1265
|
+
function createGeometryFromRawAttributes(positions, normals, uvs, groups) {
|
|
1266
|
+
const geometry = new THREE.BufferGeometry();
|
|
1267
|
+
geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
1268
|
+
geometry.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
|
|
1269
|
+
geometry.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
1270
|
+
geometry.setIndex(Array.from({ length: positions.length / 3 }, (_, i) => i));
|
|
1271
|
+
for (const group of groups) {
|
|
1272
|
+
geometry.addGroup(group.start, group.count, group.materialIndex);
|
|
1273
|
+
}
|
|
1274
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
1275
|
+
return geometry;
|
|
1276
|
+
}
|
|
1277
|
+
function mergeGeometriesPreservingGroups(geometries) {
|
|
1278
|
+
if (geometries.length === 0)
|
|
1279
|
+
return null;
|
|
1280
|
+
const merged = mergeGeometries(geometries, false);
|
|
1281
|
+
if (!merged)
|
|
1282
|
+
return null;
|
|
1283
|
+
merged.clearGroups();
|
|
1284
|
+
let indexStart = 0;
|
|
1285
|
+
for (const geometry of geometries) {
|
|
1286
|
+
if (geometry.groups.length > 0) {
|
|
1287
|
+
for (const group of geometry.groups) {
|
|
1288
|
+
merged.addGroup(indexStart + group.start, group.count, group.materialIndex ?? 0);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
else {
|
|
1292
|
+
const count = geometry.index?.count ?? geometry.getAttribute('position')?.count ?? 0;
|
|
1293
|
+
if (count > 0) {
|
|
1294
|
+
merged.addGroup(indexStart, count, 0);
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
indexStart += geometry.index?.count ?? geometry.getAttribute('position')?.count ?? 0;
|
|
1298
|
+
}
|
|
1299
|
+
return merged;
|
|
1300
|
+
}
|
|
1301
|
+
function collectGeometryPlanes(geometry) {
|
|
1302
|
+
const source = geometry.index ? geometry.toNonIndexed() : geometry;
|
|
1303
|
+
const position = source.getAttribute('position');
|
|
1304
|
+
const planes = [];
|
|
1305
|
+
const a = new THREE.Vector3();
|
|
1306
|
+
const b = new THREE.Vector3();
|
|
1307
|
+
const c = new THREE.Vector3();
|
|
1308
|
+
if (!position)
|
|
1309
|
+
return planes;
|
|
1310
|
+
for (let i = 0; i + 2 < position.count; i += 3) {
|
|
1311
|
+
a.fromBufferAttribute(position, i);
|
|
1312
|
+
b.fromBufferAttribute(position, i + 1);
|
|
1313
|
+
c.fromBufferAttribute(position, i + 2);
|
|
1314
|
+
const plane = new THREE.Plane().setFromCoplanarPoints(a, b, c);
|
|
1315
|
+
if (Number.isFinite(plane.normal.x) && plane.normal.lengthSq() > 1e-10) {
|
|
1316
|
+
planes.push(plane.normalize());
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
if (source !== geometry)
|
|
1320
|
+
source.dispose();
|
|
1321
|
+
return planes;
|
|
1322
|
+
}
|
|
1323
|
+
function splitShellByFacePlanes(geometry, planes, materialIndex) {
|
|
1324
|
+
if (planes.length === 0)
|
|
1325
|
+
return geometry;
|
|
1326
|
+
const source = geometry.index ? geometry.toNonIndexed() : geometry.clone();
|
|
1327
|
+
const position = source.getAttribute('position');
|
|
1328
|
+
const normal = source.getAttribute('normal');
|
|
1329
|
+
const uv = source.getAttribute('uv');
|
|
1330
|
+
if (!position || !normal || !uv) {
|
|
1331
|
+
source.dispose();
|
|
1332
|
+
return geometry;
|
|
1333
|
+
}
|
|
1334
|
+
const groups = source.groups.length > 0 ? source.groups : [{ start: 0, count: position.count, materialIndex }];
|
|
1335
|
+
const keptPositions = [];
|
|
1336
|
+
const keptNormals = [];
|
|
1337
|
+
const keptUvs = [];
|
|
1338
|
+
const keptGroups = [];
|
|
1339
|
+
let keptVertexCount = 0;
|
|
1340
|
+
const point = new THREE.Vector3();
|
|
1341
|
+
const triNormal = new THREE.Vector3();
|
|
1342
|
+
const pushTriangle = (targetPositions, targetNormals, targetUvs, targetGroups, startVertex, groupMaterialIndex, vertexOffset) => {
|
|
1343
|
+
if (targetGroups.length === 0 ||
|
|
1344
|
+
targetGroups[targetGroups.length - 1].materialIndex !== groupMaterialIndex) {
|
|
1345
|
+
targetGroups.push({ start: startVertex, count: 0, materialIndex: groupMaterialIndex });
|
|
1346
|
+
}
|
|
1347
|
+
const targetGroup = targetGroups[targetGroups.length - 1];
|
|
1348
|
+
for (let k = 0; k < 3; k += 1) {
|
|
1349
|
+
const vi = vertexOffset + k;
|
|
1350
|
+
targetPositions.push(position.getX(vi), position.getY(vi), position.getZ(vi));
|
|
1351
|
+
targetNormals.push(normal.getX(vi), normal.getY(vi), normal.getZ(vi));
|
|
1352
|
+
targetUvs.push(uv.getX(vi), uv.getY(vi));
|
|
1353
|
+
}
|
|
1354
|
+
targetGroup.count += 3;
|
|
1355
|
+
};
|
|
1356
|
+
const isPlaneMatch = (vertexOffset) => {
|
|
1357
|
+
if (materialIndex >= 0 &&
|
|
1358
|
+
triNormal.fromBufferAttribute(normal, vertexOffset).y <= SHINGLE_SURFACE_EPSILON) {
|
|
1359
|
+
return false;
|
|
1360
|
+
}
|
|
1361
|
+
triNormal.fromBufferAttribute(normal, vertexOffset).normalize();
|
|
1362
|
+
return planes.some((plane) => {
|
|
1363
|
+
if (Math.abs(triNormal.dot(plane.normal)) < 0.999)
|
|
1364
|
+
return false;
|
|
1365
|
+
for (let k = 0; k < 3; k += 1) {
|
|
1366
|
+
point.fromBufferAttribute(position, vertexOffset + k);
|
|
1367
|
+
if (Math.abs(plane.distanceToPoint(point)) > 1e-3)
|
|
1368
|
+
return false;
|
|
1369
|
+
}
|
|
1370
|
+
return true;
|
|
1371
|
+
});
|
|
1372
|
+
};
|
|
1373
|
+
for (const group of groups) {
|
|
1374
|
+
const groupStart = Math.max(0, group.start);
|
|
1375
|
+
const groupEnd = Math.min(position.count, group.start + group.count);
|
|
1376
|
+
for (let i = groupStart; i + 2 < groupEnd; i += 3) {
|
|
1377
|
+
if (group.materialIndex === materialIndex && isPlaneMatch(i)) {
|
|
1378
|
+
continue;
|
|
1379
|
+
}
|
|
1380
|
+
pushTriangle(keptPositions, keptNormals, keptUvs, keptGroups, keptVertexCount, group.materialIndex ?? 0, i);
|
|
1381
|
+
keptVertexCount += 3;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
source.dispose();
|
|
1385
|
+
geometry.dispose();
|
|
1386
|
+
return createGeometryFromRawAttributes(keptPositions, keptNormals, keptUvs, keptGroups);
|
|
1387
|
+
}
|
|
673
1388
|
export function remapRoofShellFaces(geometry, node) {
|
|
674
1389
|
const position = geometry.getAttribute('position');
|
|
675
1390
|
const index = geometry.getIndex();
|
|
@@ -706,14 +1421,17 @@ export function remapRoofShellFaces(geometry, node) {
|
|
|
706
1421
|
.add(b)
|
|
707
1422
|
.add(c)
|
|
708
1423
|
.multiplyScalar(1 / 3);
|
|
709
|
-
if (normal.y > SHINGLE_SURFACE_EPSILON) {
|
|
1424
|
+
if (node.roofType === 'dutch' && Math.abs(normal.y) > SHINGLE_SURFACE_EPSILON) {
|
|
1425
|
+
materialIndex = 3;
|
|
1426
|
+
}
|
|
1427
|
+
else if (normal.y > SHINGLE_SURFACE_EPSILON) {
|
|
710
1428
|
materialIndex = 3;
|
|
711
1429
|
}
|
|
712
1430
|
else if (isRakeFace(node, geometry, centroid, normal)) {
|
|
713
|
-
materialIndex =
|
|
1431
|
+
materialIndex = 1;
|
|
714
1432
|
}
|
|
715
1433
|
else {
|
|
716
|
-
materialIndex =
|
|
1434
|
+
materialIndex = 0;
|
|
717
1435
|
}
|
|
718
1436
|
}
|
|
719
1437
|
triangleMaterials[triangleIndex] = materialIndex;
|
|
@@ -754,8 +1472,6 @@ function isRakeFace(node, geometry, centroid, normal) {
|
|
|
754
1472
|
function getRakeAxis(node) {
|
|
755
1473
|
if (node.roofType === 'gable' || node.roofType === 'gambrel')
|
|
756
1474
|
return 'x';
|
|
757
|
-
if (node.roofType === 'dutch')
|
|
758
|
-
return node.width >= node.depth ? 'x' : 'z';
|
|
759
1475
|
return null;
|
|
760
1476
|
}
|
|
761
1477
|
/**
|
|
@@ -766,7 +1482,7 @@ function getRakeAxis(node) {
|
|
|
766
1482
|
* height ratios are already baked into `tanTheta` (via the slope frame)
|
|
767
1483
|
* so they don't need to be threaded again.
|
|
768
1484
|
*/
|
|
769
|
-
function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanTheta, shapeRatios) {
|
|
1485
|
+
function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanTheta, shapeRatios, dutchTopRakeThickness) {
|
|
770
1486
|
const v = (x, y, z) => new THREE.Vector3(x, y, z);
|
|
771
1487
|
const { iF = 0, iB = 0, iL = 0, iR = 0 } = insets;
|
|
772
1488
|
const b1 = v(-w / 2 + iL, baseY, d / 2 - iF);
|
|
@@ -829,57 +1545,186 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
|
|
|
829
1545
|
const m2 = v(w / 2 - i, mh, d / 2 - i);
|
|
830
1546
|
const m3 = v(w / 2 - i, mh, -d / 2 + i);
|
|
831
1547
|
const m4 = v(-w / 2 + i, mh, -d / 2 + i);
|
|
832
|
-
const
|
|
833
|
-
const
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const r1 = v(0, h, d / 2 - w / 2);
|
|
844
|
-
const r2 = v(0, h, -d / 2 + w / 2);
|
|
845
|
-
faces.push([e1, e2, r1], [e3, e4, r2], [e2, e3, r2, r1], [e4, e1, r1, r2]);
|
|
846
|
-
}
|
|
1548
|
+
const topW = w - i * 2;
|
|
1549
|
+
const topD = d - i * 2;
|
|
1550
|
+
faces.push([e1, e2, m2, m1], [e2, e3, m3, m2], [e3, e4, m4, m3], [e4, e1, m1, m4]);
|
|
1551
|
+
if (Math.abs(topW - topD) < 0.01) {
|
|
1552
|
+
const r = v(0, h, 0);
|
|
1553
|
+
faces.push([m4, m1, r], [m1, m2, r], [m2, m3, r], [m3, m4, r]);
|
|
1554
|
+
}
|
|
1555
|
+
else if (topW >= topD) {
|
|
1556
|
+
const r1 = v(-topW / 2 + topD / 2, h, 0);
|
|
1557
|
+
const r2 = v(topW / 2 - topD / 2, h, 0);
|
|
1558
|
+
faces.push([m4, m1, r1], [m2, m3, r2], [m1, m2, r2, r1], [m3, m4, r1, r2]);
|
|
847
1559
|
}
|
|
848
1560
|
else {
|
|
849
|
-
|
|
1561
|
+
const r1 = v(0, h, topD / 2 - topW / 2);
|
|
1562
|
+
const r2 = v(0, h, -topD / 2 + topW / 2);
|
|
1563
|
+
faces.push([m1, m2, r1], [m3, m4, r2], [m2, m3, r2, r1], [m4, m1, r1, r2]);
|
|
850
1564
|
}
|
|
851
1565
|
}
|
|
852
1566
|
else if (type === 'dutch') {
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
faces
|
|
1567
|
+
const dutch = getDutchRoofShapeMetrics({
|
|
1568
|
+
w,
|
|
1569
|
+
d,
|
|
1570
|
+
wh,
|
|
1571
|
+
rh,
|
|
1572
|
+
dutchI: insets.dutchI,
|
|
1573
|
+
baseW,
|
|
1574
|
+
baseD,
|
|
1575
|
+
shapeRatios,
|
|
1576
|
+
});
|
|
1577
|
+
if (!dutch)
|
|
1578
|
+
return faces;
|
|
1579
|
+
if (dutch.axis === 'width') {
|
|
1580
|
+
const m1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
1581
|
+
const m2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
1582
|
+
const m3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
1583
|
+
const m4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
1584
|
+
const o1 = v(-dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
1585
|
+
const o2 = v(dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
1586
|
+
const o3 = v(dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
1587
|
+
const o4 = v(-dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
1588
|
+
const r1 = v(-dutch.innerWaistHalfX, h, 0);
|
|
1589
|
+
const r2 = v(dutch.innerWaistHalfX, h, 0);
|
|
1590
|
+
const endSlopes = getDutchEndSlopeFaces({
|
|
1591
|
+
w,
|
|
1592
|
+
d,
|
|
1593
|
+
wh,
|
|
1594
|
+
rh,
|
|
1595
|
+
insets,
|
|
1596
|
+
baseW,
|
|
1597
|
+
baseD,
|
|
1598
|
+
shapeRatios,
|
|
1599
|
+
dutchTopRakeThickness,
|
|
1600
|
+
}).map((face) => face.map((point) => v(point.x, point.y, point.z)));
|
|
1601
|
+
faces.push([e1, e2, o2, m2, m1, o1], [e3, e4, o4, m4, m3, o3]);
|
|
1602
|
+
if (endSlopes.length === 2) {
|
|
1603
|
+
faces.push(...endSlopes);
|
|
1604
|
+
}
|
|
1605
|
+
else {
|
|
1606
|
+
faces.push([e2, e3, o3, o2], [e4, e1, o1, o4]);
|
|
1607
|
+
}
|
|
1608
|
+
faces.push([m1, m2, r2, r1], [m3, m4, r1, r2]);
|
|
1609
|
+
faces.push([m4, m1, r1], [m2, m3, r2]);
|
|
865
1610
|
}
|
|
866
1611
|
else {
|
|
867
|
-
const m1 = v(-
|
|
868
|
-
const m2 = v(
|
|
869
|
-
const m3 = v(
|
|
870
|
-
const m4 = v(-
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
|
|
1612
|
+
const m1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
1613
|
+
const m2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
|
|
1614
|
+
const m3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
1615
|
+
const m4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
|
|
1616
|
+
const o1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
|
|
1617
|
+
const o2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
|
|
1618
|
+
const o3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
|
|
1619
|
+
const o4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
|
|
1620
|
+
const r1 = v(0, h, dutch.innerWaistHalfZ);
|
|
1621
|
+
const r2 = v(0, h, -dutch.innerWaistHalfZ);
|
|
1622
|
+
const endSlopes = getDutchEndSlopeFaces({
|
|
1623
|
+
w,
|
|
1624
|
+
d,
|
|
1625
|
+
wh,
|
|
1626
|
+
rh,
|
|
1627
|
+
insets,
|
|
1628
|
+
baseW,
|
|
1629
|
+
baseD,
|
|
1630
|
+
shapeRatios,
|
|
1631
|
+
dutchTopRakeThickness,
|
|
1632
|
+
}).map((face) => face.map((point) => v(point.x, point.y, point.z)));
|
|
1633
|
+
faces.push([e2, e3, o3, m3, m2, o2], [e4, e1, o1, m1, m4, o4]);
|
|
1634
|
+
if (endSlopes.length === 2) {
|
|
1635
|
+
faces.push(...endSlopes);
|
|
1636
|
+
}
|
|
1637
|
+
else {
|
|
1638
|
+
faces.push([e1, e2, o2, o1], [e3, e4, o4, o3]);
|
|
1639
|
+
}
|
|
1640
|
+
faces.push([m2, m3, r2, r1], [m4, m1, r1, r2]);
|
|
1641
|
+
faces.push([m1, m2, r1], [m3, m4, r2]);
|
|
874
1642
|
}
|
|
875
1643
|
}
|
|
876
1644
|
return faces;
|
|
877
1645
|
}
|
|
1646
|
+
function addDutchRakeBoard(apex, base, outward, reach, thickness, topFaces, sideFaces) {
|
|
1647
|
+
if (!(reach > 0.001) || !(thickness > 0.0001))
|
|
1648
|
+
return;
|
|
1649
|
+
const apexOuter = apex.clone().addScaledVector(outward, reach);
|
|
1650
|
+
const baseOuter = base.clone().addScaledVector(outward, reach);
|
|
1651
|
+
const topPoly = [apex.clone(), base.clone(), baseOuter, apexOuter];
|
|
1652
|
+
const normal = new THREE.Vector3()
|
|
1653
|
+
.crossVectors(new THREE.Vector3().subVectors(topPoly[1], topPoly[0]), new THREE.Vector3().subVectors(topPoly[2], topPoly[0]))
|
|
1654
|
+
.normalize();
|
|
1655
|
+
if (normal.y < 0) {
|
|
1656
|
+
topPoly.reverse();
|
|
1657
|
+
normal.multiplyScalar(-1);
|
|
1658
|
+
}
|
|
1659
|
+
const top = topPoly.map((point) => point.clone().addScaledVector(normal, DUTCH_RAKE_SLOPE_SEAT_OFFSET));
|
|
1660
|
+
const bottom = top.map((point) => new THREE.Vector3(point.x, point.y - thickness, point.z));
|
|
1661
|
+
pushDoubleSidedFace(topFaces, top);
|
|
1662
|
+
pushDoubleSidedFace(sideFaces, bottom.slice().reverse());
|
|
1663
|
+
for (let i = 0; i < top.length; i += 1) {
|
|
1664
|
+
const next = (i + 1) % top.length;
|
|
1665
|
+
pushDoubleSidedFace(sideFaces, [
|
|
1666
|
+
top[i].clone(),
|
|
1667
|
+
top[next].clone(),
|
|
1668
|
+
bottom[next].clone(),
|
|
1669
|
+
bottom[i].clone(),
|
|
1670
|
+
]);
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
function buildDutchRakeBoards(W, D, wh, rh, i, shapeRatios, rake, thickness) {
|
|
1674
|
+
if (!(rake > 0.001) || !(thickness > 0.0001) || !(i > 0.001) || !(rh > 0.001)) {
|
|
1675
|
+
return null;
|
|
1676
|
+
}
|
|
1677
|
+
const dutch = getDutchRoofShapeMetrics({
|
|
1678
|
+
w: W,
|
|
1679
|
+
d: D,
|
|
1680
|
+
wh,
|
|
1681
|
+
rh,
|
|
1682
|
+
dutchI: i,
|
|
1683
|
+
baseW: W,
|
|
1684
|
+
baseD: D,
|
|
1685
|
+
shapeRatios: {
|
|
1686
|
+
...shapeRatios,
|
|
1687
|
+
dutchGabletRake: rake,
|
|
1688
|
+
},
|
|
1689
|
+
});
|
|
1690
|
+
if (!dutch || !(dutch.rakeReach > 0.001))
|
|
1691
|
+
return null;
|
|
1692
|
+
const v = (x, y, z) => new THREE.Vector3(x, y, z);
|
|
1693
|
+
const topFaces = [];
|
|
1694
|
+
const sideFaces = [];
|
|
1695
|
+
if (dutch.axis === 'width') {
|
|
1696
|
+
addDutchRakeBoard(v(-dutch.innerWaistHalfX, dutch.peakHeight, 0), v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ), v(-1, 0, 0), dutch.rakeReach, thickness, topFaces, sideFaces);
|
|
1697
|
+
addDutchRakeBoard(v(-dutch.innerWaistHalfX, dutch.peakHeight, 0), v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ), v(-1, 0, 0), dutch.rakeReach, thickness, topFaces, sideFaces);
|
|
1698
|
+
addDutchRakeBoard(v(dutch.innerWaistHalfX, dutch.peakHeight, 0), v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ), v(1, 0, 0), dutch.rakeReach, thickness, topFaces, sideFaces);
|
|
1699
|
+
addDutchRakeBoard(v(dutch.innerWaistHalfX, dutch.peakHeight, 0), v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ), v(1, 0, 0), dutch.rakeReach, thickness, topFaces, sideFaces);
|
|
1700
|
+
}
|
|
1701
|
+
else {
|
|
1702
|
+
addDutchRakeBoard(v(0, dutch.peakHeight, dutch.innerWaistHalfZ), v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ), v(0, 0, 1), dutch.rakeReach, thickness, topFaces, sideFaces);
|
|
1703
|
+
addDutchRakeBoard(v(0, dutch.peakHeight, dutch.innerWaistHalfZ), v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ), v(0, 0, 1), dutch.rakeReach, thickness, topFaces, sideFaces);
|
|
1704
|
+
addDutchRakeBoard(v(0, dutch.peakHeight, -dutch.innerWaistHalfZ), v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ), v(0, 0, -1), dutch.rakeReach, thickness, topFaces, sideFaces);
|
|
1705
|
+
addDutchRakeBoard(v(0, dutch.peakHeight, -dutch.innerWaistHalfZ), v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ), v(0, 0, -1), dutch.rakeReach, thickness, topFaces, sideFaces);
|
|
1706
|
+
}
|
|
1707
|
+
const geometries = [];
|
|
1708
|
+
if (topFaces.length > 0) {
|
|
1709
|
+
geometries.push(createGeometryFromFaces(topFaces, DUTCH_RAKE_TOP_MATERIAL_INDEX));
|
|
1710
|
+
}
|
|
1711
|
+
if (sideFaces.length > 0) {
|
|
1712
|
+
geometries.push(createGeometryFromFaces(sideFaces, DUTCH_RAKE_SIDE_MATERIAL_INDEX));
|
|
1713
|
+
}
|
|
1714
|
+
if (geometries.length === 0)
|
|
1715
|
+
return null;
|
|
1716
|
+
if (geometries.length === 1)
|
|
1717
|
+
return geometries[0];
|
|
1718
|
+
const merged = mergeGeometriesPreservingGroups(geometries);
|
|
1719
|
+
for (const geometry of geometries)
|
|
1720
|
+
geometry.dispose();
|
|
1721
|
+
return merged;
|
|
1722
|
+
}
|
|
878
1723
|
/**
|
|
879
1724
|
* Converts an array of face polygons into a BufferGeometry.
|
|
880
1725
|
* Each face is triangulated via fan triangulation.
|
|
881
1726
|
*/
|
|
882
|
-
function createGeometryFromFaces(faces, matRule = null) {
|
|
1727
|
+
function createGeometryFromFaces(faces, matRule = null, options) {
|
|
883
1728
|
const positions = [];
|
|
884
1729
|
const normals = [];
|
|
885
1730
|
const uvs = [];
|
|
@@ -895,14 +1740,19 @@ function createGeometryFromFaces(faces, matRule = null) {
|
|
|
895
1740
|
const vA = new THREE.Vector3().subVectors(p1, p0);
|
|
896
1741
|
const vB = new THREE.Vector3().subVectors(p2, p0);
|
|
897
1742
|
const normal = new THREE.Vector3().crossVectors(vA, vB).normalize();
|
|
1743
|
+
if (normal.lengthSq() < 1e-12)
|
|
1744
|
+
continue;
|
|
898
1745
|
let slopeAlignedDown = null;
|
|
899
1746
|
let slopeAlignedAcross = null;
|
|
900
1747
|
let slopeAlignedVOrigin = 0;
|
|
901
|
-
|
|
902
|
-
|
|
1748
|
+
const slopeUvNormal = options?.treatBidirectionalSlopeFacesAsSlope && normal.y < 0
|
|
1749
|
+
? normal.clone().multiplyScalar(-1)
|
|
1750
|
+
: normal;
|
|
1751
|
+
if (Math.abs(slopeUvNormal.y) > SHINGLE_SURFACE_EPSILON) {
|
|
1752
|
+
_uvDownSlope.copy(_uvWorldDown).projectOnPlane(slopeUvNormal);
|
|
903
1753
|
if (_uvDownSlope.lengthSq() > 1e-8) {
|
|
904
1754
|
_uvDownSlope.normalize();
|
|
905
|
-
_uvAcrossSlope.crossVectors(_uvDownSlope,
|
|
1755
|
+
_uvAcrossSlope.crossVectors(_uvDownSlope, slopeUvNormal).normalize();
|
|
906
1756
|
let highestPoint = face[0];
|
|
907
1757
|
for (const candidate of face) {
|
|
908
1758
|
if (candidate.y > highestPoint.y) {
|
|
@@ -967,16 +1817,21 @@ function createGeometryFromFaces(faces, matRule = null) {
|
|
|
967
1817
|
// Merge identical vertices to optimize geometry for CSG and create clean topology
|
|
968
1818
|
const mergedGeo = mergeVertices(geometry, 1e-4);
|
|
969
1819
|
geometry.dispose();
|
|
970
|
-
|
|
1820
|
+
ensureRenderableGeometryAttributes(mergedGeo);
|
|
971
1821
|
return mergedGeo;
|
|
972
1822
|
}
|
|
973
1823
|
function pushRoofUv(uvs, point, normal) {
|
|
974
|
-
|
|
1824
|
+
// Project in WORLD space (via the current segment's world transform) so
|
|
1825
|
+
// vertical gable faces tile identically to the walls below; for a local
|
|
1826
|
+
// build the matrix is identity and this is the original behaviour.
|
|
1827
|
+
const p = _uvWorldPoint.copy(point).applyMatrix4(_segmentUvMatrix);
|
|
1828
|
+
_uvFaceNormal.copy(normal).applyMatrix3(_segmentUvNormalMatrix).normalize();
|
|
1829
|
+
_uvWorldNormal.copy(_uvFaceNormal);
|
|
975
1830
|
const absX = Math.abs(_uvFaceNormal.x);
|
|
976
1831
|
const absY = Math.abs(_uvFaceNormal.y);
|
|
977
1832
|
const absZ = Math.abs(_uvFaceNormal.z);
|
|
978
1833
|
if (absY >= absX && absY >= absZ) {
|
|
979
|
-
uvs.push(
|
|
1834
|
+
uvs.push(p.x, p.z);
|
|
980
1835
|
return;
|
|
981
1836
|
}
|
|
982
1837
|
if (_uvFaceNormal.y > SHINGLE_SURFACE_EPSILON) {
|
|
@@ -984,21 +1839,18 @@ function pushRoofUv(uvs, point, normal) {
|
|
|
984
1839
|
if (_uvDownSlope.lengthSq() > 1e-8) {
|
|
985
1840
|
_uvDownSlope.normalize();
|
|
986
1841
|
_uvAcrossSlope.crossVectors(_uvDownSlope, _uvFaceNormal).normalize();
|
|
987
|
-
uvs.push(
|
|
1842
|
+
uvs.push(p.dot(_uvAcrossSlope), p.dot(_uvDownSlope));
|
|
988
1843
|
return;
|
|
989
1844
|
}
|
|
990
1845
|
}
|
|
1846
|
+
// Vertical (gable wall) faces: U = ±worldX/Z (axis across the face normal),
|
|
1847
|
+
// V = 1 - worldY — the same world-space projection the wall kind uses.
|
|
1848
|
+
const wallV = 1 - p.y;
|
|
991
1849
|
if (absX >= absZ) {
|
|
992
|
-
uvs.push(
|
|
1850
|
+
uvs.push(_uvWorldNormal.x >= 0 ? p.z : -p.z, wallV);
|
|
993
1851
|
return;
|
|
994
1852
|
}
|
|
995
|
-
uvs.push(
|
|
996
|
-
}
|
|
997
|
-
function ensureUv2Attribute(geometry) {
|
|
998
|
-
const uv = geometry.getAttribute('uv');
|
|
999
|
-
if (!uv)
|
|
1000
|
-
return;
|
|
1001
|
-
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
|
|
1853
|
+
uvs.push(_uvWorldNormal.z >= 0 ? p.x : -p.x, wallV);
|
|
1002
1854
|
}
|
|
1003
1855
|
/**
|
|
1004
1856
|
* Returns the outer roof surface frame (point + normal) at a given segment-local XZ.
|
|
@@ -1045,6 +1897,9 @@ export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
|
|
|
1045
1897
|
const shinTopRh = shinBotRh + stCos;
|
|
1046
1898
|
const topBaseY = 0;
|
|
1047
1899
|
const baseI = Math.min(width, depth) * 0.25;
|
|
1900
|
+
// Dutch gablet waist tracks dutchHipWidthRatio (see getRoofSegmentBrushes);
|
|
1901
|
+
// the generic baseI above still drives the bottom-rect insets for other types.
|
|
1902
|
+
const dutchBaseI = Math.min(width, depth) * segment.dutchHipWidthRatio;
|
|
1048
1903
|
const getInsets = (_wh, _baseY, isVoid, _wV, _dV) => {
|
|
1049
1904
|
const inset = Math.max(0.01, baseI);
|
|
1050
1905
|
let iF = 0;
|
|
@@ -1070,7 +1925,7 @@ export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
|
|
|
1070
1925
|
else if (roofType === 'shed') {
|
|
1071
1926
|
iF = inset;
|
|
1072
1927
|
}
|
|
1073
|
-
let structuralI =
|
|
1928
|
+
let structuralI = dutchBaseI;
|
|
1074
1929
|
if (isVoid) {
|
|
1075
1930
|
structuralI += shingleThickness;
|
|
1076
1931
|
}
|
|
@@ -1081,8 +1936,11 @@ export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
|
|
|
1081
1936
|
gambrelLowerWidthRatio: segment.gambrelLowerWidthRatio,
|
|
1082
1937
|
mansardSteepWidthRatio: segment.mansardSteepWidthRatio,
|
|
1083
1938
|
dutchHipWidthRatio: segment.dutchHipWidthRatio,
|
|
1939
|
+
dutchHipHeightRatio: segment.dutchHipHeightRatio,
|
|
1940
|
+
dutchWaistLengthRatio: segment.dutchWaistLengthRatio ?? ROOF_SHAPE_DEFAULTS.dutchWaistLengthRatio,
|
|
1941
|
+
dutchGabletRake: segment.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake,
|
|
1084
1942
|
};
|
|
1085
|
-
const topFaces = getModuleFaces(roofType, shinTopW, shinTopD, shinTopWh, shinTopRh, topBaseY, insetsTop, width, depth, tanTheta, shapeRatios);
|
|
1943
|
+
const topFaces = getModuleFaces(roofType, shinTopW, shinTopD, shinTopWh, shinTopRh, topBaseY, insetsTop, width, depth, tanTheta, shapeRatios, segment.dutchTopRakeThickness);
|
|
1086
1944
|
const topGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : 1);
|
|
1087
1945
|
if (transZ !== 0)
|
|
1088
1946
|
topGeo.translate(0, 0, transZ);
|