@pascal-app/viewer 0.9.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/error-boundary.d.ts +6 -0
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/error-boundary.js +6 -0
- package/dist/components/renderers/node-renderer.d.ts +5 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +5 -2
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/parametric-node-renderer.js +7 -4
- package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
- package/dist/components/viewer/bvh-ecctrl.js +552 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +39 -4
- package/dist/components/viewer/glb-interactive.d.ts +41 -0
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
- package/dist/components/viewer/glb-interactive.js +394 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
- package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
- package/dist/components/viewer/glb-reference-nodes.js +80 -0
- package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
- package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
- package/dist/components/viewer/glb-replace-instances.js +61 -0
- package/dist/components/viewer/glb-scene.d.ts +54 -0
- package/dist/components/viewer/glb-scene.d.ts.map +1 -0
- package/dist/components/viewer/glb-scene.js +920 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +12 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
- package/dist/components/viewer/glb-walkthrough-controller.js +357 -0
- package/dist/components/viewer/index.d.ts +19 -2
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +104 -12
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +36 -5
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +17 -2
- package/dist/components/viewer/post-processing.d.ts +7 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +179 -24
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +6 -4
- package/dist/components/viewer/scene-bvh.d.ts +2 -1
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +3 -0
- package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
- package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.test.js +13 -0
- package/dist/components/viewer/scene-environment.d.ts +2 -0
- package/dist/components/viewer/scene-environment.d.ts.map +1 -0
- package/dist/components/viewer/scene-environment.js +79 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +18 -10
- package/dist/components/viewer/walkthrough-controls.d.ts +1 -0
- package/dist/components/viewer/walkthrough-controls.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.js +20 -0
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +3 -22
- package/dist/hooks/use-node-events.d.ts +1 -1
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +22 -14
- package/dist/index.d.ts +19 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +22 -8
- package/dist/lib/backdrop.d.ts +33 -0
- package/dist/lib/backdrop.d.ts.map +1 -0
- package/dist/lib/backdrop.js +92 -0
- package/dist/lib/box-uv.d.ts +13 -0
- package/dist/lib/box-uv.d.ts.map +1 -0
- package/dist/lib/box-uv.js +32 -0
- package/dist/lib/csg-utils.d.ts +3 -9
- package/dist/lib/csg-utils.d.ts.map +1 -1
- package/dist/lib/csg-utils.js +65 -5
- package/dist/lib/csg-utils.test.d.ts +2 -0
- package/dist/lib/csg-utils.test.d.ts.map +1 -0
- package/dist/lib/csg-utils.test.js +45 -0
- package/dist/lib/edge-style.d.ts +1 -0
- package/dist/lib/edge-style.d.ts.map +1 -1
- package/dist/lib/edge-style.js +26 -6
- package/dist/lib/ink-edges.d.ts +1 -1
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +13 -7
- package/dist/lib/ktx2-loader.d.ts +43 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -0
- package/dist/lib/ktx2-loader.js +136 -0
- package/dist/lib/layers.d.ts +10 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +10 -0
- package/dist/lib/materials.d.ts +14 -1
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +103 -26
- package/dist/lib/scene-themes.d.ts +1 -0
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +25 -16
- package/dist/lib/shadow-only.d.ts +4 -0
- package/dist/lib/shadow-only.d.ts.map +1 -0
- package/dist/lib/shadow-only.js +36 -0
- package/dist/lib/tsl-compat.d.ts +12 -0
- package/dist/lib/tsl-compat.d.ts.map +1 -0
- package/dist/lib/tsl-compat.js +13 -0
- package/dist/store/use-viewer.d.ts +60 -18
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +175 -3
- package/dist/store/use-viewer.test.d.ts +2 -0
- package/dist/store/use-viewer.test.d.ts.map +1 -0
- package/dist/store/use-viewer.test.js +42 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +4 -1
- package/dist/systems/door/door-animation.test.d.ts +2 -0
- package/dist/systems/door/door-animation.test.d.ts.map +1 -0
- package/dist/systems/door/door-animation.test.js +51 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
- package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
- package/dist/systems/door/door-floor-alignment.test.js +50 -0
- package/dist/systems/door/door-system.d.ts +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +494 -161
- package/dist/systems/fence/fence-system.d.ts +17 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +221 -53
- package/dist/systems/geometry/geometry-system.d.ts +7 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +95 -26
- package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
- package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.test.js +30 -0
- package/dist/systems/interactive/control-widget.d.ts +10 -0
- package/dist/systems/interactive/control-widget.d.ts.map +1 -0
- package/dist/systems/interactive/control-widget.js +35 -0
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +55 -56
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +20 -6
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +28 -4
- package/dist/systems/level/level-utils.d.ts +0 -3
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +2 -41
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +29 -8
- package/dist/systems/roof/roof-system.d.ts +21 -8
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1079 -221
- package/dist/systems/slab/slab-system.d.ts +5 -3
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +41 -23
- package/dist/systems/slab/slab-system.test.js +3 -2
- package/dist/systems/stair/stair-system.js +6 -3
- package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
- package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.js +167 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
- package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
- package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +46 -17
- package/dist/systems/wall/wall-materials.d.ts +11 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +244 -35
- package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
- package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
- package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-support-extension.test.js +48 -0
- package/dist/systems/wall/wall-system.d.ts +2 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +329 -174
- package/dist/systems/window/window-animation-system.d.ts +12 -0
- package/dist/systems/window/window-animation-system.d.ts.map +1 -1
- package/dist/systems/window/window-animation-system.js +16 -5
- package/dist/systems/window/window-system.d.ts +7 -0
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +240 -14
- package/package.json +4 -4
- package/dist/components/viewer/ground-occluder.d.ts +0 -2
- package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
- package/dist/components/viewer/ground-occluder.js +0 -67
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
import { calculateLevelMiters, DEFAULT_WALL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getWallCurveFrameAt, getWallMiterBoundaryPoints, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, sceneRegistry, spatialGridManager, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { calculateLevelMiters, DEFAULT_WALL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getWallBandSlotId, getWallCurveFrameAt, getWallFaceBandConfig, getWallFaceBandForHeight, getWallMiterBoundaryPoints, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, sceneRegistry, spatialGridManager, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
4
|
import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
5
5
|
import { computeBoundsTree } from 'three-mesh-bvh';
|
|
6
|
+
import { ensureRenderableGeometryAttributes, prepareBrushForCSG } from '../../lib/csg-utils';
|
|
7
|
+
import { buildOpeningCutoutGeometry, getOpeningCutoutBottomPadding, } from './opening-cutout-geometry';
|
|
6
8
|
// Reusable CSG evaluator for better performance
|
|
7
9
|
const csgEvaluator = new Evaluator();
|
|
10
|
+
csgEvaluator.attributes = ['position', 'normal', 'uv', 'uv2'];
|
|
8
11
|
const CURVED_WALL_3D_ENDPOINT_INSET = 0.0015;
|
|
9
12
|
const WALL_FACE_NORMAL_Y_EPSILON = 0.6;
|
|
10
13
|
const WALL_FACE_EDGE_DISTANCE_EPSILON = 0.003;
|
|
14
|
+
const WALL_BAND_SPLIT_EPSILON = 1e-5;
|
|
15
|
+
const WALL_BAND_SLOT_MATERIAL_INDEX = {
|
|
16
|
+
interior: 1,
|
|
17
|
+
exterior: 2,
|
|
18
|
+
lowerInterior: 3,
|
|
19
|
+
middleInterior: 4,
|
|
20
|
+
upperInterior: 5,
|
|
21
|
+
topInterior: 6,
|
|
22
|
+
lowerExterior: 7,
|
|
23
|
+
middleExterior: 8,
|
|
24
|
+
upperExterior: 9,
|
|
25
|
+
topExterior: 10,
|
|
26
|
+
skirtingInterior: 0,
|
|
27
|
+
skirtingExterior: 0,
|
|
28
|
+
crownInterior: 0,
|
|
29
|
+
crownExterior: 0,
|
|
30
|
+
chairRailInterior: 0,
|
|
31
|
+
chairRailExterior: 0,
|
|
32
|
+
};
|
|
11
33
|
function computeGeometryBoundsTree(geometry) {
|
|
12
34
|
;
|
|
13
35
|
geometry.computeBoundsTree = computeBoundsTree;
|
|
@@ -16,12 +38,6 @@ function computeGeometryBoundsTree(geometry) {
|
|
|
16
38
|
function csgGeometry(brush) {
|
|
17
39
|
return brush.geometry;
|
|
18
40
|
}
|
|
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
41
|
function insetCurvedWallBoundaryPointsFor3D(wall, boundaryPoints, miterData) {
|
|
26
42
|
if (!(boundaryPoints && isCurvedWall(wall))) {
|
|
27
43
|
return boundaryPoints;
|
|
@@ -117,14 +133,15 @@ function distanceToWallBoundaryEdge(point, edge) {
|
|
|
117
133
|
const closestZ = edge.start.y + edgeDz * t;
|
|
118
134
|
return Math.hypot(point.x - closestX, point.y - closestZ);
|
|
119
135
|
}
|
|
120
|
-
function getWallFaceMaterialIndex(wall, face) {
|
|
136
|
+
function getWallFaceMaterialIndex(wall, face, y) {
|
|
121
137
|
const semantic = face === 'front' ? wall.frontSide : wall.backSide;
|
|
122
|
-
const fallback = face === 'front' ?
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
return
|
|
127
|
-
|
|
138
|
+
const fallback = face === 'front' ? 'interior' : 'exterior';
|
|
139
|
+
const side = semantic === 'interior' || semantic === 'exterior' ? semantic : fallback;
|
|
140
|
+
const bands = getWallFaceBandConfig(wall);
|
|
141
|
+
if (!bands.enabled)
|
|
142
|
+
return WALL_BAND_SLOT_MATERIAL_INDEX[side];
|
|
143
|
+
const band = getWallFaceBandForHeight(wall, y);
|
|
144
|
+
return WALL_BAND_SLOT_MATERIAL_INDEX[getWallBandSlotId(side, band)];
|
|
128
145
|
}
|
|
129
146
|
function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
|
|
130
147
|
const position = geometry.getAttribute('position');
|
|
@@ -189,7 +206,7 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
|
|
|
189
206
|
triangleMaterials[triangleIndex] = 0;
|
|
190
207
|
continue;
|
|
191
208
|
}
|
|
192
|
-
triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag);
|
|
209
|
+
triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag, centroid.y);
|
|
193
210
|
}
|
|
194
211
|
geometry.clearGroups();
|
|
195
212
|
let currentMaterial = triangleMaterials[0] ?? 0;
|
|
@@ -204,6 +221,105 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
|
|
|
204
221
|
}
|
|
205
222
|
geometry.addGroup(groupStart * 3, (triangleCount - groupStart) * 3, currentMaterial);
|
|
206
223
|
}
|
|
224
|
+
function interpolateSplitVertex(a, b, t) {
|
|
225
|
+
return {
|
|
226
|
+
x: a.x + (b.x - a.x) * t,
|
|
227
|
+
y: a.y + (b.y - a.y) * t,
|
|
228
|
+
z: a.z + (b.z - a.z) * t,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function clipPolygonByY(polygon, planeY, keepBelow) {
|
|
232
|
+
const out = [];
|
|
233
|
+
if (polygon.length === 0)
|
|
234
|
+
return out;
|
|
235
|
+
const isInside = (vertex) => keepBelow
|
|
236
|
+
? vertex.y <= planeY + WALL_BAND_SPLIT_EPSILON
|
|
237
|
+
: vertex.y >= planeY - WALL_BAND_SPLIT_EPSILON;
|
|
238
|
+
for (let index = 0; index < polygon.length; index += 1) {
|
|
239
|
+
const current = polygon[index];
|
|
240
|
+
const previous = polygon[(index + polygon.length - 1) % polygon.length];
|
|
241
|
+
const currentInside = isInside(current);
|
|
242
|
+
const previousInside = isInside(previous);
|
|
243
|
+
if (currentInside !== previousInside) {
|
|
244
|
+
const denom = current.y - previous.y;
|
|
245
|
+
if (Math.abs(denom) > WALL_BAND_SPLIT_EPSILON) {
|
|
246
|
+
out.push(interpolateSplitVertex(previous, current, (planeY - previous.y) / denom));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (currentInside)
|
|
250
|
+
out.push(current);
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
function triangulateSplitPolygon(polygon, positions) {
|
|
255
|
+
if (polygon.length < 3)
|
|
256
|
+
return;
|
|
257
|
+
const first = polygon[0];
|
|
258
|
+
for (let index = 1; index < polygon.length - 1; index += 1) {
|
|
259
|
+
const b = polygon[index];
|
|
260
|
+
const c = polygon[index + 1];
|
|
261
|
+
positions.push(first.x, first.y, first.z, b.x, b.y, b.z, c.x, c.y, c.z);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function splitGeometryAtHorizontalPlanes(geometry, planes) {
|
|
265
|
+
const splitPlanes = Array.from(new Set(planes
|
|
266
|
+
.filter((plane) => Number.isFinite(plane) && plane > WALL_BAND_SPLIT_EPSILON)
|
|
267
|
+
.map((plane) => Math.round(plane / WALL_BAND_SPLIT_EPSILON) * WALL_BAND_SPLIT_EPSILON))).sort((a, b) => a - b);
|
|
268
|
+
if (splitPlanes.length === 0)
|
|
269
|
+
return geometry;
|
|
270
|
+
const source = geometry.index ? geometry.toNonIndexed() : geometry;
|
|
271
|
+
const position = source.getAttribute('position');
|
|
272
|
+
if (!position || position.count === 0)
|
|
273
|
+
return source;
|
|
274
|
+
const positions = [];
|
|
275
|
+
for (let index = 0; index < position.count; index += 3) {
|
|
276
|
+
let polygons = [
|
|
277
|
+
[
|
|
278
|
+
{ x: position.getX(index), y: position.getY(index), z: position.getZ(index) },
|
|
279
|
+
{ x: position.getX(index + 1), y: position.getY(index + 1), z: position.getZ(index + 1) },
|
|
280
|
+
{ x: position.getX(index + 2), y: position.getY(index + 2), z: position.getZ(index + 2) },
|
|
281
|
+
],
|
|
282
|
+
];
|
|
283
|
+
for (const plane of splitPlanes) {
|
|
284
|
+
const next = [];
|
|
285
|
+
for (const polygon of polygons) {
|
|
286
|
+
const minY = Math.min(...polygon.map((vertex) => vertex.y));
|
|
287
|
+
const maxY = Math.max(...polygon.map((vertex) => vertex.y));
|
|
288
|
+
if (plane <= minY + WALL_BAND_SPLIT_EPSILON || plane >= maxY - WALL_BAND_SPLIT_EPSILON) {
|
|
289
|
+
next.push(polygon);
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const below = clipPolygonByY(polygon, plane, true);
|
|
293
|
+
const above = clipPolygonByY(polygon, plane, false);
|
|
294
|
+
if (below.length >= 3)
|
|
295
|
+
next.push(below);
|
|
296
|
+
if (above.length >= 3)
|
|
297
|
+
next.push(above);
|
|
298
|
+
}
|
|
299
|
+
polygons = next;
|
|
300
|
+
}
|
|
301
|
+
for (const polygon of polygons)
|
|
302
|
+
triangulateSplitPolygon(polygon, positions);
|
|
303
|
+
}
|
|
304
|
+
if (source !== geometry)
|
|
305
|
+
geometry.dispose();
|
|
306
|
+
source.dispose();
|
|
307
|
+
const split = new THREE.BufferGeometry();
|
|
308
|
+
split.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
309
|
+
split.computeVertexNormals();
|
|
310
|
+
return split;
|
|
311
|
+
}
|
|
312
|
+
function getWallBandSplitPlanes(wall) {
|
|
313
|
+
const bands = getWallFaceBandConfig(wall);
|
|
314
|
+
if (!bands.enabled)
|
|
315
|
+
return [];
|
|
316
|
+
const planes = [bands.lowerTop];
|
|
317
|
+
if (bands.count >= 3)
|
|
318
|
+
planes.push(bands.middleTop);
|
|
319
|
+
if (bands.count >= 4)
|
|
320
|
+
planes.push(bands.upperTop);
|
|
321
|
+
return planes.filter((plane) => plane > WALL_BAND_SPLIT_EPSILON && plane < (wall.height ?? 2.5) - WALL_BAND_SPLIT_EPSILON);
|
|
322
|
+
}
|
|
207
323
|
// ============================================================================
|
|
208
324
|
// WALL SYSTEM
|
|
209
325
|
// ============================================================================
|
|
@@ -258,7 +374,7 @@ export const WallSystem = () => {
|
|
|
258
374
|
if (hasDirty) {
|
|
259
375
|
dirtyNodes.forEach((id) => {
|
|
260
376
|
const node = nodes[id];
|
|
261
|
-
if (
|
|
377
|
+
if (node?.type !== 'wall')
|
|
262
378
|
return;
|
|
263
379
|
const levelId = node.parentId;
|
|
264
380
|
if (!levelId)
|
|
@@ -387,7 +503,7 @@ function getEffectiveWall(wall) {
|
|
|
387
503
|
function getLevelWalls(levelId) {
|
|
388
504
|
const { nodes } = useScene.getState();
|
|
389
505
|
const level = nodes[levelId];
|
|
390
|
-
if (
|
|
506
|
+
if (level?.type !== 'level')
|
|
391
507
|
return [];
|
|
392
508
|
const walls = [];
|
|
393
509
|
for (const childId of level.children) {
|
|
@@ -406,14 +522,15 @@ function getLevelWalls(levelId) {
|
|
|
406
522
|
function updateWallGeometry(wallId, miterData) {
|
|
407
523
|
const nodes = useScene.getState().nodes;
|
|
408
524
|
const sceneNode = nodes[wallId];
|
|
409
|
-
if (
|
|
525
|
+
if (sceneNode?.type !== 'wall')
|
|
410
526
|
return;
|
|
411
527
|
const node = getEffectiveWall(sceneNode);
|
|
412
528
|
const mesh = sceneRegistry.nodes.get(wallId);
|
|
413
529
|
if (!mesh)
|
|
414
530
|
return;
|
|
415
531
|
const levelId = resolveLevelId(node, nodes);
|
|
416
|
-
const
|
|
532
|
+
const slabSupport = spatialGridManager.getSlabSupportForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness);
|
|
533
|
+
const slabElevation = slabSupport.elevation;
|
|
417
534
|
const childrenIds = node.children || [];
|
|
418
535
|
// Merge live overrides into door / window children so cutouts track an
|
|
419
536
|
// in-flight resize drag (door width arrow, window height arrow, etc.)
|
|
@@ -427,22 +544,27 @@ function updateWallGeometry(wallId, miterData) {
|
|
|
427
544
|
return child;
|
|
428
545
|
// `getEffectiveNode` folds in resize overrides (width/height arrows).
|
|
429
546
|
// Position moves publish to `useLiveTransforms` instead, so fold that
|
|
430
|
-
// in too —
|
|
431
|
-
//
|
|
432
|
-
// (rectangular cutouts already track via the live mesh matrixWorld).
|
|
547
|
+
// in too — opening cutout brushes are rebuilt directly from the
|
|
548
|
+
// effective node position rather than from the rendered proxy mesh.
|
|
433
549
|
const effective = getEffectiveNode(child);
|
|
434
550
|
const live = useLiveTransforms.getState().get(child.id);
|
|
435
551
|
if (!live?.position)
|
|
436
552
|
return effective;
|
|
437
553
|
return { ...effective, position: live.position };
|
|
438
554
|
});
|
|
439
|
-
const
|
|
555
|
+
const builtGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments);
|
|
556
|
+
const wallAngle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
|
|
557
|
+
// World transform the render mesh will apply (position + Y-rotation below).
|
|
558
|
+
// Reproduce it here so the UVs can be projected in WORLD space — see
|
|
559
|
+
// `applyWorldPlanarWallUVs`.
|
|
560
|
+
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);
|
|
561
|
+
const newGeo = applyWorldPlanarWallUVs(builtGeo, wallWorldMatrix);
|
|
440
562
|
mesh.geometry.dispose();
|
|
441
563
|
mesh.geometry = newGeo;
|
|
442
564
|
// Update collision mesh
|
|
443
565
|
const collisionMesh = mesh.getObjectByName('collision-mesh');
|
|
444
566
|
if (collisionMesh) {
|
|
445
|
-
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation);
|
|
567
|
+
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments);
|
|
446
568
|
collisionMesh.geometry.dispose();
|
|
447
569
|
collisionMesh.geometry = collisionGeo;
|
|
448
570
|
}
|
|
@@ -450,19 +572,79 @@ function updateWallGeometry(wallId, miterData) {
|
|
|
450
572
|
const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
|
|
451
573
|
mesh.rotation.y = -angle;
|
|
452
574
|
}
|
|
575
|
+
const WALL_UV_Y_AXIS = new THREE.Vector3(0, 1, 0);
|
|
576
|
+
const WALL_UV_UNIT_SCALE = new THREE.Vector3(1, 1, 1);
|
|
577
|
+
/**
|
|
578
|
+
* Re-project a wall's UVs in WORLD space (1 UV unit = 1 m) so the finish tiles
|
|
579
|
+
* continuously across adjacent walls and lines up with the roof gable above —
|
|
580
|
+
* instead of THREE's `ExtrudeGeometry` UVs, which restart at each wall's own
|
|
581
|
+
* start/end. Matches `roof-system`'s `pushRoofUv` projection exactly: vertical
|
|
582
|
+
* faces use `U = ±worldX/Z` (the axis across the face normal) and `V = 1 -
|
|
583
|
+
* worldY`; the thin top/bottom caps use `(worldX, worldZ)`. De-indexes first so
|
|
584
|
+
* every triangle projects by its own face normal (no shared-vertex seams at
|
|
585
|
+
* edges). Applied only to the render mesh; collision/floorplan geometry is
|
|
586
|
+
* untouched.
|
|
587
|
+
*/
|
|
588
|
+
function applyWorldPlanarWallUVs(geometry, worldMatrix) {
|
|
589
|
+
const target = geometry.index ? geometry.toNonIndexed() : geometry;
|
|
590
|
+
if (target !== geometry)
|
|
591
|
+
geometry.dispose();
|
|
592
|
+
const position = target.getAttribute('position');
|
|
593
|
+
if (!position || position.count === 0)
|
|
594
|
+
return target;
|
|
595
|
+
const a = new THREE.Vector3();
|
|
596
|
+
const b = new THREE.Vector3();
|
|
597
|
+
const c = new THREE.Vector3();
|
|
598
|
+
const normal = new THREE.Vector3();
|
|
599
|
+
const edgeAB = new THREE.Vector3();
|
|
600
|
+
const edgeAC = new THREE.Vector3();
|
|
601
|
+
const uvs = new Float32Array(position.count * 2);
|
|
602
|
+
for (let i = 0; i < position.count; i += 3) {
|
|
603
|
+
a.fromBufferAttribute(position, i).applyMatrix4(worldMatrix);
|
|
604
|
+
b.fromBufferAttribute(position, i + 1).applyMatrix4(worldMatrix);
|
|
605
|
+
c.fromBufferAttribute(position, i + 2).applyMatrix4(worldMatrix);
|
|
606
|
+
edgeAB.subVectors(b, a);
|
|
607
|
+
edgeAC.subVectors(c, a);
|
|
608
|
+
normal.crossVectors(edgeAB, edgeAC).normalize();
|
|
609
|
+
const absX = Math.abs(normal.x);
|
|
610
|
+
const absY = Math.abs(normal.y);
|
|
611
|
+
const absZ = Math.abs(normal.z);
|
|
612
|
+
for (let k = 0; k < 3; k += 1) {
|
|
613
|
+
const p = k === 0 ? a : k === 1 ? b : c;
|
|
614
|
+
let u;
|
|
615
|
+
let v;
|
|
616
|
+
if (absY >= absX && absY >= absZ) {
|
|
617
|
+
u = p.x;
|
|
618
|
+
v = p.z;
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
v = 1 - p.y;
|
|
622
|
+
u = absX >= absZ ? (normal.x >= 0 ? p.z : -p.z) : normal.z >= 0 ? p.x : -p.x;
|
|
623
|
+
}
|
|
624
|
+
uvs[(i + k) * 2] = u;
|
|
625
|
+
uvs[(i + k) * 2 + 1] = v;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
target.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
|
|
629
|
+
target.setAttribute('uv2', new THREE.Float32BufferAttribute(uvs.slice(), 2));
|
|
630
|
+
return target;
|
|
631
|
+
}
|
|
453
632
|
/**
|
|
454
633
|
* Generates extruded wall geometry with mitering and cutouts
|
|
455
634
|
*
|
|
456
635
|
* Key insight from demo: polygon is built in WORLD coordinates first,
|
|
457
636
|
* then we transform to wall-local for the 3D mesh.
|
|
458
637
|
*/
|
|
459
|
-
export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0
|
|
638
|
+
export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0, baseElevation = slabElevation, baseSegments = [
|
|
639
|
+
{ start: 0, end: 1, elevation: baseElevation },
|
|
640
|
+
]) {
|
|
460
641
|
const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
|
|
461
642
|
const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
|
|
462
|
-
// Positive slab: shift the whole wall up (full height preserved)
|
|
463
|
-
// Negative slab: extend wall downward so top stays fixed at wallNode.height
|
|
464
643
|
const wallHeight = wallNode.height ?? DEFAULT_WALL_HEIGHT;
|
|
465
|
-
const
|
|
644
|
+
const topElevation = slabElevation > 0 ? slabElevation + wallHeight : wallHeight;
|
|
645
|
+
const effectiveBaseElevation = Math.min(baseElevation, slabElevation);
|
|
646
|
+
const localBottom = effectiveBaseElevation - slabElevation;
|
|
647
|
+
const height = topElevation - effectiveBaseElevation;
|
|
466
648
|
const thickness = getWallThickness(wallNode);
|
|
467
649
|
// Wall direction and normal (exactly like demo)
|
|
468
650
|
const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y };
|
|
@@ -509,24 +691,118 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
509
691
|
});
|
|
510
692
|
// Rotate so extrusion direction (Z) becomes height direction (Y)
|
|
511
693
|
geometry.rotateX(-Math.PI / 2);
|
|
694
|
+
if (Math.abs(localBottom) > 1e-9)
|
|
695
|
+
geometry.translate(0, localBottom, 0);
|
|
512
696
|
geometry.computeVertexNormals();
|
|
513
697
|
assignWallMaterialGroups(geometry, wallNode, boundaryEdges);
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
|
|
698
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
699
|
+
// Start with the lowest required wall prism, then remove the volume below
|
|
700
|
+
// each higher-supported run. This keeps the existing mitered footprint and
|
|
701
|
+
// opening CSG while giving one wall a stepped longitudinal base.
|
|
702
|
+
const baseProfileCutouts = [];
|
|
703
|
+
for (const segment of baseSegments) {
|
|
704
|
+
const segmentElevation = Math.min(segment.elevation, slabElevation);
|
|
705
|
+
const cutHeight = segmentElevation - effectiveBaseElevation;
|
|
706
|
+
if (cutHeight <= 1e-6 || segment.end - segment.start <= 1e-7)
|
|
707
|
+
continue;
|
|
708
|
+
const segmentStart = THREE.MathUtils.clamp(segment.start, 0, 1);
|
|
709
|
+
const segmentEnd = THREE.MathUtils.clamp(segment.end, 0, 1);
|
|
710
|
+
const cutHalfWidth = Math.max(thickness * 2, 0.2);
|
|
711
|
+
const worldCutoutPoints = [];
|
|
712
|
+
if (isCurvedWall(wallNode)) {
|
|
713
|
+
const sampleCount = Math.max(2, Math.ceil((segmentEnd - segmentStart) * 24));
|
|
714
|
+
const left = [];
|
|
715
|
+
const right = [];
|
|
716
|
+
for (let index = 0; index <= sampleCount; index++) {
|
|
717
|
+
const t = segmentStart + ((segmentEnd - segmentStart) * index) / sampleCount;
|
|
718
|
+
const frame = getWallCurveFrameAt(wallNode, t);
|
|
719
|
+
const endpointExtension = index === 0 && segmentStart <= 1e-7
|
|
720
|
+
? -cutHalfWidth
|
|
721
|
+
: index === sampleCount && segmentEnd >= 1 - 1e-7
|
|
722
|
+
? cutHalfWidth
|
|
723
|
+
: 0;
|
|
724
|
+
const center = {
|
|
725
|
+
x: frame.point.x + frame.tangent.x * endpointExtension,
|
|
726
|
+
y: frame.point.y + frame.tangent.y * endpointExtension,
|
|
727
|
+
};
|
|
728
|
+
left.push({
|
|
729
|
+
x: center.x + frame.normal.x * cutHalfWidth,
|
|
730
|
+
y: center.y + frame.normal.y * cutHalfWidth,
|
|
731
|
+
});
|
|
732
|
+
right.push({
|
|
733
|
+
x: center.x - frame.normal.x * cutHalfWidth,
|
|
734
|
+
y: center.y - frame.normal.y * cutHalfWidth,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
worldCutoutPoints.push(...left, ...right.reverse());
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
const tangentX = v.x / L;
|
|
741
|
+
const tangentY = v.y / L;
|
|
742
|
+
const normalX = -tangentY;
|
|
743
|
+
const normalY = tangentX;
|
|
744
|
+
const startExtension = segmentStart <= 1e-7 ? cutHalfWidth : 0;
|
|
745
|
+
const endExtension = segmentEnd >= 1 - 1e-7 ? cutHalfWidth : 0;
|
|
746
|
+
const startPoint = {
|
|
747
|
+
x: wallStart.x + tangentX * (segmentStart * L - startExtension),
|
|
748
|
+
y: wallStart.y + tangentY * (segmentStart * L - startExtension),
|
|
749
|
+
};
|
|
750
|
+
const endPoint = {
|
|
751
|
+
x: wallStart.x + tangentX * (segmentEnd * L + endExtension),
|
|
752
|
+
y: wallStart.y + tangentY * (segmentEnd * L + endExtension),
|
|
753
|
+
};
|
|
754
|
+
worldCutoutPoints.push({
|
|
755
|
+
x: startPoint.x + normalX * cutHalfWidth,
|
|
756
|
+
y: startPoint.y + normalY * cutHalfWidth,
|
|
757
|
+
}, { x: endPoint.x + normalX * cutHalfWidth, y: endPoint.y + normalY * cutHalfWidth }, { x: endPoint.x - normalX * cutHalfWidth, y: endPoint.y - normalY * cutHalfWidth }, {
|
|
758
|
+
x: startPoint.x - normalX * cutHalfWidth,
|
|
759
|
+
y: startPoint.y - normalY * cutHalfWidth,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
const localCutoutPoints = worldCutoutPoints.map(worldToLocal);
|
|
763
|
+
if (localCutoutPoints.length < 3)
|
|
764
|
+
continue;
|
|
765
|
+
const cutoutShape = new THREE.Shape();
|
|
766
|
+
cutoutShape.moveTo(localCutoutPoints[0].x, -localCutoutPoints[0].z);
|
|
767
|
+
for (let index = 1; index < localCutoutPoints.length; index++) {
|
|
768
|
+
cutoutShape.lineTo(localCutoutPoints[index].x, -localCutoutPoints[index].z);
|
|
769
|
+
}
|
|
770
|
+
cutoutShape.closePath();
|
|
771
|
+
const cutoutBottom = localBottom - 0.01;
|
|
772
|
+
const cutoutTop = segmentElevation - slabElevation;
|
|
773
|
+
const cutoutGeometry = new THREE.ExtrudeGeometry(cutoutShape, {
|
|
774
|
+
depth: cutoutTop - cutoutBottom,
|
|
775
|
+
bevelEnabled: false,
|
|
776
|
+
});
|
|
777
|
+
cutoutGeometry.rotateX(-Math.PI / 2);
|
|
778
|
+
cutoutGeometry.translate(0, cutoutBottom, 0);
|
|
779
|
+
computeGeometryBoundsTree(cutoutGeometry);
|
|
780
|
+
baseProfileCutouts.push(new Brush(cutoutGeometry));
|
|
781
|
+
}
|
|
782
|
+
// Apply base-profile and opening cutouts in one CSG pass.
|
|
783
|
+
const cutoutBrushes = [
|
|
784
|
+
...baseProfileCutouts,
|
|
785
|
+
...collectCutoutBrushes(wallNode, childrenNodes, thickness),
|
|
786
|
+
];
|
|
517
787
|
if (cutoutBrushes.length === 0) {
|
|
518
|
-
|
|
788
|
+
const splitGeometry = splitGeometryAtHorizontalPlanes(geometry, getWallBandSplitPlanes(wallNode));
|
|
789
|
+
splitGeometry.computeVertexNormals();
|
|
790
|
+
assignWallMaterialGroups(splitGeometry, wallNode, boundaryEdges);
|
|
791
|
+
ensureRenderableGeometryAttributes(splitGeometry);
|
|
792
|
+
return splitGeometry;
|
|
519
793
|
}
|
|
520
794
|
// Create wall brush from geometry
|
|
521
795
|
// Pre-compute BVH with new API to avoid deprecation warning
|
|
796
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
522
797
|
computeGeometryBoundsTree(geometry);
|
|
523
798
|
const wallBrush = new Brush(geometry);
|
|
524
799
|
wallBrush.updateMatrixWorld();
|
|
525
800
|
// Subtract each cutout from the wall
|
|
526
801
|
let resultBrush = wallBrush;
|
|
527
802
|
for (const cutoutBrush of cutoutBrushes) {
|
|
528
|
-
cutoutBrush
|
|
803
|
+
prepareBrushForCSG(cutoutBrush);
|
|
529
804
|
const newResult = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
|
|
805
|
+
prepareBrushForCSG(newResult);
|
|
530
806
|
if (resultBrush !== wallBrush) {
|
|
531
807
|
csgGeometry(resultBrush).dispose();
|
|
532
808
|
}
|
|
@@ -538,14 +814,16 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
538
814
|
csgGeometry(brush).dispose();
|
|
539
815
|
}
|
|
540
816
|
const resultGeometry = csgGeometry(resultBrush);
|
|
541
|
-
resultGeometry
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
817
|
+
const splitResultGeometry = splitGeometryAtHorizontalPlanes(resultGeometry, getWallBandSplitPlanes(wallNode));
|
|
818
|
+
splitResultGeometry.computeVertexNormals();
|
|
819
|
+
assignWallMaterialGroups(splitResultGeometry, wallNode, boundaryEdges);
|
|
820
|
+
ensureRenderableGeometryAttributes(splitResultGeometry);
|
|
821
|
+
return splitResultGeometry;
|
|
545
822
|
}
|
|
546
823
|
/**
|
|
547
|
-
* Collects
|
|
548
|
-
*
|
|
824
|
+
* Collects opening and item cutout brushes for CSG subtraction. Door/window
|
|
825
|
+
* cuts come directly from node geometry; item proxy meshes are transformed
|
|
826
|
+
* into wall-local boxes that pass through the wall.
|
|
549
827
|
*/
|
|
550
828
|
function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
551
829
|
const brushes = [];
|
|
@@ -558,15 +836,8 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
|
558
836
|
for (const child of childrenNodes) {
|
|
559
837
|
if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door')
|
|
560
838
|
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));
|
|
839
|
+
if (child.type === 'door' || child.type === 'window') {
|
|
840
|
+
brushes.push(createOpeningCutoutBrush(child, wallThickness));
|
|
570
841
|
continue;
|
|
571
842
|
}
|
|
572
843
|
const childMesh = sceneRegistry.nodes.get(child.id);
|
|
@@ -609,132 +880,16 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
|
609
880
|
}
|
|
610
881
|
return brushes;
|
|
611
882
|
}
|
|
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) {
|
|
883
|
+
function createOpeningCutoutBrush(opening, wallThickness) {
|
|
631
884
|
const halfWidth = opening.width / 2;
|
|
632
885
|
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();
|
|
886
|
+
const bottomPadding = getOpeningCutoutBottomPadding(opening, bottom);
|
|
887
|
+
const geometry = buildOpeningCutoutGeometry(opening, {
|
|
888
|
+
left: opening.position[0] - halfWidth,
|
|
889
|
+
right: opening.position[0] + halfWidth,
|
|
890
|
+
bottom: bottom - bottomPadding,
|
|
891
|
+
top: opening.position[1] + opening.height / 2,
|
|
892
|
+
}, wallThickness * 2, wallThickness);
|
|
893
|
+
computeGeometryBoundsTree(geometry);
|
|
894
|
+
return new Brush(geometry);
|
|
740
895
|
}
|
|
@@ -1,2 +1,14 @@
|
|
|
1
|
+
import { type WindowNode } from '@pascal-app/core';
|
|
2
|
+
import type { Object3D } from 'three';
|
|
3
|
+
/**
|
|
4
|
+
* Pose a window's moving parts (sash/panel/slats) at `value` (0 = closed,
|
|
5
|
+
* 1 = open) by mutating the named child groups under `mesh`. Returns true when
|
|
6
|
+
* the window type has a direct pose path and the named parts were found.
|
|
7
|
+
*
|
|
8
|
+
* This is the single source of truth for window kinematics: the live animation
|
|
9
|
+
* system poses the registered scene mesh, and the GLB exporter poses an export
|
|
10
|
+
* clone to sample the open/close keyframes for a baked animation clip.
|
|
11
|
+
*/
|
|
12
|
+
export declare function poseWindowMovingParts(node: WindowNode, mesh: Object3D | undefined, value: number): boolean;
|
|
1
13
|
export declare const WindowAnimationSystem: () => null;
|
|
2
14
|
//# sourceMappingURL=window-animation-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"window-animation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-animation-system.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"window-animation-system.d.ts","sourceRoot":"","sources":["../../../src/systems/window/window-animation-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,UAAU,EAChB,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAsBrC;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,UAAU,EAChB,IAAI,EAAE,QAAQ,GAAG,SAAS,EAC1B,KAAK,EAAE,MAAM,GACZ,OAAO,CAqFT;AAQD,eAAO,MAAM,qBAAqB,YA+CjC,CAAA"}
|