@pascal-app/viewer 1.0.3 → 1.0.5
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/capture/capture-runtime.js +1 -1
- package/dist/components/error-boundary.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.d.ts +1 -1
- package/dist/components/renderers/node-renderer.d.ts.map +1 -1
- package/dist/components/renderers/node-renderer.js +12 -4
- package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
- package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.d.ts +1 -0
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +22 -3
- package/dist/components/viewer/glb-interactive.d.ts +25 -1
- package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
- package/dist/components/viewer/glb-interactive.js +253 -86
- package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
- package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
- package/dist/components/viewer/glb-mechanisms.js +19 -0
- package/dist/components/viewer/glb-reference-nodes.js +1 -1
- package/dist/components/viewer/glb-replace-instances.js +1 -1
- package/dist/components/viewer/glb-scene.d.ts +45 -27
- package/dist/components/viewer/glb-scene.d.ts.map +1 -1
- package/dist/components/viewer/glb-scene.js +421 -508
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +13 -2
- package/dist/components/viewer/index.d.ts +24 -1
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +111 -37
- package/dist/components/viewer/lights.d.ts.map +1 -1
- package/dist/components/viewer/lights.js +113 -22
- package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
- package/dist/components/viewer/perf-monitor.js +6 -0
- package/dist/components/viewer/perf-panel.d.ts.map +1 -1
- package/dist/components/viewer/perf-panel.js +1 -1
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +104 -36
- package/dist/components/viewer/registered-systems.d.ts.map +1 -1
- package/dist/components/viewer/registered-systems.js +17 -3
- package/dist/components/viewer/render-error.d.ts +18 -0
- package/dist/components/viewer/render-error.d.ts.map +1 -0
- package/dist/components/viewer/render-error.js +29 -0
- package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
- package/dist/components/viewer/scene-bvh.js +1 -1
- package/dist/components/viewer/scene-environment.d.ts.map +1 -1
- package/dist/components/viewer/scene-environment.js +31 -11
- package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
- package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
- package/dist/components/viewer/scene-ground-replacement.js +15 -0
- package/dist/components/viewer/selection-manager.d.ts.map +1 -1
- package/dist/components/viewer/selection-manager.js +25 -7
- package/dist/components/viewer/underground-ambiance.d.ts +9 -0
- package/dist/components/viewer/underground-ambiance.d.ts.map +1 -0
- package/dist/components/viewer/underground-ambiance.js +105 -0
- package/dist/components/viewer/viewer-camera.d.ts +4 -0
- package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
- package/dist/components/viewer/viewer-camera.js +12 -2
- package/dist/components/viewer/viewer-presentations.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -0
- package/dist/hooks/use-node-events.d.ts.map +1 -1
- package/dist/index.d.ts +36 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -19
- package/dist/lib/asset-url.d.ts.map +1 -1
- package/dist/lib/asset-url.js +7 -1
- package/dist/lib/backdrop.d.ts +17 -0
- package/dist/lib/backdrop.d.ts.map +1 -1
- package/dist/lib/backdrop.js +18 -1
- package/dist/lib/construction-dust.d.ts +57 -0
- package/dist/lib/construction-dust.d.ts.map +1 -0
- package/dist/lib/construction-dust.js +187 -0
- package/dist/lib/csg-utils.js +1 -1
- package/dist/lib/display-state.d.ts +25 -0
- package/dist/lib/display-state.d.ts.map +1 -0
- package/dist/lib/display-state.js +1 -0
- package/dist/lib/foundation-materials.d.ts +26 -0
- package/dist/lib/foundation-materials.d.ts.map +1 -0
- package/dist/lib/foundation-materials.js +65 -0
- package/dist/lib/gltf-unlit.d.ts +10 -0
- package/dist/lib/gltf-unlit.d.ts.map +1 -0
- package/dist/lib/gltf-unlit.js +23 -0
- package/dist/lib/ground-window.d.ts +33 -0
- package/dist/lib/ground-window.d.ts.map +1 -0
- package/dist/lib/ground-window.js +114 -0
- package/dist/lib/hero-pose.d.ts +13 -3
- package/dist/lib/hero-pose.d.ts.map +1 -1
- package/dist/lib/hero-pose.js +72 -5
- package/dist/lib/ink-edges.d.ts +6 -3
- package/dist/lib/ink-edges.d.ts.map +1 -1
- package/dist/lib/ink-edges.js +17 -7
- package/dist/lib/key-light-override.d.ts +11 -0
- package/dist/lib/key-light-override.d.ts.map +1 -0
- package/dist/lib/key-light-override.js +14 -0
- package/dist/lib/ktx2-loader.d.ts +7 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +35 -0
- package/dist/lib/layer-pass.d.ts.map +1 -1
- package/dist/lib/layers.d.ts +11 -7
- package/dist/lib/layers.d.ts.map +1 -1
- package/dist/lib/layers.js +16 -6
- package/dist/lib/live-frame-hold.d.ts +14 -0
- package/dist/lib/live-frame-hold.d.ts.map +1 -0
- package/dist/lib/live-frame-hold.js +28 -0
- package/dist/lib/materials.d.ts +19 -7
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +65 -9
- package/dist/lib/merged-outline-node.d.ts +5 -0
- package/dist/lib/merged-outline-node.d.ts.map +1 -1
- package/dist/lib/merged-outline-node.js +80 -2
- package/dist/lib/node-top-surface-height.d.ts +6 -0
- package/dist/lib/node-top-surface-height.d.ts.map +1 -0
- package/dist/lib/node-top-surface-height.js +74 -0
- package/dist/lib/perf-probe-state.d.ts +39 -0
- package/dist/lib/perf-probe-state.d.ts.map +1 -0
- package/dist/lib/perf-probe-state.js +17 -0
- package/dist/lib/pointer-events.d.ts +26 -2
- package/dist/lib/pointer-events.d.ts.map +1 -1
- package/dist/lib/pointer-events.js +99 -12
- package/dist/lib/post-processing-resources.d.ts.map +1 -1
- package/dist/lib/procedural-emission.d.ts +7 -0
- package/dist/lib/procedural-emission.d.ts.map +1 -0
- package/dist/lib/procedural-emission.js +54 -0
- package/dist/lib/reveal-candidates.d.ts +80 -0
- package/dist/lib/reveal-candidates.d.ts.map +1 -0
- package/dist/lib/reveal-candidates.js +524 -0
- package/dist/lib/reveal-ghost.d.ts +9 -0
- package/dist/lib/reveal-ghost.d.ts.map +1 -0
- package/dist/lib/reveal-ghost.js +62 -0
- package/dist/lib/reveal-pending.d.ts +12 -0
- package/dist/lib/reveal-pending.d.ts.map +1 -0
- package/dist/lib/reveal-pending.js +12 -0
- package/dist/lib/reveal-pose.d.ts +70 -0
- package/dist/lib/reveal-pose.d.ts.map +1 -0
- package/dist/lib/reveal-pose.js +248 -0
- package/dist/lib/reveal-schedule.d.ts +208 -0
- package/dist/lib/reveal-schedule.d.ts.map +1 -0
- package/dist/lib/reveal-schedule.js +410 -0
- package/dist/lib/reveal-texture-scale.d.ts +5 -0
- package/dist/lib/reveal-texture-scale.d.ts.map +1 -0
- package/dist/lib/reveal-texture-scale.js +113 -0
- package/dist/lib/reveal-windows.d.ts +23 -0
- package/dist/lib/reveal-windows.d.ts.map +1 -0
- package/dist/lib/reveal-windows.js +31 -0
- package/dist/lib/reveal-wipe.d.ts +71 -0
- package/dist/lib/reveal-wipe.d.ts.map +1 -0
- package/dist/lib/reveal-wipe.js +105 -0
- package/dist/lib/scene-pass-mrt.d.ts +12 -0
- package/dist/lib/scene-pass-mrt.d.ts.map +1 -0
- package/dist/lib/scene-pass-mrt.js +23 -0
- package/dist/lib/scene-support-height.d.ts +4 -0
- package/dist/lib/scene-support-height.d.ts.map +1 -0
- package/dist/lib/scene-support-height.js +34 -0
- package/dist/lib/scene-themes.d.ts +30 -8
- package/dist/lib/scene-themes.d.ts.map +1 -1
- package/dist/lib/scene-themes.js +36 -0
- package/dist/lib/scene-visibility.d.ts +17 -1
- package/dist/lib/scene-visibility.d.ts.map +1 -1
- package/dist/lib/scene-visibility.js +53 -3
- package/dist/lib/scripted-opening.d.ts +11 -0
- package/dist/lib/scripted-opening.d.ts.map +1 -0
- package/dist/lib/scripted-opening.js +20 -0
- package/dist/lib/site-surfaces.d.ts +32 -0
- package/dist/lib/site-surfaces.d.ts.map +1 -0
- package/dist/lib/site-surfaces.js +70 -0
- package/dist/lib/snapshot-pipeline.d.ts +34 -3
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
- package/dist/lib/snapshot-pipeline.js +247 -117
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
- package/dist/lib/terrain-perimeter-fill.js +22 -14
- package/dist/lib/tsl-compat.d.ts +2 -2
- package/dist/lib/tsl-compat.d.ts.map +1 -1
- package/dist/lib/underground-ambiance.d.ts +168 -0
- package/dist/lib/underground-ambiance.d.ts.map +1 -0
- package/dist/lib/underground-ambiance.js +334 -0
- package/dist/lib/zone-geometry.d.ts +11 -0
- package/dist/lib/zone-geometry.d.ts.map +1 -0
- package/dist/lib/zone-geometry.js +46 -0
- package/dist/store/use-item-light-pool.d.ts +14 -10
- package/dist/store/use-item-light-pool.d.ts.map +1 -1
- package/dist/store/use-item-light-pool.js +52 -26
- package/dist/store/use-viewer.d.ts +194 -26
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +164 -21
- package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +88 -6
- package/dist/systems/construction-reveal/construction-dust.d.ts +16 -0
- package/dist/systems/construction-reveal/construction-dust.d.ts.map +1 -0
- package/dist/systems/construction-reveal/construction-dust.js +92 -0
- package/dist/systems/construction-reveal/construction-reveal.d.ts +276 -0
- package/dist/systems/construction-reveal/construction-reveal.d.ts.map +1 -0
- package/dist/systems/construction-reveal/construction-reveal.js +2383 -0
- package/dist/systems/construction-reveal/dust-material.d.ts +10 -0
- package/dist/systems/construction-reveal/dust-material.d.ts.map +1 -0
- package/dist/systems/construction-reveal/dust-material.js +17 -0
- package/dist/systems/construction-reveal/ghost-dress.d.ts +10 -0
- package/dist/systems/construction-reveal/ghost-dress.d.ts.map +1 -0
- package/dist/systems/construction-reveal/ghost-dress.js +24 -0
- package/dist/systems/door/door-system.d.ts.map +1 -1
- package/dist/systems/door/door-system.js +55 -17
- package/dist/systems/elevator/elevator-interaction-system.d.ts +7 -0
- package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
- package/dist/systems/elevator/elevator-interaction-system.js +18 -1
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +33 -5
- package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
- package/dist/systems/geometry/geometry-system.js +71 -11
- package/dist/systems/interactive/control-widget.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
- package/dist/systems/interactive/interactive-system.js +52 -17
- package/dist/systems/interactive/procedural-controls.d.ts +13 -0
- package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
- package/dist/systems/interactive/procedural-controls.js +17 -0
- package/dist/systems/interactive/scripted-clips.d.ts +19 -0
- package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
- package/dist/systems/interactive/scripted-clips.js +134 -0
- package/dist/systems/item/item-system.d.ts.map +1 -1
- package/dist/systems/item/item-system.js +9 -3
- package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
- package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
- package/dist/systems/item/object-cut-invalidation.js +68 -0
- 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 +48 -53
- package/dist/systems/level/level-hold.d.ts +4 -0
- package/dist/systems/level/level-hold.d.ts.map +1 -0
- package/dist/systems/level/level-hold.js +14 -0
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +19 -6
- package/dist/systems/level/level-utils.d.ts +19 -6
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +68 -9
- package/dist/systems/level/shadow-only-levels.d.ts +9 -0
- package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
- package/dist/systems/level/shadow-only-levels.js +7 -0
- package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts +103 -0
- package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts.map +1 -0
- package/dist/systems/roof/__fixtures__/roof-plane-cases.js +317 -0
- package/dist/systems/roof/roof-materials.d.ts +16 -2
- package/dist/systems/roof/roof-materials.d.ts.map +1 -1
- package/dist/systems/roof/roof-materials.js +64 -31
- package/dist/systems/roof/roof-system.d.ts +21 -4
- package/dist/systems/roof/roof-system.d.ts.map +1 -1
- package/dist/systems/roof/roof-system.js +374 -49
- package/dist/systems/slab/slab-system.d.ts +1 -1
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +49 -19
- package/dist/systems/stair/stair-system.d.ts.map +1 -1
- package/dist/systems/stair/stair-system.js +133 -354
- package/dist/systems/wall/wall-cutout-cache.d.ts +39 -3
- package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout-cache.js +141 -19
- package/dist/systems/wall/wall-cutout.d.ts +6 -0
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +42 -28
- package/dist/systems/wall/wall-finish-data.d.ts +22 -0
- package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
- package/dist/systems/wall/wall-finish-data.js +17 -0
- package/dist/systems/wall/wall-material-variant.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.d.ts +9 -4
- package/dist/systems/wall/wall-materials.d.ts.map +1 -1
- package/dist/systems/wall/wall-materials.js +135 -153
- package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.d.ts +33 -2
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +621 -159
- package/dist/systems/window/corner-frame.d.ts +10 -0
- package/dist/systems/window/corner-frame.d.ts.map +1 -0
- package/dist/systems/window/corner-frame.js +17 -0
- package/dist/systems/window/window-system.d.ts +1 -1
- package/dist/systems/window/window-system.d.ts.map +1 -1
- package/dist/systems/window/window-system.js +71 -27
- package/package.json +10 -10
- package/dist/systems/fence/fence-system.d.ts +0 -22
- package/dist/systems/fence/fence-system.d.ts.map +0 -1
- package/dist/systems/fence/fence-system.js +0 -357
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getEffectiveNode,
|
|
1
|
+
import { buildWallFinishLayout, containsPoint, cornerPair, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getOpeningWallCut, getWallBodyCenterOffset, getWallCurveFrameAt, getWallCurveLength, getWallFaceOffsets, getWallLevelZones, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, isCutterName, pointToKey, resolveCutterHost, resolveLevelId, resolveWallFaceBottom, resolveWallFinish, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, WALL_SURFACE_SLOT_DEFAULTS, wallFinishMaterialIndex, zoneHasWallFinish, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import { useEffect } from 'react';
|
|
4
4
|
import * as THREE from 'three';
|
|
5
5
|
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
6
|
-
import { ADDITION, Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
6
|
+
import { ADDITION, Brush, Evaluator, INTERSECTION, SUBTRACTION } from 'three-bvh-csg';
|
|
7
7
|
import { computeBoundsTree } from 'three-mesh-bvh';
|
|
8
8
|
import { ensureRenderableGeometryAttributes, prepareBrushForCSG } from '../../lib/csg-utils';
|
|
9
9
|
import { setGroupsSortedByMaterial } from '../../lib/geometry-groups';
|
|
10
10
|
import { timeSpan } from '../../lib/perf-tracks';
|
|
11
|
+
import { isRevealWaiting } from '../../lib/reveal-pending';
|
|
11
12
|
import { buildTerrainPerimeterFillGeometry } from '../../lib/terrain-perimeter-fill';
|
|
13
|
+
import { cornerFrameStale } from '../window/corner-frame';
|
|
12
14
|
import { clearLevelMiterCache, getCachedLevelMiters } from './level-miter-cache';
|
|
13
15
|
import { buildOpeningCutoutGeometry, getOpeningCutoutBottomPadding, } from './opening-cutout-geometry';
|
|
14
16
|
import { drainStats, endInitialBuild, initiallyBuiltWalls, isWallInitialBuildActive, pendingAdjacentByLevel, publishWallDrainStats, } from './wall-build-lifecycle';
|
|
17
|
+
import { getWallFaceBaseAt, } from './wall-finish-data';
|
|
15
18
|
import { sweepUnbuiltWalls, WALL_PLACEHOLDER_SWEEP_INTERVAL } from './wall-placeholder-sweep';
|
|
16
19
|
import { notifyWallRebuilt } from './wall-rebuild-notifications';
|
|
17
20
|
export { isWallInitialBuildActive } from './wall-build-lifecycle';
|
|
@@ -23,28 +26,10 @@ const CURVED_WALL_3D_ENDPOINT_INSET = 0.0015;
|
|
|
23
26
|
const WALL_FACE_NORMAL_Y_EPSILON = 0.6;
|
|
24
27
|
const WALL_FACE_EDGE_DISTANCE_EPSILON = 0.003;
|
|
25
28
|
const WALL_BAND_SPLIT_EPSILON = 1e-5;
|
|
26
|
-
const WALL_BAND_SLOT_MATERIAL_INDEX = {
|
|
27
|
-
interior: 1,
|
|
28
|
-
exterior: 2,
|
|
29
|
-
lowerInterior: 3,
|
|
30
|
-
middleInterior: 4,
|
|
31
|
-
upperInterior: 5,
|
|
32
|
-
topInterior: 6,
|
|
33
|
-
lowerExterior: 7,
|
|
34
|
-
middleExterior: 8,
|
|
35
|
-
upperExterior: 9,
|
|
36
|
-
topExterior: 10,
|
|
37
|
-
skirtingInterior: 0,
|
|
38
|
-
skirtingExterior: 0,
|
|
39
|
-
crownInterior: 0,
|
|
40
|
-
crownExterior: 0,
|
|
41
|
-
chairRailInterior: 0,
|
|
42
|
-
chairRailExterior: 0,
|
|
43
|
-
};
|
|
44
29
|
function computeGeometryBoundsTree(geometry) {
|
|
45
30
|
;
|
|
46
31
|
geometry.computeBoundsTree = computeBoundsTree;
|
|
47
|
-
geometry.computeBoundsTree({
|
|
32
|
+
geometry.computeBoundsTree({ targetLeafSize: 10 });
|
|
48
33
|
}
|
|
49
34
|
function csgGeometry(brush) {
|
|
50
35
|
return brush.geometry;
|
|
@@ -263,17 +248,17 @@ function distanceToWallBoundaryEdge(point, edge) {
|
|
|
263
248
|
const closestZ = edge.start.y + edgeDz * t;
|
|
264
249
|
return Math.hypot(point.x - closestX, point.y - closestZ);
|
|
265
250
|
}
|
|
266
|
-
function getWallFaceMaterialIndex(
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
return
|
|
273
|
-
const
|
|
274
|
-
return
|
|
251
|
+
function getWallFaceMaterialIndex(context, face, x, y) {
|
|
252
|
+
const { layout, foundation } = context;
|
|
253
|
+
// The underpinning's stemwall: everything under the rim depth is concrete.
|
|
254
|
+
if (foundation && y < foundation.top + WALL_BAND_SPLIT_EPSILON)
|
|
255
|
+
return foundation.index;
|
|
256
|
+
if (layout.plain)
|
|
257
|
+
return face === 'a' ? 1 : 2;
|
|
258
|
+
const hit = resolveWallFinish(layout, face, x, y - getWallFaceBaseAt(context, face, x));
|
|
259
|
+
return wallFinishMaterialIndex(layout, face, hit);
|
|
275
260
|
}
|
|
276
|
-
function assignWallMaterialGroups(geometry, wall, boundaryEdges,
|
|
261
|
+
function assignWallMaterialGroups(geometry, wall, boundaryEdges, finish) {
|
|
277
262
|
const position = geometry.getAttribute('position');
|
|
278
263
|
if (!position)
|
|
279
264
|
return;
|
|
@@ -336,7 +321,7 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges, effectiveWallHe
|
|
|
336
321
|
triangleMaterials[triangleIndex] = 0;
|
|
337
322
|
continue;
|
|
338
323
|
}
|
|
339
|
-
triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(
|
|
324
|
+
triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(finish, nearestTag === 'front' ? 'a' : 'b', centroid.x, centroid.y);
|
|
340
325
|
}
|
|
341
326
|
setGroupsSortedByMaterial(geometry, triangleMaterials);
|
|
342
327
|
}
|
|
@@ -347,22 +332,22 @@ function interpolateSplitVertex(a, b, t) {
|
|
|
347
332
|
z: a.z + (b.z - a.z) * t,
|
|
348
333
|
};
|
|
349
334
|
}
|
|
350
|
-
function
|
|
335
|
+
function clipPolygonByPlane(polygon, axis, plane, keepBelow) {
|
|
351
336
|
const out = [];
|
|
352
337
|
if (polygon.length === 0)
|
|
353
338
|
return out;
|
|
354
339
|
const isInside = (vertex) => keepBelow
|
|
355
|
-
? vertex
|
|
356
|
-
: vertex
|
|
340
|
+
? vertex[axis] <= plane + WALL_BAND_SPLIT_EPSILON
|
|
341
|
+
: vertex[axis] >= plane - WALL_BAND_SPLIT_EPSILON;
|
|
357
342
|
for (let index = 0; index < polygon.length; index += 1) {
|
|
358
343
|
const current = polygon[index];
|
|
359
344
|
const previous = polygon[(index + polygon.length - 1) % polygon.length];
|
|
360
345
|
const currentInside = isInside(current);
|
|
361
346
|
const previousInside = isInside(previous);
|
|
362
347
|
if (currentInside !== previousInside) {
|
|
363
|
-
const denom = current
|
|
348
|
+
const denom = current[axis] - previous[axis];
|
|
364
349
|
if (Math.abs(denom) > WALL_BAND_SPLIT_EPSILON) {
|
|
365
|
-
out.push(interpolateSplitVertex(previous, current, (
|
|
350
|
+
out.push(interpolateSplitVertex(previous, current, (plane - previous[axis]) / denom));
|
|
366
351
|
}
|
|
367
352
|
}
|
|
368
353
|
if (currentInside)
|
|
@@ -380,11 +365,22 @@ function triangulateSplitPolygon(polygon, positions) {
|
|
|
380
365
|
positions.push(first.x, first.y, first.z, b.x, b.y, b.z, c.x, c.y, c.z);
|
|
381
366
|
}
|
|
382
367
|
}
|
|
383
|
-
function
|
|
384
|
-
|
|
385
|
-
.filter((plane) => Number.isFinite(plane)
|
|
368
|
+
function normalizeSplitPlanes(planes) {
|
|
369
|
+
return Array.from(new Set(planes
|
|
370
|
+
.filter((plane) => Number.isFinite(plane))
|
|
386
371
|
.map((plane) => Math.round(plane / WALL_BAND_SPLIT_EPSILON) * WALL_BAND_SPLIT_EPSILON))).sort((a, b) => a - b);
|
|
387
|
-
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Cuts triangles at the vertical planes `x = const` (wall-local stations) and
|
|
375
|
+
* horizontal planes `y = const`, so each piece lies in one finish cell. With no
|
|
376
|
+
* planes the geometry is returned untouched — plain walls never pay for this.
|
|
377
|
+
*/
|
|
378
|
+
function splitGeometryAtPlanes(geometry, yPlanes, xPlanes = []) {
|
|
379
|
+
const cuts = [
|
|
380
|
+
...normalizeSplitPlanes(xPlanes).map((plane) => ({ axis: 'x', plane })),
|
|
381
|
+
...normalizeSplitPlanes(yPlanes).map((plane) => ({ axis: 'y', plane })),
|
|
382
|
+
];
|
|
383
|
+
if (cuts.length === 0)
|
|
388
384
|
return geometry;
|
|
389
385
|
const source = geometry.index ? geometry.toNonIndexed() : geometry;
|
|
390
386
|
const position = source.getAttribute('position');
|
|
@@ -399,17 +395,17 @@ function splitGeometryAtHorizontalPlanes(geometry, planes) {
|
|
|
399
395
|
{ x: position.getX(index + 2), y: position.getY(index + 2), z: position.getZ(index + 2) },
|
|
400
396
|
],
|
|
401
397
|
];
|
|
402
|
-
for (const plane of
|
|
398
|
+
for (const { axis, plane } of cuts) {
|
|
403
399
|
const next = [];
|
|
404
400
|
for (const polygon of polygons) {
|
|
405
|
-
const
|
|
406
|
-
const
|
|
407
|
-
if (plane <=
|
|
401
|
+
const min = Math.min(...polygon.map((vertex) => vertex[axis]));
|
|
402
|
+
const max = Math.max(...polygon.map((vertex) => vertex[axis]));
|
|
403
|
+
if (plane <= min + WALL_BAND_SPLIT_EPSILON || plane >= max - WALL_BAND_SPLIT_EPSILON) {
|
|
408
404
|
next.push(polygon);
|
|
409
405
|
continue;
|
|
410
406
|
}
|
|
411
|
-
const below =
|
|
412
|
-
const above =
|
|
407
|
+
const below = clipPolygonByPlane(polygon, axis, plane, true);
|
|
408
|
+
const above = clipPolygonByPlane(polygon, axis, plane, false);
|
|
413
409
|
if (below.length >= 3)
|
|
414
410
|
next.push(below);
|
|
415
411
|
if (above.length >= 3)
|
|
@@ -428,16 +424,26 @@ function splitGeometryAtHorizontalPlanes(geometry, planes) {
|
|
|
428
424
|
split.computeVertexNormals();
|
|
429
425
|
return split;
|
|
430
426
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
|
|
440
|
-
|
|
427
|
+
/** Split planes a finish layout needs, in wall-local coordinates. */
|
|
428
|
+
function getWallFinishSplitPlanes(finish) {
|
|
429
|
+
const { layout, faceBase } = finish;
|
|
430
|
+
if (layout.plain)
|
|
431
|
+
return { x: [], y: [] };
|
|
432
|
+
const x = [...layout.uSplits];
|
|
433
|
+
const bases = new Set([0]);
|
|
434
|
+
if (faceBase && layout.vSplits.length > 0) {
|
|
435
|
+
bases.clear();
|
|
436
|
+
for (const run of [...faceBase.a, ...faceBase.b]) {
|
|
437
|
+
bases.add(run.y);
|
|
438
|
+
// A face base that steps moves every height with it, so cut at the step too.
|
|
439
|
+
if (run.start > 0)
|
|
440
|
+
x.push(run.start);
|
|
441
|
+
if (run.end < layout.length)
|
|
442
|
+
x.push(run.end);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const y = [...bases].flatMap((base) => layout.vSplits.map((v) => base + v));
|
|
446
|
+
return { x, y };
|
|
441
447
|
}
|
|
442
448
|
// ============================================================================
|
|
443
449
|
// WALL SYSTEM
|
|
@@ -477,10 +483,16 @@ function wallRebuildExitReason(wallId, nodes, rebuiltThisFrame, elapsedMs, initi
|
|
|
477
483
|
let cutouts = 0;
|
|
478
484
|
for (const childId of getEffectiveWall(wall).children ?? []) {
|
|
479
485
|
const child = nodes[childId];
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
(
|
|
483
|
-
|
|
486
|
+
let hasCutter = false;
|
|
487
|
+
if (child?.type === 'item') {
|
|
488
|
+
sceneRegistry.nodes.get(childId)?.traverse((object) => {
|
|
489
|
+
const mesh = object;
|
|
490
|
+
const name = typeof mesh.userData.name === 'string' ? mesh.userData.name : mesh.name;
|
|
491
|
+
if (mesh.isMesh && isCutterName(name) && mesh.geometry?.getAttribute('position')?.count)
|
|
492
|
+
hasCutter = true;
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
if (child?.type === 'door' || child?.type === 'window' || hasCutter) {
|
|
484
496
|
cutouts++;
|
|
485
497
|
if (cutouts >= HEAVY_WALL_OPENINGS)
|
|
486
498
|
return 'heavy';
|
|
@@ -499,7 +511,13 @@ let placeholderSweepCountdown = WALL_PLACEHOLDER_SWEEP_INTERVAL;
|
|
|
499
511
|
export const WallSystem = ({ geometryAdapter } = {}) => {
|
|
500
512
|
useScene((state) => state.dirtyNodes);
|
|
501
513
|
useLiveNodeOverrides((s) => s.overrides);
|
|
502
|
-
useEffect(() => () =>
|
|
514
|
+
useEffect(() => () => {
|
|
515
|
+
clearLevelMiterCache();
|
|
516
|
+
zoneFinishRecords.clear();
|
|
517
|
+
zoneFinishNodes = null;
|
|
518
|
+
levelZones.clear();
|
|
519
|
+
levelZonesNodes = null;
|
|
520
|
+
}, []);
|
|
503
521
|
useFrame(() => runWallBuildFrame(geometryAdapter), 4);
|
|
504
522
|
return null;
|
|
505
523
|
};
|
|
@@ -518,7 +536,94 @@ export function runWallBuildFrame(geometryAdapter) {
|
|
|
518
536
|
publishWallDrainStats();
|
|
519
537
|
}
|
|
520
538
|
}
|
|
539
|
+
// Zones per level for the current scene snapshot: a floor's walls rebuild
|
|
540
|
+
// together, and each one would otherwise rescan the level's children.
|
|
541
|
+
const levelZones = new Map();
|
|
542
|
+
let levelZonesNodes = null;
|
|
543
|
+
function getCachedLevelZones(wall, nodes) {
|
|
544
|
+
if (nodes !== levelZonesNodes) {
|
|
545
|
+
levelZones.clear();
|
|
546
|
+
levelZonesNodes = nodes;
|
|
547
|
+
}
|
|
548
|
+
const key = wall.parentId ?? '';
|
|
549
|
+
let zones = levelZones.get(key);
|
|
550
|
+
if (!zones) {
|
|
551
|
+
zones = getWallLevelZones(wall, nodes);
|
|
552
|
+
levelZones.set(key, zones);
|
|
553
|
+
}
|
|
554
|
+
return zones;
|
|
555
|
+
}
|
|
556
|
+
const zoneFinishRecords = new Map();
|
|
557
|
+
let zoneFinishNodes = null;
|
|
558
|
+
function zoneFinishSignature(zone) {
|
|
559
|
+
const painted = zoneHasWallFinish(zone);
|
|
560
|
+
if (!painted && zone.floor?.elevation === undefined)
|
|
561
|
+
return null;
|
|
562
|
+
return JSON.stringify([
|
|
563
|
+
zone.parentId,
|
|
564
|
+
zone.floor?.elevation,
|
|
565
|
+
zone.wallMaterial,
|
|
566
|
+
zone.wallOverrides,
|
|
567
|
+
// A floor-only room reshape does not change the datum of every boundary wall.
|
|
568
|
+
// Endpoint edits already invalidate their walls and queue adjacent miters.
|
|
569
|
+
painted ? zone.polygon : undefined,
|
|
570
|
+
painted ? zone.holes : undefined,
|
|
571
|
+
zone.boundaryWallIds,
|
|
572
|
+
]);
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* A room's wall finish lives on its zone, so a zone edit must rebuild the walls
|
|
576
|
+
* bounding it — the old boundary and the new one. Zones without a wall finish
|
|
577
|
+
* never touch their walls, so ordinary room edits cost nothing here.
|
|
578
|
+
*/
|
|
579
|
+
export function markWallsForZoneFinishChanges() {
|
|
580
|
+
const state = useScene.getState();
|
|
581
|
+
const nodes = state.nodes;
|
|
582
|
+
if (nodes === zoneFinishNodes)
|
|
583
|
+
return;
|
|
584
|
+
zoneFinishNodes = nodes;
|
|
585
|
+
const walls = new Set();
|
|
586
|
+
const seen = new Set();
|
|
587
|
+
const collect = (zone) => {
|
|
588
|
+
for (const id of zone.boundaryWallIds ?? [])
|
|
589
|
+
walls.add(id);
|
|
590
|
+
for (const entry of zone.wallOverrides ?? [])
|
|
591
|
+
walls.add(entry.wallId);
|
|
592
|
+
};
|
|
593
|
+
for (const id in nodes) {
|
|
594
|
+
const node = nodes[id];
|
|
595
|
+
if (node?.type !== 'zone')
|
|
596
|
+
continue;
|
|
597
|
+
seen.add(id);
|
|
598
|
+
const previous = zoneFinishRecords.get(id);
|
|
599
|
+
if (previous?.zone === node)
|
|
600
|
+
continue;
|
|
601
|
+
const signature = zoneFinishSignature(node);
|
|
602
|
+
if (signature !== (previous?.signature ?? null)) {
|
|
603
|
+
if (previous)
|
|
604
|
+
collect(previous.zone);
|
|
605
|
+
collect(node);
|
|
606
|
+
}
|
|
607
|
+
zoneFinishRecords.set(id, { zone: node, signature });
|
|
608
|
+
}
|
|
609
|
+
for (const [id, record] of zoneFinishRecords) {
|
|
610
|
+
if (seen.has(id))
|
|
611
|
+
continue;
|
|
612
|
+
if (record.signature !== null)
|
|
613
|
+
collect(record.zone);
|
|
614
|
+
zoneFinishRecords.delete(id);
|
|
615
|
+
}
|
|
616
|
+
for (const id of walls) {
|
|
617
|
+
const wall = nodes[id];
|
|
618
|
+
if (wall?.type === 'wall') {
|
|
619
|
+
state.markDirty(wall.id);
|
|
620
|
+
for (const child of wall.children)
|
|
621
|
+
state.markDirty(child);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
521
625
|
function consumeWallBuildFrame(initialBuild, geometryAdapter) {
|
|
626
|
+
markWallsForZoneFinishChanges();
|
|
522
627
|
const clearDirty = useScene.getState().clearDirty;
|
|
523
628
|
// Self-heal: any registered wall still on its mount-time placeholder
|
|
524
629
|
// geometry with NO dirty mark gets re-marked, so a lost mark (system
|
|
@@ -539,7 +644,7 @@ function consumeWallBuildFrame(initialBuild, geometryAdapter) {
|
|
|
539
644
|
const dirtyNodes = useScene.getState().dirtyNodes;
|
|
540
645
|
const hasDirty = dirtyNodes.size > 0;
|
|
541
646
|
const hasPending = pendingAdjacentByLevel.size > 0;
|
|
542
|
-
if (!hasDirty
|
|
647
|
+
if (!(hasDirty || hasPending)) {
|
|
543
648
|
endInitialBuild();
|
|
544
649
|
return;
|
|
545
650
|
}
|
|
@@ -613,7 +718,7 @@ function consumeWallBuildFrame(initialBuild, geometryAdapter) {
|
|
|
613
718
|
else {
|
|
614
719
|
drainStats.reinvalidationBuilds++;
|
|
615
720
|
}
|
|
616
|
-
if (!initialBuild
|
|
721
|
+
if (!(initialBuild && firstBuild))
|
|
617
722
|
rebuiltWallIds.add(wallId);
|
|
618
723
|
rebuiltWallsThisFrame += 1;
|
|
619
724
|
drainStats.wallsConsumedThisFrame++;
|
|
@@ -641,7 +746,7 @@ function consumeWallBuildFrame(initialBuild, geometryAdapter) {
|
|
|
641
746
|
pendingAdjacentByLevel.set(levelId, pending);
|
|
642
747
|
}
|
|
643
748
|
for (const wallId of adjacentWallIds) {
|
|
644
|
-
if (!dirtyWallIds.has(wallId)
|
|
749
|
+
if (!(dirtyWallIds.has(wallId) || pending.has(wallId))) {
|
|
645
750
|
pending.add(wallId);
|
|
646
751
|
drainStats.pendingNeighbours++;
|
|
647
752
|
drainStats.neighbourEnqueues++;
|
|
@@ -777,24 +882,11 @@ function updateWallGeometry(wallId, miterData, geometryAdapter) {
|
|
|
777
882
|
return;
|
|
778
883
|
const levelId = resolveLevelId(node, nodes);
|
|
779
884
|
// Covering-clamped plane: a flush/thick slab on the level above shortens
|
|
780
|
-
// the plane-bound walls below it
|
|
885
|
+
// the plane-bound walls below it.
|
|
781
886
|
const planeTop = getWallPlaneTop(node, levelId, nodes);
|
|
782
|
-
const slabSupport = spatialGridManager.getSlabSupportForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness, node.supportSlabId, undefined, node.supportOffset);
|
|
887
|
+
const slabSupport = spatialGridManager.getSlabSupportForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness, node.supportSlabId, undefined, node.supportOffset, node.justification);
|
|
783
888
|
const slabElevation = slabSupport.elevation;
|
|
784
|
-
const
|
|
785
|
-
? (x, z) => terrainSupportLift(nodes, levelId, x, z)
|
|
786
|
-
: undefined;
|
|
787
|
-
const childrenIds = node.children || [];
|
|
788
|
-
const childrenNodes = childrenIds
|
|
789
|
-
.map((childId) => nodes[childId])
|
|
790
|
-
.filter((n) => n !== undefined)
|
|
791
|
-
.map((child) => {
|
|
792
|
-
if (child.type !== 'door' && child.type !== 'window')
|
|
793
|
-
return child;
|
|
794
|
-
const effective = getEffectiveNode(child);
|
|
795
|
-
const live = useLiveTransforms.getState().get(child.id);
|
|
796
|
-
return live?.position ? { ...effective, position: live.position } : effective;
|
|
797
|
-
});
|
|
889
|
+
const childrenNodes = wallGeometryChildren(node, nodes);
|
|
798
890
|
const prepared = geometryAdapter?.prepareChildren?.(node, childrenNodes, {
|
|
799
891
|
isLive: (id) => useLiveNodeOverrides.getState().get(id) !== undefined ||
|
|
800
892
|
useLiveTransforms.getState().get(id) !== undefined,
|
|
@@ -802,7 +894,20 @@ function updateWallGeometry(wallId, miterData, geometryAdapter) {
|
|
|
802
894
|
envelopeChildren: childrenNodes,
|
|
803
895
|
renderChildren: childrenNodes,
|
|
804
896
|
};
|
|
805
|
-
const
|
|
897
|
+
const fillMasks = node.fillToTerrain
|
|
898
|
+
? [
|
|
899
|
+
...Object.values(nodes).flatMap((slab) => slab.type === 'slab' && slab.parentId === levelId && slab.support !== 'open'
|
|
900
|
+
? [{ outer: slab.polygon, holes: slab.holes }]
|
|
901
|
+
: []),
|
|
902
|
+
...childrenNodes.flatMap((child) => child.type === 'door' || child.type === 'window'
|
|
903
|
+
? getOpeningWallCut(node, child, nodes, slabSupport).aperture
|
|
904
|
+
: []),
|
|
905
|
+
]
|
|
906
|
+
: [];
|
|
907
|
+
const terrainBottomAt = node.fillToTerrain
|
|
908
|
+
? (x, z) => containsPoint(fillMasks, [x, z]) ? slabElevation : terrainSupportLift(nodes, levelId, x, z)
|
|
909
|
+
: undefined;
|
|
910
|
+
const builtGeo = generateExtrudedWall(node, prepared.envelopeChildren, miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments, planeTop, terrainBottomAt, slabSupport.faceDatum, buildWallFinishLayout(node, getCachedLevelZones(node, nodes)));
|
|
806
911
|
const wallAngle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
|
|
807
912
|
// World transform the render mesh will apply (position + Y-rotation below).
|
|
808
913
|
// Reproduce it here so the UVs can be projected in WORLD space — see
|
|
@@ -819,13 +924,43 @@ function updateWallGeometry(wallId, miterData, geometryAdapter) {
|
|
|
819
924
|
// Update collision mesh
|
|
820
925
|
const collisionMesh = mesh.getObjectByName('collision-mesh');
|
|
821
926
|
if (collisionMesh) {
|
|
822
|
-
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments, planeTop, terrainBottomAt);
|
|
927
|
+
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments, planeTop, terrainBottomAt, slabSupport.faceDatum);
|
|
823
928
|
collisionMesh.geometry.dispose();
|
|
824
929
|
collisionMesh.geometry = collisionGeo;
|
|
825
930
|
}
|
|
826
931
|
mesh.position.set(node.start[0], slabElevation, node.start[1]);
|
|
827
932
|
const angle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
|
|
828
933
|
mesh.rotation.y = -angle;
|
|
934
|
+
const offsets = getWallFaceOffsets(node);
|
|
935
|
+
const frameKey = `${offsets.a}:${offsets.b}`;
|
|
936
|
+
// Child systems otherwise only see their own edits. Invalidate on a frame
|
|
937
|
+
// change once, avoiding a wall → opening → wall rebuild loop.
|
|
938
|
+
if (mesh.userData.wallFrameKey !== undefined && mesh.userData.wallFrameKey !== frameKey) {
|
|
939
|
+
for (const child of childrenNodes)
|
|
940
|
+
useScene.getState().markDirty(child.id);
|
|
941
|
+
}
|
|
942
|
+
mesh.userData.wallFrameKey = frameKey;
|
|
943
|
+
// A wall moved apart from the other wall of a corner window, or back, makes or breaks the pair:
|
|
944
|
+
// its windows rebuild their frames once (they read the pair only then).
|
|
945
|
+
for (const child of childrenNodes)
|
|
946
|
+
if (cornerFrameStale(child, nodes))
|
|
947
|
+
useScene.getState().markDirty(child.id);
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* The children a wall builds around, as they stand live: an opening still waiting for its
|
|
951
|
+
* construction reveal is left out, so the wall rises whole and is cut at the opening's turn.
|
|
952
|
+
*/
|
|
953
|
+
export function wallGeometryChildren(node, nodes) {
|
|
954
|
+
return (node.children ?? [])
|
|
955
|
+
.map((childId) => nodes[childId])
|
|
956
|
+
.filter((child) => child !== undefined && !isRevealWaiting(child.id))
|
|
957
|
+
.map((child) => {
|
|
958
|
+
if (child.type !== 'door' && child.type !== 'window')
|
|
959
|
+
return child;
|
|
960
|
+
const effective = getEffectiveNode(child);
|
|
961
|
+
const live = useLiveTransforms.getState().get(child.id);
|
|
962
|
+
return live?.position ? { ...effective, position: live.position } : effective;
|
|
963
|
+
});
|
|
829
964
|
}
|
|
830
965
|
const WALL_UV_Y_AXIS = new THREE.Vector3(0, 1, 0);
|
|
831
966
|
const WALL_UV_UNIT_SCALE = new THREE.Vector3(1, 1, 1);
|
|
@@ -842,8 +977,10 @@ const WALL_UV_UNIT_SCALE = new THREE.Vector3(1, 1, 1);
|
|
|
842
977
|
*/
|
|
843
978
|
function applyWorldPlanarWallUVs(geometry, worldMatrix) {
|
|
844
979
|
const target = geometry.index ? geometry.toNonIndexed() : geometry;
|
|
845
|
-
if (target !== geometry)
|
|
980
|
+
if (target !== geometry) {
|
|
981
|
+
target.userData = geometry.userData;
|
|
846
982
|
geometry.dispose();
|
|
983
|
+
}
|
|
847
984
|
const position = target.getAttribute('position');
|
|
848
985
|
if (!position || position.count === 0)
|
|
849
986
|
return target;
|
|
@@ -919,34 +1056,183 @@ function buildWallTerrainFillGeometry(perimeter, worldToLocal, wallBaseElevation
|
|
|
919
1056
|
});
|
|
920
1057
|
return buildTerrainPerimeterFillGeometry(localPoints, bottomY, 0);
|
|
921
1058
|
}
|
|
922
|
-
|
|
923
|
-
|
|
1059
|
+
/**
|
|
1060
|
+
* The underpinning under a wall (`WallNode.underpinning`): a skirt of the
|
|
1061
|
+
* wall's own faces `rim` deep below the base, then the stemwall skirt from
|
|
1062
|
+
* there down `stem` more — or to the terrain wherever that is lower when
|
|
1063
|
+
* the wall also fills to terrain. Two fills, split at the rim depth, so the
|
|
1064
|
+
* material groups can paint the stem concrete and the rim in the finish.
|
|
1065
|
+
*/
|
|
1066
|
+
function buildWallUnderpinningGeometry(perimeter, worldToLocal, wallBaseElevation, wall, terrainBottomAt, curved = false) {
|
|
1067
|
+
const underpinning = wall.underpinning;
|
|
1068
|
+
if (!underpinning)
|
|
1069
|
+
return [];
|
|
1070
|
+
const worldPoints = densifyClosedWallPerimeter(perimeter);
|
|
1071
|
+
if (worldPoints.length < 3)
|
|
1072
|
+
return [];
|
|
1073
|
+
const localPoints = worldPoints.map(worldToLocal);
|
|
1074
|
+
const rimBottom = -underpinning.rim;
|
|
1075
|
+
const fills = [];
|
|
1076
|
+
if (underpinning.rim > 1e-6) {
|
|
1077
|
+
const rim = buildTerrainPerimeterFillGeometry(localPoints, worldPoints.map(() => rimBottom), 0);
|
|
1078
|
+
if (rim)
|
|
1079
|
+
fills.push(rim);
|
|
1080
|
+
}
|
|
1081
|
+
const stemBottom = worldPoints.map((point) => {
|
|
1082
|
+
let y = rimBottom - underpinning.stem;
|
|
1083
|
+
if (terrainBottomAt) {
|
|
1084
|
+
const terrainElevation = terrainBottomAt(point.x, point.y);
|
|
1085
|
+
if (terrainElevation != null)
|
|
1086
|
+
y = Math.min(y, terrainElevation - wallBaseElevation);
|
|
1087
|
+
}
|
|
1088
|
+
return y;
|
|
1089
|
+
});
|
|
1090
|
+
if (stemBottom.some((y) => y < rimBottom - 1e-6)) {
|
|
1091
|
+
const stem = buildTerrainPerimeterFillGeometry(localPoints, stemBottom, rimBottom);
|
|
1092
|
+
if (stem) {
|
|
1093
|
+
fills.push(underpinning.openings?.length && !curved
|
|
1094
|
+
? cutUnderpinningOpenings(stem, localPoints, rimBottom, underpinning.openings)
|
|
1095
|
+
: stem);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return fills;
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* The stem skirt with its openings (`WallUnderpinning.openings`) cut through
|
|
1102
|
+
* it: the skirt is closed with a cap at its top so it is a solid, then each
|
|
1103
|
+
* opening's box is subtracted across the full thickness. The cap lands on
|
|
1104
|
+
* the rim's (or the body's) own bottom face, inside the wall.
|
|
1105
|
+
*/
|
|
1106
|
+
function cutUnderpinningOpenings(stem, localPoints, topY, openings) {
|
|
1107
|
+
const stemPositions = stem.getAttribute('position');
|
|
1108
|
+
const positions = Array.from(stemPositions.array);
|
|
1109
|
+
const faces = THREE.ShapeUtils.triangulateShape(localPoints.map((point) => new THREE.Vector2(point.x, point.z)), []);
|
|
1110
|
+
for (const [ia, ib, ic] of faces) {
|
|
1111
|
+
const a = localPoints[ia];
|
|
1112
|
+
const b = localPoints[ib];
|
|
1113
|
+
const c = localPoints[ic];
|
|
1114
|
+
const cross = (b.x - a.x) * (c.z - a.z) - (b.z - a.z) * (c.x - a.x);
|
|
1115
|
+
const [second, third] = cross >= 0 ? [c, b] : [b, c];
|
|
1116
|
+
positions.push(a.x, topY, a.z, second.x, topY, second.z, third.x, topY, third.z);
|
|
1117
|
+
}
|
|
1118
|
+
stem.dispose();
|
|
1119
|
+
const solid = new THREE.BufferGeometry();
|
|
1120
|
+
solid.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
1121
|
+
solid.computeVertexNormals();
|
|
1122
|
+
ensureRenderableGeometryAttributes(solid);
|
|
1123
|
+
computeGeometryBoundsTree(solid);
|
|
1124
|
+
let minZ = Number.POSITIVE_INFINITY;
|
|
1125
|
+
let maxZ = Number.NEGATIVE_INFINITY;
|
|
1126
|
+
for (const point of localPoints) {
|
|
1127
|
+
minZ = Math.min(minZ, point.z);
|
|
1128
|
+
maxZ = Math.max(maxZ, point.z);
|
|
1129
|
+
}
|
|
1130
|
+
const depth = maxZ - minZ + 0.2;
|
|
1131
|
+
const centerZ = (minZ + maxZ) / 2;
|
|
1132
|
+
let result = new Brush(solid);
|
|
1133
|
+
result.updateMatrixWorld();
|
|
1134
|
+
for (const opening of openings) {
|
|
1135
|
+
const height = opening.bottom - opening.top;
|
|
1136
|
+
if (height <= 1e-6)
|
|
1137
|
+
continue;
|
|
1138
|
+
const box = new THREE.BoxGeometry(opening.width, height, depth);
|
|
1139
|
+
box.translate(opening.u, -(opening.top + opening.bottom) / 2, centerZ);
|
|
1140
|
+
const cutter = new Brush(box);
|
|
1141
|
+
prepareBrushForCSG(cutter);
|
|
1142
|
+
const next = csgEvaluator.evaluate(result, cutter, SUBTRACTION);
|
|
1143
|
+
csgGeometry(cutter).dispose();
|
|
1144
|
+
csgGeometry(result).dispose();
|
|
1145
|
+
result = next;
|
|
1146
|
+
}
|
|
1147
|
+
// the other skirts and the body merge non-indexed
|
|
1148
|
+
const cut = csgGeometry(result);
|
|
1149
|
+
const geometry = cut.index ? cut.toNonIndexed() : cut;
|
|
1150
|
+
if (geometry !== cut)
|
|
1151
|
+
cut.dispose();
|
|
1152
|
+
geometry.clearGroups();
|
|
1153
|
+
return geometry;
|
|
1154
|
+
}
|
|
1155
|
+
function mergeWallTerrainFill(body, fills, wall, boundaryEdges, finish) {
|
|
1156
|
+
const present = fills.filter((fill) => fill !== null);
|
|
1157
|
+
if (present.length === 0)
|
|
924
1158
|
return body;
|
|
925
1159
|
const bodyGeometry = body.index ? body.toNonIndexed() : body;
|
|
926
1160
|
if (bodyGeometry !== body)
|
|
927
1161
|
body.dispose();
|
|
1162
|
+
// The fill is part of each face below the base: it splits at the same finish bounds.
|
|
1163
|
+
const planes = getWallFinishSplitPlanes(finish);
|
|
1164
|
+
const splitFills = present.map((fill) => splitGeometryAtPlanes(fill, planes.y, planes.x));
|
|
928
1165
|
ensureRenderableGeometryAttributes(bodyGeometry);
|
|
929
|
-
|
|
930
|
-
|
|
1166
|
+
for (const fill of splitFills)
|
|
1167
|
+
ensureRenderableGeometryAttributes(fill);
|
|
1168
|
+
const merged = mergeGeometries([bodyGeometry, ...splitFills], false);
|
|
931
1169
|
if (!merged) {
|
|
932
|
-
fill
|
|
1170
|
+
for (const fill of splitFills)
|
|
1171
|
+
fill.dispose();
|
|
933
1172
|
return bodyGeometry;
|
|
934
1173
|
}
|
|
935
1174
|
bodyGeometry.dispose();
|
|
936
|
-
fill
|
|
1175
|
+
for (const fill of splitFills)
|
|
1176
|
+
fill.dispose();
|
|
937
1177
|
merged.computeVertexNormals();
|
|
938
|
-
assignWallMaterialGroups(merged, wall, boundaryEdges,
|
|
1178
|
+
assignWallMaterialGroups(merged, wall, boundaryEdges, finish);
|
|
939
1179
|
ensureRenderableGeometryAttributes(merged);
|
|
940
1180
|
return merged;
|
|
941
1181
|
}
|
|
1182
|
+
/** Wall-local x of a support-segment parameter (curve parameter on arcs). */
|
|
1183
|
+
function wallStationX(wall, t, length) {
|
|
1184
|
+
if (!isCurvedWall(wall))
|
|
1185
|
+
return t * length;
|
|
1186
|
+
const point = getWallCurveFrameAt(wall, t).point;
|
|
1187
|
+
const dx = wall.end[0] - wall.start[0];
|
|
1188
|
+
const dz = wall.end[1] - wall.start[1];
|
|
1189
|
+
return ((point.x - wall.start[0]) * dx + (point.y - wall.start[1]) * dz) / length;
|
|
1190
|
+
}
|
|
1191
|
+
function buildWallFinishContext(wall, layout, faceBase, slabElevation) {
|
|
1192
|
+
const refs = [...layout.refs];
|
|
1193
|
+
let foundation = null;
|
|
1194
|
+
if (wall.underpinning) {
|
|
1195
|
+
const ref = wall.slots?.foundation ?? WALL_SURFACE_SLOT_DEFAULTS.foundation;
|
|
1196
|
+
if (!refs.includes(ref))
|
|
1197
|
+
refs.push(ref);
|
|
1198
|
+
foundation = { index: 3 + refs.indexOf(ref), top: -wall.underpinning.rim };
|
|
1199
|
+
}
|
|
1200
|
+
if (!faceBase || layout.plain)
|
|
1201
|
+
return { layout, faceBase: null, refs, foundation };
|
|
1202
|
+
const runs = (segments) => segments
|
|
1203
|
+
.map((segment) => ({
|
|
1204
|
+
start: wallStationX(wall, segment.start, layout.length),
|
|
1205
|
+
end: wallStationX(wall, segment.end, layout.length),
|
|
1206
|
+
y: segment.elevation - slabElevation,
|
|
1207
|
+
}))
|
|
1208
|
+
.sort((left, right) => left.start - right.start);
|
|
1209
|
+
return { layout, faceBase: { a: runs(faceBase.a), b: runs(faceBase.b) }, refs, foundation };
|
|
1210
|
+
}
|
|
942
1211
|
export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0, baseElevation = slabElevation, baseSegments = [
|
|
943
1212
|
{ start: 0, end: 1, elevation: baseElevation },
|
|
944
|
-
], storeyHeight = DEFAULT_LEVEL_HEIGHT, terrainBottomAt) {
|
|
1213
|
+
], storeyHeight = DEFAULT_LEVEL_HEIGHT, terrainBottomAt, faceBase, finishLayout = buildWallFinishLayout(wallNode, []), sceneNodes = useScene.getState().nodes) {
|
|
945
1214
|
const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
|
|
946
1215
|
const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
|
|
947
|
-
const
|
|
948
|
-
const
|
|
949
|
-
const
|
|
1216
|
+
const levelId = resolveLevelId(wallNode, sceneNodes);
|
|
1217
|
+
const level = sceneNodes[levelId];
|
|
1218
|
+
const authoredStoreyHeight = level?.type === 'level' ? (level.height ?? null) : null;
|
|
1219
|
+
const topElevation = resolveWallTop(wallNode, storeyHeight, slabElevation, authoredStoreyHeight);
|
|
1220
|
+
const faceDatum = faceBase;
|
|
1221
|
+
if (faceBase)
|
|
1222
|
+
faceBase = {
|
|
1223
|
+
a: resolveWallFaceBottom(faceBase.a, baseSegments, slabElevation),
|
|
1224
|
+
b: resolveWallFaceBottom(faceBase.b, baseSegments, slabElevation),
|
|
1225
|
+
};
|
|
1226
|
+
const faceSegments = faceBase ? [...faceBase.a, ...faceBase.b] : [];
|
|
1227
|
+
const sameBase = faceBase &&
|
|
1228
|
+
[faceBase.a, faceBase.b].every((segments) => JSON.stringify(segments) === JSON.stringify(baseSegments));
|
|
1229
|
+
const useFaceBase = faceBase && !sameBase;
|
|
1230
|
+
const effectiveBaseElevation = useFaceBase
|
|
1231
|
+
? Math.min(...faceSegments.flatMap((segment) => [
|
|
1232
|
+
segment.elevation,
|
|
1233
|
+
segment.endElevation ?? segment.elevation,
|
|
1234
|
+
]), slabElevation)
|
|
1235
|
+
: Math.min(baseElevation, slabElevation);
|
|
950
1236
|
const localBottom = effectiveBaseElevation - slabElevation;
|
|
951
1237
|
const height = topElevation - effectiveBaseElevation;
|
|
952
1238
|
// A slab at or above the storey plane leaves a plane-bound wall with no
|
|
@@ -984,9 +1270,15 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
984
1270
|
// Convert polygon to local coordinates
|
|
985
1271
|
const localPoints = polyPoints.map(worldToLocal);
|
|
986
1272
|
const boundaryEdges = buildTaggedWallBoundaryEdges(wallNode, localPoints, miterData);
|
|
987
|
-
const
|
|
1273
|
+
const finish = buildWallFinishContext(wallNode, finishLayout, faceDatum, slabElevation);
|
|
1274
|
+
const finishPlanes = getWallFinishSplitPlanes(finish);
|
|
1275
|
+
// An underpinned wall's stem skirt reaches the terrain itself; the plain
|
|
1276
|
+
// terrain fill is for a wall with no underpinning.
|
|
1277
|
+
const underpinningFills = buildWallUnderpinningGeometry(polyPoints, worldToLocal, slabElevation, wallNode, terrainBottomAt, isCurvedWall(wallNode));
|
|
1278
|
+
const terrainFill = terrainBottomAt && !wallNode.underpinning
|
|
988
1279
|
? buildWallTerrainFillGeometry(polyPoints, worldToLocal, slabElevation, terrainBottomAt)
|
|
989
1280
|
: null;
|
|
1281
|
+
const belowBaseFills = [terrainFill, ...underpinningFills];
|
|
990
1282
|
// Build THREE.js shape
|
|
991
1283
|
// Shape uses (x, y) where we map: shape.x = local.x, shape.y = -local.z
|
|
992
1284
|
// The negation is needed because after rotateX(-PI/2), shape.y becomes -geometry.z
|
|
@@ -1006,20 +1298,31 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
1006
1298
|
if (Math.abs(localBottom) > 1e-9)
|
|
1007
1299
|
geometry.translate(0, localBottom, 0);
|
|
1008
1300
|
geometry.computeVertexNormals();
|
|
1009
|
-
assignWallMaterialGroups(geometry, wallNode, boundaryEdges,
|
|
1301
|
+
assignWallMaterialGroups(geometry, wallNode, boundaryEdges, finish);
|
|
1010
1302
|
ensureRenderableGeometryAttributes(geometry);
|
|
1011
1303
|
// Start with the lowest required wall prism, then remove the volume below
|
|
1012
1304
|
// each higher-supported run. This keeps the existing mitered footprint and
|
|
1013
1305
|
// opening CSG while giving one wall a stepped longitudinal base.
|
|
1014
1306
|
const baseProfileCutouts = [];
|
|
1015
|
-
|
|
1016
|
-
|
|
1307
|
+
const profiles = useFaceBase && faceBase
|
|
1308
|
+
? [
|
|
1309
|
+
...faceBase.a.map((segment) => ({ segment, face: 'a' })),
|
|
1310
|
+
...faceBase.b.map((segment) => ({ segment, face: 'b' })),
|
|
1311
|
+
]
|
|
1312
|
+
: baseSegments.map((segment) => ({ segment, face: undefined }));
|
|
1313
|
+
for (const { segment, face } of profiles) {
|
|
1314
|
+
const segmentElevation = face
|
|
1315
|
+
? Math.min(Math.max(segment.elevation, segment.endElevation ?? segment.elevation), topElevation)
|
|
1316
|
+
: Math.min(segment.elevation, slabElevation);
|
|
1017
1317
|
const cutHeight = segmentElevation - effectiveBaseElevation;
|
|
1018
1318
|
if (cutHeight <= 1e-6 || segment.end - segment.start <= 1e-7)
|
|
1019
1319
|
continue;
|
|
1020
1320
|
const segmentStart = THREE.MathUtils.clamp(segment.start, 0, 1);
|
|
1021
1321
|
const segmentEnd = THREE.MathUtils.clamp(segment.end, 0, 1);
|
|
1022
1322
|
const cutHalfWidth = Math.max(thickness * 2, 0.2);
|
|
1323
|
+
const centerOffset = getWallBodyCenterOffset(wallNode);
|
|
1324
|
+
const leftOffset = face === 'b' ? centerOffset : cutHalfWidth;
|
|
1325
|
+
const rightOffset = face === 'a' ? centerOffset : -cutHalfWidth;
|
|
1023
1326
|
const worldCutoutPoints = [];
|
|
1024
1327
|
if (isCurvedWall(wallNode)) {
|
|
1025
1328
|
const sampleCount = Math.max(2, Math.ceil((segmentEnd - segmentStart) * 24));
|
|
@@ -1038,12 +1341,12 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
1038
1341
|
y: frame.point.y + frame.tangent.y * endpointExtension,
|
|
1039
1342
|
};
|
|
1040
1343
|
left.push({
|
|
1041
|
-
x: center.x + frame.normal.x *
|
|
1042
|
-
y: center.y + frame.normal.y *
|
|
1344
|
+
x: center.x + frame.normal.x * leftOffset,
|
|
1345
|
+
y: center.y + frame.normal.y * leftOffset,
|
|
1043
1346
|
});
|
|
1044
1347
|
right.push({
|
|
1045
|
-
x: center.x
|
|
1046
|
-
y: center.y
|
|
1348
|
+
x: center.x + frame.normal.x * rightOffset,
|
|
1349
|
+
y: center.y + frame.normal.y * rightOffset,
|
|
1047
1350
|
});
|
|
1048
1351
|
}
|
|
1049
1352
|
worldCutoutPoints.push(...left, ...right.reverse());
|
|
@@ -1064,11 +1367,11 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
1064
1367
|
y: wallStart.y + tangentY * (segmentEnd * L + endExtension),
|
|
1065
1368
|
};
|
|
1066
1369
|
worldCutoutPoints.push({
|
|
1067
|
-
x: startPoint.x + normalX *
|
|
1068
|
-
y: startPoint.y + normalY *
|
|
1069
|
-
}, { x: endPoint.x + normalX *
|
|
1070
|
-
x: startPoint.x
|
|
1071
|
-
y: startPoint.y
|
|
1370
|
+
x: startPoint.x + normalX * leftOffset,
|
|
1371
|
+
y: startPoint.y + normalY * leftOffset,
|
|
1372
|
+
}, { x: endPoint.x + normalX * leftOffset, y: endPoint.y + normalY * leftOffset }, { x: endPoint.x + normalX * rightOffset, y: endPoint.y + normalY * rightOffset }, {
|
|
1373
|
+
x: startPoint.x + normalX * rightOffset,
|
|
1374
|
+
y: startPoint.y + normalY * rightOffset,
|
|
1072
1375
|
});
|
|
1073
1376
|
}
|
|
1074
1377
|
const localCutoutPoints = worldCutoutPoints.map(worldToLocal);
|
|
@@ -1080,7 +1383,11 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
1080
1383
|
cutoutShape.lineTo(localCutoutPoints[index].x, -localCutoutPoints[index].z);
|
|
1081
1384
|
}
|
|
1082
1385
|
cutoutShape.closePath();
|
|
1083
|
-
const
|
|
1386
|
+
const slopeExtension = segment.endElevation === undefined
|
|
1387
|
+
? 0
|
|
1388
|
+
: (Math.abs(segment.endElevation - segment.elevation) * cutHalfWidth) /
|
|
1389
|
+
((segmentEnd - segmentStart) * L);
|
|
1390
|
+
const cutoutBottom = localBottom - 0.01 - slopeExtension;
|
|
1084
1391
|
const cutoutTop = segmentElevation - slabElevation;
|
|
1085
1392
|
const cutoutGeometry = new THREE.ExtrudeGeometry(cutoutShape, {
|
|
1086
1393
|
depth: cutoutTop - cutoutBottom,
|
|
@@ -1088,19 +1395,31 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
1088
1395
|
});
|
|
1089
1396
|
cutoutGeometry.rotateX(-Math.PI / 2);
|
|
1090
1397
|
cutoutGeometry.translate(0, cutoutBottom, 0);
|
|
1398
|
+
if (segment.endElevation !== undefined) {
|
|
1399
|
+
const from = wallStationX(wallNode, segmentStart, L);
|
|
1400
|
+
const to = wallStationX(wallNode, segmentEnd, L);
|
|
1401
|
+
const positions = cutoutGeometry.getAttribute('position');
|
|
1402
|
+
for (let i = 0; i < positions.count; i++) {
|
|
1403
|
+
if (Math.abs(positions.getY(i) - cutoutTop) > 1e-5)
|
|
1404
|
+
continue;
|
|
1405
|
+
const t = (positions.getX(i) - from) / (to - from);
|
|
1406
|
+
positions.setY(i, Math.min(topElevation, segment.elevation + (segment.endElevation - segment.elevation) * t) - slabElevation);
|
|
1407
|
+
}
|
|
1408
|
+
cutoutGeometry.computeVertexNormals();
|
|
1409
|
+
}
|
|
1091
1410
|
computeGeometryBoundsTree(cutoutGeometry);
|
|
1092
1411
|
baseProfileCutouts.push(new Brush(cutoutGeometry));
|
|
1093
1412
|
}
|
|
1094
1413
|
const cutoutBrushes = [
|
|
1095
1414
|
...baseProfileCutouts,
|
|
1096
|
-
...collectCutoutBrushes(wallNode, childrenNodes, thickness),
|
|
1415
|
+
...collectCutoutBrushes(wallNode, childrenNodes, thickness, faceDatum, slabElevation, sceneNodes, baseSegments),
|
|
1097
1416
|
];
|
|
1098
1417
|
if (cutoutBrushes.length === 0) {
|
|
1099
|
-
const splitGeometry =
|
|
1418
|
+
const splitGeometry = splitGeometryAtPlanes(geometry, finishPlanes.y, finishPlanes.x);
|
|
1100
1419
|
splitGeometry.computeVertexNormals();
|
|
1101
|
-
assignWallMaterialGroups(splitGeometry, wallNode, boundaryEdges,
|
|
1420
|
+
assignWallMaterialGroups(splitGeometry, wallNode, boundaryEdges, finish);
|
|
1102
1421
|
ensureRenderableGeometryAttributes(splitGeometry);
|
|
1103
|
-
return mergeWallTerrainFill(splitGeometry,
|
|
1422
|
+
return withWallFinishData(mergeWallTerrainFill(splitGeometry, belowBaseFills, wallNode, boundaryEdges, finish), finish);
|
|
1104
1423
|
}
|
|
1105
1424
|
// Create wall brush from geometry
|
|
1106
1425
|
// Pre-compute BVH with new API to avoid deprecation warning
|
|
@@ -1143,18 +1462,59 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
1143
1462
|
csgGeometry(brush).dispose();
|
|
1144
1463
|
}
|
|
1145
1464
|
const resultGeometry = csgGeometry(resultBrush);
|
|
1146
|
-
const splitResultGeometry =
|
|
1465
|
+
const splitResultGeometry = splitGeometryAtPlanes(resultGeometry, finishPlanes.y, finishPlanes.x);
|
|
1147
1466
|
splitResultGeometry.computeVertexNormals();
|
|
1148
|
-
assignWallMaterialGroups(splitResultGeometry, wallNode, boundaryEdges,
|
|
1467
|
+
assignWallMaterialGroups(splitResultGeometry, wallNode, boundaryEdges, finish);
|
|
1149
1468
|
ensureRenderableGeometryAttributes(splitResultGeometry);
|
|
1150
|
-
return mergeWallTerrainFill(splitResultGeometry,
|
|
1469
|
+
return withWallFinishData(mergeWallTerrainFill(splitResultGeometry, belowBaseFills, wallNode, boundaryEdges, finish), finish);
|
|
1470
|
+
}
|
|
1471
|
+
function withWallFinishData(geometry, finish) {
|
|
1472
|
+
if (!finish.layout.plain || finish.foundation) {
|
|
1473
|
+
geometry.userData.wallFinish = {
|
|
1474
|
+
refs: finish.refs,
|
|
1475
|
+
faceBase: finish.faceBase,
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
return geometry;
|
|
1151
1479
|
}
|
|
1152
1480
|
/**
|
|
1153
1481
|
* Collects opening and item cutout brushes for CSG subtraction. Door/window
|
|
1154
1482
|
* cuts come directly from node geometry; item proxy meshes are transformed
|
|
1155
1483
|
* into wall-local boxes that pass through the wall.
|
|
1156
1484
|
*/
|
|
1157
|
-
|
|
1485
|
+
/**
|
|
1486
|
+
* A CSG brush from an authored object's `cutout` mesh: the mesh in wall-local
|
|
1487
|
+
* space, its depth stretched to twice the wall's thickness about the wall's
|
|
1488
|
+
* body centre so it overshoots both faces. Null when the mesh has no depth to
|
|
1489
|
+
* stretch (a flat cutter), so the caller falls back to its bounding box.
|
|
1490
|
+
*/
|
|
1491
|
+
function authoredCutoutBrush(cutoutMesh, wallMatrixInverse, wallThickness, wallNode) {
|
|
1492
|
+
const geometry = cutoutMesh.geometry.clone();
|
|
1493
|
+
geometry.applyMatrix4(new THREE.Matrix4().multiplyMatrices(wallMatrixInverse, cutoutMesh.matrixWorld));
|
|
1494
|
+
geometry.computeBoundingBox();
|
|
1495
|
+
const box = geometry.boundingBox;
|
|
1496
|
+
const depth = box.max.z - box.min.z;
|
|
1497
|
+
if (!(depth > 1e-4)) {
|
|
1498
|
+
geometry.dispose();
|
|
1499
|
+
return null;
|
|
1500
|
+
}
|
|
1501
|
+
const centre = getWallBodyCenterOffset(wallNode);
|
|
1502
|
+
const meshCentre = (box.min.z + box.max.z) / 2;
|
|
1503
|
+
const scale = (wallThickness * 2) / depth;
|
|
1504
|
+
const positions = geometry.getAttribute('position');
|
|
1505
|
+
for (let i = 0; i < positions.count; i++) {
|
|
1506
|
+
positions.setZ(i, centre + (positions.getZ(i) - meshCentre) * scale);
|
|
1507
|
+
}
|
|
1508
|
+
positions.needsUpdate = true;
|
|
1509
|
+
// The evaluator needs the attributes every brush carries.
|
|
1510
|
+
if (!geometry.getAttribute('uv')) {
|
|
1511
|
+
geometry.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(positions.count * 2), 2));
|
|
1512
|
+
}
|
|
1513
|
+
geometry.computeVertexNormals();
|
|
1514
|
+
computeGeometryBoundsTree(geometry);
|
|
1515
|
+
return new Brush(geometry);
|
|
1516
|
+
}
|
|
1517
|
+
function collectCutoutBrushes(wallNode, childrenNodes, wallThickness, faceBase, elevation = 0, sceneNodes = useScene.getState().nodes, baseSegments = [{ start: 0, end: 1, elevation }]) {
|
|
1158
1518
|
const brushes = [];
|
|
1159
1519
|
const wallMesh = sceneRegistry.nodes.get(wallNode.id);
|
|
1160
1520
|
if (!wallMesh)
|
|
@@ -1162,63 +1522,165 @@ function collectCutoutBrushes(wallNode, childrenNodes, wallThickness) {
|
|
|
1162
1522
|
// Get wall's world matrix inverse to transform cutouts to wall-local space
|
|
1163
1523
|
wallMesh.updateMatrixWorld();
|
|
1164
1524
|
const wallMatrixInverse = wallMesh.matrixWorld.clone().invert();
|
|
1525
|
+
// A wall child can only bind to its parent, so the wall alone resolves every cutter.
|
|
1526
|
+
const hostNodes = { [wallNode.id]: wallNode };
|
|
1165
1527
|
for (const child of childrenNodes) {
|
|
1166
1528
|
if (child.type !== 'item' && child.type !== 'window' && child.type !== 'door')
|
|
1167
1529
|
continue;
|
|
1168
|
-
|
|
1169
|
-
|
|
1530
|
+
const cutterMeshes = [];
|
|
1531
|
+
sceneRegistry.nodes.get(child.id)?.traverse((object) => {
|
|
1532
|
+
const name = typeof object.userData.name === 'string' ? object.userData.name : object.name;
|
|
1533
|
+
if (object.isMesh &&
|
|
1534
|
+
resolveCutterHost(child, name, hostNodes)?.id === wallNode.id) {
|
|
1535
|
+
cutterMeshes.push(object);
|
|
1536
|
+
}
|
|
1537
|
+
});
|
|
1538
|
+
// A window or door built from a script cuts its `cutout` mesh like an
|
|
1539
|
+
// authored item (below); without one, or until it loads, its outline.
|
|
1540
|
+
const scriptedCutout = child.type !== 'item' && child.source && cutterMeshes.length > 0;
|
|
1541
|
+
if ((child.type === 'door' || child.type === 'window') && !scriptedCutout) {
|
|
1542
|
+
const nodes = {
|
|
1543
|
+
...sceneNodes,
|
|
1544
|
+
[wallNode.id]: wallNode,
|
|
1545
|
+
...Object.fromEntries(childrenNodes.map((node) => [node.id, node])),
|
|
1546
|
+
};
|
|
1547
|
+
const datum = faceBase ?? {
|
|
1548
|
+
a: [{ start: 0, end: 1, elevation }],
|
|
1549
|
+
b: [{ start: 0, end: 1, elevation }],
|
|
1550
|
+
};
|
|
1551
|
+
const support = {
|
|
1552
|
+
elevation,
|
|
1553
|
+
electedSlabId: null,
|
|
1554
|
+
baseElevation: elevation,
|
|
1555
|
+
baseSegments: [...baseSegments],
|
|
1556
|
+
faceDatum: { a: [...datum.a], b: [...datum.b] },
|
|
1557
|
+
faceBottom: {
|
|
1558
|
+
a: resolveWallFaceBottom(datum.a, baseSegments, elevation),
|
|
1559
|
+
b: resolveWallFaceBottom(datum.b, baseSegments, elevation),
|
|
1560
|
+
},
|
|
1561
|
+
};
|
|
1562
|
+
const cut = getOpeningWallCut(wallNode, child, nodes, support);
|
|
1563
|
+
// The cutter must overshoot the faces it cuts through; `cut.aperture` is
|
|
1564
|
+
// clipped to the wall's own footprint (the doorway floor), so extruding it
|
|
1565
|
+
// would leave coplanar faces and CSG would drop the cut.
|
|
1566
|
+
if (!isCurvedWall(wallNode)) {
|
|
1567
|
+
brushes.push(createOpeningCutoutBrush({
|
|
1568
|
+
...child,
|
|
1569
|
+
position: [
|
|
1570
|
+
child.position[0],
|
|
1571
|
+
child.position[1] + cut.datum - elevation,
|
|
1572
|
+
child.position[2],
|
|
1573
|
+
],
|
|
1574
|
+
}, wallThickness, getWallBodyCenterOffset(wallNode), cut.bottom - elevation, child.type === 'window' ? cornerPair(sceneNodes, child)?.end : undefined));
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
if (cut.band.length < 3)
|
|
1578
|
+
continue;
|
|
1579
|
+
const angle = Math.atan2(wallNode.end[1] - wallNode.start[1], wallNode.end[0] - wallNode.start[0]);
|
|
1580
|
+
const points = cut.band.map(([x, z]) => {
|
|
1581
|
+
const dx = x - wallNode.start[0], dz = z - wallNode.start[1];
|
|
1582
|
+
return new THREE.Vector2(dx * Math.cos(angle) + dz * Math.sin(angle), dx * Math.sin(angle) - dz * Math.cos(angle));
|
|
1583
|
+
});
|
|
1584
|
+
const padding = getOpeningCutoutBottomPadding(child, cut.bottom - elevation);
|
|
1585
|
+
const geometry = new THREE.ExtrudeGeometry(new THREE.Shape(points), {
|
|
1586
|
+
depth: cut.top - cut.bottom + padding,
|
|
1587
|
+
bevelEnabled: false,
|
|
1588
|
+
});
|
|
1589
|
+
geometry.rotateX(-Math.PI / 2);
|
|
1590
|
+
geometry.translate(0, cut.bottom - elevation - padding, 0);
|
|
1591
|
+
computeGeometryBoundsTree(geometry);
|
|
1592
|
+
const bandBrush = new Brush(geometry);
|
|
1593
|
+
if (child.openingShape !== 'arch' && child.openingShape !== 'rounded') {
|
|
1594
|
+
brushes.push(bandBrush);
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
const shaped = createOpeningCutoutBrush({
|
|
1598
|
+
...child,
|
|
1599
|
+
position: [
|
|
1600
|
+
child.position[0],
|
|
1601
|
+
child.position[1] + cut.datum - elevation,
|
|
1602
|
+
child.position[2],
|
|
1603
|
+
],
|
|
1604
|
+
}, wallThickness, getWallBodyCenterOffset(wallNode), cut.bottom - elevation);
|
|
1605
|
+
const frame = getWallCurveFrameAt(wallNode, child.position[0] / getWallCurveLength(wallNode));
|
|
1606
|
+
const offset = getWallBodyCenterOffset(wallNode);
|
|
1607
|
+
const dx = frame.point.x + frame.normal.x * offset - wallNode.start[0], dz = frame.point.y + frame.normal.y * offset - wallNode.start[1];
|
|
1608
|
+
csgGeometry(shaped).translate(-child.position[0], 0, -offset);
|
|
1609
|
+
csgGeometry(shaped).rotateY(angle - Math.atan2(frame.tangent.y, frame.tangent.x));
|
|
1610
|
+
csgGeometry(shaped).translate(dx * Math.cos(angle) + dz * Math.sin(angle), 0, -dx * Math.sin(angle) + dz * Math.cos(angle));
|
|
1611
|
+
prepareBrushForCSG(bandBrush);
|
|
1612
|
+
prepareBrushForCSG(shaped);
|
|
1613
|
+
bandBrush.updateMatrixWorld();
|
|
1614
|
+
shaped.updateMatrixWorld();
|
|
1615
|
+
brushes.push(csgEvaluator.evaluate(bandBrush, shaped, INTERSECTION));
|
|
1616
|
+
geometry.dispose();
|
|
1617
|
+
csgGeometry(shaped).dispose();
|
|
1170
1618
|
continue;
|
|
1171
1619
|
}
|
|
1172
|
-
const
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1620
|
+
for (const cutoutMesh of cutterMeshes) {
|
|
1621
|
+
// Get the cutout's bounding box in world space
|
|
1622
|
+
cutoutMesh.updateMatrixWorld();
|
|
1623
|
+
const positions = cutoutMesh.geometry?.attributes?.position;
|
|
1624
|
+
if (!positions)
|
|
1625
|
+
continue;
|
|
1626
|
+
// Calculate bounds in wall-local space
|
|
1627
|
+
const v3 = new THREE.Vector3();
|
|
1628
|
+
let minX = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY;
|
|
1629
|
+
let minY = Number.POSITIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY;
|
|
1630
|
+
for (let i = 0; i < positions.count; i++) {
|
|
1631
|
+
v3.fromBufferAttribute(positions, i);
|
|
1632
|
+
v3.applyMatrix4(cutoutMesh.matrixWorld);
|
|
1633
|
+
v3.applyMatrix4(wallMatrixInverse);
|
|
1634
|
+
minX = Math.min(minX, v3.x);
|
|
1635
|
+
maxX = Math.max(maxX, v3.x);
|
|
1636
|
+
minY = Math.min(minY, v3.y);
|
|
1637
|
+
maxY = Math.max(maxY, v3.y);
|
|
1638
|
+
}
|
|
1639
|
+
if (!Number.isFinite(minX))
|
|
1640
|
+
continue;
|
|
1641
|
+
// An authored object's cutout keeps its shape (an arch, a circle): its own
|
|
1642
|
+
// geometry in wall space, stretched across the wall so it cuts both faces.
|
|
1643
|
+
if (child.source) {
|
|
1644
|
+
const shaped = authoredCutoutBrush(cutoutMesh, wallMatrixInverse, wallThickness, wallNode);
|
|
1645
|
+
if (shaped) {
|
|
1646
|
+
brushes.push(shaped);
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
// Create a box geometry that extends through the wall thickness
|
|
1651
|
+
const width = maxX - minX;
|
|
1652
|
+
const height = maxY - minY;
|
|
1653
|
+
const depth = wallThickness * 2; // Extend beyond wall to ensure clean cut
|
|
1654
|
+
const boxGeo = new THREE.BoxGeometry(width, height, depth);
|
|
1655
|
+
// Position box at the center of the cutout
|
|
1656
|
+
boxGeo.translate(minX + width / 2, minY + height / 2, getWallBodyCenterOffset(wallNode));
|
|
1657
|
+
// Pre-compute BVH with new API to avoid deprecation warning
|
|
1658
|
+
computeGeometryBoundsTree(boxGeo);
|
|
1659
|
+
const brush = new Brush(boxGeo);
|
|
1660
|
+
brushes.push(brush);
|
|
1195
1661
|
}
|
|
1196
|
-
if (!Number.isFinite(minX))
|
|
1197
|
-
continue;
|
|
1198
|
-
// Create a box geometry that extends through the wall thickness
|
|
1199
|
-
const width = maxX - minX;
|
|
1200
|
-
const height = maxY - minY;
|
|
1201
|
-
const depth = wallThickness * 2; // Extend beyond wall to ensure clean cut
|
|
1202
|
-
const boxGeo = new THREE.BoxGeometry(width, height, depth);
|
|
1203
|
-
// Position box at the center of the cutout
|
|
1204
|
-
boxGeo.translate(minX + width / 2, minY + height / 2, 0);
|
|
1205
|
-
// Pre-compute BVH with new API to avoid deprecation warning
|
|
1206
|
-
computeGeometryBoundsTree(boxGeo);
|
|
1207
|
-
const brush = new Brush(boxGeo);
|
|
1208
|
-
brushes.push(brush);
|
|
1209
1662
|
}
|
|
1210
1663
|
return brushes;
|
|
1211
1664
|
}
|
|
1212
|
-
|
|
1665
|
+
/**
|
|
1666
|
+
* How far a corner window's cut runs past its wall's end: past this wall's half of the corner at any
|
|
1667
|
+
* angle (the miter stops at five thicknesses), so the two sides open the corner between them. The
|
|
1668
|
+
* cut only takes this wall's own geometry, so running past it touches nothing else.
|
|
1669
|
+
*/
|
|
1670
|
+
const CORNER_CUT_REACH = 6;
|
|
1671
|
+
function createOpeningCutoutBrush(opening, wallThickness, centerOffset, cutBottom, cornerEnd) {
|
|
1213
1672
|
const halfWidth = opening.width / 2;
|
|
1214
|
-
const bottom = opening.position[1] - opening.height / 2;
|
|
1673
|
+
const bottom = cutBottom ?? opening.position[1] - opening.height / 2;
|
|
1215
1674
|
const bottomPadding = getOpeningCutoutBottomPadding(opening, bottom);
|
|
1675
|
+
const reach = CORNER_CUT_REACH * wallThickness;
|
|
1216
1676
|
const geometry = buildOpeningCutoutGeometry(opening, {
|
|
1217
|
-
left: opening.position[0] - halfWidth,
|
|
1218
|
-
right: opening.position[0] + halfWidth,
|
|
1677
|
+
left: opening.position[0] - halfWidth - (cornerEnd === 'start' ? reach : 0),
|
|
1678
|
+
right: opening.position[0] + halfWidth + (cornerEnd === 'end' ? reach : 0),
|
|
1219
1679
|
bottom: bottom - bottomPadding,
|
|
1220
1680
|
top: opening.position[1] + opening.height / 2,
|
|
1221
1681
|
}, wallThickness * 2, wallThickness);
|
|
1682
|
+
if (centerOffset !== 0)
|
|
1683
|
+
geometry.translate(0, 0, centerOffset);
|
|
1222
1684
|
computeGeometryBoundsTree(geometry);
|
|
1223
1685
|
return new Brush(geometry);
|
|
1224
1686
|
}
|