@pascal-app/viewer 0.7.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/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +5 -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 +38 -1
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +183 -60
- 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 +217 -61
- 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 -63
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +11 -11
- 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 +10 -6
- 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/guide/guide-system.d.ts.map +1 -1
- package/dist/systems/guide/guide-system.js +1 -26
- 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 +411 -71
- package/dist/systems/scan/scan-system.d.ts.map +1 -1
- package/dist/systems/scan/scan-system.js +1 -26
- 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 +58 -71
- 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 +1 -1
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +179 -31
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +71 -7
- package/package.json +4 -5
- 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 -594
- 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 -29
- 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 -32
- 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
- package/dist/lib/suppress-three-clock-warning.d.ts +0 -25
- package/dist/lib/suppress-three-clock-warning.d.ts.map +0 -1
- package/dist/lib/suppress-three-clock-warning.js +0 -51
- package/dist/systems/export/export-system.d.ts +0 -2
- package/dist/systems/export/export-system.d.ts.map +0 -1
- package/dist/systems/export/export-system.js +0 -60
- package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts +0 -14
- package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.d.ts.map +0 -1
- package/dist/systems/item-mesh-metadata/compute-item-mesh-metadata.js +0 -155
- package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts +0 -6
- package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.d.ts.map +0 -1
- package/dist/systems/item-mesh-metadata/item-mesh-metadata-system.js +0 -75
- package/dist/systems/item-mesh-metadata/sync-request.d.ts +0 -7
- package/dist/systems/item-mesh-metadata/sync-request.d.ts.map +0 -1
- package/dist/systems/item-mesh-metadata/sync-request.js +0 -25
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { getEffectiveNode, getSegmentSlopeFrame, hasSegmentMaterialOverride, nodeRegistry, sceneRegistry, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
|
|
1
2
|
import { useFrame } from '@react-three/fiber';
|
|
2
|
-
import { sceneRegistry, useScene, } from '@pascal-app/core';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
import { 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
|
+
function csgGeometry(brush) {
|
|
8
|
+
return brush.geometry;
|
|
9
|
+
}
|
|
10
|
+
function csgMaterials(brush) {
|
|
11
|
+
const mat = brush.material;
|
|
12
|
+
return Array.isArray(mat) ? mat : [mat];
|
|
13
|
+
}
|
|
7
14
|
const csgEvaluator = new Evaluator();
|
|
8
15
|
csgEvaluator.useGroups = true;
|
|
9
16
|
csgEvaluator.consolidateGroups = false; // shared dummyMats across brushes causes consolidation to misalign groupIndices vs groupOrder indices → crash
|
|
@@ -24,8 +31,22 @@ const _quaternion = new THREE.Quaternion();
|
|
|
24
31
|
const _scale = new THREE.Vector3(1, 1, 1);
|
|
25
32
|
const _yAxis = new THREE.Vector3(0, 1, 0);
|
|
26
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();
|
|
27
47
|
// Pending merged-roof updates carried across frames (for throttling)
|
|
28
48
|
const pendingRoofUpdates = new Set();
|
|
49
|
+
const warnedMergedRoofNaNIds = new Set();
|
|
29
50
|
const MAX_ROOFS_PER_FRAME = 1;
|
|
30
51
|
const MAX_SEGMENTS_PER_FRAME = 3;
|
|
31
52
|
// ============================================================================
|
|
@@ -35,10 +56,16 @@ export const RoofSystem = () => {
|
|
|
35
56
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
36
57
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
37
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);
|
|
38
64
|
useFrame(() => {
|
|
39
65
|
// Clear stale pending updates when the scene is unloaded
|
|
40
66
|
if (rootNodeIds.length === 0) {
|
|
41
67
|
pendingRoofUpdates.clear();
|
|
68
|
+
warnedMergedRoofNaNIds.clear();
|
|
42
69
|
return;
|
|
43
70
|
}
|
|
44
71
|
if (dirtyNodes.size === 0 && pendingRoofUpdates.size === 0)
|
|
@@ -50,14 +77,37 @@ export const RoofSystem = () => {
|
|
|
50
77
|
const node = nodes[id];
|
|
51
78
|
if (!node)
|
|
52
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
|
+
}
|
|
53
98
|
if (node.type === 'roof-segment') {
|
|
54
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);
|
|
55
105
|
if (mesh) {
|
|
56
106
|
// Only compute expensive individual CSG when the segment is actually rendered
|
|
57
107
|
// (its parent group is visible = the roof is selected for editing)
|
|
58
108
|
const isVisible = mesh.parent?.visible !== false;
|
|
59
109
|
if (isVisible && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
|
|
60
|
-
updateRoofSegmentGeometry(
|
|
110
|
+
updateRoofSegmentGeometry(effectiveSegment, mesh);
|
|
61
111
|
segmentsProcessed++;
|
|
62
112
|
}
|
|
63
113
|
else if (isVisible) {
|
|
@@ -71,12 +121,16 @@ export const RoofSystem = () => {
|
|
|
71
121
|
if (mesh.geometry.type === 'BoxGeometry') {
|
|
72
122
|
mesh.geometry.dispose();
|
|
73
123
|
const placeholder = new THREE.BufferGeometry();
|
|
74
|
-
|
|
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));
|
|
75
129
|
computeGeometryBoundsTree(placeholder);
|
|
76
130
|
mesh.geometry = placeholder;
|
|
77
131
|
}
|
|
78
|
-
mesh.position.set(
|
|
79
|
-
mesh.rotation.y =
|
|
132
|
+
mesh.position.set(effectiveSegment.position[0], effectiveSegment.position[1], effectiveSegment.position[2]);
|
|
133
|
+
mesh.rotation.y = effectiveSegment.rotation;
|
|
80
134
|
}
|
|
81
135
|
clearDirty(id);
|
|
82
136
|
}
|
|
@@ -84,8 +138,8 @@ export const RoofSystem = () => {
|
|
|
84
138
|
clearDirty(id);
|
|
85
139
|
}
|
|
86
140
|
// Queue the parent roof for a merged geometry update
|
|
87
|
-
if (
|
|
88
|
-
pendingRoofUpdates.add(
|
|
141
|
+
if (effectiveSegment.parentId) {
|
|
142
|
+
pendingRoofUpdates.add(effectiveSegment.parentId);
|
|
89
143
|
}
|
|
90
144
|
}
|
|
91
145
|
else if (node.type === 'roof') {
|
|
@@ -104,13 +158,15 @@ export const RoofSystem = () => {
|
|
|
104
158
|
continue;
|
|
105
159
|
}
|
|
106
160
|
const group = sceneRegistry.nodes.get(id);
|
|
107
|
-
if (group)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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++;
|
|
114
170
|
}
|
|
115
171
|
pendingRoofUpdates.delete(id);
|
|
116
172
|
}
|
|
@@ -132,9 +188,21 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
132
188
|
const mergedMesh = group.getObjectByName('merged-roof');
|
|
133
189
|
if (!mergedMesh)
|
|
134
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.
|
|
135
200
|
const children = (roofNode.children ?? [])
|
|
136
|
-
.map((id) =>
|
|
137
|
-
|
|
201
|
+
.map((id) => {
|
|
202
|
+
const scn = nodes[id];
|
|
203
|
+
return scn ? getEffectiveNode(scn) : undefined;
|
|
204
|
+
})
|
|
205
|
+
.filter((n) => n !== undefined && !hasSegmentMaterialOverride(n));
|
|
138
206
|
if (children.length === 0) {
|
|
139
207
|
mergedMesh.geometry.dispose();
|
|
140
208
|
// Keep a valid position attribute so Drei's BVH can index safely.
|
|
@@ -149,9 +217,76 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
149
217
|
const brushes = getRoofSegmentBrushes(child);
|
|
150
218
|
if (!brushes)
|
|
151
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;
|
|
152
287
|
_matrix.compose(_position.set(child.position[0], child.position[1], child.position[2]), _quaternion.setFromAxisAngle(_yAxis, child.rotation), _scale);
|
|
153
288
|
const applyTransform = (brush) => {
|
|
154
|
-
brush.
|
|
289
|
+
csgGeometry(brush).applyMatrix4(_matrix);
|
|
155
290
|
brush.updateMatrixWorld();
|
|
156
291
|
};
|
|
157
292
|
applyTransform(brushes.shinSlab);
|
|
@@ -206,10 +341,24 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
206
341
|
const finalWallTrimmed = csgEvaluator.evaluate(totalWall, totalInner, SUBTRACTION);
|
|
207
342
|
const shinDeck = csgEvaluator.evaluate(finalShinTrimmed, finalDeckTrimmed, ADDITION);
|
|
208
343
|
const combined = csgEvaluator.evaluate(shinDeck, finalWallTrimmed, ADDITION);
|
|
209
|
-
const resultGeo = combined
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
361
|
+
const resultMaterials = csgMaterials(combined);
|
|
213
362
|
const matToIndex = new Map([
|
|
214
363
|
[dummyMats[0], 0],
|
|
215
364
|
[dummyMats[1], 1],
|
|
@@ -219,8 +368,8 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
219
368
|
for (const g of resultGeo.groups) {
|
|
220
369
|
g.materialIndex = mapRoofGroupMaterialIndex(g.materialIndex, resultMaterials, matToIndex);
|
|
221
370
|
}
|
|
222
|
-
ensureUv2Attribute(resultGeo);
|
|
223
371
|
resultGeo.computeVertexNormals();
|
|
372
|
+
ensureUv2Attribute(resultGeo);
|
|
224
373
|
mergedMesh.geometry.dispose();
|
|
225
374
|
mergedMesh.geometry = resultGeo;
|
|
226
375
|
finalShinTrimmed.geometry.dispose();
|
|
@@ -237,19 +386,54 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
|
|
|
237
386
|
totalInner.geometry.dispose();
|
|
238
387
|
}
|
|
239
388
|
}
|
|
240
|
-
|
|
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 = [
|
|
241
408
|
new THREE.MeshBasicMaterial(),
|
|
242
409
|
new THREE.MeshBasicMaterial(),
|
|
243
410
|
new THREE.MeshBasicMaterial(),
|
|
244
411
|
new THREE.MeshBasicMaterial(),
|
|
245
412
|
];
|
|
246
|
-
|
|
247
|
-
|
|
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) {
|
|
248
417
|
if (groupMaterialIndex === undefined)
|
|
249
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.
|
|
250
422
|
const sourceMaterial = csgMaterials[groupMaterialIndex];
|
|
251
423
|
const mappedIndex = sourceMaterial ? matToIndex.get(sourceMaterial) : undefined;
|
|
252
|
-
|
|
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);
|
|
253
437
|
}
|
|
254
438
|
function normalizeRoofMaterialIndex(materialIndex) {
|
|
255
439
|
if (materialIndex === undefined || !Number.isFinite(materialIndex))
|
|
@@ -267,38 +451,20 @@ const RAKE_FACE_ALIGNMENT_EPSILON = 0.35;
|
|
|
267
451
|
* Ports the prototype's CSG approach using three-bvh-csg.
|
|
268
452
|
*/
|
|
269
453
|
export function getRoofSegmentBrushes(node) {
|
|
270
|
-
const { roofType, width, depth, wallHeight,
|
|
271
|
-
const activeRh
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
if (roofType === 'gable') {
|
|
278
|
-
run = depth / 2;
|
|
279
|
-
}
|
|
280
|
-
if (roofType === 'gambrel') {
|
|
281
|
-
run = depth / 4;
|
|
282
|
-
rise = activeRh * 0.6;
|
|
283
|
-
}
|
|
284
|
-
if (roofType === 'mansard') {
|
|
285
|
-
run = Math.min(width, depth) * 0.15;
|
|
286
|
-
rise = activeRh * 0.7;
|
|
287
|
-
}
|
|
288
|
-
if (roofType === 'dutch') {
|
|
289
|
-
run = Math.min(width, depth) * 0.25;
|
|
290
|
-
rise = activeRh * 0.5;
|
|
291
|
-
}
|
|
292
|
-
const tanTheta = run > 0 ? rise / run : 0;
|
|
293
|
-
const cosTheta = Math.cos(Math.atan2(rise, run)) || 1;
|
|
294
|
-
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
|
+
};
|
|
295
461
|
const verticalRt = activeRh > 0 ? deckThickness / cosTheta : deckThickness;
|
|
296
462
|
const baseI = Math.min(width, depth) * 0.25;
|
|
297
463
|
const getVol = (wExt, vOffset, baseY, matIndex, isVoid) => {
|
|
298
464
|
const wV = Math.max(0.01, width + 2 * wExt);
|
|
299
465
|
const dV = Math.max(0.01, depth + 2 * wExt);
|
|
300
466
|
const autoDrop = wExt * tanTheta;
|
|
301
|
-
const whV = wallHeight - autoDrop + vOffset;
|
|
467
|
+
const whV = Math.max(0.01, wallHeight - autoDrop + vOffset);
|
|
302
468
|
let rhV = activeRh;
|
|
303
469
|
if (activeRh > 0) {
|
|
304
470
|
rhV = activeRh + autoDrop;
|
|
@@ -310,7 +476,7 @@ export function getRoofSegmentBrushes(node) {
|
|
|
310
476
|
if (isVoid) {
|
|
311
477
|
structuralI += deckThickness;
|
|
312
478
|
}
|
|
313
|
-
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);
|
|
314
480
|
return createGeometryFromFaces(faces, matIndex);
|
|
315
481
|
};
|
|
316
482
|
const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false);
|
|
@@ -384,8 +550,8 @@ export function getRoofSegmentBrushes(node) {
|
|
|
384
550
|
};
|
|
385
551
|
const insetsBot = getInsets(shinBotWh, botBaseY, true, shinBotW, shinBotD);
|
|
386
552
|
const insetsTop = getInsets(shinTopWh, topBaseY, false, shinTopW, shinTopD);
|
|
387
|
-
const botFaces = getModuleFaces(roofType, shinBotW, shinBotD, shinBotWh, shinBotRh, botBaseY, insetsBot, width, depth, tanTheta);
|
|
388
|
-
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);
|
|
389
555
|
const shinBotGeo = createGeometryFromFaces(botFaces, 1);
|
|
390
556
|
const shinTopGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : 1);
|
|
391
557
|
if (transZ !== 0) {
|
|
@@ -477,10 +643,8 @@ export function generateRoofSegmentGeometry(node) {
|
|
|
477
643
|
const hollowWall = csgEvaluator.evaluate(wallBrush, innerBrush, SUBTRACTION);
|
|
478
644
|
const shinDeck = csgEvaluator.evaluate(shinSlab, deckSlab, ADDITION);
|
|
479
645
|
const combined = csgEvaluator.evaluate(shinDeck, hollowWall, ADDITION);
|
|
480
|
-
resultGeo = combined
|
|
481
|
-
const resultMaterials =
|
|
482
|
-
? combined.material
|
|
483
|
-
: [combined.material];
|
|
646
|
+
resultGeo = csgGeometry(combined);
|
|
647
|
+
const resultMaterials = csgMaterials(combined);
|
|
484
648
|
const matToIndex = new Map([
|
|
485
649
|
[dummyMats[0], 0],
|
|
486
650
|
[dummyMats[1], 1],
|
|
@@ -496,17 +660,17 @@ export function generateRoofSegmentGeometry(node) {
|
|
|
496
660
|
}
|
|
497
661
|
catch (e) {
|
|
498
662
|
console.error('Roof CSG failed:', e);
|
|
499
|
-
resultGeo = wallBrush.
|
|
663
|
+
resultGeo = csgGeometry(wallBrush).clone();
|
|
500
664
|
}
|
|
501
665
|
deckSlab.geometry.dispose();
|
|
502
666
|
shinSlab.geometry.dispose();
|
|
503
667
|
wallBrush.geometry.dispose();
|
|
504
668
|
innerBrush.geometry.dispose();
|
|
505
|
-
ensureUv2Attribute(resultGeo);
|
|
506
669
|
resultGeo.computeVertexNormals();
|
|
670
|
+
ensureUv2Attribute(resultGeo);
|
|
507
671
|
return resultGeo;
|
|
508
672
|
}
|
|
509
|
-
function remapRoofShellFaces(geometry, node) {
|
|
673
|
+
export function remapRoofShellFaces(geometry, node) {
|
|
510
674
|
const position = geometry.getAttribute('position');
|
|
511
675
|
const index = geometry.getIndex();
|
|
512
676
|
if (!(position && index) || index.count === 0 || geometry.groups.length === 0)
|
|
@@ -597,8 +761,12 @@ function getRakeAxis(node) {
|
|
|
597
761
|
/**
|
|
598
762
|
* Generates faces for a roof module volume.
|
|
599
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.
|
|
600
768
|
*/
|
|
601
|
-
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) {
|
|
602
770
|
const v = (x, y, z) => new THREE.Vector3(x, y, z);
|
|
603
771
|
const { iF = 0, iB = 0, iL = 0, iR = 0 } = insets;
|
|
604
772
|
const b1 = v(-w / 2 + iL, baseY, d / 2 - iF);
|
|
@@ -643,7 +811,7 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
|
|
|
643
811
|
faces.push([e1, e2, t2, t1], [e2, e3, t2], [e3, e4, t1, t2], [e4, e1, t1]);
|
|
644
812
|
}
|
|
645
813
|
else if (type === 'gambrel') {
|
|
646
|
-
const mz = (baseD / 2) *
|
|
814
|
+
const mz = (baseD / 2) * shapeRatios.gambrelLowerWidthRatio;
|
|
647
815
|
const dist = d / 2 - mz;
|
|
648
816
|
const mh = wh + dist * (tanTheta || 0);
|
|
649
817
|
const m1 = v(-w / 2, mh, mz);
|
|
@@ -655,7 +823,7 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
|
|
|
655
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]);
|
|
656
824
|
}
|
|
657
825
|
else if (type === 'mansard') {
|
|
658
|
-
const i = Math.min(baseW, baseD) *
|
|
826
|
+
const i = Math.min(baseW, baseD) * shapeRatios.mansardSteepWidthRatio;
|
|
659
827
|
const mh = wh + i * (tanTheta || 0);
|
|
660
828
|
const m1 = v(-w / 2 + i, mh, d / 2 - i);
|
|
661
829
|
const m2 = v(w / 2 - i, mh, d / 2 - i);
|
|
@@ -682,7 +850,9 @@ function getModuleFaces(type, w, d, wh, rh, baseY, insets, baseW, baseD, tanThet
|
|
|
682
850
|
}
|
|
683
851
|
}
|
|
684
852
|
else if (type === 'dutch') {
|
|
685
|
-
const i = insets.dutchI !== undefined
|
|
853
|
+
const i = insets.dutchI !== undefined
|
|
854
|
+
? insets.dutchI
|
|
855
|
+
: Math.min(baseW, baseD) * shapeRatios.dutchHipWidthRatio;
|
|
686
856
|
const mh = wh + i * (tanTheta || 0);
|
|
687
857
|
if (w >= d) {
|
|
688
858
|
const m1 = v(-w / 2 + i, mh, d / 2 - i);
|
|
@@ -725,6 +895,25 @@ function createGeometryFromFaces(faces, matRule = null) {
|
|
|
725
895
|
const vA = new THREE.Vector3().subVectors(p1, p0);
|
|
726
896
|
const vB = new THREE.Vector3().subVectors(p2, p0);
|
|
727
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
|
+
}
|
|
728
917
|
let assignedMatIndex = 0;
|
|
729
918
|
if (typeof matRule === 'function') {
|
|
730
919
|
assignedMatIndex = matRule(normal);
|
|
@@ -747,9 +936,16 @@ function createGeometryFromFaces(faces, matRule = null) {
|
|
|
747
936
|
normals.push(normal.x, normal.y, normal.z);
|
|
748
937
|
normals.push(normal.x, normal.y, normal.z);
|
|
749
938
|
normals.push(normal.x, normal.y, normal.z);
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
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
|
+
}
|
|
753
949
|
indices.push(vertexCount, vertexCount + 1, vertexCount + 2);
|
|
754
950
|
faceVertexCount += 3;
|
|
755
951
|
vertexCount += 3;
|
|
@@ -783,11 +979,20 @@ function pushRoofUv(uvs, point, normal) {
|
|
|
783
979
|
uvs.push(point.x, point.z);
|
|
784
980
|
return;
|
|
785
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
|
+
}
|
|
786
991
|
if (absX >= absZ) {
|
|
787
|
-
uvs.push(point.z, point.y);
|
|
992
|
+
uvs.push(_uvFaceNormal.x >= 0 ? point.z : -point.z, -point.y);
|
|
788
993
|
return;
|
|
789
994
|
}
|
|
790
|
-
uvs.push(point.x, point.y);
|
|
995
|
+
uvs.push(_uvFaceNormal.z >= 0 ? point.x : -point.x, -point.y);
|
|
791
996
|
}
|
|
792
997
|
function ensureUv2Attribute(geometry) {
|
|
793
998
|
const uv = geometry.getAttribute('uv');
|
|
@@ -795,3 +1000,138 @@ function ensureUv2Attribute(geometry) {
|
|
|
795
1000
|
return;
|
|
796
1001
|
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
|
|
797
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
|
+
}
|