@pascal-app/viewer 0.9.1 → 1.0.0-beta.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/README.md +20 -5
- package/dist/components/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +5 -2
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/parametric-node-renderer.js +7 -4
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +59 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
- package/dist/components/viewer/index.d.ts +19 -2
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +113 -12
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +36 -5
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +17 -2
- package/dist/components/viewer/post-processing.d.ts +7 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +180 -24
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +6 -4
- package/dist/components/viewer/scene-bvh.d.ts +2 -1
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +3 -0
- package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
- package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.test.js +13 -0
- package/dist/components/viewer/scene-environment.d.ts +2 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -0
- package/dist/components/viewer/scene-environment.js +79 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +23 -12
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -24
- package/dist/hooks/use-node-events.d.ts +1 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +22 -14
- package/dist/index.d.ts +24 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -13
- package/dist/lib/backdrop.d.ts +33 -0
- package/dist/lib/backdrop.d.ts.map +1 -0
- package/dist/lib/backdrop.js +92 -0
- package/dist/lib/box-uv.d.ts +13 -0
- package/dist/lib/box-uv.d.ts.map +1 -0
- package/dist/lib/box-uv.js +32 -0
- package/dist/lib/csg-utils.d.ts +3 -9
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +65 -5
- package/dist/lib/csg-utils.test.d.ts +2 -0
- package/dist/lib/csg-utils.test.d.ts.map +1 -0
- package/dist/lib/csg-utils.test.js +45 -0
- package/dist/lib/edge-style.d.ts +1 -0
- package/dist/lib/edge-style.d.ts.map +1 -1
- package/dist/lib/edge-style.js +26 -6
- package/dist/lib/hero-pose.d.ts +47 -0
- package/dist/lib/hero-pose.d.ts.map +1 -0
- package/dist/lib/hero-pose.js +227 -0
- package/dist/lib/ink-edges.d.ts +1 -1
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +13 -7
- package/dist/lib/ktx2-loader.d.ts +46 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +142 -0
- package/dist/lib/layers.d.ts +10 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +10 -0
- package/dist/lib/materials.d.ts +16 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +158 -34
- package/dist/lib/materials.test.d.ts +2 -0
- package/dist/lib/materials.test.d.ts.map +1 -0
- package/dist/lib/materials.test.js +25 -0
- package/dist/lib/merged-outline-node.test.d.ts +2 -0
- package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
- package/dist/lib/merged-outline-node.test.js +22 -0
- package/dist/lib/scene-themes.d.ts +1 -0
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +25 -16
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/snapshot-pipeline.d.ts +42 -0
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
- package/dist/lib/snapshot-pipeline.js +254 -0
- package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
- package/dist/lib/terrain-perimeter-fill.js +75 -0
- package/dist/lib/texture-node-guard.d.ts +13 -0
- package/dist/lib/texture-node-guard.d.ts.map +1 -0
- package/dist/lib/texture-node-guard.js +39 -0
- package/dist/lib/texture-reference.d.ts +31 -0
- package/dist/lib/texture-reference.d.ts.map +1 -0
- package/dist/lib/texture-reference.js +139 -0
- package/dist/lib/texture-reference.test.d.ts +2 -0
- package/dist/lib/texture-reference.test.d.ts.map +1 -0
- package/dist/lib/texture-reference.test.js +86 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +73 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +191 -4
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +61 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +12 -5
- package/dist/systems/door/door-animation.test.d.ts +2 -0
- package/dist/systems/door/door-animation.test.d.ts.map +1 -0
- package/dist/systems/door/door-animation.test.js +51 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
- package/dist/systems/door/door-floor-alignment.test.js +50 -0
- package/dist/systems/door/door-system.d.ts +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +494 -161
- package/dist/systems/fence/fence-system.d.ts +17 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +221 -53
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
- package/dist/systems/geometry/geometry-system.d.ts +7 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +95 -26
- package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
- package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.test.js +30 -0
- package/dist/systems/interactive/control-widget.d.ts +10 -0
- package/dist/systems/interactive/control-widget.d.ts.map +1 -0
- package/dist/systems/interactive/control-widget.js +35 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +55 -56
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +20 -6
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +34 -9
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +7 -45
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +29 -8
- package/dist/systems/roof/roof-system.d.ts +21 -8
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1079 -221
- package/dist/systems/slab/slab-system.d.ts +7 -4
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +31 -83
- package/dist/systems/slab/slab-system.test.js +54 -14
- package/dist/systems/stair/stair-system.js +7 -4
- package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.js +167 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +49 -17
- package/dist/systems/wall/wall-materials.d.ts +11 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +244 -35
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +116 -0
- package/dist/systems/wall/wall-system.d.ts +4 -8
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +397 -177
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
- package/dist/systems/window/window-animation-system.d.ts +12 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +16 -5
- package/dist/systems/window/window-system.d.ts +7 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +240 -14
- package/package.json +4 -4
- package/dist/components/viewer/ground-occluder.d.ts +0 -2
- package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
- package/dist/components/viewer/ground-occluder.js +0 -67
- package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
- package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
- package/dist/components/viewer/walkthrough-controls.js +0 -131
|
@@ -1,13 +1,37 @@
|
|
|
1
|
-
import { calculateLevelMiters,
|
|
1
|
+
import { calculateLevelMiters, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getWallBandSlotId, getWallCurveFrameAt, getWallFaceBandConfig, getWallFaceBandForHeight, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
4
5
|
import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
5
6
|
import { computeBoundsTree } from 'three-mesh-bvh';
|
|
7
|
+
import { ensureRenderableGeometryAttributes, prepareBrushForCSG } from '../../lib/csg-utils';
|
|
8
|
+
import { buildTerrainPerimeterFillGeometry } from '../../lib/terrain-perimeter-fill';
|
|
9
|
+
import { buildOpeningCutoutGeometry, getOpeningCutoutBottomPadding, } from './opening-cutout-geometry';
|
|
6
10
|
// Reusable CSG evaluator for better performance
|
|
7
11
|
const csgEvaluator = new Evaluator();
|
|
12
|
+
csgEvaluator.attributes = ['position', 'normal', 'uv', 'uv2'];
|
|
8
13
|
const CURVED_WALL_3D_ENDPOINT_INSET = 0.0015;
|
|
9
14
|
const WALL_FACE_NORMAL_Y_EPSILON = 0.6;
|
|
10
15
|
const WALL_FACE_EDGE_DISTANCE_EPSILON = 0.003;
|
|
16
|
+
const WALL_BAND_SPLIT_EPSILON = 1e-5;
|
|
17
|
+
const WALL_BAND_SLOT_MATERIAL_INDEX = {
|
|
18
|
+
interior: 1,
|
|
19
|
+
exterior: 2,
|
|
20
|
+
lowerInterior: 3,
|
|
21
|
+
middleInterior: 4,
|
|
22
|
+
upperInterior: 5,
|
|
23
|
+
topInterior: 6,
|
|
24
|
+
lowerExterior: 7,
|
|
25
|
+
middleExterior: 8,
|
|
26
|
+
upperExterior: 9,
|
|
27
|
+
topExterior: 10,
|
|
28
|
+
skirtingInterior: 0,
|
|
29
|
+
skirtingExterior: 0,
|
|
30
|
+
crownInterior: 0,
|
|
31
|
+
crownExterior: 0,
|
|
32
|
+
chairRailInterior: 0,
|
|
33
|
+
chairRailExterior: 0,
|
|
34
|
+
};
|
|
11
35
|
function computeGeometryBoundsTree(geometry) {
|
|
12
36
|
;
|
|
13
37
|
geometry.computeBoundsTree = computeBoundsTree;
|
|
@@ -16,12 +40,6 @@ function computeGeometryBoundsTree(geometry) {
|
|
|
16
40
|
function csgGeometry(brush) {
|
|
17
41
|
return brush.geometry;
|
|
18
42
|
}
|
|
19
|
-
function ensureUv2Attribute(geometry) {
|
|
20
|
-
const uv = geometry.getAttribute('uv');
|
|
21
|
-
if (!uv)
|
|
22
|
-
return;
|
|
23
|
-
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
|
|
24
|
-
}
|
|
25
43
|
function insetCurvedWallBoundaryPointsFor3D(wall, boundaryPoints, miterData) {
|
|
26
44
|
if (!(boundaryPoints && isCurvedWall(wall))) {
|
|
27
45
|
return boundaryPoints;
|
|
@@ -117,16 +135,17 @@ function distanceToWallBoundaryEdge(point, edge) {
|
|
|
117
135
|
const closestZ = edge.start.y + edgeDz * t;
|
|
118
136
|
return Math.hypot(point.x - closestX, point.y - closestZ);
|
|
119
137
|
}
|
|
120
|
-
function getWallFaceMaterialIndex(wall, face) {
|
|
138
|
+
function getWallFaceMaterialIndex(wall, face, y, effectiveWallHeight) {
|
|
121
139
|
const semantic = face === 'front' ? wall.frontSide : wall.backSide;
|
|
122
|
-
const fallback = face === 'front' ?
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
return
|
|
127
|
-
|
|
140
|
+
const fallback = face === 'front' ? 'interior' : 'exterior';
|
|
141
|
+
const side = semantic === 'interior' || semantic === 'exterior' ? semantic : fallback;
|
|
142
|
+
const bands = getWallFaceBandConfig(wall, effectiveWallHeight);
|
|
143
|
+
if (!bands.enabled)
|
|
144
|
+
return WALL_BAND_SLOT_MATERIAL_INDEX[side];
|
|
145
|
+
const band = getWallFaceBandForHeight(wall, y, effectiveWallHeight);
|
|
146
|
+
return WALL_BAND_SLOT_MATERIAL_INDEX[getWallBandSlotId(side, band)];
|
|
128
147
|
}
|
|
129
|
-
function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
|
|
148
|
+
function assignWallMaterialGroups(geometry, wall, boundaryEdges, effectiveWallHeight) {
|
|
130
149
|
const position = geometry.getAttribute('position');
|
|
131
150
|
if (!position)
|
|
132
151
|
return;
|
|
@@ -189,7 +208,7 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
|
|
|
189
208
|
triangleMaterials[triangleIndex] = 0;
|
|
190
209
|
continue;
|
|
191
210
|
}
|
|
192
|
-
triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag);
|
|
211
|
+
triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag, centroid.y, effectiveWallHeight);
|
|
193
212
|
}
|
|
194
213
|
geometry.clearGroups();
|
|
195
214
|
let currentMaterial = triangleMaterials[0] ?? 0;
|
|
@@ -204,6 +223,105 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
|
|
|
204
223
|
}
|
|
205
224
|
geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
|
|
206
225
|
}
|
|
226
|
+
function interpolateSplitVertex(a, b, t) {
|
|
227
|
+
return {
|
|
228
|
+
x: a.x + (b.x - a.x) * t,
|
|
229
|
+
y: a.y + (b.y - a.y) * t,
|
|
230
|
+
z: a.z + (b.z - a.z) * t,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
function clipPolygonByY(polygon, planeY, keepBelow) {
|
|
234
|
+
const out = [];
|
|
235
|
+
if (polygon.length === 0)
|
|
236
|
+
return out;
|
|
237
|
+
const isInside = (vertex) => keepBelow
|
|
238
|
+
? vertex.y <= planeY + WALL_BAND_SPLIT_EPSILON
|
|
239
|
+
: vertex.y >= planeY - WALL_BAND_SPLIT_EPSILON;
|
|
240
|
+
for (let index = 0; index < polygon.length; index += 1) {
|
|
241
|
+
const current = polygon[index];
|
|
242
|
+
const previous = polygon[(index + polygon.length - 1) % polygon.length];
|
|
243
|
+
const currentInside = isInside(current);
|
|
244
|
+
const previousInside = isInside(previous);
|
|
245
|
+
if (currentInside !== previousInside) {
|
|
246
|
+
const denom = current.y - previous.y;
|
|
247
|
+
if (Math.abs(denom) > WALL_BAND_SPLIT_EPSILON) {
|
|
248
|
+
out.push(interpolateSplitVertex(previous, current, (planeY - previous.y) / denom));
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (currentInside)
|
|
252
|
+
out.push(current);
|
|
253
|
+
}
|
|
254
|
+
return out;
|
|
255
|
+
}
|
|
256
|
+
function triangulateSplitPolygon(polygon, positions) {
|
|
257
|
+
if (polygon.length < 3)
|
|
258
|
+
return;
|
|
259
|
+
const first = polygon[0];
|
|
260
|
+
for (let index = 1; index < polygon.length - 1; index += 1) {
|
|
261
|
+
const b = polygon[index];
|
|
262
|
+
const c = polygon[index + 1];
|
|
263
|
+
positions.push(first.x, first.y, first.z, b.x, b.y, b.z, c.x, c.y, c.z);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
function splitGeometryAtHorizontalPlanes(geometry, planes) {
|
|
267
|
+
const splitPlanes = Array.from(new Set(planes
|
|
268
|
+
.filter((plane) => Number.isFinite(plane) && plane > WALL_BAND_SPLIT_EPSILON)
|
|
269
|
+
.map((plane) => Math.round(plane / WALL_BAND_SPLIT_EPSILON) * WALL_BAND_SPLIT_EPSILON))).sort((a, b) => a - b);
|
|
270
|
+
if (splitPlanes.length === 0)
|
|
271
|
+
return geometry;
|
|
272
|
+
const source = geometry.index ? geometry.toNonIndexed() : geometry;
|
|
273
|
+
const position = source.getAttribute('position');
|
|
274
|
+
if (!position || position.count === 0)
|
|
275
|
+
return source;
|
|
276
|
+
const positions = [];
|
|
277
|
+
for (let index = 0; index < position.count; index += 3) {
|
|
278
|
+
let polygons = [
|
|
279
|
+
[
|
|
280
|
+
{ x: position.getX(index), y: position.getY(index), z: position.getZ(index) },
|
|
281
|
+
{ x: position.getX(index + 1), y: position.getY(index + 1), z: position.getZ(index + 1) },
|
|
282
|
+
{ x: position.getX(index + 2), y: position.getY(index + 2), z: position.getZ(index + 2) },
|
|
283
|
+
],
|
|
284
|
+
];
|
|
285
|
+
for (const plane of splitPlanes) {
|
|
286
|
+
const next = [];
|
|
287
|
+
for (const polygon of polygons) {
|
|
288
|
+
const minY = Math.min(...polygon.map((vertex) => vertex.y));
|
|
289
|
+
const maxY = Math.max(...polygon.map((vertex) => vertex.y));
|
|
290
|
+
if (plane <= minY + WALL_BAND_SPLIT_EPSILON || plane >= maxY - WALL_BAND_SPLIT_EPSILON) {
|
|
291
|
+
next.push(polygon);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const below = clipPolygonByY(polygon, plane, true);
|
|
295
|
+
const above = clipPolygonByY(polygon, plane, false);
|
|
296
|
+
if (below.length >= 3)
|
|
297
|
+
next.push(below);
|
|
298
|
+
if (above.length >= 3)
|
|
299
|
+
next.push(above);
|
|
300
|
+
}
|
|
301
|
+
polygons = next;
|
|
302
|
+
}
|
|
303
|
+
for (const polygon of polygons)
|
|
304
|
+
triangulateSplitPolygon(polygon, positions);
|
|
305
|
+
}
|
|
306
|
+
if (source !== geometry)
|
|
307
|
+
geometry.dispose();
|
|
308
|
+
source.dispose();
|
|
309
|
+
const split = new THREE.BufferGeometry();
|
|
310
|
+
split.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
311
|
+
split.computeVertexNormals();
|
|
312
|
+
return split;
|
|
313
|
+
}
|
|
314
|
+
function getWallBandSplitPlanes(wall, effectiveWallHeight) {
|
|
315
|
+
const bands = getWallFaceBandConfig(wall, effectiveWallHeight);
|
|
316
|
+
if (!bands.enabled)
|
|
317
|
+
return [];
|
|
318
|
+
const planes = [bands.lowerTop];
|
|
319
|
+
if (bands.count >= 3)
|
|
320
|
+
planes.push(bands.middleTop);
|
|
321
|
+
if (bands.count >= 4)
|
|
322
|
+
planes.push(bands.upperTop);
|
|
323
|
+
return planes.filter((plane) => plane > WALL_BAND_SPLIT_EPSILON && plane < effectiveWallHeight - WALL_BAND_SPLIT_EPSILON);
|
|
324
|
+
}
|
|
207
325
|
// ============================================================================
|
|
208
326
|
// WALL SYSTEM
|
|
209
327
|
// ============================================================================
|
|
@@ -258,7 +376,7 @@ export const WallSystem = () => {
|
|
|
258
376
|
if (hasDirty) {
|
|
259
377
|
dirtyNodes.forEach((id) => {
|
|
260
378
|
const node = nodes[id];
|
|
261
|
-
if (
|
|
379
|
+
if (node?.type !== 'wall')
|
|
262
380
|
return;
|
|
263
381
|
const levelId = node.parentId;
|
|
264
382
|
if (!levelId)
|
|
@@ -387,7 +505,7 @@ function getEffectiveWall(wall) {
|
|
|
387
505
|
function getLevelWalls(levelId) {
|
|
388
506
|
const { nodes } = useScene.getState();
|
|
389
507
|
const level = nodes[levelId];
|
|
390
|
-
if (
|
|
508
|
+
if (level?.type !== 'level')
|
|
391
509
|
return [];
|
|
392
510
|
const walls = [];
|
|
393
511
|
for (const childId of level.children) {
|
|
@@ -406,14 +524,21 @@ function getLevelWalls(levelId) {
|
|
|
406
524
|
function updateWallGeometry(wallId, miterData) {
|
|
407
525
|
const nodes = useScene.getState().nodes;
|
|
408
526
|
const sceneNode = nodes[wallId];
|
|
409
|
-
if (
|
|
527
|
+
if (sceneNode?.type !== 'wall')
|
|
410
528
|
return;
|
|
411
529
|
const node = getEffectiveWall(sceneNode);
|
|
412
530
|
const mesh = sceneRegistry.nodes.get(wallId);
|
|
413
531
|
if (!mesh)
|
|
414
532
|
return;
|
|
415
533
|
const levelId = resolveLevelId(node, nodes);
|
|
416
|
-
|
|
534
|
+
// Covering-clamped plane: a flush/thick slab on the level above shortens
|
|
535
|
+
// the plane-bound walls below it (explicit-height walls ignore the value).
|
|
536
|
+
const planeTop = getWallPlaneTop(node, levelId, nodes);
|
|
537
|
+
const slabSupport = spatialGridManager.getSlabSupportForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness, node.supportSlabId, undefined, node.supportOffset);
|
|
538
|
+
const slabElevation = slabSupport.elevation;
|
|
539
|
+
const terrainBottomAt = node.fillToTerrain
|
|
540
|
+
? (x, z) => terrainSupportLift(nodes, levelId, x, z)
|
|
541
|
+
: undefined;
|
|
417
542
|
const childrenIds = node.children || [];
|
|
418
543
|
// Merge live overrides into door / window children so cutouts track an
|
|
419
544
|
// in-flight resize drag (door width arrow, window height arrow, etc.)
|
|
@@ -427,22 +552,27 @@ function updateWallGeometry(wallId, miterData) {
|
|
|
427
552
|
return child;
|
|
428
553
|
// `getEffectiveNode` folds in resize overrides (width/height arrows).
|
|
429
554
|
// Position moves publish to `useLiveTransforms` instead, so fold that
|
|
430
|
-
// in too —
|
|
431
|
-
//
|
|
432
|
-
// (rectangular cutouts already track via the live mesh matrixWorld).
|
|
555
|
+
// in too — opening cutout brushes are rebuilt directly from the
|
|
556
|
+
// effective node position rather than from the rendered proxy mesh.
|
|
433
557
|
const effective = getEffectiveNode(child);
|
|
434
558
|
const live = useLiveTransforms.getState().get(child.id);
|
|
435
559
|
if (!live?.position)
|
|
436
560
|
return effective;
|
|
437
561
|
return { ...effective, position: live.position };
|
|
438
562
|
});
|
|
439
|
-
const
|
|
563
|
+
const builtGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments, planeTop, terrainBottomAt);
|
|
564
|
+
const wallAngle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
|
|
565
|
+
// World transform the render mesh will apply (position + Y-rotation below).
|
|
566
|
+
// Reproduce it here so the UVs can be projected in WORLD space — see
|
|
567
|
+
// `applyWorldPlanarWallUVs`.
|
|
568
|
+
const wallWorldMatrix = new THREE.Matrix4().compose(new THREE.Vector3(node.start[0], slabElevation, node.start[1]), new THREE.Quaternion().setFromAxisAngle(WALL_UV_Y_AXIS, -wallAngle), WALL_UV_UNIT_SCALE);
|
|
569
|
+
const newGeo = applyWorldPlanarWallUVs(builtGeo, wallWorldMatrix);
|
|
440
570
|
mesh.geometry.dispose();
|
|
441
571
|
mesh.geometry = newGeo;
|
|
442
572
|
// Update collision mesh
|
|
443
573
|
const collisionMesh = mesh.getObjectByName('collision-mesh');
|
|
444
574
|
if (collisionMesh) {
|
|
445
|
-
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation);
|
|
575
|
+
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments, planeTop, terrainBottomAt);
|
|
446
576
|
collisionMesh.geometry.dispose();
|
|
447
577
|
collisionMesh.geometry = collisionGeo;
|
|
448
578
|
}
|
|
@@ -450,19 +580,133 @@ function updateWallGeometry(wallId, miterData) {
|
|
|
450
580
|
const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
|
|
451
581
|
mesh.rotation.y = -angle;
|
|
452
582
|
}
|
|
583
|
+
const WALL_UV_Y_AXIS = new THREE.Vector3(0, 1, 0);
|
|
584
|
+
const WALL_UV_UNIT_SCALE = new THREE.Vector3(1, 1, 1);
|
|
585
|
+
/**
|
|
586
|
+
* Re-project a wall's UVs in WORLD space (1 UV unit = 1 m) so the finish tiles
|
|
587
|
+
* continuously across adjacent walls and lines up with the roof gable above —
|
|
588
|
+
* instead of THREE's `ExtrudeGeometry` UVs, which restart at each wall's own
|
|
589
|
+
* start/end. Matches `roof-system`'s `pushRoofUv` projection exactly: vertical
|
|
590
|
+
* faces use `U = ±worldX/Z` (the axis across the face normal) and `V = 1 -
|
|
591
|
+
* worldY`; the thin top/bottom caps use `(worldX, worldZ)`. De-indexes first so
|
|
592
|
+
* every triangle projects by its own face normal (no shared-vertex seams at
|
|
593
|
+
* edges). Applied only to the render mesh; collision/floorplan geometry is
|
|
594
|
+
* untouched.
|
|
595
|
+
*/
|
|
596
|
+
function applyWorldPlanarWallUVs(geometry, worldMatrix) {
|
|
597
|
+
const target = geometry.index ? geometry.toNonIndexed() : geometry;
|
|
598
|
+
if (target !== geometry)
|
|
599
|
+
geometry.dispose();
|
|
600
|
+
const position = target.getAttribute('position');
|
|
601
|
+
if (!position || position.count === 0)
|
|
602
|
+
return target;
|
|
603
|
+
const a = new THREE.Vector3();
|
|
604
|
+
const b = new THREE.Vector3();
|
|
605
|
+
const c = new THREE.Vector3();
|
|
606
|
+
const normal = new THREE.Vector3();
|
|
607
|
+
const edgeAB = new THREE.Vector3();
|
|
608
|
+
const edgeAC = new THREE.Vector3();
|
|
609
|
+
const uvs = new Float32Array(position.count * 2);
|
|
610
|
+
for (let i = 0; i < position.count; i += 3) {
|
|
611
|
+
a.fromBufferAttribute(position, i).applyMatrix4(worldMatrix);
|
|
612
|
+
b.fromBufferAttribute(position, i + 1).applyMatrix4(worldMatrix);
|
|
613
|
+
c.fromBufferAttribute(position, i + 2).applyMatrix4(worldMatrix);
|
|
614
|
+
edgeAB.subVectors(b, a);
|
|
615
|
+
edgeAC.subVectors(c, a);
|
|
616
|
+
normal.crossVectors(edgeAB, edgeAC).normalize();
|
|
617
|
+
const absX = Math.abs(normal.x);
|
|
618
|
+
const absY = Math.abs(normal.y);
|
|
619
|
+
const absZ = Math.abs(normal.z);
|
|
620
|
+
for (let k = 0; k < 3; k += 1) {
|
|
621
|
+
const p = k === 0 ? a : k === 1 ? b : c;
|
|
622
|
+
let u;
|
|
623
|
+
let v;
|
|
624
|
+
if (absY >= absX && absY >= absZ) {
|
|
625
|
+
u = p.x;
|
|
626
|
+
v = p.z;
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
v = 1 - p.y;
|
|
630
|
+
u = absX >= absZ ? (normal.x >= 0 ? p.z : -p.z) : normal.z >= 0 ? p.x : -p.x;
|
|
631
|
+
}
|
|
632
|
+
uvs[(i + k) * 2] = u;
|
|
633
|
+
uvs[(i + k) * 2 + 1] = v;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
target.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
637
|
+
target.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2));
|
|
638
|
+
return target;
|
|
639
|
+
}
|
|
453
640
|
/**
|
|
454
641
|
* Generates extruded wall geometry with mitering and cutouts
|
|
455
642
|
*
|
|
456
643
|
* Key insight from demo: polygon is built in WORLD coordinates first,
|
|
457
644
|
* then we transform to wall-local for the 3D mesh.
|
|
458
645
|
*/
|
|
459
|
-
|
|
646
|
+
const WALL_TERRAIN_SAMPLE_STEP = 0.25;
|
|
647
|
+
function densifyClosedWallPerimeter(points) {
|
|
648
|
+
const dense = [];
|
|
649
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
650
|
+
const start = points[index];
|
|
651
|
+
const end = points[(index + 1) % points.length];
|
|
652
|
+
const length = Math.hypot(end.x - start.x, end.y - start.y);
|
|
653
|
+
const segments = Math.max(1, Math.ceil(length / WALL_TERRAIN_SAMPLE_STEP));
|
|
654
|
+
for (let segment = 0; segment < segments; segment += 1) {
|
|
655
|
+
const t = segment / segments;
|
|
656
|
+
dense.push({
|
|
657
|
+
x: start.x + (end.x - start.x) * t,
|
|
658
|
+
y: start.y + (end.y - start.y) * t,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return dense;
|
|
663
|
+
}
|
|
664
|
+
function buildWallTerrainFillGeometry(perimeter, worldToLocal, wallBaseElevation, terrainBottomAt) {
|
|
665
|
+
const worldPoints = densifyClosedWallPerimeter(perimeter);
|
|
666
|
+
if (worldPoints.length < 3)
|
|
667
|
+
return null;
|
|
668
|
+
const localPoints = worldPoints.map(worldToLocal);
|
|
669
|
+
const bottomY = worldPoints.map((point) => {
|
|
670
|
+
const terrainElevation = terrainBottomAt(point.x, point.y);
|
|
671
|
+
return terrainElevation == null ? 0 : Math.min(0, terrainElevation - wallBaseElevation);
|
|
672
|
+
});
|
|
673
|
+
return buildTerrainPerimeterFillGeometry(localPoints, bottomY, 0);
|
|
674
|
+
}
|
|
675
|
+
function mergeWallTerrainFill(body, fill, wall, boundaryEdges, effectiveWallHeight) {
|
|
676
|
+
if (!fill)
|
|
677
|
+
return body;
|
|
678
|
+
const bodyGeometry = body.index ? body.toNonIndexed() : body;
|
|
679
|
+
if (bodyGeometry !== body)
|
|
680
|
+
body.dispose();
|
|
681
|
+
ensureRenderableGeometryAttributes(bodyGeometry);
|
|
682
|
+
ensureRenderableGeometryAttributes(fill);
|
|
683
|
+
const merged = mergeGeometries([bodyGeometry, fill], false);
|
|
684
|
+
if (!merged) {
|
|
685
|
+
fill.dispose();
|
|
686
|
+
return bodyGeometry;
|
|
687
|
+
}
|
|
688
|
+
bodyGeometry.dispose();
|
|
689
|
+
fill.dispose();
|
|
690
|
+
merged.computeVertexNormals();
|
|
691
|
+
assignWallMaterialGroups(merged, wall, boundaryEdges, effectiveWallHeight);
|
|
692
|
+
ensureRenderableGeometryAttributes(merged);
|
|
693
|
+
return merged;
|
|
694
|
+
}
|
|
695
|
+
export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0, baseElevation = slabElevation, baseSegments = [
|
|
696
|
+
{ start: 0, end: 1, elevation: baseElevation },
|
|
697
|
+
], storeyHeight = DEFAULT_LEVEL_HEIGHT, terrainBottomAt) {
|
|
460
698
|
const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
|
|
461
699
|
const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
const
|
|
700
|
+
const topElevation = resolveWallTop(wallNode, storeyHeight, slabElevation);
|
|
701
|
+
const effectiveWallHeight = topElevation - slabElevation;
|
|
702
|
+
const effectiveBaseElevation = Math.min(baseElevation, slabElevation);
|
|
703
|
+
const localBottom = effectiveBaseElevation - slabElevation;
|
|
704
|
+
const height = topElevation - effectiveBaseElevation;
|
|
705
|
+
// A slab at or above the storey plane leaves a plane-bound wall with no
|
|
706
|
+
// body — bail before ExtrudeGeometry sees a non-positive depth.
|
|
707
|
+
if (height <= 1e-9) {
|
|
708
|
+
return new THREE.BufferGeometry();
|
|
709
|
+
}
|
|
466
710
|
const thickness = getWallThickness(wallNode);
|
|
467
711
|
// Wall direction and normal (exactly like demo)
|
|
468
712
|
const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y };
|
|
@@ -493,6 +737,9 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
493
737
|
// Convert polygon to local coordinates
|
|
494
738
|
const localPoints = polyPoints.map(worldToLocal);
|
|
495
739
|
const boundaryEdges = buildTaggedWallBoundaryEdges(wallNode, localPoints, miterData);
|
|
740
|
+
const terrainFill = terrainBottomAt
|
|
741
|
+
? buildWallTerrainFillGeometry(polyPoints, worldToLocal, slabElevation, terrainBottomAt)
|
|
742
|
+
: null;
|
|
496
743
|
// Build THREE.js shape
|
|
497
744
|
// Shape uses (x, y) where we map: shape.x = local.x, shape.y = -local.z
|
|
498
745
|
// The negation is needed because after rotateX(-PI/2), shape.y becomes -geometry.z
|
|
@@ -509,24 +756,118 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
509
756
|
});
|
|
510
757
|
// Rotate so extrusion direction (Z) becomes height direction (Y)
|
|
511
758
|
geometry.rotateX(-Math.PI / 2);
|
|
759
|
+
if (Math.abs(localBottom) > 1e-9)
|
|
760
|
+
geometry.translate(0, localBottom, 0);
|
|
512
761
|
geometry.computeVertexNormals();
|
|
513
|
-
assignWallMaterialGroups(geometry, wallNode, boundaryEdges);
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
|
|
762
|
+
assignWallMaterialGroups(geometry, wallNode, boundaryEdges, effectiveWallHeight);
|
|
763
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
764
|
+
// Start with the lowest required wall prism, then remove the volume below
|
|
765
|
+
// each higher-supported run. This keeps the existing mitered footprint and
|
|
766
|
+
// opening CSG while giving one wall a stepped longitudinal base.
|
|
767
|
+
const baseProfileCutouts = [];
|
|
768
|
+
for (const segment of baseSegments) {
|
|
769
|
+
const segmentElevation = Math.min(segment.elevation, slabElevation);
|
|
770
|
+
const cutHeight = segmentElevation - effectiveBaseElevation;
|
|
771
|
+
if (cutHeight <= 1e-6 || segment.end - segment.start <= 1e-7)
|
|
772
|
+
continue;
|
|
773
|
+
const segmentStart = THREE.MathUtils.clamp(segment.start, 0, 1);
|
|
774
|
+
const segmentEnd = THREE.MathUtils.clamp(segment.end, 0, 1);
|
|
775
|
+
const cutHalfWidth = Math.max(thickness * 2, 0.2);
|
|
776
|
+
const worldCutoutPoints = [];
|
|
777
|
+
if (isCurvedWall(wallNode)) {
|
|
778
|
+
const sampleCount = Math.max(2, Math.ceil((segmentEnd - segmentStart) * 24));
|
|
779
|
+
const left = [];
|
|
780
|
+
const right = [];
|
|
781
|
+
for (let index = 0; index <= sampleCount; index++) {
|
|
782
|
+
const t = segmentStart + ((segmentEnd - segmentStart) * index) / sampleCount;
|
|
783
|
+
const frame = getWallCurveFrameAt(wallNode, t);
|
|
784
|
+
const endpointExtension = index === 0 && segmentStart <= 1e-7
|
|
785
|
+
? -cutHalfWidth
|
|
786
|
+
: index === sampleCount && segmentEnd >= 1 - 1e-7
|
|
787
|
+
? cutHalfWidth
|
|
788
|
+
: 0;
|
|
789
|
+
const center = {
|
|
790
|
+
x: frame.point.x + frame.tangent.x * endpointExtension,
|
|
791
|
+
y: frame.point.y + frame.tangent.y * endpointExtension,
|
|
792
|
+
};
|
|
793
|
+
left.push({
|
|
794
|
+
x: center.x + frame.normal.x * cutHalfWidth,
|
|
795
|
+
y: center.y + frame.normal.y * cutHalfWidth,
|
|
796
|
+
});
|
|
797
|
+
right.push({
|
|
798
|
+
x: center.x - frame.normal.x * cutHalfWidth,
|
|
799
|
+
y: center.y - frame.normal.y * cutHalfWidth,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
worldCutoutPoints.push(...left, ...right.reverse());
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
const tangentX = v.x / L;
|
|
806
|
+
const tangentY = v.y / L;
|
|
807
|
+
const normalX = -tangentY;
|
|
808
|
+
const normalY = tangentX;
|
|
809
|
+
const startExtension = segmentStart <= 1e-7 ? cutHalfWidth : 0;
|
|
810
|
+
const endExtension = segmentEnd >= 1 - 1e-7 ? cutHalfWidth : 0;
|
|
811
|
+
const startPoint = {
|
|
812
|
+
x: wallStart.x + tangentX * (segmentStart * L - startExtension),
|
|
813
|
+
y: wallStart.y + tangentY * (segmentStart * L - startExtension),
|
|
814
|
+
};
|
|
815
|
+
const endPoint = {
|
|
816
|
+
x: wallStart.x + tangentX * (segmentEnd * L + endExtension),
|
|
817
|
+
y: wallStart.y + tangentY * (segmentEnd * L + endExtension),
|
|
818
|
+
};
|
|
819
|
+
worldCutoutPoints.push({
|
|
820
|
+
x: startPoint.x + normalX * cutHalfWidth,
|
|
821
|
+
y: startPoint.y + normalY * cutHalfWidth,
|
|
822
|
+
}, { x: endPoint.x + normalX * cutHalfWidth, y: endPoint.y + normalY * cutHalfWidth }, { x: endPoint.x - normalX * cutHalfWidth, y: endPoint.y - normalY * cutHalfWidth }, {
|
|
823
|
+
x: startPoint.x - normalX * cutHalfWidth,
|
|
824
|
+
y: startPoint.y - normalY * cutHalfWidth,
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
const localCutoutPoints = worldCutoutPoints.map(worldToLocal);
|
|
828
|
+
if (localCutoutPoints.length < 3)
|
|
829
|
+
continue;
|
|
830
|
+
const cutoutShape = new THREE.Shape();
|
|
831
|
+
cutoutShape.moveTo(localCutoutPoints[0].x, -localCutoutPoints[0].z);
|
|
832
|
+
for (let index = 1; index < localCutoutPoints.length; index++) {
|
|
833
|
+
cutoutShape.lineTo(localCutoutPoints[index].x, -localCutoutPoints[index].z);
|
|
834
|
+
}
|
|
835
|
+
cutoutShape.closePath();
|
|
836
|
+
const cutoutBottom = localBottom - 0.01;
|
|
837
|
+
const cutoutTop = segmentElevation - slabElevation;
|
|
838
|
+
const cutoutGeometry = new THREE.ExtrudeGeometry(cutoutShape, {
|
|
839
|
+
depth: cutoutTop - cutoutBottom,
|
|
840
|
+
bevelEnabled: false,
|
|
841
|
+
});
|
|
842
|
+
cutoutGeometry.rotateX(-Math.PI / 2);
|
|
843
|
+
cutoutGeometry.translate(0, cutoutBottom, 0);
|
|
844
|
+
computeGeometryBoundsTree(cutoutGeometry);
|
|
845
|
+
baseProfileCutouts.push(new Brush(cutoutGeometry));
|
|
846
|
+
}
|
|
847
|
+
// Apply base-profile and opening cutouts in one CSG pass.
|
|
848
|
+
const cutoutBrushes = [
|
|
849
|
+
...baseProfileCutouts,
|
|
850
|
+
...collectCutoutBrushes(wallNode, childrenNodes, thickness),
|
|
851
|
+
];
|
|
517
852
|
if (cutoutBrushes.length === 0) {
|
|
518
|
-
|
|
853
|
+
const splitGeometry = splitGeometryAtHorizontalPlanes(geometry, getWallBandSplitPlanes(wallNode, effectiveWallHeight));
|
|
854
|
+
splitGeometry.computeVertexNormals();
|
|
855
|
+
assignWallMaterialGroups(splitGeometry, wallNode, boundaryEdges, effectiveWallHeight);
|
|
856
|
+
ensureRenderableGeometryAttributes(splitGeometry);
|
|
857
|
+
return mergeWallTerrainFill(splitGeometry, terrainFill, wallNode, boundaryEdges, effectiveWallHeight);
|
|
519
858
|
}
|
|
520
859
|
// Create wall brush from geometry
|
|
521
860
|
// Pre-compute BVH with new API to avoid deprecation warning
|
|
861
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
522
862
|
computeGeometryBoundsTree(geometry);
|
|
523
863
|
const wallBrush = new Brush(geometry);
|
|
524
864
|
wallBrush.updateMatrixWorld();
|
|
525
865
|
// Subtract each cutout from the wall
|
|
526
866
|
let resultBrush = wallBrush;
|
|
527
867
|
for (const cutoutBrush of cutoutBrushes) {
|
|
528
|
-
cutoutBrush
|
|
868
|
+
prepareBrushForCSG(cutoutBrush);
|
|
529
869
|
const newResult = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
|
|
870
|
+
prepareBrushForCSG(newResult);
|
|
530
871
|
if (resultBrush !== wallBrush) {
|
|
531
872
|
csgGeometry(resultBrush).dispose();
|
|
532
873
|
}
|
|
@@ -538,14 +879,16 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
538
879
|
csgGeometry(brush).dispose();
|
|
539
880
|
}
|
|
540
881
|
const resultGeometry = csgGeometry(resultBrush);
|
|
541
|
-
resultGeometry
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
882
|
+
const splitResultGeometry = splitGeometryAtHorizontalPlanes(resultGeometry, getWallBandSplitPlanes(wallNode, effectiveWallHeight));
|
|
883
|
+
splitResultGeometry.computeVertexNormals();
|
|
884
|
+
assignWallMaterialGroups(splitResultGeometry, wallNode, boundaryEdges, effectiveWallHeight);
|
|
885
|
+
ensureRenderableGeometryAttributes(splitResultGeometry);
|
|
886
|
+
return mergeWallTerrainFill(splitResultGeometry, terrainFill, wallNode, boundaryEdges, effectiveWallHeight);
|
|
545
887
|
}
|
|
546
888
|
/**
|
|
547
|
-
* Collects
|
|
548
|
-
*
|
|
889
|
+
* Collects opening and item cutout brushes for CSG subtraction. Door/window
|
|
890
|
+
* cuts come directly from node geometry; item proxy meshes are transformed
|
|
891
|
+
* into wall-local boxes that pass through the wall.
|
|
549
892
|
*/
|
|
550
893
|
function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
551
894
|
const brushes = [];
|
|
@@ -558,15 +901,8 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
|
558
901
|
for (const child of childrenNodes) {
|
|
559
902
|
if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door')
|
|
560
903
|
continue;
|
|
561
|
-
if (
|
|
562
|
-
(child
|
|
563
|
-
child.openingKind === 'door' &&
|
|
564
|
-
(child.openingShape === 'arch' || child.openingShape === 'rounded')) ||
|
|
565
|
-
(child.type === 'window' && child.openingKind === 'opening') ||
|
|
566
|
-
(child.type === 'window' &&
|
|
567
|
-
child.openingKind === 'window' &&
|
|
568
|
-
(child.openingShape === 'arch' || child.openingShape === 'rounded'))) {
|
|
569
|
-
brushes.push(createShapedOpeningCutoutBrush(child, wallThickness));
|
|
904
|
+
if (child.type === 'door' || child.type === 'window') {
|
|
905
|
+
brushes.push(createOpeningCutoutBrush(child, wallThickness));
|
|
570
906
|
continue;
|
|
571
907
|
}
|
|
572
908
|
const childMesh = sceneRegistry.nodes.get(child.id);
|
|
@@ -609,132 +945,16 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
|
609
945
|
}
|
|
610
946
|
return brushes;
|
|
611
947
|
}
|
|
612
|
-
function
|
|
613
|
-
const shape = createShapedOpeningCutoutShape(opening);
|
|
614
|
-
const depth = wallThickness * 2;
|
|
615
|
-
const bevelSize = opening.openingShape === 'rounded'
|
|
616
|
-
? Math.min(Math.max(opening.openingRevealRadius ?? 0.025, 0), Math.max(wallThickness * 0.45, 0.001), Math.max((opening.cornerRadius ?? 0.15) * 0.45, 0.001))
|
|
617
|
-
: 0;
|
|
618
|
-
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
619
|
-
depth,
|
|
620
|
-
bevelEnabled: bevelSize > 0,
|
|
621
|
-
bevelSegments: bevelSize > 0 ? 8 : 0,
|
|
622
|
-
bevelSize,
|
|
623
|
-
bevelThickness: bevelSize,
|
|
624
|
-
curveSegments: 24,
|
|
625
|
-
});
|
|
626
|
-
geometry.translate(0, 0, -depth / 2);
|
|
627
|
-
computeGeometryBoundsTree(geometry);
|
|
628
|
-
return new Brush(geometry);
|
|
629
|
-
}
|
|
630
|
-
function createShapedOpeningCutoutShape(opening) {
|
|
948
|
+
function createOpeningCutoutBrush(opening, wallThickness) {
|
|
631
949
|
const halfWidth = opening.width / 2;
|
|
632
950
|
const bottom = opening.position[1] - opening.height / 2;
|
|
633
|
-
const
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const springY = top - archHeight;
|
|
643
|
-
const segments = 32;
|
|
644
|
-
shape.moveTo(left, bottom);
|
|
645
|
-
shape.lineTo(right, bottom);
|
|
646
|
-
shape.lineTo(right, springY);
|
|
647
|
-
for (let index = 1; index <= segments; index += 1) {
|
|
648
|
-
const x = right + (left - right) * (index / segments);
|
|
649
|
-
const normalizedX = Math.min(Math.abs((x - centerX) / halfWidth), 1);
|
|
650
|
-
const y = springY + archHeight * Math.sqrt(Math.max(1 - normalizedX * normalizedX, 0));
|
|
651
|
-
shape.lineTo(x, y);
|
|
652
|
-
}
|
|
653
|
-
shape.lineTo(left, bottom);
|
|
654
|
-
shape.closePath();
|
|
655
|
-
return shape;
|
|
656
|
-
}
|
|
657
|
-
if (opening.openingShape === 'rounded') {
|
|
658
|
-
const radii = getRoundedOpeningRadii(opening, width, height);
|
|
659
|
-
applyRoundedOpeningShape(shape, left, right, bottom, top, radii);
|
|
660
|
-
return shape;
|
|
661
|
-
}
|
|
662
|
-
shape.moveTo(left, bottom);
|
|
663
|
-
shape.lineTo(right, bottom);
|
|
664
|
-
shape.lineTo(right, top);
|
|
665
|
-
shape.lineTo(left, top);
|
|
666
|
-
shape.closePath();
|
|
667
|
-
return shape;
|
|
668
|
-
}
|
|
669
|
-
function getRoundedOpeningRadii(opening, width, height) {
|
|
670
|
-
if (opening.type !== 'window') {
|
|
671
|
-
if (opening.openingRadiusMode === 'individual') {
|
|
672
|
-
const [topLeft = 0, topRight = 0] = opening.openingTopRadii ?? [0.15, 0.15];
|
|
673
|
-
return normalizeCornerRadii({
|
|
674
|
-
topLeft: Math.max(topLeft, 0),
|
|
675
|
-
topRight: Math.max(topRight, 0),
|
|
676
|
-
bottomRight: 0,
|
|
677
|
-
bottomLeft: 0,
|
|
678
|
-
}, width, height);
|
|
679
|
-
}
|
|
680
|
-
const maxRadius = Math.min(width / 2, height);
|
|
681
|
-
const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
|
|
682
|
-
return { topLeft: radius, topRight: radius, bottomRight: 0, bottomLeft: 0 };
|
|
683
|
-
}
|
|
684
|
-
if (opening.openingRadiusMode === 'individual') {
|
|
685
|
-
const [topLeft = 0, topRight = 0, bottomRight = 0, bottomLeft = 0] = opening.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15];
|
|
686
|
-
return normalizeCornerRadii({
|
|
687
|
-
topLeft: Math.max(topLeft, 0),
|
|
688
|
-
topRight: Math.max(topRight, 0),
|
|
689
|
-
bottomRight: Math.max(bottomRight, 0),
|
|
690
|
-
bottomLeft: Math.max(bottomLeft, 0),
|
|
691
|
-
}, width, height);
|
|
692
|
-
}
|
|
693
|
-
const maxRadius = Math.min(width / 2, height / 2);
|
|
694
|
-
const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
|
|
695
|
-
return { topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius };
|
|
696
|
-
}
|
|
697
|
-
function normalizeCornerRadii(radii, width, height) {
|
|
698
|
-
const next = { ...radii };
|
|
699
|
-
const maxScale = Math.min(1, width / Math.max(next.topLeft + next.topRight, 1e-6), width / Math.max(next.bottomLeft + next.bottomRight, 1e-6), height / Math.max(next.topLeft + next.bottomLeft, 1e-6), height / Math.max(next.topRight + next.bottomRight, 1e-6));
|
|
700
|
-
if (maxScale < 1) {
|
|
701
|
-
next.topLeft *= maxScale;
|
|
702
|
-
next.topRight *= maxScale;
|
|
703
|
-
next.bottomRight *= maxScale;
|
|
704
|
-
next.bottomLeft *= maxScale;
|
|
705
|
-
}
|
|
706
|
-
return next;
|
|
707
|
-
}
|
|
708
|
-
function applyRoundedOpeningShape(shape, left, right, bottom, top, radii) {
|
|
709
|
-
const { topLeft, topRight, bottomRight, bottomLeft } = radii;
|
|
710
|
-
shape.moveTo(left + bottomLeft, bottom);
|
|
711
|
-
shape.lineTo(right - bottomRight, bottom);
|
|
712
|
-
if (bottomRight > 1e-6) {
|
|
713
|
-
shape.absarc(right - bottomRight, bottom + bottomRight, bottomRight, -Math.PI / 2, 0, false);
|
|
714
|
-
}
|
|
715
|
-
else {
|
|
716
|
-
shape.lineTo(right, bottom);
|
|
717
|
-
}
|
|
718
|
-
shape.lineTo(right, top - topRight);
|
|
719
|
-
if (topRight > 1e-6) {
|
|
720
|
-
shape.absarc(right - topRight, top - topRight, topRight, 0, Math.PI / 2, false);
|
|
721
|
-
}
|
|
722
|
-
else {
|
|
723
|
-
shape.lineTo(right, top);
|
|
724
|
-
}
|
|
725
|
-
shape.lineTo(left + topLeft, top);
|
|
726
|
-
if (topLeft > 1e-6) {
|
|
727
|
-
shape.absarc(left + topLeft, top - topLeft, topLeft, Math.PI / 2, Math.PI, false);
|
|
728
|
-
}
|
|
729
|
-
else {
|
|
730
|
-
shape.lineTo(left, top);
|
|
731
|
-
}
|
|
732
|
-
shape.lineTo(left, bottom + bottomLeft);
|
|
733
|
-
if (bottomLeft > 1e-6) {
|
|
734
|
-
shape.absarc(left + bottomLeft, bottom + bottomLeft, bottomLeft, Math.PI, Math.PI * 1.5, false);
|
|
735
|
-
}
|
|
736
|
-
else {
|
|
737
|
-
shape.lineTo(left, bottom);
|
|
738
|
-
}
|
|
739
|
-
shape.closePath();
|
|
951
|
+
const bottomPadding = getOpeningCutoutBottomPadding(opening, bottom);
|
|
952
|
+
const geometry = buildOpeningCutoutGeometry(opening, {
|
|
953
|
+
left: opening.position[0] - halfWidth,
|
|
954
|
+
right: opening.position[0] + halfWidth,
|
|
955
|
+
bottom: bottom - bottomPadding,
|
|
956
|
+
top: opening.position[1] + opening.height / 2,
|
|
957
|
+
}, wallThickness * 2, wallThickness);
|
|
958
|
+
computeGeometryBoundsTree(geometry);
|
|
959
|
+
return new Brush(geometry);
|
|
740
960
|
}
|