@pascal-app/viewer 0.8.0 → 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 +6 -2
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +41 -22
- package/dist/components/renderers/parametric-node-renderer.d.ts +5 -0
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -0
- package/dist/components/renderers/parametric-node-renderer.js +37 -0
- package/dist/components/renderers/scene-renderer.d.ts +1 -1
- package/dist/components/renderers/scene-renderer.d.ts.map +1 -1
- package/dist/components/viewer/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 +56 -3
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +280 -65
- package/dist/components/viewer/lights.d.ts +1 -1
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +201 -43
- package/dist/components/viewer/perf-monitor.d.ts +1 -1
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +115 -12
- 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 +395 -63
- package/dist/components/viewer/registered-systems.d.ts +12 -0
- package/dist/components/viewer/registered-systems.d.ts.map +1 -0
- package/dist/components/viewer/registered-systems.js +49 -0
- package/dist/components/viewer/scene-bvh.d.ts +18 -0
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -0
- package/dist/components/viewer/scene-bvh.js +97 -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 +1 -1
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +37 -14
- package/dist/components/viewer/viewer-camera.d.ts +1 -1
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/walkthrough-controls.d.ts +2 -1
- 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 +6 -74
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/hooks/use-node-events.js +28 -8
- package/dist/index.d.ts +48 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +82 -4
- 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 +10 -0
- package/dist/lib/csg-utils.d.ts.map +1 -0
- package/dist/lib/csg-utils.js +93 -0
- 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/drawable-geometry.d.ts +21 -0
- package/dist/lib/drawable-geometry.d.ts.map +1 -0
- package/dist/lib/drawable-geometry.js +22 -0
- package/dist/lib/edge-style.d.ts +4 -0
- package/dist/lib/edge-style.d.ts.map +1 -0
- package/dist/lib/edge-style.js +31 -0
- package/dist/lib/gpu-perf.d.ts +4 -0
- package/dist/lib/gpu-perf.d.ts.map +1 -0
- package/dist/lib/gpu-perf.js +23 -0
- package/dist/lib/ink-edges.d.ts +9 -0
- package/dist/lib/ink-edges.d.ts.map +1 -0
- package/dist/lib/ink-edges.js +49 -0
- package/dist/lib/isolation.d.ts +39 -0
- package/dist/lib/isolation.d.ts.map +1 -0
- package/dist/lib/isolation.js +103 -0
- package/dist/lib/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 +28 -0
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +28 -0
- package/dist/lib/materials.d.ts +38 -15
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +364 -72
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +7 -0
- package/dist/lib/polygon-union.d.ts +4 -0
- package/dist/lib/polygon-union.d.ts.map +1 -0
- package/dist/lib/polygon-union.js +289 -0
- package/dist/lib/polygon-union.test.d.ts +2 -0
- package/dist/lib/polygon-union.test.d.ts.map +1 -0
- package/dist/lib/polygon-union.test.js +105 -0
- package/dist/lib/scene-themes.d.ts +30 -0
- package/dist/lib/scene-themes.d.ts.map +1 -0
- package/dist/lib/scene-themes.js +202 -0
- 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 +92 -12
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +201 -6
- 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 +6 -2
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +66 -13
- 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 +864 -180
- package/dist/systems/elevator/elevator-interaction-system.d.ts +2 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -0
- package/dist/systems/elevator/elevator-interaction-system.js +62 -0
- package/dist/systems/fence/fence-system.d.ts +20 -0
- package/dist/systems/fence/fence-system.d.ts.map +1 -1
- package/dist/systems/fence/fence-system.js +229 -57
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +20 -0
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -0
- package/dist/systems/floor-elevation/floor-elevation-system.js +78 -0
- package/dist/systems/geometry/geometry-system.d.ts +41 -0
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -0
- package/dist/systems/geometry/geometry-system.js +348 -0
- 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 +1 -1
- 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 +7 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +28 -17
- package/dist/systems/item-light/item-light-system.d.ts +1 -1
- package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
- package/dist/systems/item-light/item-light-system.js +18 -5
- package/dist/systems/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 +3 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +59 -14
- package/dist/systems/roof/roof-system.d.ts +46 -6
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +1365 -170
- 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 +204 -68
- package/dist/systems/slab/slab-system.test.d.ts +2 -0
- package/dist/systems/slab/slab-system.test.d.ts.map +1 -0
- package/dist/systems/slab/slab-system.test.js +63 -0
- package/dist/systems/stair/stair-materials.d.ts +4 -3
- package/dist/systems/stair/stair-materials.d.ts.map +1 -1
- package/dist/systems/stair/stair-materials.js +30 -15
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +55 -63
- package/dist/systems/surface-hole-geometry.d.ts +3 -0
- package/dist/systems/surface-hole-geometry.d.ts.map +1 -0
- package/dist/systems/surface-hole-geometry.js +4 -0
- package/dist/systems/wall/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 +62 -17
- package/dist/systems/wall/wall-materials.d.ts +12 -7
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +279 -46
- 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 +493 -193
- 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 +308 -18
- package/package.json +5 -6
- package/dist/components/renderers/building/building-renderer.d.ts +0 -5
- package/dist/components/renderers/building/building-renderer.d.ts.map +0 -1
- package/dist/components/renderers/building/building-renderer.js +0 -11
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts +0 -5
- package/dist/components/renderers/ceiling/ceiling-renderer.d.ts.map +0 -1
- package/dist/components/renderers/ceiling/ceiling-renderer.js +0 -58
- package/dist/components/renderers/column/column-renderer.d.ts +0 -5
- package/dist/components/renderers/column/column-renderer.d.ts.map +0 -1
- package/dist/components/renderers/column/column-renderer.js +0 -597
- package/dist/components/renderers/door/door-renderer.d.ts +0 -5
- package/dist/components/renderers/door/door-renderer.d.ts.map +0 -1
- package/dist/components/renderers/door/door-renderer.js +0 -16
- package/dist/components/renderers/fence/fence-renderer.d.ts +0 -5
- package/dist/components/renderers/fence/fence-renderer.d.ts.map +0 -1
- package/dist/components/renderers/fence/fence-renderer.js +0 -15
- package/dist/components/renderers/guide/guide-renderer.d.ts +0 -5
- package/dist/components/renderers/guide/guide-renderer.d.ts.map +0 -1
- package/dist/components/renderers/guide/guide-renderer.js +0 -38
- package/dist/components/renderers/item/item-renderer.d.ts +0 -5
- package/dist/components/renderers/item/item-renderer.d.ts.map +0 -1
- package/dist/components/renderers/item/item-renderer.js +0 -172
- package/dist/components/renderers/level/level-renderer.d.ts +0 -5
- package/dist/components/renderers/level/level-renderer.d.ts.map +0 -1
- package/dist/components/renderers/level/level-renderer.js +0 -11
- package/dist/components/renderers/roof/roof-materials.d.ts +0 -4
- package/dist/components/renderers/roof/roof-materials.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-materials.js +0 -16
- package/dist/components/renderers/roof/roof-renderer.d.ts +0 -5
- package/dist/components/renderers/roof/roof-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof/roof-renderer.js +0 -32
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/roof-segment/roof-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/roof-segment/roof-segment-renderer.js +0 -40
- package/dist/components/renderers/scan/scan-renderer.d.ts +0 -5
- package/dist/components/renderers/scan/scan-renderer.d.ts.map +0 -1
- package/dist/components/renderers/scan/scan-renderer.js +0 -54
- package/dist/components/renderers/site/site-renderer.d.ts +0 -5
- package/dist/components/renderers/site/site-renderer.d.ts.map +0 -1
- package/dist/components/renderers/site/site-renderer.js +0 -106
- package/dist/components/renderers/slab/slab-renderer.d.ts +0 -5
- package/dist/components/renderers/slab/slab-renderer.d.ts.map +0 -1
- package/dist/components/renderers/slab/slab-renderer.js +0 -58
- package/dist/components/renderers/spawn/spawn-renderer.d.ts +0 -5
- package/dist/components/renderers/spawn/spawn-renderer.d.ts.map +0 -1
- package/dist/components/renderers/spawn/spawn-renderer.js +0 -32
- package/dist/components/renderers/stair/stair-renderer.d.ts +0 -5
- package/dist/components/renderers/stair/stair-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair/stair-renderer.js +0 -603
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts +0 -5
- package/dist/components/renderers/stair-segment/stair-segment-renderer.d.ts.map +0 -1
- package/dist/components/renderers/stair-segment/stair-segment-renderer.js +0 -51
- package/dist/components/renderers/wall/wall-renderer.d.ts +0 -5
- package/dist/components/renderers/wall/wall-renderer.d.ts.map +0 -1
- package/dist/components/renderers/wall/wall-renderer.js +0 -16
- package/dist/components/renderers/window/window-renderer.d.ts +0 -5
- package/dist/components/renderers/window/window-renderer.d.ts.map +0 -1
- package/dist/components/renderers/window/window-renderer.js +0 -21
- package/dist/components/renderers/zone/zone-renderer.d.ts +0 -5
- package/dist/components/renderers/zone/zone-renderer.d.ts.map +0 -1
- package/dist/components/renderers/zone/zone-renderer.js +0 -181
- 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 -79
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
import { calculateLevelMiters, DEFAULT_WALL_HEIGHT, getAdjacentWallIds, getWallCurveFrameAt, getWallMiterBoundaryPoints, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, sceneRegistry, spatialGridManager, 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,53 +221,261 @@ 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
|
// ============================================================================
|
|
210
326
|
let useFrameNb = 0;
|
|
327
|
+
// ─── Drag-throttle state (singleton — one WallSystem mounted globally) ──
|
|
328
|
+
//
|
|
329
|
+
// Endpoint drags fire `markDirty(wallId)` on every pointermove tick. Without
|
|
330
|
+
// throttling, each tick rebuilds the dragged wall (~1 CSG + miter pass) AND
|
|
331
|
+
// every adjacent wall sharing a corner (3–4× in a t-junction or room).
|
|
332
|
+
// Visible as drag lag, especially on walls with door/window cutouts.
|
|
333
|
+
//
|
|
334
|
+
// Strategy: rebuild the dragged wall every tick (so the drag follows the
|
|
335
|
+
// cursor with full fidelity), but defer adjacent rebuilds to a trailing-
|
|
336
|
+
// edge flush DRAG_FLUSH_MS after the dirty stream stops. Visually, neighbor
|
|
337
|
+
// corners stay at their pre-drag miter until release, then snap into place
|
|
338
|
+
// within ~80ms. Standard CAD-app behavior. Speeds up t-junction drags ~3×,
|
|
339
|
+
// 4-corner-room drags ~4×.
|
|
340
|
+
const DRAG_FLUSH_MS = 80;
|
|
341
|
+
const MAX_WALL_REBUILDS_PER_FRAME = 8;
|
|
342
|
+
const WALL_PROGRESSIVE_DIRTY_THRESHOLD = MAX_WALL_REBUILDS_PER_FRAME;
|
|
343
|
+
const WALL_PROGRESSIVE_TIME_BUDGET_MS = 8;
|
|
344
|
+
let lastWallDirtyAtMs = 0;
|
|
345
|
+
const pendingAdjacentByLevel = new Map();
|
|
346
|
+
function getPendingAdjacentCount() {
|
|
347
|
+
let count = 0;
|
|
348
|
+
for (const ids of pendingAdjacentByLevel.values()) {
|
|
349
|
+
count += ids.size;
|
|
350
|
+
}
|
|
351
|
+
return count;
|
|
352
|
+
}
|
|
211
353
|
export const WallSystem = () => {
|
|
212
354
|
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
213
355
|
const clearDirty = useScene((state) => state.clearDirty);
|
|
356
|
+
// Subscribe so override-only changes (no scene write) still re-run
|
|
357
|
+
// this component, which lets the gate below pick up the latest
|
|
358
|
+
// `dirtyNodes` set from the same render pass that received the
|
|
359
|
+
// override-publishing `markDirty` call. Without this, very fast
|
|
360
|
+
// drags could land an override and a markDirty in the same React
|
|
361
|
+
// tick and the next `useFrame` would still see the stale closure.
|
|
362
|
+
useLiveNodeOverrides((s) => s.overrides);
|
|
214
363
|
useFrame(() => {
|
|
215
|
-
|
|
364
|
+
const hasDirty = dirtyNodes.size > 0;
|
|
365
|
+
const hasPending = pendingAdjacentByLevel.size > 0;
|
|
366
|
+
if (!hasDirty && !hasPending)
|
|
216
367
|
return;
|
|
217
368
|
const nodes = useScene.getState().nodes;
|
|
369
|
+
const now = performance.now();
|
|
218
370
|
// Collect dirty walls and their levels
|
|
219
371
|
const dirtyWallsByLevel = new Map();
|
|
372
|
+
let dirtyWallCount = 0;
|
|
220
373
|
useFrameNb += 1;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
dirtyWallsByLevel.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
374
|
+
if (hasDirty) {
|
|
375
|
+
dirtyNodes.forEach((id) => {
|
|
376
|
+
const node = nodes[id];
|
|
377
|
+
if (node?.type !== 'wall')
|
|
378
|
+
return;
|
|
379
|
+
const levelId = node.parentId;
|
|
380
|
+
if (!levelId)
|
|
381
|
+
return;
|
|
382
|
+
if (!dirtyWallsByLevel.has(levelId)) {
|
|
383
|
+
dirtyWallsByLevel.set(levelId, new Set());
|
|
384
|
+
}
|
|
385
|
+
dirtyWallsByLevel.get(levelId)?.add(id);
|
|
386
|
+
dirtyWallCount += 1;
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const hasDirtyWalls = dirtyWallsByLevel.size > 0;
|
|
390
|
+
if (hasDirtyWalls) {
|
|
391
|
+
lastWallDirtyAtMs = now;
|
|
392
|
+
}
|
|
393
|
+
const useProgressiveWallRebuilds = dirtyWallCount > WALL_PROGRESSIVE_DIRTY_THRESHOLD;
|
|
394
|
+
let rebuiltWallsThisFrame = 0;
|
|
395
|
+
const rebuildFrameStartedAt = now;
|
|
233
396
|
// Process each level that has dirty walls
|
|
234
397
|
for (const [levelId, dirtyWallIds] of dirtyWallsByLevel) {
|
|
398
|
+
if (useProgressiveWallRebuilds && rebuiltWallsThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
235
401
|
const levelWalls = getLevelWalls(levelId);
|
|
236
402
|
const miterData = calculateLevelMiters(levelWalls);
|
|
237
|
-
|
|
403
|
+
const rebuiltWallIds = new Set();
|
|
404
|
+
// Update dirty walls — always, no throttling. The dragged wall must
|
|
405
|
+
// follow the cursor with full fidelity (cutouts and all). Large imports
|
|
406
|
+
// enter the progressive path so initial load can't lock the tab.
|
|
238
407
|
for (const wallId of dirtyWallIds) {
|
|
408
|
+
if (useProgressiveWallRebuilds) {
|
|
409
|
+
if (rebuiltWallsThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
if (rebuiltWallsThisFrame > 0 &&
|
|
413
|
+
performance.now() - rebuildFrameStartedAt >= WALL_PROGRESSIVE_TIME_BUDGET_MS) {
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
239
417
|
const mesh = sceneRegistry.nodes.get(wallId);
|
|
240
418
|
if (mesh) {
|
|
241
419
|
updateWallGeometry(wallId, miterData);
|
|
242
420
|
clearDirty(wallId);
|
|
421
|
+
rebuiltWallIds.add(wallId);
|
|
422
|
+
rebuiltWallsThisFrame += 1;
|
|
243
423
|
}
|
|
244
424
|
// If mesh not found, keep it dirty for next frame
|
|
245
425
|
}
|
|
246
|
-
|
|
247
|
-
|
|
426
|
+
if (rebuiltWallIds.size === 0) {
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
// Adjacent walls sharing junctions — *defer* during active drag
|
|
430
|
+
// (dirty arrived this frame), flush on the trailing edge.
|
|
431
|
+
const adjacentWallIds = getAdjacentWallIds(levelWalls, rebuiltWallIds);
|
|
432
|
+
let pending = pendingAdjacentByLevel.get(levelId);
|
|
433
|
+
if (!pending) {
|
|
434
|
+
pending = new Set();
|
|
435
|
+
pendingAdjacentByLevel.set(levelId, pending);
|
|
436
|
+
}
|
|
248
437
|
for (const wallId of adjacentWallIds) {
|
|
249
438
|
if (!dirtyWallIds.has(wallId)) {
|
|
439
|
+
pending.add(wallId);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Trailing-edge flush: if no new dirty marks for DRAG_FLUSH_MS, the
|
|
444
|
+
// drag has ended — rebuild the queued neighbors so corners snap into
|
|
445
|
+
// their correct miter joins.
|
|
446
|
+
const quiet = !hasDirtyWalls && now - lastWallDirtyAtMs >= DRAG_FLUSH_MS;
|
|
447
|
+
if (quiet && pendingAdjacentByLevel.size > 0) {
|
|
448
|
+
const pendingCount = getPendingAdjacentCount();
|
|
449
|
+
const useProgressiveAdjacentRebuilds = pendingCount > WALL_PROGRESSIVE_DIRTY_THRESHOLD;
|
|
450
|
+
let rebuiltAdjacentThisFrame = 0;
|
|
451
|
+
const adjacentFrameStartedAt = performance.now();
|
|
452
|
+
for (const [levelId, pendingIds] of pendingAdjacentByLevel) {
|
|
453
|
+
if (pendingIds.size === 0)
|
|
454
|
+
continue;
|
|
455
|
+
const levelWalls = getLevelWalls(levelId);
|
|
456
|
+
const miterData = calculateLevelMiters(levelWalls);
|
|
457
|
+
for (const wallId of Array.from(pendingIds)) {
|
|
458
|
+
if (useProgressiveAdjacentRebuilds) {
|
|
459
|
+
if (rebuiltAdjacentThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
if (rebuiltAdjacentThisFrame > 0 &&
|
|
463
|
+
performance.now() - adjacentFrameStartedAt >= WALL_PROGRESSIVE_TIME_BUDGET_MS) {
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
250
467
|
const mesh = sceneRegistry.nodes.get(wallId);
|
|
251
|
-
if (mesh)
|
|
468
|
+
if (mesh)
|
|
252
469
|
updateWallGeometry(wallId, miterData);
|
|
253
|
-
|
|
470
|
+
pendingIds.delete(wallId);
|
|
471
|
+
rebuiltAdjacentThisFrame += 1;
|
|
472
|
+
}
|
|
473
|
+
if (pendingIds.size === 0) {
|
|
474
|
+
pendingAdjacentByLevel.delete(levelId);
|
|
475
|
+
}
|
|
476
|
+
if (useProgressiveAdjacentRebuilds &&
|
|
477
|
+
rebuiltAdjacentThisFrame >= MAX_WALL_REBUILDS_PER_FRAME) {
|
|
478
|
+
break;
|
|
254
479
|
}
|
|
255
480
|
}
|
|
256
481
|
}
|
|
@@ -258,46 +483,88 @@ export const WallSystem = () => {
|
|
|
258
483
|
return null;
|
|
259
484
|
};
|
|
260
485
|
/**
|
|
261
|
-
*
|
|
486
|
+
* Merge any live override for a wall into the scene record. Lets the
|
|
487
|
+
* 2D move handler publish `{ start, end, curveOffset }` to
|
|
488
|
+
* `useLiveNodeOverrides` and have the geometry / miter pipeline use
|
|
489
|
+
* those values without zustand churn during the drag. When no
|
|
490
|
+
* override is set, the wall is returned unchanged.
|
|
491
|
+
*/
|
|
492
|
+
function getEffectiveWall(wall) {
|
|
493
|
+
const override = useLiveNodeOverrides.getState().get(wall.id);
|
|
494
|
+
if (!override || Object.keys(override).length === 0)
|
|
495
|
+
return wall;
|
|
496
|
+
return { ...wall, ...override };
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Gets all walls that belong to a level, with any live overrides
|
|
500
|
+
* merged in so miters compute against the cursor-driven positions
|
|
501
|
+
* (not the pre-drag scene state).
|
|
262
502
|
*/
|
|
263
503
|
function getLevelWalls(levelId) {
|
|
264
504
|
const { nodes } = useScene.getState();
|
|
265
505
|
const level = nodes[levelId];
|
|
266
|
-
if (
|
|
506
|
+
if (level?.type !== 'level')
|
|
267
507
|
return [];
|
|
268
508
|
const walls = [];
|
|
269
509
|
for (const childId of level.children) {
|
|
270
510
|
const child = nodes[childId];
|
|
271
511
|
if (child?.type === 'wall') {
|
|
272
|
-
walls.push(child);
|
|
512
|
+
walls.push(getEffectiveWall(child));
|
|
273
513
|
}
|
|
274
514
|
}
|
|
275
515
|
return walls;
|
|
276
516
|
}
|
|
277
517
|
/**
|
|
278
|
-
* Updates the geometry for a single wall
|
|
518
|
+
* Updates the geometry for a single wall. Reads the effective node
|
|
519
|
+
* (override-merged) so a 2D drag visibly moves the 3D mesh without
|
|
520
|
+
* having touched `useScene` mid-drag.
|
|
279
521
|
*/
|
|
280
522
|
function updateWallGeometry(wallId, miterData) {
|
|
281
523
|
const nodes = useScene.getState().nodes;
|
|
282
|
-
const
|
|
283
|
-
if (
|
|
524
|
+
const sceneNode = nodes[wallId];
|
|
525
|
+
if (sceneNode?.type !== 'wall')
|
|
284
526
|
return;
|
|
527
|
+
const node = getEffectiveWall(sceneNode);
|
|
285
528
|
const mesh = sceneRegistry.nodes.get(wallId);
|
|
286
529
|
if (!mesh)
|
|
287
530
|
return;
|
|
288
531
|
const levelId = resolveLevelId(node, nodes);
|
|
289
|
-
const
|
|
532
|
+
const slabSupport = spatialGridManager.getSlabSupportForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness);
|
|
533
|
+
const slabElevation = slabSupport.elevation;
|
|
290
534
|
const childrenIds = node.children || [];
|
|
535
|
+
// Merge live overrides into door / window children so cutouts track an
|
|
536
|
+
// in-flight resize drag (door width arrow, window height arrow, etc.)
|
|
537
|
+
// without waiting on the scene store. Non-cutout children pass through
|
|
538
|
+
// unchanged.
|
|
291
539
|
const childrenNodes = childrenIds
|
|
292
540
|
.map((childId) => nodes[childId])
|
|
293
|
-
.filter((n) => n !== undefined)
|
|
294
|
-
|
|
541
|
+
.filter((n) => n !== undefined)
|
|
542
|
+
.map((child) => {
|
|
543
|
+
if (child.type !== 'door' && child.type !== 'window')
|
|
544
|
+
return child;
|
|
545
|
+
// `getEffectiveNode` folds in resize overrides (width/height arrows).
|
|
546
|
+
// Position moves publish to `useLiveTransforms` instead, so fold that
|
|
547
|
+
// in too — opening cutout brushes are rebuilt directly from the
|
|
548
|
+
// effective node position rather than from the rendered proxy mesh.
|
|
549
|
+
const effective = getEffectiveNode(child);
|
|
550
|
+
const live = useLiveTransforms.getState().get(child.id);
|
|
551
|
+
if (!live?.position)
|
|
552
|
+
return effective;
|
|
553
|
+
return { ...effective, position: live.position };
|
|
554
|
+
});
|
|
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);
|
|
295
562
|
mesh.geometry.dispose();
|
|
296
563
|
mesh.geometry = newGeo;
|
|
297
564
|
// Update collision mesh
|
|
298
565
|
const collisionMesh = mesh.getObjectByName('collision-mesh');
|
|
299
566
|
if (collisionMesh) {
|
|
300
|
-
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation);
|
|
567
|
+
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments);
|
|
301
568
|
collisionMesh.geometry.dispose();
|
|
302
569
|
collisionMesh.geometry = collisionGeo;
|
|
303
570
|
}
|
|
@@ -305,19 +572,79 @@ function updateWallGeometry(wallId, miterData) {
|
|
|
305
572
|
const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
|
|
306
573
|
mesh.rotation.y = -angle;
|
|
307
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
|
+
}
|
|
308
632
|
/**
|
|
309
633
|
* Generates extruded wall geometry with mitering and cutouts
|
|
310
634
|
*
|
|
311
635
|
* Key insight from demo: polygon is built in WORLD coordinates first,
|
|
312
636
|
* then we transform to wall-local for the 3D mesh.
|
|
313
637
|
*/
|
|
314
|
-
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
|
+
]) {
|
|
315
641
|
const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
|
|
316
642
|
const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
|
|
317
|
-
// Positive slab: shift the whole wall up (full height preserved)
|
|
318
|
-
// Negative slab: extend wall downward so top stays fixed at wallNode.height
|
|
319
643
|
const wallHeight = wallNode.height ?? DEFAULT_WALL_HEIGHT;
|
|
320
|
-
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;
|
|
321
648
|
const thickness = getWallThickness(wallNode);
|
|
322
649
|
// Wall direction and normal (exactly like demo)
|
|
323
650
|
const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y };
|
|
@@ -364,24 +691,118 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
364
691
|
});
|
|
365
692
|
// Rotate so extrusion direction (Z) becomes height direction (Y)
|
|
366
693
|
geometry.rotateX(-Math.PI / 2);
|
|
694
|
+
if (Math.abs(localBottom) > 1e-9)
|
|
695
|
+
geometry.translate(0, localBottom, 0);
|
|
367
696
|
geometry.computeVertexNormals();
|
|
368
697
|
assignWallMaterialGroups(geometry, wallNode, boundaryEdges);
|
|
369
|
-
|
|
370
|
-
//
|
|
371
|
-
|
|
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
|
+
];
|
|
372
787
|
if (cutoutBrushes.length === 0) {
|
|
373
|
-
|
|
788
|
+
const splitGeometry = splitGeometryAtHorizontalPlanes(geometry, getWallBandSplitPlanes(wallNode));
|
|
789
|
+
splitGeometry.computeVertexNormals();
|
|
790
|
+
assignWallMaterialGroups(splitGeometry, wallNode, boundaryEdges);
|
|
791
|
+
ensureRenderableGeometryAttributes(splitGeometry);
|
|
792
|
+
return splitGeometry;
|
|
374
793
|
}
|
|
375
794
|
// Create wall brush from geometry
|
|
376
795
|
// Pre-compute BVH with new API to avoid deprecation warning
|
|
796
|
+
ensureRenderableGeometryAttributes(geometry);
|
|
377
797
|
computeGeometryBoundsTree(geometry);
|
|
378
798
|
const wallBrush = new Brush(geometry);
|
|
379
799
|
wallBrush.updateMatrixWorld();
|
|
380
800
|
// Subtract each cutout from the wall
|
|
381
801
|
let resultBrush = wallBrush;
|
|
382
802
|
for (const cutoutBrush of cutoutBrushes) {
|
|
383
|
-
cutoutBrush
|
|
803
|
+
prepareBrushForCSG(cutoutBrush);
|
|
384
804
|
const newResult = csgEvaluator.evaluate(resultBrush, cutoutBrush, SUBTRACTION);
|
|
805
|
+
prepareBrushForCSG(newResult);
|
|
385
806
|
if (resultBrush !== wallBrush) {
|
|
386
807
|
csgGeometry(resultBrush).dispose();
|
|
387
808
|
}
|
|
@@ -393,14 +814,16 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
393
814
|
csgGeometry(brush).dispose();
|
|
394
815
|
}
|
|
395
816
|
const resultGeometry = csgGeometry(resultBrush);
|
|
396
|
-
resultGeometry
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
817
|
+
const splitResultGeometry = splitGeometryAtHorizontalPlanes(resultGeometry, getWallBandSplitPlanes(wallNode));
|
|
818
|
+
splitResultGeometry.computeVertexNormals();
|
|
819
|
+
assignWallMaterialGroups(splitResultGeometry, wallNode, boundaryEdges);
|
|
820
|
+
ensureRenderableGeometryAttributes(splitResultGeometry);
|
|
821
|
+
return splitResultGeometry;
|
|
400
822
|
}
|
|
401
823
|
/**
|
|
402
|
-
* Collects
|
|
403
|
-
*
|
|
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.
|
|
404
827
|
*/
|
|
405
828
|
function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
406
829
|
const brushes = [];
|
|
@@ -413,15 +836,8 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
|
413
836
|
for (const child of childrenNodes) {
|
|
414
837
|
if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door')
|
|
415
838
|
continue;
|
|
416
|
-
if (
|
|
417
|
-
(child
|
|
418
|
-
child.openingKind === 'door' &&
|
|
419
|
-
(child.openingShape === 'arch' || child.openingShape === 'rounded')) ||
|
|
420
|
-
(child.type === 'window' && child.openingKind === 'opening') ||
|
|
421
|
-
(child.type === 'window' &&
|
|
422
|
-
child.openingKind === 'window' &&
|
|
423
|
-
(child.openingShape === 'arch' || child.openingShape === 'rounded'))) {
|
|
424
|
-
brushes.push(createShapedOpeningCutoutBrush(child, wallThickness));
|
|
839
|
+
if (child.type === 'door' || child.type === 'window') {
|
|
840
|
+
brushes.push(createOpeningCutoutBrush(child, wallThickness));
|
|
425
841
|
continue;
|
|
426
842
|
}
|
|
427
843
|
const childMesh = sceneRegistry.nodes.get(child.id);
|
|
@@ -464,132 +880,16 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
|
464
880
|
}
|
|
465
881
|
return brushes;
|
|
466
882
|
}
|
|
467
|
-
function
|
|
468
|
-
const shape = createShapedOpeningCutoutShape(opening);
|
|
469
|
-
const depth = wallThickness * 2;
|
|
470
|
-
const bevelSize = opening.openingShape === 'rounded'
|
|
471
|
-
? 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))
|
|
472
|
-
: 0;
|
|
473
|
-
const geometry = new THREE.ExtrudeGeometry(shape, {
|
|
474
|
-
depth,
|
|
475
|
-
bevelEnabled: bevelSize > 0,
|
|
476
|
-
bevelSegments: bevelSize > 0 ? 8 : 0,
|
|
477
|
-
bevelSize,
|
|
478
|
-
bevelThickness: bevelSize,
|
|
479
|
-
curveSegments: 24,
|
|
480
|
-
});
|
|
481
|
-
geometry.translate(0, 0, -depth / 2);
|
|
482
|
-
computeGeometryBoundsTree(geometry);
|
|
483
|
-
return new Brush(geometry);
|
|
484
|
-
}
|
|
485
|
-
function createShapedOpeningCutoutShape(opening) {
|
|
883
|
+
function createOpeningCutoutBrush(opening, wallThickness) {
|
|
486
884
|
const halfWidth = opening.width / 2;
|
|
487
885
|
const bottom = opening.position[1] - opening.height / 2;
|
|
488
|
-
const
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
const springY = top - archHeight;
|
|
498
|
-
const segments = 32;
|
|
499
|
-
shape.moveTo(left, bottom);
|
|
500
|
-
shape.lineTo(right, bottom);
|
|
501
|
-
shape.lineTo(right, springY);
|
|
502
|
-
for (let index = 1; index <= segments; index += 1) {
|
|
503
|
-
const x = right + (left - right) * (index / segments);
|
|
504
|
-
const normalizedX = Math.min(Math.abs((x - centerX) / halfWidth), 1);
|
|
505
|
-
const y = springY + archHeight * Math.sqrt(Math.max(1 - normalizedX * normalizedX, 0));
|
|
506
|
-
shape.lineTo(x, y);
|
|
507
|
-
}
|
|
508
|
-
shape.lineTo(left, bottom);
|
|
509
|
-
shape.closePath();
|
|
510
|
-
return shape;
|
|
511
|
-
}
|
|
512
|
-
if (opening.openingShape === 'rounded') {
|
|
513
|
-
const radii = getRoundedOpeningRadii(opening, width, height);
|
|
514
|
-
applyRoundedOpeningShape(shape, left, right, bottom, top, radii);
|
|
515
|
-
return shape;
|
|
516
|
-
}
|
|
517
|
-
shape.moveTo(left, bottom);
|
|
518
|
-
shape.lineTo(right, bottom);
|
|
519
|
-
shape.lineTo(right, top);
|
|
520
|
-
shape.lineTo(left, top);
|
|
521
|
-
shape.closePath();
|
|
522
|
-
return shape;
|
|
523
|
-
}
|
|
524
|
-
function getRoundedOpeningRadii(opening, width, height) {
|
|
525
|
-
if (opening.type !== 'window') {
|
|
526
|
-
if (opening.openingRadiusMode === 'individual') {
|
|
527
|
-
const [topLeft = 0, topRight = 0] = opening.openingTopRadii ?? [0.15, 0.15];
|
|
528
|
-
return normalizeCornerRadii({
|
|
529
|
-
topLeft: Math.max(topLeft, 0),
|
|
530
|
-
topRight: Math.max(topRight, 0),
|
|
531
|
-
bottomRight: 0,
|
|
532
|
-
bottomLeft: 0,
|
|
533
|
-
}, width, height);
|
|
534
|
-
}
|
|
535
|
-
const maxRadius = Math.min(width / 2, height);
|
|
536
|
-
const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
|
|
537
|
-
return { topLeft: radius, topRight: radius, bottomRight: 0, bottomLeft: 0 };
|
|
538
|
-
}
|
|
539
|
-
if (opening.openingRadiusMode === 'individual') {
|
|
540
|
-
const [topLeft = 0, topRight = 0, bottomRight = 0, bottomLeft = 0] = opening.openingCornerRadii ?? [0.15, 0.15, 0.15, 0.15];
|
|
541
|
-
return normalizeCornerRadii({
|
|
542
|
-
topLeft: Math.max(topLeft, 0),
|
|
543
|
-
topRight: Math.max(topRight, 0),
|
|
544
|
-
bottomRight: Math.max(bottomRight, 0),
|
|
545
|
-
bottomLeft: Math.max(bottomLeft, 0),
|
|
546
|
-
}, width, height);
|
|
547
|
-
}
|
|
548
|
-
const maxRadius = Math.min(width / 2, height / 2);
|
|
549
|
-
const radius = Math.min(Math.max(opening.cornerRadius ?? 0.15, 0), maxRadius);
|
|
550
|
-
return { topLeft: radius, topRight: radius, bottomRight: radius, bottomLeft: radius };
|
|
551
|
-
}
|
|
552
|
-
function normalizeCornerRadii(radii, width, height) {
|
|
553
|
-
const next = { ...radii };
|
|
554
|
-
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));
|
|
555
|
-
if (maxScale < 1) {
|
|
556
|
-
next.topLeft *= maxScale;
|
|
557
|
-
next.topRight *= maxScale;
|
|
558
|
-
next.bottomRight *= maxScale;
|
|
559
|
-
next.bottomLeft *= maxScale;
|
|
560
|
-
}
|
|
561
|
-
return next;
|
|
562
|
-
}
|
|
563
|
-
function applyRoundedOpeningShape(shape, left, right, bottom, top, radii) {
|
|
564
|
-
const { topLeft, topRight, bottomRight, bottomLeft } = radii;
|
|
565
|
-
shape.moveTo(left + bottomLeft, bottom);
|
|
566
|
-
shape.lineTo(right - bottomRight, bottom);
|
|
567
|
-
if (bottomRight > 1e-6) {
|
|
568
|
-
shape.absarc(right - bottomRight, bottom + bottomRight, bottomRight, -Math.PI / 2, 0, false);
|
|
569
|
-
}
|
|
570
|
-
else {
|
|
571
|
-
shape.lineTo(right, bottom);
|
|
572
|
-
}
|
|
573
|
-
shape.lineTo(right, top - topRight);
|
|
574
|
-
if (topRight > 1e-6) {
|
|
575
|
-
shape.absarc(right - topRight, top - topRight, topRight, 0, Math.PI / 2, false);
|
|
576
|
-
}
|
|
577
|
-
else {
|
|
578
|
-
shape.lineTo(right, top);
|
|
579
|
-
}
|
|
580
|
-
shape.lineTo(left + topLeft, top);
|
|
581
|
-
if (topLeft > 1e-6) {
|
|
582
|
-
shape.absarc(left + topLeft, top - topLeft, topLeft, Math.PI / 2, Math.PI, false);
|
|
583
|
-
}
|
|
584
|
-
else {
|
|
585
|
-
shape.lineTo(left, top);
|
|
586
|
-
}
|
|
587
|
-
shape.lineTo(left, bottom + bottomLeft);
|
|
588
|
-
if (bottomLeft > 1e-6) {
|
|
589
|
-
shape.absarc(left + bottomLeft, bottom + bottomLeft, bottomLeft, Math.PI, Math.PI * 1.5, false);
|
|
590
|
-
}
|
|
591
|
-
else {
|
|
592
|
-
shape.lineTo(left, bottom);
|
|
593
|
-
}
|
|
594
|
-
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);
|
|
595
895
|
}
|