@pascal-app/viewer 0.8.0 → 0.9.1
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/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 +38 -22
- package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
- package/dist/components/renderers/parametric-node-renderer.js +34 -0
- package/dist/components/renderers/scene-renderer.d.ts +1 -1
- package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
- package/dist/components/viewer/ground-occluder.d.ts +1 -1
- package/dist/components/viewer/ground-occluder.d.ts.map +1 -1
- package/dist/components/viewer/ground-occluder.js +12 -24
- package/dist/components/viewer/index.d.ts +37 -1
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +186 -63
- package/dist/components/viewer/lights.d.ts +1 -1
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +170 -43
- package/dist/components/viewer/perf-monitor.d.ts +1 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +99 -11
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +231 -54
- package/dist/components/viewer/registered-systems.d.ts +12 -0
- package/dist/components/viewer/registered-systems.d.ts.map +1 -0
- package/dist/components/viewer/registered-systems.js +47 -0
- package/dist/components/viewer/scene-bvh.d.ts +17 -0
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.js +94 -0
- package/dist/components/viewer/selection-manager.d.ts +1 -1
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +20 -5
- package/dist/components/viewer/viewer-camera.d.ts +1 -1
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.d.ts +1 -1
- package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-node-events.d.ts +5 -73
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +19 -7
- package/dist/index.d.ts +36 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +67 -3
- package/dist/lib/csg-utils.d.ts +16 -0
- package/dist/lib/csg-utils.d.ts.map +1 -0
- package/dist/lib/csg-utils.js +33 -0
- package/dist/lib/drawable-geometry.d.ts +21 -0
- package/dist/lib/drawable-geometry.d.ts.map +1 -0
- package/dist/lib/drawable-geometry.js +22 -0
- package/dist/lib/edge-style.d.ts +3 -0
- package/dist/lib/edge-style.d.ts.map +1 -0
- package/dist/lib/edge-style.js +11 -0
- package/dist/lib/gpu-perf.d.ts +4 -0
- package/dist/lib/gpu-perf.d.ts.map +1 -0
- package/dist/lib/gpu-perf.js +23 -0
- package/dist/lib/ink-edges.d.ts +9 -0
- package/dist/lib/ink-edges.d.ts.map +1 -0
- package/dist/lib/ink-edges.js +43 -0
- package/dist/lib/isolation.d.ts +39 -0
- package/dist/lib/isolation.d.ts.map +1 -0
- package/dist/lib/isolation.js +103 -0
- package/dist/lib/layers.d.ts +18 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +18 -0
- package/dist/lib/materials.d.ts +25 -15
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +277 -62
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -0
- package/dist/lib/polygon-union.d.ts +4 -0
- package/dist/lib/polygon-union.d.ts.map +1 -0
- package/dist/lib/polygon-union.js +289 -0
- package/dist/lib/polygon-union.test.d.ts +2 -0
- package/dist/lib/polygon-union.test.d.ts.map +1 -0
- package/dist/lib/polygon-union.test.js +105 -0
- package/dist/lib/scene-themes.d.ts +29 -0
- package/dist/lib/scene-themes.d.ts.map +1 -0
- package/dist/lib/scene-themes.js +193 -0
- package/dist/store/use-viewer.d.ts +42 -4
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +26 -3
- package/dist/systems/ceiling/ceiling-system.d.ts +6 -2
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +62 -12
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +373 -22
- package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-interaction-system.js +62 -0
- package/dist/systems/fence/fence-system.d.ts +3 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +9 -5
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
- package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
- package/dist/systems/geometry/geometry-system.d.ts +34 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.js +279 -0
- package/dist/systems/interactive/interactive-system.d.ts +1 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.d.ts +7 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +8 -11
- 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 +18 -5
- package/dist/systems/roof/roof-materials.d.ts +3 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +38 -14
- package/dist/systems/roof/roof-system.d.ts +27 -0
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +396 -59
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +167 -49
- package/dist/systems/slab/slab-system.test.d.ts +2 -0
- package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
- package/dist/systems/slab/slab-system.test.js +62 -0
- package/dist/systems/stair/stair-materials.d.ts +4 -3
- package/dist/systems/stair/stair-materials.d.ts.map +1 -1
- package/dist/systems/stair/stair-materials.js +30 -15
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +50 -61
- package/dist/systems/surface-hole-geometry.d.ts +3 -0
- package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
- package/dist/systems/surface-hole-geometry.js +4 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +18 -2
- package/dist/systems/wall/wall-materials.d.ts +4 -3
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +52 -28
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +171 -26
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +71 -7
- package/package.json +3 -4
- package/dist/components/renderers/building/building-renderer.d.ts +0 -5
- package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
- package/dist/components/renderers/building/building-renderer.js +0 -11
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
- package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
- package/dist/components/renderers/column/column-renderer.d.ts +0 -5
- package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
- package/dist/components/renderers/column/column-renderer.js +0 -597
- package/dist/components/renderers/door/door-renderer.d.ts +0 -5
- package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
- package/dist/components/renderers/door/door-renderer.js +0 -16
- package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
- package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
- package/dist/components/renderers/fence/fence-renderer.js +0 -15
- package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
- package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
- package/dist/components/renderers/guide/guide-renderer.js +0 -38
- package/dist/components/renderers/item/item-renderer.d.ts +0 -5
- package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
- package/dist/components/renderers/item/item-renderer.js +0 -172
- package/dist/components/renderers/level/level-renderer.d.ts +0 -5
- package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
- package/dist/components/renderers/level/level-renderer.js +0 -11
- package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
- package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-materials.js +0 -16
- package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-renderer.js +0 -32
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
- package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
- package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
- package/dist/components/renderers/scan/scan-renderer.js +0 -54
- package/dist/components/renderers/site/site-renderer.d.ts +0 -5
- package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
- package/dist/components/renderers/site/site-renderer.js +0 -106
- package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
- package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
- package/dist/components/renderers/slab/slab-renderer.js +0 -58
- package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
- package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
- package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
- package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
- package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair/stair-renderer.js +0 -603
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
- package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
- package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
- package/dist/components/renderers/wall/wall-renderer.js +0 -16
- package/dist/components/renderers/window/window-renderer.d.ts +0 -5
- package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
- package/dist/components/renderers/window/window-renderer.js +0 -21
- package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
- package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
- package/dist/components/renderers/zone/zone-renderer.js +0 -181
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sceneRegistry, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { getEffectiveNode, getSegmentSlopeFrame, hasSegmentMaterialOverride, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
@@ -31,8 +31,22 @@ const _quaternion = new THREE.Quaternion();
|
|
|
31
31
|
const _scale = new THREE.Vector3(1, 1, 1);
|
|
32
32
|
const _yAxis = new THREE.Vector3(0, 1, 0);
|
|
33
33
|
const _uvFaceNormal = new THREE.Vector3();
|
|
34
|
+
const _uvWorldDown = new THREE.Vector3(0, -1, 0);
|
|
35
|
+
const _uvDownSlope = new THREE.Vector3();
|
|
36
|
+
const _uvAcrossSlope = new THREE.Vector3();
|
|
37
|
+
const _tmpVec3A = new THREE.Vector3();
|
|
38
|
+
const _tmpVec3B = new THREE.Vector3();
|
|
39
|
+
const _surfaceRay = new THREE.Ray();
|
|
40
|
+
const _surfaceOrigin = new THREE.Vector3();
|
|
41
|
+
const _surfaceDir = new THREE.Vector3(0, -1, 0);
|
|
42
|
+
const _surfaceHits = [];
|
|
43
|
+
const _surfaceV0 = new THREE.Vector3();
|
|
44
|
+
const _surfaceV1 = new THREE.Vector3();
|
|
45
|
+
const _surfaceV2 = new THREE.Vector3();
|
|
46
|
+
const _surfaceFaceNormal = new THREE.Vector3();
|
|
34
47
|
// Pending merged-roof updates carried across frames (for throttling)
|
|
35
48
|
const pendingRoofUpdates = new Set();
|
|
49
|
+
const warnedMergedRoofNaNIds = new Set();
|
|
36
50
|
const MAX_ROOFS_PER_FRAME = 1;
|
|
37
51
|
const MAX_SEGMENTS_PER_FRAME = 3;
|
|
38
52
|
// ============================================================================
|
|
@@ -42,10 +56,16 @@ export const RoofSystem = () => {
|
|
|
42
56
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
43
57
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
44
58
|
const rootNodeIds = useScene((state) => state.rootNodeIds);
|
|
59
|
+
// Subscribe so an override-only update (no scene write) still re-runs
|
|
60
|
+
// the component, letting the useFrame loop pick up the latest dirtyNodes
|
|
61
|
+
// set from the render pass that received the override-publishing
|
|
62
|
+
// `markDirty` call. Mirrors WallSystem / DoorSystem.
|
|
63
|
+
useLiveNodeOverrides((s) => s.overrides);
|
|
45
64
|
useFrame(() => {
|
|
46
65
|
// Clear stale pending updates when the scene is unloaded
|
|
47
66
|
if (rootNodeIds.length === 0) {
|
|
48
67
|
pendingRoofUpdates.clear();
|
|
68
|
+
warnedMergedRoofNaNIds.clear();
|
|
49
69
|
return;
|
|
50
70
|
}
|
|
51
71
|
if (dirtyNodes.size === 0 && pendingRoofUpdates.size === 0)
|
|
@@ -57,14 +77,37 @@ export const RoofSystem = () => {
|
|
|
57
77
|
const node = nodes[id];
|
|
58
78
|
if (!node)
|
|
59
79
|
return;
|
|
80
|
+
// Roof accessories (chimney, skylight, solar-panel, dormer,
|
|
81
|
+
// ridge-vent, box-vent — anything declaring
|
|
82
|
+
// `capabilities.roofAccessory` on its NodeDefinition) cascade
|
|
83
|
+
// their dirty mark to the host segment's parent roof so the
|
|
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.
|
|
88
|
+
const def = nodeRegistry.get(node.type);
|
|
89
|
+
if (def?.capabilities?.roofAccessory) {
|
|
90
|
+
const segId = node.roofSegmentId;
|
|
91
|
+
const seg = segId ? nodes[segId] : undefined;
|
|
92
|
+
if (seg?.parentId) {
|
|
93
|
+
pendingRoofUpdates.add(seg.parentId);
|
|
94
|
+
}
|
|
95
|
+
clearDirty(id);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
60
98
|
if (node.type === 'roof-segment') {
|
|
61
99
|
const mesh = sceneRegistry.nodes.get(id);
|
|
100
|
+
// Merge any live override (width / depth / wallHeight / pitch /
|
|
101
|
+
// rotation) so the mesh rebuild reflects the in-flight handle drag
|
|
102
|
+
// without zustand churn. When no override is set this returns the
|
|
103
|
+
// scene node unchanged. Same pattern as DoorSystem / WallSystem.
|
|
104
|
+
const effectiveSegment = getEffectiveNode(node);
|
|
62
105
|
if (mesh) {
|
|
63
106
|
// Only compute expensive individual CSG when the segment is actually rendered
|
|
64
107
|
// (its parent group is visible = the roof is selected for editing)
|
|
65
108
|
const isVisible = mesh.parent?.visible !== false;
|
|
66
109
|
if (isVisible && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
|
|
67
|
-
updateRoofSegmentGeometry(
|
|
110
|
+
updateRoofSegmentGeometry(effectiveSegment, mesh);
|
|
68
111
|
segmentsProcessed++;
|
|
69
112
|
}
|
|
70
113
|
else if (isVisible) {
|
|
@@ -78,12 +121,16 @@ export const RoofSystem = () => {
|
|
|
78
121
|
if (mesh.geometry.type === 'BoxGeometry') {
|
|
79
122
|
mesh.geometry.dispose();
|
|
80
123
|
const placeholder = new THREE.BufferGeometry();
|
|
81
|
-
|
|
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));
|
|
82
129
|
computeGeometryBoundsTree(placeholder);
|
|
83
130
|
mesh.geometry = placeholder;
|
|
84
131
|
}
|
|
85
|
-
mesh.position.set(
|
|
86
|
-
mesh.rotation.y =
|
|
132
|
+
mesh.position.set(effectiveSegment.position[0], effectiveSegment.position[1], effectiveSegment.position[2]);
|
|
133
|
+
mesh.rotation.y = effectiveSegment.rotation;
|
|
87
134
|
}
|
|
88
135
|
clearDirty(id);
|
|
89
136
|
}
|
|
@@ -91,8 +138,8 @@ export const RoofSystem = () => {
|
|
|
91
138
|
clearDirty(id);
|
|
92
139
|
}
|
|
93
140
|
// Queue the parent roof for a merged geometry update
|
|
94
|
-
if (
|
|
95
|
-
pendingRoofUpdates.add(
|
|
141
|
+
if (effectiveSegment.parentId) {
|
|
142
|
+
pendingRoofUpdates.add(effectiveSegment.parentId);
|
|
96
143
|
}
|
|
97
144
|
}
|
|
98
145
|
else if (node.type === 'roof') {
|
|
@@ -111,13 +158,15 @@ export const RoofSystem = () => {
|
|
|
111
158
|
continue;
|
|
112
159
|
}
|
|
113
160
|
const group = sceneRegistry.nodes.get(id);
|
|
114
|
-
if (group)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
161
|
+
if (!group)
|
|
162
|
+
continue;
|
|
163
|
+
const mergedMesh = group.getObjectByName('merged-roof');
|
|
164
|
+
if (!mergedMesh)
|
|
165
|
+
continue;
|
|
166
|
+
if (mergedMesh.visible !== false) {
|
|
167
|
+
// Only rebuild when visible — RoofEditSystem re-triggers via markDirty on edit mode exit
|
|
168
|
+
updateMergedRoofGeometry(node, group, nodes);
|
|
169
|
+
roofsProcessed++;
|
|
121
170
|
}
|
|
122
171
|
pendingRoofUpdates.delete(id);
|
|
123
172
|
}
|
|
@@ -139,9 +188,21 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
139
188
|
const mergedMesh = group.getObjectByName('merged-roof');
|
|
140
189
|
if (!mergedMesh)
|
|
141
190
|
return;
|
|
191
|
+
// Segments that carry their own material / preset (catch-all or any of
|
|
192
|
+
// the role-specific fields) are rendered as their own per-segment mesh
|
|
193
|
+
// in `RoofRenderer` so the painted material is preserved. Exclude them
|
|
194
|
+
// from the merged shell — otherwise the merged mesh would draw on top
|
|
195
|
+
// with the roof's default material.
|
|
196
|
+
//
|
|
197
|
+
// Merge each child through `getEffectiveNode` so an in-flight handle
|
|
198
|
+
// drag (live override on width / depth / wallHeight / pitch / rotation)
|
|
199
|
+
// is reflected in the merged shell during the drag, not only on commit.
|
|
142
200
|
const children = (roofNode.children ?? [])
|
|
143
|
-
.map((id) =>
|
|
144
|
-
|
|
201
|
+
.map((id) => {
|
|
202
|
+
const scn = nodes[id];
|
|
203
|
+
return scn ? getEffectiveNode(scn) : undefined;
|
|
204
|
+
})
|
|
205
|
+
.filter((n) => n !== undefined && !hasSegmentMaterialOverride(n));
|
|
145
206
|
if (children.length === 0) {
|
|
146
207
|
mergedMesh.geometry.dispose();
|
|
147
208
|
// Keep a valid position attribute so Drei's BVH can index safely.
|
|
@@ -156,6 +217,73 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
156
217
|
const brushes = getRoofSegmentBrushes(child);
|
|
157
218
|
if (!brushes)
|
|
158
219
|
continue;
|
|
220
|
+
// Per-child cuts in SEGMENT-LOCAL space: subtract every accessory
|
|
221
|
+
// that contributes a cut (declares
|
|
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
|
+
}
|
|
283
|
+
}
|
|
284
|
+
brushes.shinSlab = workingShin;
|
|
285
|
+
brushes.deckSlab = workingDeck;
|
|
286
|
+
brushes.wallBrush = workingWall;
|
|
159
287
|
_matrix.compose(_position.set(child.position[0], child.position[1], child.position[2]), _quaternion.setFromAxisAngle(_yAxis, child.rotation), _scale);
|
|
160
288
|
const applyTransform = (brush) => {
|
|
161
289
|
csgGeometry(brush).applyMatrix4(_matrix);
|
|
@@ -214,6 +342,22 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
214
342
|
const shinDeck = csgEvaluator.evaluate(finalShinTrimmed, finalDeckTrimmed, ADDITION);
|
|
215
343
|
const combined = csgEvaluator.evaluate(shinDeck, finalWallTrimmed, ADDITION);
|
|
216
344
|
const resultGeo = csgGeometry(combined);
|
|
345
|
+
if (geometryHasNaNPositions(resultGeo)) {
|
|
346
|
+
if (!warnedMergedRoofNaNIds.has(roofNode.id)) {
|
|
347
|
+
console.warn('[RoofSystem] Skipping merged roof geometry with NaN positions', roofNode.id);
|
|
348
|
+
warnedMergedRoofNaNIds.add(roofNode.id);
|
|
349
|
+
}
|
|
350
|
+
resultGeo.dispose();
|
|
351
|
+
finalShinTrimmed.geometry.dispose();
|
|
352
|
+
finalDeckTrimmed.geometry.dispose();
|
|
353
|
+
finalWallTrimmed.geometry.dispose();
|
|
354
|
+
shinDeck.geometry.dispose();
|
|
355
|
+
totalShinSlab.geometry.dispose();
|
|
356
|
+
totalDeckSlab.geometry.dispose();
|
|
357
|
+
totalWall.geometry.dispose();
|
|
358
|
+
totalInner.geometry.dispose();
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
217
361
|
const resultMaterials = csgMaterials(combined);
|
|
218
362
|
const matToIndex = new Map([
|
|
219
363
|
[dummyMats[0], 0],
|
|
@@ -242,19 +386,54 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
242
386
|
totalInner.geometry.dispose();
|
|
243
387
|
}
|
|
244
388
|
}
|
|
245
|
-
|
|
389
|
+
function geometryHasNaNPositions(geometry) {
|
|
390
|
+
const position = geometry.getAttribute('position');
|
|
391
|
+
if (!position)
|
|
392
|
+
return false;
|
|
393
|
+
for (let i = 0; i < position.array.length; i++) {
|
|
394
|
+
if (Number.isNaN(position.array[i]))
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Four dummy materials used as identity placeholders during CSG. Shared
|
|
401
|
+
* across every input brush so three-bvh-csg can preserve reference
|
|
402
|
+
* equality on the result and `mapRoofGroupMaterialIndex` can map result
|
|
403
|
+
* groups back to slots 0..3. Exposed so kinds that compose additional
|
|
404
|
+
* CSG ops on top of `getRoofSegmentBrushes` (e.g. dormer) use the same
|
|
405
|
+
* identity refs.
|
|
406
|
+
*/
|
|
407
|
+
export const roofCsgDummyMats = [
|
|
246
408
|
new THREE.MeshBasicMaterial(),
|
|
247
409
|
new THREE.MeshBasicMaterial(),
|
|
248
410
|
new THREE.MeshBasicMaterial(),
|
|
249
411
|
new THREE.MeshBasicMaterial(),
|
|
250
412
|
];
|
|
251
|
-
|
|
252
|
-
|
|
413
|
+
// Internal alias kept so the surrounding file's many call sites don't churn.
|
|
414
|
+
const dummyMats = roofCsgDummyMats;
|
|
415
|
+
export const ROOF_MATERIAL_SLOT_COUNT = 4;
|
|
416
|
+
export function mapRoofGroupMaterialIndex(groupMaterialIndex, csgMaterials, matToIndex) {
|
|
253
417
|
if (groupMaterialIndex === undefined)
|
|
254
418
|
return 0;
|
|
419
|
+
// Primary path — reference-equality lookup. Fast and exact when
|
|
420
|
+
// three-bvh-csg preserves the original `dummyMats` references on
|
|
421
|
+
// the result brush.
|
|
255
422
|
const sourceMaterial = csgMaterials[groupMaterialIndex];
|
|
256
423
|
const mappedIndex = sourceMaterial ? matToIndex.get(sourceMaterial) : undefined;
|
|
257
|
-
|
|
424
|
+
if (mappedIndex !== undefined)
|
|
425
|
+
return mappedIndex;
|
|
426
|
+
// Robust fallback — every input brush was constructed with the same
|
|
427
|
+
// 4-slot `dummyMats` array, so after N union/subtraction passes the
|
|
428
|
+
// result's material array is `[dummyMats[0..3], dummyMats[0..3], ...]`
|
|
429
|
+
// and the group's materialIndex is `slot + (brushOffset * 4)`. The
|
|
430
|
+
// slot we care about is therefore `materialIndex % 4`. Without this
|
|
431
|
+
// fallback, any CSG pass that returns a fresh `Material` object (or
|
|
432
|
+
// clones the dummyMats refs) makes every group collapse to slot 0
|
|
433
|
+
// (Wall) — which is the "shape is there but the wrong colour"
|
|
434
|
+
// symptom roofs show after deselect / refresh.
|
|
435
|
+
return (((groupMaterialIndex % ROOF_MATERIAL_SLOT_COUNT) + ROOF_MATERIAL_SLOT_COUNT) %
|
|
436
|
+
ROOF_MATERIAL_SLOT_COUNT);
|
|
258
437
|
}
|
|
259
438
|
function normalizeRoofMaterialIndex(materialIndex) {
|
|
260
439
|
if (materialIndex === undefined || !Number.isFinite(materialIndex))
|
|
@@ -272,38 +451,20 @@ const RAKE_FACE_ALIGNMENT_EPSILON = 0.35;
|
|
|
272
451
|
* Ports the prototype's CSG approach using three-bvh-csg.
|
|
273
452
|
*/
|
|
274
453
|
export function getRoofSegmentBrushes(node) {
|
|
275
|
-
const { roofType, width, depth, wallHeight,
|
|
276
|
-
const activeRh
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
if (roofType === 'gable') {
|
|
283
|
-
run = depth / 2;
|
|
284
|
-
}
|
|
285
|
-
if (roofType === 'gambrel') {
|
|
286
|
-
run = depth / 4;
|
|
287
|
-
rise = activeRh * 0.6;
|
|
288
|
-
}
|
|
289
|
-
if (roofType === 'mansard') {
|
|
290
|
-
run = Math.min(width, depth) * 0.15;
|
|
291
|
-
rise = activeRh * 0.7;
|
|
292
|
-
}
|
|
293
|
-
if (roofType === 'dutch') {
|
|
294
|
-
run = Math.min(width, depth) * 0.25;
|
|
295
|
-
rise = activeRh * 0.5;
|
|
296
|
-
}
|
|
297
|
-
const tanTheta = run > 0 ? rise / run : 0;
|
|
298
|
-
const cosTheta = Math.cos(Math.atan2(rise, run)) || 1;
|
|
299
|
-
const sinTheta = Math.sin(Math.atan2(rise, run)) || 0;
|
|
454
|
+
const { roofType, width, depth, wallHeight, wallThickness, deckThickness, overhang, shingleThickness, } = node;
|
|
455
|
+
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node);
|
|
456
|
+
const shapeRatios = {
|
|
457
|
+
gambrelLowerWidthRatio: node.gambrelLowerWidthRatio,
|
|
458
|
+
mansardSteepWidthRatio: node.mansardSteepWidthRatio,
|
|
459
|
+
dutchHipWidthRatio: node.dutchHipWidthRatio,
|
|
460
|
+
};
|
|
300
461
|
const verticalRt = activeRh > 0 ? deckThickness / cosTheta : deckThickness;
|
|
301
462
|
const baseI = Math.min(width, depth) * 0.25;
|
|
302
463
|
const getVol = (wExt, vOffset, baseY, matIndex, isVoid) => {
|
|
303
464
|
const wV = Math.max(0.01, width + 2 * wExt);
|
|
304
465
|
const dV = Math.max(0.01, depth + 2 * wExt);
|
|
305
466
|
const autoDrop = wExt * tanTheta;
|
|
306
|
-
const whV = wallHeight - autoDrop + vOffset;
|
|
467
|
+
const whV = Math.max(0.01, wallHeight - autoDrop + vOffset);
|
|
307
468
|
let rhV = activeRh;
|
|
308
469
|
if (activeRh > 0) {
|
|
309
470
|
rhV = activeRh + autoDrop;
|
|
@@ -315,7 +476,7 @@ export function getRoofSegmentBrushes(node) {
|
|
|
315
476
|
if (isVoid) {
|
|
316
477
|
structuralI += deckThickness;
|
|
317
478
|
}
|
|
318
|
-
const faces = getModuleFaces(roofType, wV, dV, whV, rhV, safeBaseY, { dutchI: structuralI }, width, depth, tanTheta);
|
|
479
|
+
const faces = getModuleFaces(roofType, wV, dV, whV, rhV, safeBaseY, { dutchI: structuralI }, width, depth, tanTheta, shapeRatios);
|
|
319
480
|
return createGeometryFromFaces(faces, matIndex);
|
|
320
481
|
};
|
|
321
482
|
const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false);
|
|
@@ -389,8 +550,8 @@ export function getRoofSegmentBrushes(node) {
|
|
|
389
550
|
};
|
|
390
551
|
const insetsBot = getInsets(shinBotWh, botBaseY, true, shinBotW, shinBotD);
|
|
391
552
|
const insetsTop = getInsets(shinTopWh, topBaseY, false, shinTopW, shinTopD);
|
|
392
|
-
const botFaces = getModuleFaces(roofType, shinBotW, shinBotD, shinBotWh, shinBotRh, botBaseY, insetsBot, width, depth, tanTheta);
|
|
393
|
-
const topFaces = getModuleFaces(roofType, shinTopW, shinTopD, shinTopWh, shinTopRh, topBaseY, insetsTop, width, depth, tanTheta);
|
|
553
|
+
const botFaces = getModuleFaces(roofType, shinBotW, shinBotD, shinBotWh, shinBotRh, botBaseY, insetsBot, width, depth, tanTheta, shapeRatios);
|
|
554
|
+
const topFaces = getModuleFaces(roofType, shinTopW, shinTopD, shinTopWh, shinTopRh, topBaseY, insetsTop, width, depth, tanTheta, shapeRatios);
|
|
394
555
|
const shinBotGeo = createGeometryFromFaces(botFaces, 1);
|
|
395
556
|
const shinTopGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : 1);
|
|
396
557
|
if (transZ !== 0) {
|
|
@@ -509,7 +670,7 @@ export function generateRoofSegmentGeometry(node) {
|
|
|
509
670
|
ensureUv2Attribute(resultGeo);
|
|
510
671
|
return resultGeo;
|
|
511
672
|
}
|
|
512
|
-
function remapRoofShellFaces(geometry, node) {
|
|
673
|
+
export function remapRoofShellFaces(geometry, node) {
|
|
513
674
|
const position = geometry.getAttribute('position');
|
|
514
675
|
const index = geometry.getIndex();
|
|
515
676
|
if (!(position && index) || index.count === 0 || geometry.groups.length === 0)
|
|
@@ -600,8 +761,12 @@ function getRakeAxis(node) {
|
|
|
600
761
|
/**
|
|
601
762
|
* Generates faces for a roof module volume.
|
|
602
763
|
* Supports: hip, gable, shed, gambrel, dutch, mansard, flat.
|
|
764
|
+
*
|
|
765
|
+
* `shapeRatios` controls the kink positions on multi-slope roofs. The
|
|
766
|
+
* height ratios are already baked into `tanTheta` (via the slope frame)
|
|
767
|
+
* so they don't need to be threaded again.
|
|
603
768
|
*/
|
|
604
|
-
function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanTheta) {
|
|
769
|
+
function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanTheta, shapeRatios) {
|
|
605
770
|
const v = (x, y, z) => new THREE.Vector3(x, y, z);
|
|
606
771
|
const { iF = 0, iB = 0, iL = 0, iR = 0 } = insets;
|
|
607
772
|
const b1 = v(-w / 2 + iL, baseY, d / 2 - iF);
|
|
@@ -646,7 +811,7 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
|
|
|
646
811
|
faces.push([e1, e2, t2, t1], [e2, e3, t2], [e3, e4, t1, t2], [e4, e1, t1]);
|
|
647
812
|
}
|
|
648
813
|
else if (type === 'gambrel') {
|
|
649
|
-
const mz = (baseD / 2) *
|
|
814
|
+
const mz = (baseD / 2) * shapeRatios.gambrelLowerWidthRatio;
|
|
650
815
|
const dist = d / 2 - mz;
|
|
651
816
|
const mh = wh + dist * (tanTheta || 0);
|
|
652
817
|
const m1 = v(-w / 2, mh, mz);
|
|
@@ -658,7 +823,7 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
|
|
|
658
823
|
faces.push([e4, e1, m1, r1, m4], [e2, e3, m3, r2, m2], [e1, e2, m2, m1], [m1, m2, r2, r1], [e3, e4, m4, m3], [m3, m4, r1, r2]);
|
|
659
824
|
}
|
|
660
825
|
else if (type === 'mansard') {
|
|
661
|
-
const i = Math.min(baseW, baseD) *
|
|
826
|
+
const i = Math.min(baseW, baseD) * shapeRatios.mansardSteepWidthRatio;
|
|
662
827
|
const mh = wh + i * (tanTheta || 0);
|
|
663
828
|
const m1 = v(-w / 2 + i, mh, d / 2 - i);
|
|
664
829
|
const m2 = v(w / 2 - i, mh, d / 2 - i);
|
|
@@ -685,7 +850,9 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
|
|
|
685
850
|
}
|
|
686
851
|
}
|
|
687
852
|
else if (type === 'dutch') {
|
|
688
|
-
const i = insets.dutchI !== undefined
|
|
853
|
+
const i = insets.dutchI !== undefined
|
|
854
|
+
? insets.dutchI
|
|
855
|
+
: Math.min(baseW, baseD) * shapeRatios.dutchHipWidthRatio;
|
|
689
856
|
const mh = wh + i * (tanTheta || 0);
|
|
690
857
|
if (w >= d) {
|
|
691
858
|
const m1 = v(-w / 2 + i, mh, d / 2 - i);
|
|
@@ -728,6 +895,25 @@ function createGeometryFromFaces(faces, matRule = null) {
|
|
|
728
895
|
const vA = new THREE.Vector3().subVectors(p1, p0);
|
|
729
896
|
const vB = new THREE.Vector3().subVectors(p2, p0);
|
|
730
897
|
const normal = new THREE.Vector3().crossVectors(vA, vB).normalize();
|
|
898
|
+
let slopeAlignedDown = null;
|
|
899
|
+
let slopeAlignedAcross = null;
|
|
900
|
+
let slopeAlignedVOrigin = 0;
|
|
901
|
+
if (normal.y > SHINGLE_SURFACE_EPSILON) {
|
|
902
|
+
_uvDownSlope.copy(_uvWorldDown).projectOnPlane(normal);
|
|
903
|
+
if (_uvDownSlope.lengthSq() > 1e-8) {
|
|
904
|
+
_uvDownSlope.normalize();
|
|
905
|
+
_uvAcrossSlope.crossVectors(_uvDownSlope, normal).normalize();
|
|
906
|
+
let highestPoint = face[0];
|
|
907
|
+
for (const candidate of face) {
|
|
908
|
+
if (candidate.y > highestPoint.y) {
|
|
909
|
+
highestPoint = candidate;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
slopeAlignedDown = _uvDownSlope.clone();
|
|
913
|
+
slopeAlignedAcross = _uvAcrossSlope.clone();
|
|
914
|
+
slopeAlignedVOrigin = highestPoint.dot(slopeAlignedDown);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
731
917
|
let assignedMatIndex = 0;
|
|
732
918
|
if (typeof matRule === 'function') {
|
|
733
919
|
assignedMatIndex = matRule(normal);
|
|
@@ -750,9 +936,16 @@ function createGeometryFromFaces(faces, matRule = null) {
|
|
|
750
936
|
normals.push(normal.x, normal.y, normal.z);
|
|
751
937
|
normals.push(normal.x, normal.y, normal.z);
|
|
752
938
|
normals.push(normal.x, normal.y, normal.z);
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
939
|
+
if (slopeAlignedDown && slopeAlignedAcross) {
|
|
940
|
+
uvs.push(p0.dot(slopeAlignedAcross), slopeAlignedVOrigin - p0.dot(slopeAlignedDown));
|
|
941
|
+
uvs.push(fi.dot(slopeAlignedAcross), slopeAlignedVOrigin - fi.dot(slopeAlignedDown));
|
|
942
|
+
uvs.push(fi1.dot(slopeAlignedAcross), slopeAlignedVOrigin - fi1.dot(slopeAlignedDown));
|
|
943
|
+
}
|
|
944
|
+
else {
|
|
945
|
+
pushRoofUv(uvs, p0, normal);
|
|
946
|
+
pushRoofUv(uvs, fi, normal);
|
|
947
|
+
pushRoofUv(uvs, fi1, normal);
|
|
948
|
+
}
|
|
756
949
|
indices.push(vertexCount, vertexCount + 1, vertexCount + 2);
|
|
757
950
|
faceVertexCount += 3;
|
|
758
951
|
vertexCount += 3;
|
|
@@ -786,11 +979,20 @@ function pushRoofUv(uvs, point, normal) {
|
|
|
786
979
|
uvs.push(point.x, point.z);
|
|
787
980
|
return;
|
|
788
981
|
}
|
|
982
|
+
if (_uvFaceNormal.y > SHINGLE_SURFACE_EPSILON) {
|
|
983
|
+
_uvDownSlope.copy(_uvWorldDown).projectOnPlane(_uvFaceNormal);
|
|
984
|
+
if (_uvDownSlope.lengthSq() > 1e-8) {
|
|
985
|
+
_uvDownSlope.normalize();
|
|
986
|
+
_uvAcrossSlope.crossVectors(_uvDownSlope, _uvFaceNormal).normalize();
|
|
987
|
+
uvs.push(point.dot(_uvAcrossSlope), point.dot(_uvDownSlope));
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
789
991
|
if (absX >= absZ) {
|
|
790
|
-
uvs.push(point.z, point.y);
|
|
992
|
+
uvs.push(_uvFaceNormal.x >= 0 ? point.z : -point.z, -point.y);
|
|
791
993
|
return;
|
|
792
994
|
}
|
|
793
|
-
uvs.push(point.x, point.y);
|
|
995
|
+
uvs.push(_uvFaceNormal.z >= 0 ? point.x : -point.x, -point.y);
|
|
794
996
|
}
|
|
795
997
|
function ensureUv2Attribute(geometry) {
|
|
796
998
|
const uv = geometry.getAttribute('uv');
|
|
@@ -798,3 +1000,138 @@ function ensureUv2Attribute(geometry) {
|
|
|
798
1000
|
return;
|
|
799
1001
|
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
|
|
800
1002
|
}
|
|
1003
|
+
/**
|
|
1004
|
+
* Returns the outer roof surface frame (point + normal) at a given segment-local XZ.
|
|
1005
|
+
* This is used for skylight placement + cut direction so cutouts remain perpendicular
|
|
1006
|
+
* to the true roof surface even on multi-slope roofs (gambrel/mansard/dutch).
|
|
1007
|
+
*/
|
|
1008
|
+
export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
|
|
1009
|
+
const { roofType, width, depth, wallHeight, wallThickness, deckThickness, overhang, shingleThickness, } = segment;
|
|
1010
|
+
const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(segment);
|
|
1011
|
+
if (roofType === 'flat' || activeRh === 0) {
|
|
1012
|
+
return {
|
|
1013
|
+
point: new THREE.Vector3(lx, wallHeight + deckThickness + shingleThickness, lz),
|
|
1014
|
+
normal: new THREE.Vector3(0, 1, 0),
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
const verticalRt = deckThickness / cosTheta;
|
|
1018
|
+
const horizontalOverhang = overhang * cosTheta;
|
|
1019
|
+
const deckExt = wallThickness / 2 + horizontalOverhang;
|
|
1020
|
+
const stSin = shingleThickness * sinTheta;
|
|
1021
|
+
const stCos = shingleThickness * cosTheta;
|
|
1022
|
+
const shinBotW = Math.max(0.01, width + 2 * deckExt);
|
|
1023
|
+
const shinBotD = Math.max(0.01, depth + 2 * deckExt);
|
|
1024
|
+
const deckDrop = deckExt * tanTheta;
|
|
1025
|
+
const shinBotWh = wallHeight - deckDrop + verticalRt;
|
|
1026
|
+
let shinBotRh = activeRh;
|
|
1027
|
+
if (activeRh > 0) {
|
|
1028
|
+
shinBotRh = activeRh + deckDrop;
|
|
1029
|
+
if (roofType === 'shed')
|
|
1030
|
+
shinBotRh = activeRh + 2 * deckDrop;
|
|
1031
|
+
}
|
|
1032
|
+
let shinTopW = shinBotW;
|
|
1033
|
+
let shinTopD = shinBotD;
|
|
1034
|
+
let transZ = 0;
|
|
1035
|
+
if (['hip', 'mansard', 'dutch'].includes(roofType)) {
|
|
1036
|
+
shinTopW += 2 * stSin;
|
|
1037
|
+
shinTopD += 2 * stSin;
|
|
1038
|
+
}
|
|
1039
|
+
else {
|
|
1040
|
+
shinTopW += 2 * stSin;
|
|
1041
|
+
shinTopD += 2 * stSin;
|
|
1042
|
+
transZ = stSin;
|
|
1043
|
+
}
|
|
1044
|
+
const shinTopWh = shinBotWh + stCos;
|
|
1045
|
+
const shinTopRh = shinBotRh + stCos;
|
|
1046
|
+
const topBaseY = 0;
|
|
1047
|
+
const baseI = Math.min(width, depth) * 0.25;
|
|
1048
|
+
const getInsets = (_wh, _baseY, isVoid, _wV, _dV) => {
|
|
1049
|
+
const inset = Math.max(0.01, baseI);
|
|
1050
|
+
let iF = 0;
|
|
1051
|
+
let iB = 0;
|
|
1052
|
+
let iL = 0;
|
|
1053
|
+
let iR = 0;
|
|
1054
|
+
if (roofType === 'hip') {
|
|
1055
|
+
iF = inset;
|
|
1056
|
+
iB = inset;
|
|
1057
|
+
iL = inset;
|
|
1058
|
+
iR = inset;
|
|
1059
|
+
}
|
|
1060
|
+
else if (roofType === 'gable' || roofType === 'gambrel') {
|
|
1061
|
+
iL = inset;
|
|
1062
|
+
iR = inset;
|
|
1063
|
+
}
|
|
1064
|
+
else if (roofType === 'mansard' || roofType === 'dutch') {
|
|
1065
|
+
iF = inset;
|
|
1066
|
+
iB = inset;
|
|
1067
|
+
iL = inset;
|
|
1068
|
+
iR = inset;
|
|
1069
|
+
}
|
|
1070
|
+
else if (roofType === 'shed') {
|
|
1071
|
+
iF = inset;
|
|
1072
|
+
}
|
|
1073
|
+
let structuralI = baseI;
|
|
1074
|
+
if (isVoid) {
|
|
1075
|
+
structuralI += shingleThickness;
|
|
1076
|
+
}
|
|
1077
|
+
return { iF, iB, iL, iR, dutchI: structuralI };
|
|
1078
|
+
};
|
|
1079
|
+
const insetsTop = getInsets(shinTopWh, topBaseY, false, shinTopW, shinTopD);
|
|
1080
|
+
const shapeRatios = {
|
|
1081
|
+
gambrelLowerWidthRatio: segment.gambrelLowerWidthRatio,
|
|
1082
|
+
mansardSteepWidthRatio: segment.mansardSteepWidthRatio,
|
|
1083
|
+
dutchHipWidthRatio: segment.dutchHipWidthRatio,
|
|
1084
|
+
};
|
|
1085
|
+
const topFaces = getModuleFaces(roofType, shinTopW, shinTopD, shinTopWh, shinTopRh, topBaseY, insetsTop, width, depth, tanTheta, shapeRatios);
|
|
1086
|
+
const topGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : 1);
|
|
1087
|
+
if (transZ !== 0)
|
|
1088
|
+
topGeo.translate(0, 0, transZ);
|
|
1089
|
+
topGeo.computeBoundingBox();
|
|
1090
|
+
const topY = wallHeight + activeRh + deckThickness + shingleThickness + 10;
|
|
1091
|
+
_surfaceOrigin.set(lx, topY, lz);
|
|
1092
|
+
_surfaceRay.set(_surfaceOrigin, _surfaceDir);
|
|
1093
|
+
_surfaceHits.length = 0;
|
|
1094
|
+
const pos = topGeo.getAttribute('position');
|
|
1095
|
+
const index = topGeo.getIndex();
|
|
1096
|
+
if (!pos || !index) {
|
|
1097
|
+
topGeo.dispose();
|
|
1098
|
+
return {
|
|
1099
|
+
point: new THREE.Vector3(lx, wallHeight, lz),
|
|
1100
|
+
normal: new THREE.Vector3(0, 1, 0),
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
let bestT = Number.POSITIVE_INFINITY;
|
|
1104
|
+
let bestPoint = null;
|
|
1105
|
+
let bestNormal = null;
|
|
1106
|
+
for (let i = 0; i < index.count; i += 3) {
|
|
1107
|
+
const a = index.getX(i);
|
|
1108
|
+
const b = index.getX(i + 1);
|
|
1109
|
+
const c = index.getX(i + 2);
|
|
1110
|
+
_surfaceV0.fromBufferAttribute(pos, a);
|
|
1111
|
+
_surfaceV1.fromBufferAttribute(pos, b);
|
|
1112
|
+
_surfaceV2.fromBufferAttribute(pos, c);
|
|
1113
|
+
const hit = _surfaceRay.intersectTriangle(_surfaceV0, _surfaceV1, _surfaceV2, false, _tmpVec3A);
|
|
1114
|
+
if (!hit)
|
|
1115
|
+
continue;
|
|
1116
|
+
const t = hit.distanceTo(_surfaceOrigin);
|
|
1117
|
+
if (t < bestT) {
|
|
1118
|
+
bestT = t;
|
|
1119
|
+
bestPoint = hit.clone();
|
|
1120
|
+
_surfaceFaceNormal
|
|
1121
|
+
.subVectors(_surfaceV1, _surfaceV0)
|
|
1122
|
+
.cross(_tmpVec3B.subVectors(_surfaceV2, _surfaceV0))
|
|
1123
|
+
.normalize();
|
|
1124
|
+
bestNormal = _surfaceFaceNormal.clone();
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
topGeo.dispose();
|
|
1128
|
+
if (!bestPoint || !bestNormal) {
|
|
1129
|
+
return {
|
|
1130
|
+
point: new THREE.Vector3(lx, wallHeight, lz),
|
|
1131
|
+
normal: new THREE.Vector3(0, 1, 0),
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
if (bestNormal.y < 0)
|
|
1135
|
+
bestNormal.multiplyScalar(-1);
|
|
1136
|
+
return { point: bestPoint, normal: bestNormal };
|
|
1137
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"slab-system.d.ts","sourceRoot":"","sources":["../../../src/systems/slab/slab-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,QAAQ,EAGd,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAe9B,eAAO,MAAM,UAAU,YAkCtB,CAAA;AAkBD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAAC,cAAc,CAG7E"}
|