@pascal-app/viewer 1.0.0-beta.4 → 1.0.0-beta.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.
Files changed (103) hide show
  1. package/README.md +52 -0
  2. package/dist/components/viewer/frame-limiter.d.ts +11 -0
  3. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  4. package/dist/components/viewer/frame-limiter.js +58 -18
  5. package/dist/components/viewer/index.d.ts +15 -0
  6. package/dist/components/viewer/index.d.ts.map +1 -1
  7. package/dist/components/viewer/index.js +34 -50
  8. package/dist/components/viewer/perf-monitor.js +2 -2
  9. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  10. package/dist/components/viewer/selection-manager.js +7 -2
  11. package/dist/components/viewer/unsupported-gpu-fallback.d.ts +2 -0
  12. package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -0
  13. package/dist/components/viewer/unsupported-gpu-fallback.js +4 -0
  14. package/dist/index.d.ts +8 -5
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +7 -4
  17. package/dist/lib/dispose-object3d.d.ts +4 -0
  18. package/dist/lib/dispose-object3d.d.ts.map +1 -0
  19. package/dist/lib/dispose-object3d.js +27 -0
  20. package/dist/lib/geometry-groups.d.ts +17 -0
  21. package/dist/lib/geometry-groups.d.ts.map +1 -0
  22. package/dist/lib/geometry-groups.js +53 -0
  23. package/dist/lib/isolation.d.ts +8 -8
  24. package/dist/lib/isolation.d.ts.map +1 -1
  25. package/dist/lib/isolation.js +18 -30
  26. package/dist/lib/layers.d.ts +16 -0
  27. package/dist/lib/layers.d.ts.map +1 -1
  28. package/dist/lib/layers.js +18 -0
  29. package/dist/lib/renderer-capability.d.ts +56 -0
  30. package/dist/lib/renderer-capability.d.ts.map +1 -0
  31. package/dist/lib/renderer-capability.js +114 -0
  32. package/dist/lib/scene-visibility.d.ts +14 -0
  33. package/dist/lib/scene-visibility.d.ts.map +1 -0
  34. package/dist/lib/scene-visibility.js +48 -0
  35. package/dist/lib/shadow-only.d.ts +16 -0
  36. package/dist/lib/shadow-only.d.ts.map +1 -1
  37. package/dist/lib/shadow-only.js +8 -17
  38. package/dist/lib/snapshot-pipeline.d.ts +9 -0
  39. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  40. package/dist/lib/snapshot-pipeline.js +33 -9
  41. package/dist/store/use-viewer.d.ts +5 -4
  42. package/dist/store/use-viewer.d.ts.map +1 -1
  43. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  44. package/dist/systems/floor-elevation/floor-elevation-system.js +9 -0
  45. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  46. package/dist/systems/geometry/geometry-system.js +2 -26
  47. package/dist/systems/level/level-system.d.ts.map +1 -1
  48. package/dist/systems/level/level-system.js +7 -1
  49. package/dist/systems/wall/level-miter-cache.d.ts +10 -0
  50. package/dist/systems/wall/level-miter-cache.d.ts.map +1 -0
  51. package/dist/systems/wall/level-miter-cache.js +40 -0
  52. package/dist/systems/wall/wall-cutout.d.ts +12 -0
  53. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  54. package/dist/systems/wall/wall-cutout.js +41 -11
  55. package/dist/systems/wall/wall-system.d.ts +4 -0
  56. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  57. package/dist/systems/wall/wall-system.js +31 -18
  58. package/package.json +4 -2
  59. package/dist/components/viewer/scene-bvh.test.d.ts +0 -2
  60. package/dist/components/viewer/scene-bvh.test.d.ts.map +0 -1
  61. package/dist/components/viewer/scene-bvh.test.js +0 -13
  62. package/dist/lib/csg-utils.test.d.ts +0 -2
  63. package/dist/lib/csg-utils.test.d.ts.map +0 -1
  64. package/dist/lib/csg-utils.test.js +0 -45
  65. package/dist/lib/materials.test.d.ts +0 -2
  66. package/dist/lib/materials.test.d.ts.map +0 -1
  67. package/dist/lib/materials.test.js +0 -25
  68. package/dist/lib/merged-outline-node.test.d.ts +0 -2
  69. package/dist/lib/merged-outline-node.test.d.ts.map +0 -1
  70. package/dist/lib/merged-outline-node.test.js +0 -22
  71. package/dist/lib/polygon-union.test.d.ts +0 -2
  72. package/dist/lib/polygon-union.test.d.ts.map +0 -1
  73. package/dist/lib/polygon-union.test.js +0 -105
  74. package/dist/lib/texture-reference.test.d.ts +0 -2
  75. package/dist/lib/texture-reference.test.d.ts.map +0 -1
  76. package/dist/lib/texture-reference.test.js +0 -86
  77. package/dist/store/use-viewer.test.d.ts +0 -2
  78. package/dist/store/use-viewer.test.d.ts.map +0 -1
  79. package/dist/store/use-viewer.test.js +0 -61
  80. package/dist/systems/door/door-animation.test.d.ts +0 -2
  81. package/dist/systems/door/door-animation.test.d.ts.map +0 -1
  82. package/dist/systems/door/door-animation.test.js +0 -51
  83. package/dist/systems/door/door-floor-alignment.test.d.ts +0 -2
  84. package/dist/systems/door/door-floor-alignment.test.d.ts.map +0 -1
  85. package/dist/systems/door/door-floor-alignment.test.js +0 -50
  86. package/dist/systems/geometry/geometry-system.test.d.ts +0 -2
  87. package/dist/systems/geometry/geometry-system.test.d.ts.map +0 -1
  88. package/dist/systems/geometry/geometry-system.test.js +0 -30
  89. package/dist/systems/slab/slab-system.test.d.ts +0 -2
  90. package/dist/systems/slab/slab-system.test.d.ts.map +0 -1
  91. package/dist/systems/slab/slab-system.test.js +0 -102
  92. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +0 -2
  93. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +0 -1
  94. package/dist/systems/wall/opening-cutout-geometry.test.js +0 -172
  95. package/dist/systems/wall/wall-opening-cutout.test.d.ts +0 -2
  96. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +0 -1
  97. package/dist/systems/wall/wall-opening-cutout.test.js +0 -59
  98. package/dist/systems/wall/wall-support-extension.test.d.ts +0 -2
  99. package/dist/systems/wall/wall-support-extension.test.d.ts.map +0 -1
  100. package/dist/systems/wall/wall-support-extension.test.js +0 -116
  101. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +0 -2
  102. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +0 -1
  103. package/dist/systems/wall/wall-terrain-bottom.test.js +0 -34
@@ -1,172 +0,0 @@
1
- // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
- // depend on @types/bun so the import type is unresolved at compile time.
3
- import { describe, expect, test } from 'bun:test';
4
- import { DoorNode, WindowNode } from '@pascal-app/core';
5
- import { buildOpeningCutoutGeometry, buildOpeningCutoutShape, getOpeningCutoutBottomPadding, hasFlatOpeningCutoutBottom, } from './opening-cutout-geometry';
6
- function containsPoint(points, x, y) {
7
- return points.some((point) => Math.abs(point.x - x) < 1e-6 && Math.abs(point.y - y) < 1e-6);
8
- }
9
- function getBounds(points) {
10
- let minX = Number.POSITIVE_INFINITY;
11
- let maxX = Number.NEGATIVE_INFINITY;
12
- let minY = Number.POSITIVE_INFINITY;
13
- let maxY = Number.NEGATIVE_INFINITY;
14
- for (const point of points) {
15
- minX = Math.min(minX, point.x);
16
- maxX = Math.max(maxX, point.x);
17
- minY = Math.min(minY, point.y);
18
- maxY = Math.max(maxY, point.y);
19
- }
20
- return { minX, maxX, minY, maxY };
21
- }
22
- describe('buildOpeningCutoutShape', () => {
23
- test('rectangle profile passes the rect through unchanged', () => {
24
- const door = DoorNode.parse({});
25
- const rect = { left: 1.2, right: 2.1, bottom: 0, top: 2.1 };
26
- const points = buildOpeningCutoutShape(door, rect).getPoints();
27
- expect(containsPoint(points, rect.left, rect.bottom)).toBe(true);
28
- expect(containsPoint(points, rect.right, rect.bottom)).toBe(true);
29
- expect(containsPoint(points, rect.right, rect.top)).toBe(true);
30
- expect(containsPoint(points, rect.left, rect.top)).toBe(true);
31
- const bounds = getBounds(points);
32
- expect(bounds.minX).toBeCloseTo(rect.left, 9);
33
- expect(bounds.maxX).toBeCloseTo(rect.right, 9);
34
- expect(bounds.minY).toBeCloseTo(rect.bottom, 9);
35
- expect(bounds.maxY).toBeCloseTo(rect.top, 9);
36
- });
37
- test('door rounded profile rounds only the top corners', () => {
38
- const door = DoorNode.parse({ openingShape: 'rounded', cornerRadius: 0.2 });
39
- const rect = { left: -0.45, right: 0.45, bottom: 0, top: 2.1 };
40
- const points = buildOpeningCutoutShape(door, rect).getPoints();
41
- expect(containsPoint(points, rect.left, rect.bottom)).toBe(true);
42
- expect(containsPoint(points, rect.right, rect.bottom)).toBe(true);
43
- expect(containsPoint(points, rect.left, rect.top)).toBe(false);
44
- expect(containsPoint(points, rect.right, rect.top)).toBe(false);
45
- expect(containsPoint(points, rect.left, rect.top - 0.2)).toBe(true);
46
- expect(containsPoint(points, rect.left + 0.2, rect.top)).toBe(true);
47
- expect(containsPoint(points, rect.right, rect.top - 0.2)).toBe(true);
48
- expect(containsPoint(points, rect.right - 0.2, rect.top)).toBe(true);
49
- });
50
- test('window rounded profile rounds all four corners', () => {
51
- const window = WindowNode.parse({ openingShape: 'rounded', cornerRadius: 0.2 });
52
- const rect = { left: -0.75, right: 0.75, bottom: 0.9, top: 2.4 };
53
- const points = buildOpeningCutoutShape(window, rect).getPoints();
54
- expect(containsPoint(points, rect.left, rect.bottom)).toBe(false);
55
- expect(containsPoint(points, rect.right, rect.bottom)).toBe(false);
56
- expect(containsPoint(points, rect.left, rect.top)).toBe(false);
57
- expect(containsPoint(points, rect.right, rect.top)).toBe(false);
58
- expect(containsPoint(points, rect.left + 0.2, rect.bottom)).toBe(true);
59
- expect(containsPoint(points, rect.left, rect.bottom + 0.2)).toBe(true);
60
- expect(containsPoint(points, rect.right - 0.2, rect.top)).toBe(true);
61
- expect(containsPoint(points, rect.right, rect.top - 0.2)).toBe(true);
62
- const bounds = getBounds(points);
63
- expect(bounds.minX).toBeCloseTo(rect.left, 9);
64
- expect(bounds.maxX).toBeCloseTo(rect.right, 9);
65
- expect(bounds.minY).toBeCloseTo(rect.bottom, 9);
66
- expect(bounds.maxY).toBeCloseTo(rect.top, 9);
67
- });
68
- test('shared corner radius is clamped to the opening half-extent', () => {
69
- const window = WindowNode.parse({ openingShape: 'rounded', cornerRadius: 10 });
70
- const rect = { left: -0.75, right: 0.75, bottom: 0.9, top: 2.4 };
71
- const points = buildOpeningCutoutShape(window, rect).getPoints();
72
- // 1.5 × 1.5 opening → radius clamps to 0.75; arcs meet at edge midpoints.
73
- expect(containsPoint(points, rect.left, rect.bottom + 0.75)).toBe(true);
74
- expect(containsPoint(points, rect.right, rect.top - 0.75)).toBe(true);
75
- const bounds = getBounds(points);
76
- expect(bounds.minX).toBeCloseTo(rect.left, 9);
77
- expect(bounds.maxX).toBeCloseTo(rect.right, 9);
78
- });
79
- test('individual radii normalize when their sum exceeds the opening width', () => {
80
- const window = WindowNode.parse({
81
- openingShape: 'rounded',
82
- openingRadiusMode: 'individual',
83
- openingCornerRadii: [4, 4, 0, 0],
84
- });
85
- const rect = { left: -0.5, right: 0.5, bottom: 0, top: 2 };
86
- const points = buildOpeningCutoutShape(window, rect).getPoints();
87
- // Width 1 with top radii summing to 8 → scaled down to 0.5 each.
88
- expect(containsPoint(points, rect.left, rect.top - 0.5)).toBe(true);
89
- expect(containsPoint(points, rect.left + 0.5, rect.top)).toBe(true);
90
- expect(containsPoint(points, rect.left, rect.bottom)).toBe(true);
91
- expect(containsPoint(points, rect.right, rect.bottom)).toBe(true);
92
- });
93
- test('arch profile springs at top - archHeight and peaks at the rect top', () => {
94
- const door = DoorNode.parse({ openingShape: 'arch', archHeight: 0.45 });
95
- const rect = { left: -0.45, right: 0.45, bottom: 0, top: 2.1 };
96
- const points = buildOpeningCutoutShape(door, rect).getPoints();
97
- expect(containsPoint(points, rect.left, rect.bottom)).toBe(true);
98
- expect(containsPoint(points, rect.right, rect.bottom)).toBe(true);
99
- expect(containsPoint(points, rect.right, rect.top - 0.45)).toBe(true);
100
- expect(containsPoint(points, 0, rect.top)).toBe(true);
101
- expect(containsPoint(points, rect.left, rect.top)).toBe(false);
102
- expect(containsPoint(points, rect.right, rect.top)).toBe(false);
103
- });
104
- test('profiles are origin-agnostic — offset rects yield translated points', () => {
105
- const window = WindowNode.parse({ openingShape: 'rounded', cornerRadius: 0.2 });
106
- const centered = buildOpeningCutoutShape(window, {
107
- left: -0.75,
108
- right: 0.75,
109
- bottom: -0.75,
110
- top: 0.75,
111
- }).getPoints();
112
- const offset = buildOpeningCutoutShape(window, {
113
- left: 2.25,
114
- right: 3.75,
115
- bottom: 0.9,
116
- top: 2.4,
117
- }).getPoints();
118
- for (const point of centered) {
119
- expect(containsPoint(offset, point.x + 3, point.y + 1.65)).toBe(true);
120
- }
121
- for (const point of offset) {
122
- expect(containsPoint(centered, point.x - 3, point.y - 1.65)).toBe(true);
123
- }
124
- });
125
- });
126
- describe('buildOpeningCutoutGeometry', () => {
127
- test('extrudes the rect through the depth, centered on the mid-plane', () => {
128
- const door = DoorNode.parse({});
129
- const geometry = buildOpeningCutoutGeometry(door, { left: -0.45, right: 0.45, bottom: -1.05, top: 1.05 }, 0.24, 0.1);
130
- geometry.computeBoundingBox();
131
- // Float32 position buffer → ~1e-7 relative precision.
132
- const box = geometry.boundingBox;
133
- expect(box.min.x).toBeCloseTo(-0.45, 6);
134
- expect(box.max.x).toBeCloseTo(0.45, 6);
135
- expect(box.min.y).toBeCloseTo(-1.05, 6);
136
- expect(box.max.y).toBeCloseTo(1.05, 6);
137
- expect(box.min.z).toBeCloseTo(-0.12, 6);
138
- expect(box.max.z).toBeCloseTo(0.12, 6);
139
- });
140
- });
141
- describe('hasFlatOpeningCutoutBottom', () => {
142
- test('flat for rectangles, arches, and door rounded (top-only radii)', () => {
143
- expect(hasFlatOpeningCutoutBottom(DoorNode.parse({}))).toBe(true);
144
- expect(hasFlatOpeningCutoutBottom(WindowNode.parse({ openingShape: 'arch' }))).toBe(true);
145
- expect(hasFlatOpeningCutoutBottom(DoorNode.parse({ openingShape: 'rounded' }))).toBe(true);
146
- });
147
- test('rounded windows depend on their bottom radii', () => {
148
- expect(hasFlatOpeningCutoutBottom(WindowNode.parse({ openingShape: 'rounded' }))).toBe(false);
149
- expect(hasFlatOpeningCutoutBottom(WindowNode.parse({ openingShape: 'rounded', cornerRadius: 0 }))).toBe(true);
150
- expect(hasFlatOpeningCutoutBottom(WindowNode.parse({
151
- openingShape: 'rounded',
152
- openingRadiusMode: 'individual',
153
- openingCornerRadii: [0.2, 0.2, 0, 0],
154
- }))).toBe(true);
155
- expect(hasFlatOpeningCutoutBottom(WindowNode.parse({
156
- openingShape: 'rounded',
157
- openingRadiusMode: 'individual',
158
- openingCornerRadii: [0.2, 0.2, 0.2, 0.2],
159
- }))).toBe(false);
160
- });
161
- });
162
- describe('getOpeningCutoutBottomPadding', () => {
163
- test('pads floor-level openings with a flat bottom', () => {
164
- expect(getOpeningCutoutBottomPadding(DoorNode.parse({}), 0)).toBe(0.02);
165
- expect(getOpeningCutoutBottomPadding(DoorNode.parse({ openingShape: 'rounded' }), 0)).toBe(0.02);
166
- expect(getOpeningCutoutBottomPadding(WindowNode.parse({ openingShape: 'arch' }), 0)).toBe(0.02);
167
- });
168
- test('does not pad openings above the floor or rounded window bottoms', () => {
169
- expect(getOpeningCutoutBottomPadding(DoorNode.parse({}), 0.9)).toBe(0);
170
- expect(getOpeningCutoutBottomPadding(WindowNode.parse({ openingShape: 'rounded' }), 0)).toBe(0);
171
- });
172
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=wall-opening-cutout.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"wall-opening-cutout.test.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-opening-cutout.test.ts"],"names":[],"mappings":""}
@@ -1,59 +0,0 @@
1
- // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
- // depend on @types/bun so the import type is unresolved at compile time.
3
- import { describe, expect, test } from 'bun:test';
4
- import { calculateLevelMiters, DoorNode, sceneRegistry, WallNode } from '@pascal-app/core';
5
- import * as THREE from 'three';
6
- import { generateExtrudedWall } from './wall-system';
7
- describe('wall opening cutout', () => {
8
- test('cuts a floor-level door directly from node geometry without a proxy mesh', () => {
9
- const wall = WallNode.parse({
10
- id: 'wall_floor-opening-cutout',
11
- start: [0, 0],
12
- end: [2, 0],
13
- height: 2.5,
14
- thickness: 0.1,
15
- });
16
- const door = DoorNode.parse({
17
- id: 'door_floor-opening-cutout',
18
- wallId: wall.id,
19
- position: [1, 1.05, 0],
20
- width: 0.9,
21
- height: 2.1,
22
- });
23
- const wallMesh = new THREE.Mesh();
24
- sceneRegistry.nodes.set(wall.id, wallMesh);
25
- try {
26
- const geometry = generateExtrudedWall(wall, [door], calculateLevelMiters([wall]));
27
- const position = geometry.getAttribute('position');
28
- const index = geometry.index;
29
- const openingLeft = door.position[0] - door.width / 2;
30
- const openingRight = door.position[0] + door.width / 2;
31
- let wallFaceTrianglesInsideOpening = 0;
32
- let baseTrianglesInsideOpening = 0;
33
- for (let offset = 0; offset < (index?.count ?? position.count); offset += 3) {
34
- const indices = [0, 1, 2].map((corner) => index ? index.getX(offset + corner) : offset + corner);
35
- const vertices = indices.map((vertexIndex) => new THREE.Vector3(position.getX(vertexIndex), position.getY(vertexIndex), position.getZ(vertexIndex)));
36
- const centroid = vertices
37
- .reduce((sum, vertex) => sum.add(vertex), new THREE.Vector3())
38
- .multiplyScalar(1 / 3);
39
- const insideOpeningX = centroid.x > openingLeft + 1e-4 && centroid.x < openingRight - 1e-4;
40
- if (!insideOpeningX)
41
- continue;
42
- const onWallFace = vertices.every((vertex) => Math.abs(Math.abs(vertex.z) - (wall.thickness ?? 0.1) / 2) < 1e-5);
43
- if (onWallFace && centroid.y > 1e-4 && centroid.y < door.height - 1e-4) {
44
- wallFaceTrianglesInsideOpening += 1;
45
- }
46
- if (vertices.every((vertex) => Math.abs(vertex.y) < 1e-5)) {
47
- baseTrianglesInsideOpening += 1;
48
- }
49
- }
50
- expect(wallFaceTrianglesInsideOpening).toBe(0);
51
- expect(baseTrianglesInsideOpening).toBe(0);
52
- geometry.dispose();
53
- }
54
- finally {
55
- sceneRegistry.nodes.delete(wall.id);
56
- wallMesh.geometry.dispose();
57
- }
58
- });
59
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=wall-support-extension.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"wall-support-extension.test.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-support-extension.test.ts"],"names":[],"mappings":""}
@@ -1,116 +0,0 @@
1
- // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
- // depend on @types/bun so the import type is unresolved at compile time.
3
- import { describe, expect, test } from 'bun:test';
4
- import { calculateLevelMiters, getWallPlaneTop, WallNode, } from '@pascal-app/core';
5
- import { generateExtrudedWall } from './wall-system';
6
- describe('wall support extension', () => {
7
- test('preserves the raised wall origin and top while filling down to the lower support', () => {
8
- const wall = WallNode.parse({ start: [0, 0], end: [4, 0], height: 2.5, thickness: 0.1 });
9
- const geometry = generateExtrudedWall(wall, [], calculateLevelMiters([wall]), 0.6, 0.05);
10
- geometry.computeBoundingBox();
11
- expect(geometry.boundingBox?.min.y).toBeCloseTo(-0.55);
12
- expect(geometry.boundingBox?.max.y).toBeCloseTo(2.5);
13
- // With mesh.position.y = 0.6, the wall spans world Y=0.05..3.1.
14
- expect((geometry.boundingBox?.min.y ?? 0) + 0.6).toBeCloseTo(0.05);
15
- expect((geometry.boundingBox?.max.y ?? 0) + 0.6).toBeCloseTo(3.1);
16
- geometry.dispose();
17
- });
18
- test('retains the existing negative-slab top constraint', () => {
19
- const wall = WallNode.parse({ start: [0, 0], end: [4, 0], height: 2.5, thickness: 0.1 });
20
- const geometry = generateExtrudedWall(wall, [], calculateLevelMiters([wall]), -0.4, -0.4);
21
- geometry.computeBoundingBox();
22
- expect(geometry.boundingBox?.min.y).toBeCloseTo(0);
23
- expect(geometry.boundingBox?.max.y).toBeCloseTo(2.9);
24
- expect((geometry.boundingBox?.max.y ?? 0) - 0.4).toBeCloseTo(2.5);
25
- geometry.dispose();
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
- });
95
- test('raises only the high-supported part of a mixed wall run', () => {
96
- const wall = WallNode.parse({ start: [0, 0], end: [4, 0], height: 2.5, thickness: 0.1 });
97
- const geometry = generateExtrudedWall(wall, [], calculateLevelMiters([wall]), 0.6, 0.05, [
98
- { start: 0, end: 0.5, elevation: 0.6 },
99
- { start: 0.5, end: 1, elevation: 0.05 },
100
- ]);
101
- const position = geometry.getAttribute('position');
102
- let highSpanMinY = Number.POSITIVE_INFINITY;
103
- let lowSpanMinY = Number.POSITIVE_INFINITY;
104
- for (let index = 0; index < position.count; index++) {
105
- const x = position.getX(index);
106
- const y = position.getY(index);
107
- if (x < 1.9)
108
- highSpanMinY = Math.min(highSpanMinY, y);
109
- if (x > 2.1)
110
- lowSpanMinY = Math.min(lowSpanMinY, y);
111
- }
112
- expect(highSpanMinY).toBeCloseTo(0);
113
- expect(lowSpanMinY).toBeCloseTo(-0.55);
114
- geometry.dispose();
115
- });
116
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=wall-terrain-bottom.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"wall-terrain-bottom.test.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-terrain-bottom.test.ts"],"names":[],"mappings":""}
@@ -1,34 +0,0 @@
1
- // @ts-expect-error — bun:test is provided by the Bun runtime; viewer does not
2
- // depend on @types/bun so the import type is unresolved at compile time.
3
- import { describe, expect, test } from 'bun:test';
4
- import { calculateLevelMiters, WallNode } from '@pascal-app/core';
5
- import { generateExtrudedWall } from './wall-system';
6
- describe('wall terrain bottom', () => {
7
- test('samples both faces independently without changing the authored body height', () => {
8
- const wall = WallNode.parse({
9
- start: [0, 0],
10
- end: [4, 0],
11
- height: 2.5,
12
- thickness: 1,
13
- supportSlabId: 'ground',
14
- fillToTerrain: true,
15
- });
16
- const geometry = generateExtrudedWall(wall, [], calculateLevelMiters([wall]), 1.5, 1.5, undefined, 4, (_x, z) => (z >= 0 ? 0.2 : 0.8));
17
- geometry.computeBoundingBox();
18
- const position = geometry.getAttribute('position');
19
- let positiveFaceMinY = Number.POSITIVE_INFINITY;
20
- let negativeFaceMinY = Number.POSITIVE_INFINITY;
21
- for (let index = 0; index < position.count; index += 1) {
22
- const z = position.getZ(index);
23
- const y = position.getY(index);
24
- if (z > 0.49)
25
- positiveFaceMinY = Math.min(positiveFaceMinY, y);
26
- if (z < -0.49)
27
- negativeFaceMinY = Math.min(negativeFaceMinY, y);
28
- }
29
- expect(positiveFaceMinY).toBeCloseTo(-1.3);
30
- expect(negativeFaceMinY).toBeCloseTo(-0.7);
31
- expect(geometry.boundingBox?.max.y).toBeCloseTo(2.5);
32
- geometry.dispose();
33
- });
34
- });