@pascal-app/viewer 0.9.2 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -5
- package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
- package/dist/components/viewer/frame-limiter.js +25 -5
- package/dist/components/viewer/glb-scene.js +2 -2
- package/dist/components/viewer/glb-walkthrough-controller.d.ts +10 -0
- package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
- package/dist/components/viewer/glb-walkthrough-controller.js +96 -8
- package/dist/components/viewer/index.d.ts.map +1 -1
- package/dist/components/viewer/index.js +9 -0
- package/dist/components/viewer/post-processing.d.ts.map +1 -1
- package/dist/components/viewer/post-processing.js +1 -0
- package/dist/components/viewer/selection-manager.js +5 -2
- package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
- package/dist/hooks/use-gltf-ktx2.js +2 -5
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -8
- package/dist/lib/hero-pose.d.ts +47 -0
- package/dist/lib/hero-pose.d.ts.map +1 -0
- package/dist/lib/hero-pose.js +227 -0
- package/dist/lib/ktx2-loader.d.ts +3 -0
- package/dist/lib/ktx2-loader.d.ts.map +1 -1
- package/dist/lib/ktx2-loader.js +6 -0
- package/dist/lib/materials.d.ts +2 -0
- package/dist/lib/materials.d.ts.map +1 -1
- package/dist/lib/materials.js +56 -9
- package/dist/lib/materials.test.d.ts +2 -0
- package/dist/lib/materials.test.d.ts.map +1 -0
- package/dist/lib/materials.test.js +25 -0
- package/dist/lib/merged-outline-node.test.d.ts +2 -0
- package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
- package/dist/lib/merged-outline-node.test.js +22 -0
- package/dist/lib/snapshot-pipeline.d.ts +42 -0
- package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
- package/dist/lib/snapshot-pipeline.js +254 -0
- package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
- package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
- package/dist/lib/terrain-perimeter-fill.js +75 -0
- package/dist/lib/texture-node-guard.d.ts +13 -0
- package/dist/lib/texture-node-guard.d.ts.map +1 -0
- package/dist/lib/texture-node-guard.js +39 -0
- package/dist/lib/texture-reference.d.ts +31 -0
- package/dist/lib/texture-reference.d.ts.map +1 -0
- package/dist/lib/texture-reference.js +139 -0
- package/dist/lib/texture-reference.test.d.ts +2 -0
- package/dist/lib/texture-reference.test.d.ts.map +1 -0
- package/dist/lib/texture-reference.test.js +86 -0
- package/dist/store/use-viewer.d.ts +13 -0
- package/dist/store/use-viewer.d.ts.map +1 -1
- package/dist/store/use-viewer.js +16 -1
- package/dist/store/use-viewer.test.js +19 -0
- package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
- package/dist/systems/ceiling/ceiling-system.js +8 -4
- package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
- package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
- package/dist/systems/level/level-system.d.ts.map +1 -1
- package/dist/systems/level/level-system.js +8 -7
- package/dist/systems/level/level-utils.d.ts.map +1 -1
- package/dist/systems/level/level-utils.js +7 -6
- package/dist/systems/slab/slab-system.d.ts +5 -4
- package/dist/systems/slab/slab-system.d.ts.map +1 -1
- package/dist/systems/slab/slab-system.js +24 -94
- package/dist/systems/slab/slab-system.test.js +51 -12
- package/dist/systems/stair/stair-system.js +1 -1
- package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
- package/dist/systems/wall/wall-cutout.js +5 -2
- package/dist/systems/wall/wall-support-extension.test.js +69 -1
- package/dist/systems/wall/wall-system.d.ts +3 -7
- package/dist/systems/wall/wall-system.d.ts.map +1 -1
- package/dist/systems/wall/wall-system.js +87 -22
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
- package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
- package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
- package/package.json +2 -2
- package/dist/components/viewer/walkthrough-controls.d.ts +0 -3
- package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
- package/dist/components/viewer/walkthrough-controls.js +0 -151
|
@@ -1,95 +1,21 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { useFrame } from '@react-three/fiber';
|
|
3
|
-
import { useEffect } from 'react';
|
|
1
|
+
import { getRenderableSlabPolygon, pointInPolygon2D, polygonsIntersect, } from '@pascal-app/core';
|
|
4
2
|
import * as THREE from 'three';
|
|
5
3
|
import { subtractPolygonsFromPolygon } from '../../lib/polygon-union';
|
|
6
4
|
import { mergeSurfaceHolePolygons } from '../surface-hole-geometry';
|
|
7
|
-
function ensureUv2Attribute(geometry) {
|
|
8
|
-
const uv = geometry.getAttribute('uv');
|
|
9
|
-
if (!uv)
|
|
10
|
-
return;
|
|
11
|
-
geometry.setAttribute('uv2', new THREE.Float32BufferAttribute(Array.from(uv.array), 2));
|
|
12
|
-
}
|
|
13
5
|
// ============================================================================
|
|
14
|
-
// SLAB
|
|
6
|
+
// SLAB GEOMETRY GENERATORS
|
|
15
7
|
// ============================================================================
|
|
16
|
-
export const SlabSystem = () => {
|
|
17
|
-
const dirtyNodes = useScene((state) => state.dirtyNodes);
|
|
18
|
-
const clearDirty = useScene((state) => state.clearDirty);
|
|
19
|
-
const markDirty = useScene((state) => state.markDirty);
|
|
20
|
-
useEffect(() => {
|
|
21
|
-
const nodes = useScene.getState().nodes;
|
|
22
|
-
for (const node of Object.values(nodes)) {
|
|
23
|
-
if (node.type === 'slab') {
|
|
24
|
-
markDirty(node.id);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}, [markDirty]);
|
|
28
|
-
useFrame(() => {
|
|
29
|
-
if (dirtyNodes.size === 0)
|
|
30
|
-
return;
|
|
31
|
-
const nodes = useScene.getState().nodes;
|
|
32
|
-
const contextByLevel = new Map();
|
|
33
|
-
// Process dirty slabs
|
|
34
|
-
dirtyNodes.forEach((id) => {
|
|
35
|
-
const node = nodes[id];
|
|
36
|
-
if (node?.type !== 'slab')
|
|
37
|
-
return;
|
|
38
|
-
const mesh = sceneRegistry.nodes.get(id);
|
|
39
|
-
if (mesh) {
|
|
40
|
-
const slab = node;
|
|
41
|
-
const levelContext = contextByLevel.get(slab.parentId) ?? buildLevelSlabContext(slab.parentId, nodes);
|
|
42
|
-
contextByLevel.set(slab.parentId, levelContext);
|
|
43
|
-
updateSlabGeometry(getEffectiveNode(slab), excludeSlabFromContext(levelContext, slab.id), mesh);
|
|
44
|
-
clearDirty(id);
|
|
45
|
-
}
|
|
46
|
-
// If mesh not found, keep it dirty for next frame
|
|
47
|
-
});
|
|
48
|
-
}, 1);
|
|
49
|
-
return null;
|
|
50
|
-
};
|
|
51
|
-
function buildLevelSlabContext(levelId, nodes) {
|
|
52
|
-
const walls = [];
|
|
53
|
-
const siblingSlabs = [];
|
|
54
|
-
for (const node of Object.values(nodes)) {
|
|
55
|
-
if (node.parentId !== levelId)
|
|
56
|
-
continue;
|
|
57
|
-
if (node.type === 'wall')
|
|
58
|
-
walls.push(node);
|
|
59
|
-
else if (node.type === 'slab')
|
|
60
|
-
siblingSlabs.push(node);
|
|
61
|
-
}
|
|
62
|
-
return { walls, siblingSlabs };
|
|
63
|
-
}
|
|
64
|
-
function excludeSlabFromContext(context, slabId) {
|
|
65
|
-
return {
|
|
66
|
-
walls: context.walls,
|
|
67
|
-
siblingSlabs: context.siblingSlabs.filter((slab) => slab.id !== slabId),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Updates the geometry for a single slab
|
|
72
|
-
*/
|
|
73
|
-
function updateSlabGeometry(node, context, mesh) {
|
|
74
|
-
const newGeo = generateSlabGeometry(node, context);
|
|
75
|
-
ensureUv2Attribute(newGeo);
|
|
76
|
-
mesh.geometry.dispose();
|
|
77
|
-
mesh.geometry = newGeo;
|
|
78
|
-
// For negative elevation, shift the mesh down so the top face sits at Y=elevation
|
|
79
|
-
// rather than at Y=0. Positive elevation stays at Y=0 (slab sits at floor level).
|
|
80
|
-
const elevation = node.elevation ?? 0.05;
|
|
81
|
-
mesh.position.y = elevation < 0 ? elevation : 0;
|
|
82
|
-
}
|
|
83
8
|
/**
|
|
84
|
-
* Generates
|
|
85
|
-
*
|
|
86
|
-
*
|
|
9
|
+
* Generates slab geometry from polygon. `context` carries the slab's level
|
|
10
|
+
* neighbourhood (walls + sibling slabs) driving the per-edge render offsets —
|
|
11
|
+
* see `getRenderableSlabPolygon`. Branches on the explicit `recessed` intent:
|
|
12
|
+
* a recessed slab is an open shell (pool), everything else a solid occupying
|
|
13
|
+
* `[elevation − thickness, elevation]`.
|
|
87
14
|
*/
|
|
88
15
|
export function generateSlabGeometry(slabNode, context) {
|
|
89
|
-
|
|
90
|
-
return elevation < 0
|
|
16
|
+
return slabNode.recessed
|
|
91
17
|
? generatePoolGeometry(slabNode, context)
|
|
92
|
-
:
|
|
18
|
+
: generateSolidSlabGeometry(slabNode, context);
|
|
93
19
|
}
|
|
94
20
|
// Earcut normalizes cap triangulation regardless of input winding, but the side
|
|
95
21
|
// walls below assume a CCW contour (the unflipped quad's right-hand normal faces
|
|
@@ -131,7 +57,8 @@ function buildSlabRegions(contour, holes) {
|
|
|
131
57
|
}));
|
|
132
58
|
}
|
|
133
59
|
/**
|
|
134
|
-
*
|
|
60
|
+
* Solid slab occupying `[elevation − thickness, elevation]`: the top cap is
|
|
61
|
+
* the walking surface at `elevation`, the body grows downward by `thickness`.
|
|
135
62
|
*
|
|
136
63
|
* Built directly in 3D (Y-up) rather than via ExtrudeGeometry so the hole side
|
|
137
64
|
* walls can be emitted double-sided. The slab material is forced to FrontSide
|
|
@@ -142,9 +69,11 @@ function buildSlabRegions(contour, holes) {
|
|
|
142
69
|
* thickness visible from any angle: the two coincident triangles never z-fight
|
|
143
70
|
* because exactly one faces the camera under FrontSide culling.
|
|
144
71
|
*/
|
|
145
|
-
function
|
|
72
|
+
function generateSolidSlabGeometry(slabNode, context) {
|
|
146
73
|
const polygon = ensureCounterClockwisePolygon(getRenderableSlabPolygon(slabNode, context));
|
|
147
74
|
const elevation = slabNode.elevation ?? 0.05;
|
|
75
|
+
const thickness = slabNode.thickness ?? 0.05;
|
|
76
|
+
const bottom = elevation - thickness;
|
|
148
77
|
const holePolygons = mergeSurfaceHolePolygons(slabNode.holes ?? []);
|
|
149
78
|
if (polygon.length < 3)
|
|
150
79
|
return new THREE.BufferGeometry();
|
|
@@ -158,14 +87,14 @@ function generatePositiveSlabGeometry(slabNode, context) {
|
|
|
158
87
|
const addWall = (a, b, flipped) => {
|
|
159
88
|
const base = positions.length / 3;
|
|
160
89
|
const len = Math.max(Math.hypot(b.x - a.x, b.y - a.y), 0.001);
|
|
161
|
-
positions.push(a.x,
|
|
90
|
+
positions.push(a.x, bottom, a.y);
|
|
162
91
|
uvs.push(0, 0);
|
|
163
|
-
positions.push(b.x,
|
|
92
|
+
positions.push(b.x, bottom, b.y);
|
|
164
93
|
uvs.push(len, 0);
|
|
165
94
|
positions.push(b.x, elevation, b.y);
|
|
166
|
-
uvs.push(len,
|
|
95
|
+
uvs.push(len, thickness);
|
|
167
96
|
positions.push(a.x, elevation, a.y);
|
|
168
|
-
uvs.push(0,
|
|
97
|
+
uvs.push(0, thickness);
|
|
169
98
|
// Standard winding on a CCW polygon gives inward-facing normals (see pool
|
|
170
99
|
// path), so the unflipped quad faces outward; flipped is its back face.
|
|
171
100
|
if (!flipped) {
|
|
@@ -192,7 +121,7 @@ function generatePositiveSlabGeometry(slabNode, context) {
|
|
|
192
121
|
}
|
|
193
122
|
const bottomBase = positions.length / 3;
|
|
194
123
|
for (const p of capPoints) {
|
|
195
|
-
positions.push(p.x,
|
|
124
|
+
positions.push(p.x, bottom, p.y);
|
|
196
125
|
uvs.push(p.x, -p.y);
|
|
197
126
|
}
|
|
198
127
|
const capTris = THREE.ShapeUtils.triangulateShape(contour2d, holes2d);
|
|
@@ -222,8 +151,8 @@ function generatePositiveSlabGeometry(slabNode, context) {
|
|
|
222
151
|
return geometry;
|
|
223
152
|
}
|
|
224
153
|
/**
|
|
225
|
-
* Pool / recessed slab: floor cap at Y=0 (local) + inner walls up to
|
|
226
|
-
* No top cap — the opening at
|
|
154
|
+
* Pool / recessed slab: floor cap at Y=0 (local) + inner walls up to the rim.
|
|
155
|
+
* No top cap — the opening at the rim is handled by the ground occluder hole.
|
|
227
156
|
* mesh.position.y must be set to elevation so the floor sits at the correct world Y.
|
|
228
157
|
*
|
|
229
158
|
* Geometry is built directly in 3D (Y-up) to avoid rotation confusion:
|
|
@@ -232,7 +161,8 @@ function generatePositiveSlabGeometry(slabNode, context) {
|
|
|
232
161
|
*/
|
|
233
162
|
function generatePoolGeometry(slabNode, context) {
|
|
234
163
|
const polygon = ensureCounterClockwisePolygon(getRenderableSlabPolygon(slabNode, context));
|
|
235
|
-
const
|
|
164
|
+
const floor = slabNode.elevation ?? 0.05;
|
|
165
|
+
const depth = Math.max(0, (slabNode.recessedRimElevation ?? 0) - floor);
|
|
236
166
|
const holePolygons = mergeSurfaceHolePolygons(slabNode.holes ?? []);
|
|
237
167
|
if (polygon.length < 3)
|
|
238
168
|
return new THREE.BufferGeometry();
|
|
@@ -241,7 +171,7 @@ function generatePoolGeometry(slabNode, context) {
|
|
|
241
171
|
const indices = [];
|
|
242
172
|
const pushFloorVertex = (x, y, z) => {
|
|
243
173
|
positions.push(x, y, z);
|
|
244
|
-
// Floor UVs in metres (shape-space x, -z), matching
|
|
174
|
+
// Floor UVs in metres (shape-space x, -z), matching generateSolidSlabGeometry's
|
|
245
175
|
// cap mapping so a finish tiles at the same world scale on every surface.
|
|
246
176
|
uvs.push(x, -z);
|
|
247
177
|
};
|
|
@@ -4,6 +4,12 @@ import { describe, expect, test } from 'bun:test';
|
|
|
4
4
|
import { SlabNode } from '@pascal-app/core';
|
|
5
5
|
import { generateSlabGeometry } from './slab-system';
|
|
6
6
|
const EMPTY_CONTEXT = { walls: [], siblingSlabs: [] };
|
|
7
|
+
const SQUARE = [
|
|
8
|
+
[0, 0],
|
|
9
|
+
[4, 0],
|
|
10
|
+
[4, 3],
|
|
11
|
+
[0, 3],
|
|
12
|
+
];
|
|
7
13
|
function hasVertexAt(geometry, x, z) {
|
|
8
14
|
const positions = geometry.getAttribute('position');
|
|
9
15
|
for (let index = 0; index < positions.count; index += 1) {
|
|
@@ -13,16 +19,19 @@ function hasVertexAt(geometry, x, z) {
|
|
|
13
19
|
}
|
|
14
20
|
return false;
|
|
15
21
|
}
|
|
22
|
+
function uniqueSortedYs(geometry) {
|
|
23
|
+
const positions = geometry.getAttribute('position');
|
|
24
|
+
const ys = new Set();
|
|
25
|
+
for (let index = 0; index < positions.count; index += 1) {
|
|
26
|
+
ys.add(Math.round(positions.getY(index) * 1e4) / 1e4);
|
|
27
|
+
}
|
|
28
|
+
return [...ys].sort((a, b) => a - b);
|
|
29
|
+
}
|
|
16
30
|
describe('generateSlabGeometry', () => {
|
|
17
31
|
test('renders a boundary-overlapping hole as an open indentation', () => {
|
|
18
32
|
const slab = SlabNode.parse({
|
|
19
33
|
elevation: 0.05,
|
|
20
|
-
polygon:
|
|
21
|
-
[0, 0],
|
|
22
|
-
[4, 0],
|
|
23
|
-
[4, 3],
|
|
24
|
-
[0, 3],
|
|
25
|
-
],
|
|
34
|
+
polygon: SQUARE,
|
|
26
35
|
holes: [
|
|
27
36
|
[
|
|
28
37
|
[1, -0.5],
|
|
@@ -40,12 +49,8 @@ describe('generateSlabGeometry', () => {
|
|
|
40
49
|
test('renders a boundary-overlapping hole as an open indentation on recessed slabs', () => {
|
|
41
50
|
const slab = SlabNode.parse({
|
|
42
51
|
elevation: -0.2,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
[4, 0],
|
|
46
|
-
[4, 3],
|
|
47
|
-
[0, 3],
|
|
48
|
-
],
|
|
52
|
+
recessed: true,
|
|
53
|
+
polygon: SQUARE,
|
|
49
54
|
holes: [
|
|
50
55
|
[
|
|
51
56
|
[1, -0.5],
|
|
@@ -60,4 +65,38 @@ describe('generateSlabGeometry', () => {
|
|
|
60
65
|
expect(hasVertexAt(geometry, 1, 1)).toBe(true);
|
|
61
66
|
expect(hasVertexAt(geometry, 3, 1)).toBe(true);
|
|
62
67
|
});
|
|
68
|
+
test('solid slab occupies [elevation − thickness, elevation]', () => {
|
|
69
|
+
const slab = SlabNode.parse({ elevation: 0.3, thickness: 0.1, polygon: SQUARE });
|
|
70
|
+
const ys = uniqueSortedYs(generateSlabGeometry(slab, EMPTY_CONTEXT));
|
|
71
|
+
expect(ys).toEqual([0.2, 0.3]);
|
|
72
|
+
});
|
|
73
|
+
test('migrated legacy slab (thickness = elevation) reproduces the [0, elevation] extrusion', () => {
|
|
74
|
+
const slab = SlabNode.parse({ elevation: 0.05, thickness: 0.05, polygon: SQUARE });
|
|
75
|
+
const geometry = generateSlabGeometry(slab, EMPTY_CONTEXT);
|
|
76
|
+
// Old-style expectations: extrude-from-zero put the bottom cap at 0 and
|
|
77
|
+
// the top cap at `elevation`, with 8 cap verts + 4 side quads (16 verts)
|
|
78
|
+
// and 12 triangles for a plain quad slab.
|
|
79
|
+
expect(uniqueSortedYs(geometry)).toEqual([0, 0.05]);
|
|
80
|
+
expect(geometry.getAttribute('position').count).toBe(24);
|
|
81
|
+
expect((geometry.index?.count ?? 0) / 3).toBe(12);
|
|
82
|
+
for (const [x, z] of SQUARE) {
|
|
83
|
+
expect(hasVertexAt(geometry, x, z)).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
test('recess is keyed by the flag, not the elevation sign', () => {
|
|
87
|
+
const belowPlaneSolid = SlabNode.parse({ elevation: -0.2, thickness: 0.05, polygon: SQUARE });
|
|
88
|
+
// Without `recessed`, a negative elevation is just a solid placed below
|
|
89
|
+
// the level plane: closed body at [-0.25, -0.2], world-space Y baked in.
|
|
90
|
+
expect(uniqueSortedYs(generateSlabGeometry(belowPlaneSolid, EMPTY_CONTEXT))).toEqual([
|
|
91
|
+
-0.25, -0.2,
|
|
92
|
+
]);
|
|
93
|
+
// The recessed shell is authored at local Y=0 (floor) up to |elevation|
|
|
94
|
+
// (rim), with no top cap: 4 floor verts + 4 wall quads = 20 verts,
|
|
95
|
+
// 2 floor + 8 wall triangles.
|
|
96
|
+
const pool = SlabNode.parse({ elevation: -0.2, recessed: true, polygon: SQUARE });
|
|
97
|
+
const poolGeometry = generateSlabGeometry(pool, EMPTY_CONTEXT);
|
|
98
|
+
expect(uniqueSortedYs(poolGeometry)).toEqual([0, 0.2]);
|
|
99
|
+
expect(poolGeometry.getAttribute('position').count).toBe(20);
|
|
100
|
+
expect((poolGeometry.index?.count ?? 0) / 3).toBe(10);
|
|
101
|
+
});
|
|
63
102
|
});
|
|
@@ -106,7 +106,7 @@ export const StairSystem = () => {
|
|
|
106
106
|
if (group) {
|
|
107
107
|
const mergedMesh = group.getObjectByName('merged-stair');
|
|
108
108
|
if (mergedMesh?.visible !== false) {
|
|
109
|
-
updateMergedStairGeometry(node, group, nodes);
|
|
109
|
+
updateMergedStairGeometry(getEffectiveNode(node), group, nodes);
|
|
110
110
|
stairsProcessed++;
|
|
111
111
|
}
|
|
112
112
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAyDA,eAAO,MAAM,UAAU,YAuLtB,CAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { emitter, getWallFaceBandConfig, sceneRegistry, useScene, } from '@pascal-app/core';
|
|
1
|
+
import { emitter, getWallFaceBandConfig, getWallPlaneTop, resolveLevelId, resolveWallEffectiveHeight, sceneRegistry, spatialGridManager, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import { useEffect, useRef } from 'react';
|
|
4
4
|
import { Vector3 } from 'three/webgpu';
|
|
@@ -101,7 +101,10 @@ export const WallCutout = () => {
|
|
|
101
101
|
const hideWall = getWallHideState(wallNode, wallMesh, wallMode, u);
|
|
102
102
|
const isDeleteHighlighted = deleteHoveredWallId === wallId;
|
|
103
103
|
const isSelectionHighlighted = !isDeleteHighlighted && highlightedWallIds.has(wallId);
|
|
104
|
-
const
|
|
104
|
+
const levelId = resolveLevelId(wallNode, sceneState.nodes);
|
|
105
|
+
const support = spatialGridManager.getSlabSupportForWall(levelId, wallNode.start, wallNode.end, wallNode.curveOffset ?? 0, wallNode.thickness, wallNode.supportSlabId);
|
|
106
|
+
const effectiveWallHeight = resolveWallEffectiveHeight(wallNode, getWallPlaneTop(wallNode, levelId, sceneState.nodes), support.elevation);
|
|
107
|
+
const shouldSelectionHighlight = isSelectionHighlighted && !getWallFaceBandConfig(wallNode, effectiveWallHeight).enabled;
|
|
105
108
|
const materials = getMaterialsForWall(wallNode, shading, textures, colorPreset, sceneTheme, sceneState.materials);
|
|
106
109
|
if (wallMode === 'translucent') {
|
|
107
110
|
;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
|
|
2
2
|
// depend on @types/bun so the import type is unresolved at compile time.
|
|
3
3
|
import { describe, expect, test } from 'bun:test';
|
|
4
|
-
import { calculateLevelMiters, WallNode } from '@pascal-app/core';
|
|
4
|
+
import { calculateLevelMiters, getWallPlaneTop, WallNode, } from '@pascal-app/core';
|
|
5
5
|
import { generateExtrudedWall } from './wall-system';
|
|
6
6
|
describe('wall support extension', () => {
|
|
7
7
|
test('preserves the raised wall origin and top while filling down to the lower support', () => {
|
|
@@ -24,6 +24,74 @@ describe('wall support extension', () => {
|
|
|
24
24
|
expect((geometry.boundingBox?.max.y ?? 0) - 0.4).toBeCloseTo(2.5);
|
|
25
25
|
geometry.dispose();
|
|
26
26
|
});
|
|
27
|
+
test('plane-bound wall tops out at the storey plane regardless of slab elevation', () => {
|
|
28
|
+
const wall = WallNode.parse({ start: [0, 0], end: [4, 0], thickness: 0.1 });
|
|
29
|
+
const flat = generateExtrudedWall(wall, [], calculateLevelMiters([wall]), 0, 0, undefined, 3);
|
|
30
|
+
flat.computeBoundingBox();
|
|
31
|
+
expect(flat.boundingBox?.max.y).toBeCloseTo(3);
|
|
32
|
+
expect(flat.boundingBox?.min.y).toBeCloseTo(0);
|
|
33
|
+
flat.dispose();
|
|
34
|
+
const raised = generateExtrudedWall(wall, [], calculateLevelMiters([wall]), 0.6, 0.6, undefined, 3);
|
|
35
|
+
raised.computeBoundingBox();
|
|
36
|
+
// Mesh sits at Y=0.6, so a 2.4 local top keeps the world top at the 3m
|
|
37
|
+
// plane — the raised slab shortens the wall instead of lifting its top.
|
|
38
|
+
expect(raised.boundingBox?.max.y).toBeCloseTo(2.4);
|
|
39
|
+
expect(raised.boundingBox?.min.y).toBeCloseTo(0);
|
|
40
|
+
raised.dispose();
|
|
41
|
+
});
|
|
42
|
+
test('plane-bound wall under a flush thick deck tops out at the deck underside', () => {
|
|
43
|
+
// level_1 carries a flush deck occupying [-0.3, 0] above the storey
|
|
44
|
+
// plane: the covering-clamped plane for level_0 is 2.5 − 0.3 = 2.2.
|
|
45
|
+
const wall = WallNode.parse({
|
|
46
|
+
start: [0.5, 2],
|
|
47
|
+
end: [3.5, 2],
|
|
48
|
+
thickness: 0.1,
|
|
49
|
+
parentId: 'level_0',
|
|
50
|
+
});
|
|
51
|
+
const base = { object: 'node', parentId: null, visible: true, metadata: {}, children: [] };
|
|
52
|
+
const nodes = {
|
|
53
|
+
level_0: {
|
|
54
|
+
...base,
|
|
55
|
+
id: 'level_0',
|
|
56
|
+
type: 'level',
|
|
57
|
+
level: 0,
|
|
58
|
+
height: 2.5,
|
|
59
|
+
children: [wall.id],
|
|
60
|
+
},
|
|
61
|
+
level_1: {
|
|
62
|
+
...base,
|
|
63
|
+
id: 'level_1',
|
|
64
|
+
type: 'level',
|
|
65
|
+
level: 1,
|
|
66
|
+
height: 2.5,
|
|
67
|
+
children: ['slab_deck'],
|
|
68
|
+
},
|
|
69
|
+
slab_deck: {
|
|
70
|
+
...base,
|
|
71
|
+
id: 'slab_deck',
|
|
72
|
+
type: 'slab',
|
|
73
|
+
parentId: 'level_1',
|
|
74
|
+
polygon: [
|
|
75
|
+
[0, 0],
|
|
76
|
+
[4, 0],
|
|
77
|
+
[4, 4],
|
|
78
|
+
[0, 4],
|
|
79
|
+
],
|
|
80
|
+
holes: [],
|
|
81
|
+
elevation: 0,
|
|
82
|
+
thickness: 0.3,
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
const planeTop = getWallPlaneTop(wall, 'level_0', nodes);
|
|
86
|
+
expect(planeTop).toBeCloseTo(2.2);
|
|
87
|
+
const geometry = generateExtrudedWall(wall, [], calculateLevelMiters([wall]), 0, 0, undefined, planeTop);
|
|
88
|
+
geometry.computeBoundingBox();
|
|
89
|
+
// Mesh sits at Y=0, so the world top lands at the deck underside instead
|
|
90
|
+
// of colliding with the slab solid above.
|
|
91
|
+
expect(geometry.boundingBox?.max.y).toBeCloseTo(2.2);
|
|
92
|
+
expect(geometry.boundingBox?.min.y).toBeCloseTo(0);
|
|
93
|
+
geometry.dispose();
|
|
94
|
+
});
|
|
27
95
|
test('raises only the high-supported part of a mixed wall run', () => {
|
|
28
96
|
const wall = WallNode.parse({ start: [0, 0], end: [4, 0], height: 2.5, thickness: 0.1 });
|
|
29
97
|
const geometry = generateExtrudedWall(wall, [], calculateLevelMiters([wall]), 0.6, 0.05, [
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { type AnyNode, type WallMiterData, type WallNode, type WallSlabSupportSegment } from '@pascal-app/core';
|
|
2
2
|
import * as THREE from 'three';
|
|
3
3
|
export declare const WallSystem: () => null;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
* Key insight from demo: polygon is built in WORLD coordinates first,
|
|
8
|
-
* then we transform to wall-local for the 3D mesh.
|
|
9
|
-
*/
|
|
10
|
-
export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number, baseElevation?: number, baseSegments?: readonly WallSlabSupportSegment[]): THREE.BufferGeometry;
|
|
4
|
+
type WallTerrainBottomSampler = (x: number, z: number) => number | null;
|
|
5
|
+
export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number, baseElevation?: number, baseSegments?: readonly WallSlabSupportSegment[], storeyHeight?: number, terrainBottomAt?: WallTerrainBottomSampler): THREE.BufferGeometry;
|
|
6
|
+
export {};
|
|
11
7
|
//# sourceMappingURL=wall-system.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,
|
|
1
|
+
{"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,OAAO,EA2BZ,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,sBAAsB,EAI5B,MAAM,kBAAkB,CAAA;AAEzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAqd9B,eAAO,MAAM,UAAU,YAwJtB,CAAA;AAgND,KAAK,wBAAwB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;AAgEvE,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,QAAQ,EAClB,aAAa,EAAE,OAAO,EAAE,EACxB,SAAS,EAAE,aAAa,EACxB,aAAa,SAAI,EACjB,aAAa,SAAgB,EAC7B,YAAY,GAAE,SAAS,sBAAsB,EAE5C,EACD,YAAY,SAAuB,EACnC,eAAe,CAAC,EAAE,wBAAwB,GACzC,KAAK,CAAC,cAAc,CA0OtB"}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { calculateLevelMiters,
|
|
1
|
+
import { calculateLevelMiters, DEFAULT_LEVEL_HEIGHT, getAdjacentWallIds, getEffectiveNode, getWallBandSlotId, getWallCurveFrameAt, getWallFaceBandConfig, getWallFaceBandForHeight, getWallMiterBoundaryPoints, getWallPlaneTop, getWallPlanFootprint, getWallSurfacePolygon, getWallThickness, isCurvedWall, pointToKey, resolveLevelId, resolveWallTop, sceneRegistry, spatialGridManager, terrainSupportLift, useLiveNodeOverrides, useLiveTransforms, useScene, } from '@pascal-app/core';
|
|
2
2
|
import { useFrame } from '@react-three/fiber';
|
|
3
3
|
import * as THREE from 'three';
|
|
4
|
+
import { mergeGeometries } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
4
5
|
import { Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
|
|
5
6
|
import { computeBoundsTree } from 'three-mesh-bvh';
|
|
6
7
|
import { ensureRenderableGeometryAttributes, prepareBrushForCSG } from '../../lib/csg-utils';
|
|
8
|
+
import { buildTerrainPerimeterFillGeometry } from '../../lib/terrain-perimeter-fill';
|
|
7
9
|
import { buildOpeningCutoutGeometry, getOpeningCutoutBottomPadding, } from './opening-cutout-geometry';
|
|
8
10
|
// Reusable CSG evaluator for better performance
|
|
9
11
|
const csgEvaluator = new Evaluator();
|
|
@@ -133,17 +135,17 @@ function distanceToWallBoundaryEdge(point, edge) {
|
|
|
133
135
|
const closestZ = edge.start.y + edgeDz * t;
|
|
134
136
|
return Math.hypot(point.x - closestX, point.y - closestZ);
|
|
135
137
|
}
|
|
136
|
-
function getWallFaceMaterialIndex(wall, face, y) {
|
|
138
|
+
function getWallFaceMaterialIndex(wall, face, y, effectiveWallHeight) {
|
|
137
139
|
const semantic = face === 'front' ? wall.frontSide : wall.backSide;
|
|
138
140
|
const fallback = face === 'front' ? 'interior' : 'exterior';
|
|
139
141
|
const side = semantic === 'interior' || semantic === 'exterior' ? semantic : fallback;
|
|
140
|
-
const bands = getWallFaceBandConfig(wall);
|
|
142
|
+
const bands = getWallFaceBandConfig(wall, effectiveWallHeight);
|
|
141
143
|
if (!bands.enabled)
|
|
142
144
|
return WALL_BAND_SLOT_MATERIAL_INDEX[side];
|
|
143
|
-
const band = getWallFaceBandForHeight(wall, y);
|
|
145
|
+
const band = getWallFaceBandForHeight(wall, y, effectiveWallHeight);
|
|
144
146
|
return WALL_BAND_SLOT_MATERIAL_INDEX[getWallBandSlotId(side, band)];
|
|
145
147
|
}
|
|
146
|
-
function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
|
|
148
|
+
function assignWallMaterialGroups(geometry, wall, boundaryEdges, effectiveWallHeight) {
|
|
147
149
|
const position = geometry.getAttribute('position');
|
|
148
150
|
if (!position)
|
|
149
151
|
return;
|
|
@@ -206,7 +208,7 @@ function assignWallMaterialGroups(geometry, wall, boundaryEdges) {
|
|
|
206
208
|
triangleMaterials[triangleIndex] = 0;
|
|
207
209
|
continue;
|
|
208
210
|
}
|
|
209
|
-
triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag, centroid.y);
|
|
211
|
+
triangleMaterials[triangleIndex] = getWallFaceMaterialIndex(wall, nearestTag, centroid.y, effectiveWallHeight);
|
|
210
212
|
}
|
|
211
213
|
geometry.clearGroups();
|
|
212
214
|
let currentMaterial = triangleMaterials[0] ?? 0;
|
|
@@ -309,8 +311,8 @@ function splitGeometryAtHorizontalPlanes(geometry, planes) {
|
|
|
309
311
|
split.computeVertexNormals();
|
|
310
312
|
return split;
|
|
311
313
|
}
|
|
312
|
-
function getWallBandSplitPlanes(wall) {
|
|
313
|
-
const bands = getWallFaceBandConfig(wall);
|
|
314
|
+
function getWallBandSplitPlanes(wall, effectiveWallHeight) {
|
|
315
|
+
const bands = getWallFaceBandConfig(wall, effectiveWallHeight);
|
|
314
316
|
if (!bands.enabled)
|
|
315
317
|
return [];
|
|
316
318
|
const planes = [bands.lowerTop];
|
|
@@ -318,7 +320,7 @@ function getWallBandSplitPlanes(wall) {
|
|
|
318
320
|
planes.push(bands.middleTop);
|
|
319
321
|
if (bands.count >= 4)
|
|
320
322
|
planes.push(bands.upperTop);
|
|
321
|
-
return planes.filter((plane) => plane > WALL_BAND_SPLIT_EPSILON && plane <
|
|
323
|
+
return planes.filter((plane) => plane > WALL_BAND_SPLIT_EPSILON && plane < effectiveWallHeight - WALL_BAND_SPLIT_EPSILON);
|
|
322
324
|
}
|
|
323
325
|
// ============================================================================
|
|
324
326
|
// WALL SYSTEM
|
|
@@ -529,8 +531,14 @@ function updateWallGeometry(wallId, miterData) {
|
|
|
529
531
|
if (!mesh)
|
|
530
532
|
return;
|
|
531
533
|
const levelId = resolveLevelId(node, nodes);
|
|
532
|
-
|
|
534
|
+
// Covering-clamped plane: a flush/thick slab on the level above shortens
|
|
535
|
+
// the plane-bound walls below it (explicit-height walls ignore the value).
|
|
536
|
+
const planeTop = getWallPlaneTop(node, levelId, nodes);
|
|
537
|
+
const slabSupport = spatialGridManager.getSlabSupportForWall(levelId, node.start, node.end, node.curveOffset ?? 0, node.thickness, node.supportSlabId, undefined, node.supportOffset);
|
|
533
538
|
const slabElevation = slabSupport.elevation;
|
|
539
|
+
const terrainBottomAt = node.fillToTerrain
|
|
540
|
+
? (x, z) => terrainSupportLift(nodes, levelId, x, z)
|
|
541
|
+
: undefined;
|
|
534
542
|
const childrenIds = node.children || [];
|
|
535
543
|
// Merge live overrides into door / window children so cutouts track an
|
|
536
544
|
// in-flight resize drag (door width arrow, window height arrow, etc.)
|
|
@@ -552,7 +560,7 @@ function updateWallGeometry(wallId, miterData) {
|
|
|
552
560
|
return effective;
|
|
553
561
|
return { ...effective, position: live.position };
|
|
554
562
|
});
|
|
555
|
-
const builtGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments);
|
|
563
|
+
const builtGeo = generateExtrudedWall(node, childrenNodes, miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments, planeTop, terrainBottomAt);
|
|
556
564
|
const wallAngle = Math.atan2(node.end[1] - node.start[1], node.end[0] - node.start[0]);
|
|
557
565
|
// World transform the render mesh will apply (position + Y-rotation below).
|
|
558
566
|
// Reproduce it here so the UVs can be projected in WORLD space — see
|
|
@@ -564,7 +572,7 @@ function updateWallGeometry(wallId, miterData) {
|
|
|
564
572
|
// Update collision mesh
|
|
565
573
|
const collisionMesh = mesh.getObjectByName('collision-mesh');
|
|
566
574
|
if (collisionMesh) {
|
|
567
|
-
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments);
|
|
575
|
+
const collisionGeo = generateExtrudedWall(node, [], miterData, slabElevation, slabSupport.baseElevation, slabSupport.baseSegments, planeTop, terrainBottomAt);
|
|
568
576
|
collisionMesh.geometry.dispose();
|
|
569
577
|
collisionMesh.geometry = collisionGeo;
|
|
570
578
|
}
|
|
@@ -635,16 +643,70 @@ function applyWorldPlanarWallUVs(geometry, worldMatrix) {
|
|
|
635
643
|
* Key insight from demo: polygon is built in WORLD coordinates first,
|
|
636
644
|
* then we transform to wall-local for the 3D mesh.
|
|
637
645
|
*/
|
|
646
|
+
const WALL_TERRAIN_SAMPLE_STEP = 0.25;
|
|
647
|
+
function densifyClosedWallPerimeter(points) {
|
|
648
|
+
const dense = [];
|
|
649
|
+
for (let index = 0; index < points.length; index += 1) {
|
|
650
|
+
const start = points[index];
|
|
651
|
+
const end = points[(index + 1) % points.length];
|
|
652
|
+
const length = Math.hypot(end.x - start.x, end.y - start.y);
|
|
653
|
+
const segments = Math.max(1, Math.ceil(length / WALL_TERRAIN_SAMPLE_STEP));
|
|
654
|
+
for (let segment = 0; segment < segments; segment += 1) {
|
|
655
|
+
const t = segment / segments;
|
|
656
|
+
dense.push({
|
|
657
|
+
x: start.x + (end.x - start.x) * t,
|
|
658
|
+
y: start.y + (end.y - start.y) * t,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return dense;
|
|
663
|
+
}
|
|
664
|
+
function buildWallTerrainFillGeometry(perimeter, worldToLocal, wallBaseElevation, terrainBottomAt) {
|
|
665
|
+
const worldPoints = densifyClosedWallPerimeter(perimeter);
|
|
666
|
+
if (worldPoints.length < 3)
|
|
667
|
+
return null;
|
|
668
|
+
const localPoints = worldPoints.map(worldToLocal);
|
|
669
|
+
const bottomY = worldPoints.map((point) => {
|
|
670
|
+
const terrainElevation = terrainBottomAt(point.x, point.y);
|
|
671
|
+
return terrainElevation == null ? 0 : Math.min(0, terrainElevation - wallBaseElevation);
|
|
672
|
+
});
|
|
673
|
+
return buildTerrainPerimeterFillGeometry(localPoints, bottomY, 0);
|
|
674
|
+
}
|
|
675
|
+
function mergeWallTerrainFill(body, fill, wall, boundaryEdges, effectiveWallHeight) {
|
|
676
|
+
if (!fill)
|
|
677
|
+
return body;
|
|
678
|
+
const bodyGeometry = body.index ? body.toNonIndexed() : body;
|
|
679
|
+
if (bodyGeometry !== body)
|
|
680
|
+
body.dispose();
|
|
681
|
+
ensureRenderableGeometryAttributes(bodyGeometry);
|
|
682
|
+
ensureRenderableGeometryAttributes(fill);
|
|
683
|
+
const merged = mergeGeometries([bodyGeometry, fill], false);
|
|
684
|
+
if (!merged) {
|
|
685
|
+
fill.dispose();
|
|
686
|
+
return bodyGeometry;
|
|
687
|
+
}
|
|
688
|
+
bodyGeometry.dispose();
|
|
689
|
+
fill.dispose();
|
|
690
|
+
merged.computeVertexNormals();
|
|
691
|
+
assignWallMaterialGroups(merged, wall, boundaryEdges, effectiveWallHeight);
|
|
692
|
+
ensureRenderableGeometryAttributes(merged);
|
|
693
|
+
return merged;
|
|
694
|
+
}
|
|
638
695
|
export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabElevation = 0, baseElevation = slabElevation, baseSegments = [
|
|
639
696
|
{ start: 0, end: 1, elevation: baseElevation },
|
|
640
|
-
]) {
|
|
697
|
+
], storeyHeight = DEFAULT_LEVEL_HEIGHT, terrainBottomAt) {
|
|
641
698
|
const wallStart = { x: wallNode.start[0], y: wallNode.start[1] };
|
|
642
699
|
const wallEnd = { x: wallNode.end[0], y: wallNode.end[1] };
|
|
643
|
-
const
|
|
644
|
-
const
|
|
700
|
+
const topElevation = resolveWallTop(wallNode, storeyHeight, slabElevation);
|
|
701
|
+
const effectiveWallHeight = topElevation - slabElevation;
|
|
645
702
|
const effectiveBaseElevation = Math.min(baseElevation, slabElevation);
|
|
646
703
|
const localBottom = effectiveBaseElevation - slabElevation;
|
|
647
704
|
const height = topElevation - effectiveBaseElevation;
|
|
705
|
+
// A slab at or above the storey plane leaves a plane-bound wall with no
|
|
706
|
+
// body — bail before ExtrudeGeometry sees a non-positive depth.
|
|
707
|
+
if (height <= 1e-9) {
|
|
708
|
+
return new THREE.BufferGeometry();
|
|
709
|
+
}
|
|
648
710
|
const thickness = getWallThickness(wallNode);
|
|
649
711
|
// Wall direction and normal (exactly like demo)
|
|
650
712
|
const v = { x: wallEnd.x - wallStart.x, y: wallEnd.y - wallStart.y };
|
|
@@ -675,6 +737,9 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
675
737
|
// Convert polygon to local coordinates
|
|
676
738
|
const localPoints = polyPoints.map(worldToLocal);
|
|
677
739
|
const boundaryEdges = buildTaggedWallBoundaryEdges(wallNode, localPoints, miterData);
|
|
740
|
+
const terrainFill = terrainBottomAt
|
|
741
|
+
? buildWallTerrainFillGeometry(polyPoints, worldToLocal, slabElevation, terrainBottomAt)
|
|
742
|
+
: null;
|
|
678
743
|
// Build THREE.js shape
|
|
679
744
|
// Shape uses (x, y) where we map: shape.x = local.x, shape.y = -local.z
|
|
680
745
|
// The negation is needed because after rotateX(-PI/2), shape.y becomes -geometry.z
|
|
@@ -694,7 +759,7 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
694
759
|
if (Math.abs(localBottom) > 1e-9)
|
|
695
760
|
geometry.translate(0, localBottom, 0);
|
|
696
761
|
geometry.computeVertexNormals();
|
|
697
|
-
assignWallMaterialGroups(geometry, wallNode, boundaryEdges);
|
|
762
|
+
assignWallMaterialGroups(geometry, wallNode, boundaryEdges, effectiveWallHeight);
|
|
698
763
|
ensureRenderableGeometryAttributes(geometry);
|
|
699
764
|
// Start with the lowest required wall prism, then remove the volume below
|
|
700
765
|
// each higher-supported run. This keeps the existing mitered footprint and
|
|
@@ -785,11 +850,11 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
785
850
|
...collectCutoutBrushes(wallNode, childrenNodes, thickness),
|
|
786
851
|
];
|
|
787
852
|
if (cutoutBrushes.length === 0) {
|
|
788
|
-
const splitGeometry = splitGeometryAtHorizontalPlanes(geometry, getWallBandSplitPlanes(wallNode));
|
|
853
|
+
const splitGeometry = splitGeometryAtHorizontalPlanes(geometry, getWallBandSplitPlanes(wallNode, effectiveWallHeight));
|
|
789
854
|
splitGeometry.computeVertexNormals();
|
|
790
|
-
assignWallMaterialGroups(splitGeometry, wallNode, boundaryEdges);
|
|
855
|
+
assignWallMaterialGroups(splitGeometry, wallNode, boundaryEdges, effectiveWallHeight);
|
|
791
856
|
ensureRenderableGeometryAttributes(splitGeometry);
|
|
792
|
-
return splitGeometry;
|
|
857
|
+
return mergeWallTerrainFill(splitGeometry, terrainFill, wallNode, boundaryEdges, effectiveWallHeight);
|
|
793
858
|
}
|
|
794
859
|
// Create wall brush from geometry
|
|
795
860
|
// Pre-compute BVH with new API to avoid deprecation warning
|
|
@@ -814,11 +879,11 @@ export function generateExtrudedWall(wallNode, childrenNodes, miterData, slabEle
|
|
|
814
879
|
csgGeometry(brush).dispose();
|
|
815
880
|
}
|
|
816
881
|
const resultGeometry = csgGeometry(resultBrush);
|
|
817
|
-
const splitResultGeometry = splitGeometryAtHorizontalPlanes(resultGeometry, getWallBandSplitPlanes(wallNode));
|
|
882
|
+
const splitResultGeometry = splitGeometryAtHorizontalPlanes(resultGeometry, getWallBandSplitPlanes(wallNode, effectiveWallHeight));
|
|
818
883
|
splitResultGeometry.computeVertexNormals();
|
|
819
|
-
assignWallMaterialGroups(splitResultGeometry, wallNode, boundaryEdges);
|
|
884
|
+
assignWallMaterialGroups(splitResultGeometry, wallNode, boundaryEdges, effectiveWallHeight);
|
|
820
885
|
ensureRenderableGeometryAttributes(splitResultGeometry);
|
|
821
|
-
return splitResultGeometry;
|
|
886
|
+
return mergeWallTerrainFill(splitResultGeometry, terrainFill, wallNode, boundaryEdges, effectiveWallHeight);
|
|
822
887
|
}
|
|
823
888
|
/**
|
|
824
889
|
* Collects opening and item cutout brushes for CSG subtraction. Door/window
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wall-terrain-bottom.test.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-terrain-bottom.test.ts"],"names":[],"mappings":""}
|