@pascal-app/viewer 0.9.1 → 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.
Files changed (206) hide show
  1. package/README.md +20 -5
  2. package/dist/components/error-boundary.d.ts +6 -0
  3. package/dist/components/error-boundary.d.ts.map +1 -1
  4. package/dist/components/error-boundary.js +6 -0
  5. package/dist/components/renderers/node-renderer.d.ts +5 -1
  6. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  7. package/dist/components/renderers/node-renderer.js +5 -2
  8. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  9. package/dist/components/renderers/parametric-node-renderer.js +7 -4
  10. package/dist/components/viewer/bvh-ecctrl.d.ts +77 -0
  11. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -0
  12. package/dist/components/viewer/bvh-ecctrl.js +552 -0
  13. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  14. package/dist/components/viewer/frame-limiter.js +59 -4
  15. package/dist/components/viewer/glb-interactive.d.ts +41 -0
  16. package/dist/components/viewer/glb-interactive.d.ts.map +1 -0
  17. package/dist/components/viewer/glb-interactive.js +394 -0
  18. package/dist/components/viewer/glb-reference-nodes.d.ts +29 -0
  19. package/dist/components/viewer/glb-reference-nodes.d.ts.map +1 -0
  20. package/dist/components/viewer/glb-reference-nodes.js +80 -0
  21. package/dist/components/viewer/glb-replace-instances.d.ts +18 -0
  22. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -0
  23. package/dist/components/viewer/glb-replace-instances.js +61 -0
  24. package/dist/components/viewer/glb-scene.d.ts +54 -0
  25. package/dist/components/viewer/glb-scene.d.ts.map +1 -0
  26. package/dist/components/viewer/glb-scene.js +920 -0
  27. package/dist/components/viewer/glb-walkthrough-controller.d.ts +22 -0
  28. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -0
  29. package/dist/components/viewer/glb-walkthrough-controller.js +445 -0
  30. package/dist/components/viewer/index.d.ts +19 -2
  31. package/dist/components/viewer/index.d.ts.map +1 -1
  32. package/dist/components/viewer/index.js +113 -12
  33. package/dist/components/viewer/lights.d.ts.map +1 -1
  34. package/dist/components/viewer/lights.js +36 -5
  35. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  36. package/dist/components/viewer/perf-monitor.js +17 -2
  37. package/dist/components/viewer/post-processing.d.ts +7 -1
  38. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  39. package/dist/components/viewer/post-processing.js +180 -24
  40. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  41. package/dist/components/viewer/registered-systems.js +6 -4
  42. package/dist/components/viewer/scene-bvh.d.ts +2 -1
  43. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-bvh.js +3 -0
  45. package/dist/components/viewer/scene-bvh.test.d.ts +2 -0
  46. package/dist/components/viewer/scene-bvh.test.d.ts.map +1 -0
  47. package/dist/components/viewer/scene-bvh.test.js +13 -0
  48. package/dist/components/viewer/scene-environment.d.ts +2 -0
  49. package/dist/components/viewer/scene-environment.d.ts.map +1 -0
  50. package/dist/components/viewer/scene-environment.js +79 -0
  51. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  52. package/dist/components/viewer/selection-manager.js +23 -12
  53. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  54. package/dist/hooks/use-gltf-ktx2.js +2 -24
  55. package/dist/hooks/use-node-events.d.ts +1 -1
  56. package/dist/hooks/use-node-events.d.ts.map +1 -1
  57. package/dist/hooks/use-node-events.js +22 -14
  58. package/dist/index.d.ts +24 -10
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +29 -13
  61. package/dist/lib/backdrop.d.ts +33 -0
  62. package/dist/lib/backdrop.d.ts.map +1 -0
  63. package/dist/lib/backdrop.js +92 -0
  64. package/dist/lib/box-uv.d.ts +13 -0
  65. package/dist/lib/box-uv.d.ts.map +1 -0
  66. package/dist/lib/box-uv.js +32 -0
  67. package/dist/lib/csg-utils.d.ts +3 -9
  68. package/dist/lib/csg-utils.d.ts.map +1 -1
  69. package/dist/lib/csg-utils.js +65 -5
  70. package/dist/lib/csg-utils.test.d.ts +2 -0
  71. package/dist/lib/csg-utils.test.d.ts.map +1 -0
  72. package/dist/lib/csg-utils.test.js +45 -0
  73. package/dist/lib/edge-style.d.ts +1 -0
  74. package/dist/lib/edge-style.d.ts.map +1 -1
  75. package/dist/lib/edge-style.js +26 -6
  76. package/dist/lib/hero-pose.d.ts +47 -0
  77. package/dist/lib/hero-pose.d.ts.map +1 -0
  78. package/dist/lib/hero-pose.js +227 -0
  79. package/dist/lib/ink-edges.d.ts +1 -1
  80. package/dist/lib/ink-edges.d.ts.map +1 -1
  81. package/dist/lib/ink-edges.js +13 -7
  82. package/dist/lib/ktx2-loader.d.ts +46 -0
  83. package/dist/lib/ktx2-loader.d.ts.map +1 -0
  84. package/dist/lib/ktx2-loader.js +142 -0
  85. package/dist/lib/layers.d.ts +10 -0
  86. package/dist/lib/layers.d.ts.map +1 -1
  87. package/dist/lib/layers.js +10 -0
  88. package/dist/lib/materials.d.ts +16 -1
  89. package/dist/lib/materials.d.ts.map +1 -1
  90. package/dist/lib/materials.js +158 -34
  91. package/dist/lib/materials.test.d.ts +2 -0
  92. package/dist/lib/materials.test.d.ts.map +1 -0
  93. package/dist/lib/materials.test.js +25 -0
  94. package/dist/lib/merged-outline-node.test.d.ts +2 -0
  95. package/dist/lib/merged-outline-node.test.d.ts.map +1 -0
  96. package/dist/lib/merged-outline-node.test.js +22 -0
  97. package/dist/lib/scene-themes.d.ts +1 -0
  98. package/dist/lib/scene-themes.d.ts.map +1 -1
  99. package/dist/lib/scene-themes.js +25 -16
  100. package/dist/lib/shadow-only.d.ts +4 -0
  101. package/dist/lib/shadow-only.d.ts.map +1 -0
  102. package/dist/lib/shadow-only.js +36 -0
  103. package/dist/lib/snapshot-pipeline.d.ts +42 -0
  104. package/dist/lib/snapshot-pipeline.d.ts.map +1 -0
  105. package/dist/lib/snapshot-pipeline.js +254 -0
  106. package/dist/lib/terrain-perimeter-fill.d.ts +7 -0
  107. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -0
  108. package/dist/lib/terrain-perimeter-fill.js +75 -0
  109. package/dist/lib/texture-node-guard.d.ts +13 -0
  110. package/dist/lib/texture-node-guard.d.ts.map +1 -0
  111. package/dist/lib/texture-node-guard.js +39 -0
  112. package/dist/lib/texture-reference.d.ts +31 -0
  113. package/dist/lib/texture-reference.d.ts.map +1 -0
  114. package/dist/lib/texture-reference.js +139 -0
  115. package/dist/lib/texture-reference.test.d.ts +2 -0
  116. package/dist/lib/texture-reference.test.d.ts.map +1 -0
  117. package/dist/lib/texture-reference.test.js +86 -0
  118. package/dist/lib/tsl-compat.d.ts +12 -0
  119. package/dist/lib/tsl-compat.d.ts.map +1 -0
  120. package/dist/lib/tsl-compat.js +13 -0
  121. package/dist/store/use-viewer.d.ts +73 -18
  122. package/dist/store/use-viewer.d.ts.map +1 -1
  123. package/dist/store/use-viewer.js +191 -4
  124. package/dist/store/use-viewer.test.d.ts +2 -0
  125. package/dist/store/use-viewer.test.d.ts.map +1 -0
  126. package/dist/store/use-viewer.test.js +61 -0
  127. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  128. package/dist/systems/ceiling/ceiling-system.js +12 -5
  129. package/dist/systems/door/door-animation.test.d.ts +2 -0
  130. package/dist/systems/door/door-animation.test.d.ts.map +1 -0
  131. package/dist/systems/door/door-animation.test.js +51 -0
  132. package/dist/systems/door/door-floor-alignment.test.d.ts +2 -0
  133. package/dist/systems/door/door-floor-alignment.test.d.ts.map +1 -0
  134. package/dist/systems/door/door-floor-alignment.test.js +50 -0
  135. package/dist/systems/door/door-system.d.ts +24 -0
  136. package/dist/systems/door/door-system.d.ts.map +1 -1
  137. package/dist/systems/door/door-system.js +494 -161
  138. package/dist/systems/fence/fence-system.d.ts +17 -0
  139. package/dist/systems/fence/fence-system.d.ts.map +1 -1
  140. package/dist/systems/fence/fence-system.js +221 -53
  141. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  142. package/dist/systems/floor-elevation/floor-elevation-system.js +30 -7
  143. package/dist/systems/geometry/geometry-system.d.ts +7 -0
  144. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  145. package/dist/systems/geometry/geometry-system.js +95 -26
  146. package/dist/systems/geometry/geometry-system.test.d.ts +2 -0
  147. package/dist/systems/geometry/geometry-system.test.d.ts.map +1 -0
  148. package/dist/systems/geometry/geometry-system.test.js +30 -0
  149. package/dist/systems/interactive/control-widget.d.ts +10 -0
  150. package/dist/systems/interactive/control-widget.d.ts.map +1 -0
  151. package/dist/systems/interactive/control-widget.js +35 -0
  152. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  153. package/dist/systems/interactive/interactive-system.js +55 -56
  154. package/dist/systems/item/item-system.d.ts.map +1 -1
  155. package/dist/systems/item/item-system.js +20 -6
  156. package/dist/systems/level/level-system.d.ts.map +1 -1
  157. package/dist/systems/level/level-system.js +34 -9
  158. package/dist/systems/level/level-utils.d.ts +0 -3
  159. package/dist/systems/level/level-utils.d.ts.map +1 -1
  160. package/dist/systems/level/level-utils.js +7 -45
  161. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  162. package/dist/systems/roof/roof-materials.js +29 -8
  163. package/dist/systems/roof/roof-system.d.ts +21 -8
  164. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  165. package/dist/systems/roof/roof-system.js +1079 -221
  166. package/dist/systems/slab/slab-system.d.ts +7 -4
  167. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  168. package/dist/systems/slab/slab-system.js +31 -83
  169. package/dist/systems/slab/slab-system.test.js +54 -14
  170. package/dist/systems/stair/stair-system.js +7 -4
  171. package/dist/systems/wall/opening-cutout-geometry.d.ts +31 -0
  172. package/dist/systems/wall/opening-cutout-geometry.d.ts.map +1 -0
  173. package/dist/systems/wall/opening-cutout-geometry.js +167 -0
  174. package/dist/systems/wall/opening-cutout-geometry.test.d.ts +2 -0
  175. package/dist/systems/wall/opening-cutout-geometry.test.d.ts.map +1 -0
  176. package/dist/systems/wall/opening-cutout-geometry.test.js +172 -0
  177. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  178. package/dist/systems/wall/wall-cutout.js +49 -17
  179. package/dist/systems/wall/wall-materials.d.ts +11 -7
  180. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  181. package/dist/systems/wall/wall-materials.js +244 -35
  182. package/dist/systems/wall/wall-opening-cutout.test.d.ts +2 -0
  183. package/dist/systems/wall/wall-opening-cutout.test.d.ts.map +1 -0
  184. package/dist/systems/wall/wall-opening-cutout.test.js +59 -0
  185. package/dist/systems/wall/wall-support-extension.test.d.ts +2 -0
  186. package/dist/systems/wall/wall-support-extension.test.d.ts.map +1 -0
  187. package/dist/systems/wall/wall-support-extension.test.js +116 -0
  188. package/dist/systems/wall/wall-system.d.ts +4 -8
  189. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  190. package/dist/systems/wall/wall-system.js +397 -177
  191. package/dist/systems/wall/wall-terrain-bottom.test.d.ts +2 -0
  192. package/dist/systems/wall/wall-terrain-bottom.test.d.ts.map +1 -0
  193. package/dist/systems/wall/wall-terrain-bottom.test.js +34 -0
  194. package/dist/systems/window/window-animation-system.d.ts +12 -0
  195. package/dist/systems/window/window-animation-system.d.ts.map +1 -1
  196. package/dist/systems/window/window-animation-system.js +16 -5
  197. package/dist/systems/window/window-system.d.ts +7 -0
  198. package/dist/systems/window/window-system.d.ts.map +1 -1
  199. package/dist/systems/window/window-system.js +240 -14
  200. package/package.json +4 -4
  201. package/dist/components/viewer/ground-occluder.d.ts +0 -2
  202. package/dist/components/viewer/ground-occluder.d.ts.map +0 -1
  203. package/dist/components/viewer/ground-occluder.js +0 -67
  204. package/dist/components/viewer/walkthrough-controls.d.ts +0 -2
  205. package/dist/components/viewer/walkthrough-controls.d.ts.map +0 -1
  206. package/dist/components/viewer/walkthrough-controls.js +0 -131
@@ -0,0 +1,172 @@
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 +1 @@
1
- {"version":3,"file":"wall-cutout.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-cutout.tsx"],"names":[],"mappings":"AAsCA,eAAO,MAAM,UAAU,YAwItB,CAAA"}
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,9 +1,9 @@
1
- import { emitter, 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';
5
5
  import useViewer from '../../store/use-viewer';
6
- import { getMaterialsForWall } from './wall-materials';
6
+ import { getMaterialsForWall, getSelectionHighlightMaterials, getWallMaterialHash, } from './wall-materials';
7
7
  const tmpVec = new Vector3();
8
8
  const u = new Vector3();
9
9
  const v = new Vector3();
@@ -28,6 +28,9 @@ function getWallHideState(wallNode, wallMesh, wallMode, cameraDir) {
28
28
  }
29
29
  return hideWall;
30
30
  }
31
+ function sameMaterialArray(a, b) {
32
+ return Array.isArray(a) && a.length === b.length && a.every((material, i) => material === b[i]);
33
+ }
31
34
  export const WallCutout = () => {
32
35
  const lastCameraPosition = useRef(new Vector3());
33
36
  const lastCameraTarget = useRef(new Vector3());
@@ -36,6 +39,7 @@ export const WallCutout = () => {
36
39
  const lastShading = useRef(useViewer.getState().shading);
37
40
  const lastNumberOfWalls = useRef(0);
38
41
  const lastHighlightKey = useRef('');
42
+ const lastWallAppearanceKey = useRef('');
39
43
  const lastTextures = useRef(useViewer.getState().textures);
40
44
  const lastColorPreset = useRef(useViewer.getState().colorPreset);
41
45
  const lastSceneTheme = useRef(useViewer.getState().sceneTheme);
@@ -49,17 +53,27 @@ export const WallCutout = () => {
49
53
  const previewSelectedIds = useViewer.getState().previewSelectedIds;
50
54
  const hoveredId = useViewer.getState().hoveredId;
51
55
  const hoverHighlightMode = useViewer.getState().hoverHighlightMode;
56
+ const sceneState = useScene.getState();
52
57
  const currentTime = clock.elapsedTime;
53
58
  const currentCameraPosition = camera.position;
54
59
  camera.getWorldDirection(tmpVec);
55
60
  tmpVec.add(currentCameraPosition);
56
- const highlightedWallIds = new Set([...selectedIds, ...previewSelectedIds].filter((id) => useScene.getState().nodes[id]?.type === 'wall'));
61
+ const highlightedWallIds = new Set([...selectedIds, ...previewSelectedIds].filter((id) => sceneState.nodes[id]?.type === 'wall'));
57
62
  const deleteHoveredWallId = hoverHighlightMode === 'delete' &&
58
63
  hoveredId &&
59
- useScene.getState().nodes[hoveredId]?.type === 'wall'
64
+ sceneState.nodes[hoveredId]?.type === 'wall'
60
65
  ? hoveredId
61
66
  : null;
62
67
  const highlightKey = `${Array.from(highlightedWallIds).sort().join('|')}::${deleteHoveredWallId ?? ''}`;
68
+ const wallAppearanceKey = Array.from(sceneRegistry.byType.wall)
69
+ .sort()
70
+ .map((wallId) => {
71
+ const wallNode = sceneState.nodes[wallId];
72
+ if (wallNode?.type !== 'wall')
73
+ return `${wallId}:missing`;
74
+ return `${wallId}:${getWallMaterialHash(wallNode, shading, sceneState.materials)}:${JSON.stringify(wallNode.faceBands ?? null)}`;
75
+ })
76
+ .join('|');
63
77
  const distanceMoved = currentCameraPosition.distanceTo(lastCameraPosition.current);
64
78
  const directionChanged = tmpVec.distanceTo(lastCameraTarget.current);
65
79
  const timeSinceUpdate = currentTime - lastUpdateTime.current;
@@ -70,7 +84,8 @@ export const WallCutout = () => {
70
84
  lastColorPreset.current !== colorPreset ||
71
85
  lastSceneTheme.current !== sceneTheme ||
72
86
  sceneRegistry.byType.wall.size !== lastNumberOfWalls.current ||
73
- lastHighlightKey.current !== highlightKey) {
87
+ lastHighlightKey.current !== highlightKey ||
88
+ lastWallAppearanceKey.current !== wallAppearanceKey) {
74
89
  lastCameraPosition.current.copy(currentCameraPosition);
75
90
  lastCameraTarget.current.copy(tmpVec);
76
91
  lastUpdateTime.current = currentTime;
@@ -80,27 +95,39 @@ export const WallCutout = () => {
80
95
  const wallMesh = sceneRegistry.nodes.get(wallId);
81
96
  if (!wallMesh)
82
97
  return;
83
- const wallNode = useScene.getState().nodes[wallId];
84
- if (!wallNode || wallNode.type !== 'wall')
98
+ const wallNode = sceneState.nodes[wallId];
99
+ if (wallNode?.type !== 'wall')
85
100
  return;
86
101
  const hideWall = getWallHideState(wallNode, wallMesh, wallMode, u);
87
102
  const isDeleteHighlighted = deleteHoveredWallId === wallId;
88
103
  const isSelectionHighlighted = !isDeleteHighlighted && highlightedWallIds.has(wallId);
89
- const materials = getMaterialsForWall(wallNode, shading, textures, colorPreset, sceneTheme);
90
- if (hideWall) {
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;
108
+ const materials = getMaterialsForWall(wallNode, shading, textures, colorPreset, sceneTheme, sceneState.materials);
109
+ if (wallMode === 'translucent') {
110
+ ;
111
+ wallMesh.material = isDeleteHighlighted
112
+ ? materials.deleteTranslucent
113
+ : shouldSelectionHighlight
114
+ ? getSelectionHighlightMaterials(materials.translucent)
115
+ : materials.translucent;
116
+ }
117
+ else if (hideWall) {
91
118
  ;
92
119
  wallMesh.material = isDeleteHighlighted
93
120
  ? materials.deleteInvisible
94
- : isSelectionHighlighted
95
- ? materials.highlightedInvisible
121
+ : shouldSelectionHighlight
122
+ ? getSelectionHighlightMaterials(materials.invisible)
96
123
  : materials.invisible;
97
124
  }
98
125
  else {
99
126
  ;
100
127
  wallMesh.material = isDeleteHighlighted
101
128
  ? materials.deleteVisible
102
- : isSelectionHighlighted
103
- ? materials.highlightedVisible
129
+ : shouldSelectionHighlight
130
+ ? getSelectionHighlightMaterials(materials.visible)
104
131
  : materials.visible;
105
132
  }
106
133
  });
@@ -111,6 +138,7 @@ export const WallCutout = () => {
111
138
  lastSceneTheme.current = sceneTheme;
112
139
  lastNumberOfWalls.current = sceneRegistry.byType.wall.size;
113
140
  lastHighlightKey.current = highlightKey;
141
+ lastWallAppearanceKey.current = wallAppearanceKey;
114
142
  }
115
143
  });
116
144
  useEffect(() => {
@@ -121,17 +149,21 @@ export const WallCutout = () => {
121
149
  if (!wallMesh)
122
150
  return;
123
151
  const wallNode = useScene.getState().nodes[wallId];
124
- if (!wallNode || wallNode.type !== 'wall')
152
+ if (wallNode?.type !== 'wall')
125
153
  return;
126
- const mats = getMaterialsForWall(wallNode, useViewer.getState().shading, useViewer.getState().textures, useViewer.getState().colorPreset, useViewer.getState().sceneTheme);
154
+ const mats = getMaterialsForWall(wallNode, useViewer.getState().shading, useViewer.getState().textures, useViewer.getState().colorPreset, useViewer.getState().sceneTheme, useScene.getState().materials);
127
155
  const current = wallMesh.material;
128
156
  snapshot.set(wallMesh, current);
129
- if (current === mats.highlightedVisible || current === mats.deleteVisible) {
157
+ if (current === mats.deleteVisible) {
130
158
  wallMesh.material = mats.visible;
131
159
  }
132
- else if (current === mats.highlightedInvisible || current === mats.deleteInvisible) {
160
+ else if (current === mats.deleteInvisible) {
133
161
  wallMesh.material = mats.invisible;
134
162
  }
163
+ else if (current === mats.deleteTranslucent ||
164
+ sameMaterialArray(current, getSelectionHighlightMaterials(mats.translucent))) {
165
+ wallMesh.material = mats.translucent;
166
+ }
135
167
  });
136
168
  };
137
169
  const reapplyAfterCapture = () => {
@@ -1,17 +1,21 @@
1
- import { type WallNode } from '@pascal-app/core';
1
+ import { type SceneMaterial, type SceneMaterialId, type WallNode } from '@pascal-app/core';
2
2
  import { type Material } from 'three';
3
3
  import { type ColorPreset, type RenderShading } from '../../lib/materials';
4
- export type WallMaterialArray = [Material, Material, Material];
4
+ type SceneMaterials = Record<SceneMaterialId, SceneMaterial> | undefined;
5
+ export type WallMaterialArray = Material[];
5
6
  export interface WallMaterials {
6
7
  visible: WallMaterialArray;
7
8
  invisible: WallMaterialArray;
9
+ translucent: WallMaterialArray;
8
10
  deleteVisible: WallMaterialArray;
9
11
  deleteInvisible: WallMaterialArray;
10
- highlightedVisible: WallMaterialArray;
11
- highlightedInvisible: WallMaterialArray;
12
+ deleteTranslucent: WallMaterialArray;
12
13
  materialHash: string;
13
14
  }
14
- export declare function getWallMaterialHash(wallNode: WallNode, shading: RenderShading): string;
15
- export declare function getMaterialsForWall(wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string): WallMaterials;
16
- export declare function getVisibleWallMaterials(wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string): WallMaterialArray;
15
+ /** Lazy light-emissive selection variant of a wall's material array (keeps texture). */
16
+ export declare function getSelectionHighlightMaterials(materials: WallMaterialArray): WallMaterialArray;
17
+ export declare function getWallMaterialHash(wallNode: WallNode, shading: RenderShading, sceneMaterials?: SceneMaterials): string;
18
+ export declare function getMaterialsForWall(wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string, sceneMaterials?: SceneMaterials): WallMaterials;
19
+ export declare function getVisibleWallMaterials(wallNode: WallNode, shading?: RenderShading, textures?: boolean, colorPreset?: ColorPreset, sceneTheme?: string, sceneMaterials?: SceneMaterials): WallMaterialArray;
20
+ export {};
17
21
  //# sourceMappingURL=wall-materials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,QAAQ,EAEd,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAG5C,OAAO,EAEL,KAAK,WAAW,EAIhB,KAAK,aAAa,EAEnB,MAAM,qBAAqB,CAAA;AAqB5B,MAAM,MAAM,iBAAiB,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAE9D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,kBAAkB,EAAE,iBAAiB,CAAA;IACrC,oBAAoB,EAAE,iBAAiB,CAAA;IACvC,YAAY,EAAE,MAAM,CAAA;CACrB;AA6HD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,aAAa,GAAG,MAAM,CAUtF;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,GAClB,aAAa,CA6Ef;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,GAClB,iBAAiB,CAEnB"}
1
+ {"version":3,"file":"wall-materials.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-materials.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAClB,KAAK,eAAe,EAGpB,KAAK,QAAQ,EAId,MAAM,kBAAkB,CAAA;AACzB,OAAO,EAAS,KAAK,QAAQ,EAAE,MAAM,OAAO,CAAA;AAG5C,OAAO,EAEL,KAAK,WAAW,EAKhB,KAAK,aAAa,EAGnB,MAAM,qBAAqB,CAAA;AAE5B,KAAK,cAAc,GAAG,MAAM,CAAC,eAAe,EAAE,aAAa,CAAC,GAAG,SAAS,CAAA;AAexE,MAAM,MAAM,iBAAiB,GAAG,QAAQ,EAAE,CAAA;AAE1C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,iBAAiB,CAAA;IAC1B,SAAS,EAAE,iBAAiB,CAAA;IAC5B,WAAW,EAAE,iBAAiB,CAAA;IAC9B,aAAa,EAAE,iBAAiB,CAAA;IAChC,eAAe,EAAE,iBAAiB,CAAA;IAClC,iBAAiB,EAAE,iBAAiB,CAAA;IACpC,YAAY,EAAE,MAAM,CAAA;CACrB;AAiSD,wFAAwF;AACxF,wBAAgB,8BAA8B,CAAC,SAAS,EAAE,iBAAiB,GAAG,iBAAiB,CAE9F;AA4DD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,aAAa,EACtB,cAAc,CAAC,EAAE,cAAc,GAC9B,MAAM,CAcR;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,GAC9B,aAAa,CAgIf;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,QAAQ,EAClB,OAAO,GAAE,aAA0B,EACnC,QAAQ,UAAO,EACf,WAAW,GAAE,WAAoB,EACjC,UAAU,CAAC,EAAE,MAAM,EACnB,cAAc,CAAC,EAAE,cAAc,GAC9B,iBAAiB,CAGnB"}