@pascal-app/viewer 1.0.7 → 1.1.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 (270) hide show
  1. package/README.md +209 -19
  2. package/dist/components/viewer/drone-controller.d.ts.map +1 -1
  3. package/dist/components/viewer/drone-controller.js +1 -5
  4. package/dist/components/viewer/first-person/build-collider-world.d.ts +14 -0
  5. package/dist/components/viewer/first-person/build-collider-world.d.ts.map +1 -0
  6. package/dist/components/viewer/first-person/build-collider-world.js +426 -0
  7. package/dist/components/viewer/first-person/ground-holes.d.ts +22 -0
  8. package/dist/components/viewer/first-person/ground-holes.d.ts.map +1 -0
  9. package/dist/components/viewer/first-person/ground-holes.js +116 -0
  10. package/dist/components/viewer/first-person/mechanism-targets.d.ts +27 -0
  11. package/dist/components/viewer/first-person/mechanism-targets.d.ts.map +1 -0
  12. package/dist/components/viewer/first-person/mechanism-targets.js +79 -0
  13. package/dist/components/viewer/first-person/terrain-collider.d.ts +32 -0
  14. package/dist/components/viewer/first-person/terrain-collider.d.ts.map +1 -0
  15. package/dist/components/viewer/first-person/terrain-collider.js +175 -0
  16. package/dist/components/viewer/glb-replace-instances.d.ts +6 -3
  17. package/dist/components/viewer/glb-replace-instances.d.ts.map +1 -1
  18. package/dist/components/viewer/glb-replace-instances.js +5 -4
  19. package/dist/components/viewer/glb-scene.d.ts +3 -1
  20. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  21. package/dist/components/viewer/glb-scene.js +2 -1
  22. package/dist/components/viewer/glb-walkthrough-controller.d.ts +1 -1
  23. package/dist/components/viewer/glb-walkthrough-controller.d.ts.map +1 -1
  24. package/dist/components/viewer/glb-walkthrough-controller.js +3 -4
  25. package/dist/components/viewer/index.d.ts.map +1 -1
  26. package/dist/components/viewer/index.js +8 -4
  27. package/dist/components/viewer/live-walkthrough-controller.d.ts +17 -0
  28. package/dist/components/viewer/live-walkthrough-controller.d.ts.map +1 -0
  29. package/dist/components/viewer/live-walkthrough-controller.js +1449 -0
  30. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  31. package/dist/components/viewer/post-processing.js +22 -1
  32. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  33. package/dist/components/viewer/registered-systems.js +2 -2
  34. package/dist/components/viewer/unsupported-gpu-fallback.d.ts.map +1 -1
  35. package/dist/components/viewer/unsupported-gpu-fallback.js +21 -1
  36. package/dist/components/viewer/use-daylight-rooms.d.ts.map +1 -1
  37. package/dist/components/viewer/use-daylight-rooms.js +75 -33
  38. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  39. package/dist/hooks/use-gltf-ktx2.js +2 -1
  40. package/dist/index.d.ts +8 -1
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +8 -1
  43. package/dist/lib/asset-url.d.ts +0 -1
  44. package/dist/lib/asset-url.d.ts.map +1 -1
  45. package/dist/lib/asset-url.js +3 -3
  46. package/dist/lib/daylight/room-context.d.ts.map +1 -1
  47. package/dist/lib/daylight/room-context.js +3 -2
  48. package/dist/lib/daylight/room-map.d.ts +149 -0
  49. package/dist/lib/daylight/room-map.d.ts.map +1 -0
  50. package/dist/lib/daylight/room-map.js +813 -0
  51. package/dist/lib/daylight/room-scene.d.ts +4 -2
  52. package/dist/lib/daylight/room-scene.d.ts.map +1 -1
  53. package/dist/lib/daylight/room-scene.js +81 -40
  54. package/dist/lib/daylight/rooms.d.ts +71 -32
  55. package/dist/lib/daylight/rooms.d.ts.map +1 -1
  56. package/dist/lib/daylight/rooms.js +371 -286
  57. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  58. package/dist/lib/ktx2-loader.js +2 -1
  59. package/dist/lib/measurement-label.d.ts +4 -0
  60. package/dist/lib/measurement-label.d.ts.map +1 -0
  61. package/dist/lib/measurement-label.js +54 -0
  62. package/dist/lib/measurement-presentation.d.ts +7 -0
  63. package/dist/lib/measurement-presentation.d.ts.map +1 -0
  64. package/dist/lib/measurement-presentation.js +14 -0
  65. package/dist/lib/pointer-events.d.ts.map +1 -1
  66. package/dist/lib/pointer-events.js +3 -3
  67. package/dist/lib/texture-reference.d.ts.map +1 -1
  68. package/dist/lib/texture-reference.js +4 -3
  69. package/dist/lib/typing-target.d.ts +2 -0
  70. package/dist/lib/typing-target.d.ts.map +1 -0
  71. package/dist/lib/typing-target.js +7 -0
  72. package/dist/lib/viewer-config.d.ts +27 -0
  73. package/dist/lib/viewer-config.d.ts.map +1 -0
  74. package/dist/lib/viewer-config.js +23 -0
  75. package/dist/lib/viewer-preference-storage.d.ts +7 -0
  76. package/dist/lib/viewer-preference-storage.d.ts.map +1 -0
  77. package/dist/lib/viewer-preference-storage.js +23 -0
  78. package/dist/sfx/audio-settings.d.ts +81 -0
  79. package/dist/sfx/audio-settings.d.ts.map +1 -0
  80. package/dist/sfx/audio-settings.js +166 -0
  81. package/dist/sfx/index.d.ts +6 -0
  82. package/dist/sfx/index.d.ts.map +1 -0
  83. package/dist/sfx/index.js +5 -0
  84. package/dist/sfx/interaction-sounds.d.ts +7 -0
  85. package/dist/sfx/interaction-sounds.d.ts.map +1 -0
  86. package/dist/sfx/interaction-sounds.js +39 -0
  87. package/dist/sfx/player.d.ts +31 -0
  88. package/dist/sfx/player.d.ts.map +1 -0
  89. package/dist/sfx/player.js +255 -0
  90. package/dist/sfx/sounds.d.ts +60 -0
  91. package/dist/sfx/sounds.d.ts.map +1 -0
  92. package/dist/sfx/sounds.js +25 -0
  93. package/dist/sfx/use-viewer-ui-sounds.d.ts +8 -0
  94. package/dist/sfx/use-viewer-ui-sounds.d.ts.map +1 -0
  95. package/dist/sfx/use-viewer-ui-sounds.js +42 -0
  96. package/dist/store/use-first-person-hud.d.ts +13 -0
  97. package/dist/store/use-first-person-hud.d.ts.map +1 -0
  98. package/dist/store/use-first-person-hud.js +21 -0
  99. package/dist/store/use-viewer.d.ts +2 -56
  100. package/dist/store/use-viewer.d.ts.map +1 -1
  101. package/dist/store/use-viewer.js +3 -1
  102. package/dist/systems/elevator/elevator-interaction-system.d.ts +16 -2
  103. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  104. package/dist/systems/elevator/elevator-interaction-system.js +41 -8
  105. package/dist/systems/geometry/geometry-status.d.ts +8 -0
  106. package/dist/systems/geometry/geometry-status.d.ts.map +1 -0
  107. package/dist/systems/geometry/geometry-status.js +24 -0
  108. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  109. package/dist/systems/roof/roof-system.js +134 -78
  110. package/dist/systems/wall/wall-system.d.ts +11 -0
  111. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  112. package/dist/systems/wall/wall-system.js +56 -10
  113. package/dist/ui/components/action-button.d.ts +14 -0
  114. package/dist/ui/components/action-button.d.ts.map +1 -0
  115. package/dist/ui/components/action-button.js +21 -0
  116. package/dist/ui/components/action-menu-placement.d.ts +7 -0
  117. package/dist/ui/components/action-menu-placement.d.ts.map +1 -0
  118. package/dist/ui/components/action-menu-placement.js +10 -0
  119. package/dist/ui/components/floorplan-compass-button.d.ts +10 -0
  120. package/dist/ui/components/floorplan-compass-button.d.ts.map +1 -0
  121. package/dist/ui/components/floorplan-compass-button.js +22 -0
  122. package/dist/ui/components/follow-pascal-toggle.d.ts +6 -0
  123. package/dist/ui/components/follow-pascal-toggle.d.ts.map +1 -0
  124. package/dist/ui/components/follow-pascal-toggle.js +21 -0
  125. package/dist/ui/components/localized-html.d.ts +9 -0
  126. package/dist/ui/components/localized-html.d.ts.map +1 -0
  127. package/dist/ui/components/localized-html.js +16 -0
  128. package/dist/ui/components/render-settings-menu.d.ts +9 -0
  129. package/dist/ui/components/render-settings-menu.d.ts.map +1 -0
  130. package/dist/ui/components/render-settings-menu.js +145 -0
  131. package/dist/ui/components/rooms/room-highlight.d.ts +10 -0
  132. package/dist/ui/components/rooms/room-highlight.d.ts.map +1 -0
  133. package/dist/ui/components/rooms/room-highlight.js +56 -0
  134. package/dist/ui/components/rooms/room-labels.d.ts +278 -0
  135. package/dist/ui/components/rooms/room-labels.d.ts.map +1 -0
  136. package/dist/ui/components/rooms/room-labels.js +1295 -0
  137. package/dist/ui/components/rooms/viewer-rooms.d.ts +14 -0
  138. package/dist/ui/components/rooms/viewer-rooms.d.ts.map +1 -0
  139. package/dist/ui/components/rooms/viewer-rooms.js +123 -0
  140. package/dist/ui/components/scene-loader.d.ts +18 -0
  141. package/dist/ui/components/scene-loader.d.ts.map +1 -0
  142. package/dist/ui/components/scene-loader.js +29 -0
  143. package/dist/ui/components/sky-control.d.ts +37 -0
  144. package/dist/ui/components/sky-control.d.ts.map +1 -0
  145. package/dist/ui/components/sky-control.js +382 -0
  146. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts +4 -0
  147. package/dist/ui/components/use-viewer-camera-navigation-sync.d.ts.map +1 -0
  148. package/dist/ui/components/use-viewer-camera-navigation-sync.js +117 -0
  149. package/dist/ui/components/viewer-controls-bar.d.ts +20 -0
  150. package/dist/ui/components/viewer-controls-bar.d.ts.map +1 -0
  151. package/dist/ui/components/viewer-controls-bar.js +130 -0
  152. package/dist/ui/components/viewer-hover-label.d.ts +14 -0
  153. package/dist/ui/components/viewer-hover-label.d.ts.map +1 -0
  154. package/dist/ui/components/viewer-hover-label.js +28 -0
  155. package/dist/ui/components/viewer-scene-header.d.ts +34 -0
  156. package/dist/ui/components/viewer-scene-header.d.ts.map +1 -0
  157. package/dist/ui/components/viewer-scene-header.js +92 -0
  158. package/dist/ui/components/viewer-selection-manager.d.ts +7 -0
  159. package/dist/ui/components/viewer-selection-manager.d.ts.map +1 -0
  160. package/dist/ui/components/viewer-selection-manager.js +116 -0
  161. package/dist/ui/components/viewer-stage-modes.d.ts +7 -0
  162. package/dist/ui/components/viewer-stage-modes.d.ts.map +1 -0
  163. package/dist/ui/components/viewer-stage-modes.js +42 -0
  164. package/dist/ui/components/viewer-stage-switcher.d.ts +10 -0
  165. package/dist/ui/components/viewer-stage-switcher.d.ts.map +1 -0
  166. package/dist/ui/components/viewer-stage-switcher.js +16 -0
  167. package/dist/ui/components/viewer-stage.d.ts +25 -0
  168. package/dist/ui/components/viewer-stage.d.ts.map +1 -0
  169. package/dist/ui/components/viewer-stage.js +107 -0
  170. package/dist/ui/components/walkthrough-hud.d.ts +29 -0
  171. package/dist/ui/components/walkthrough-hud.d.ts.map +1 -0
  172. package/dist/ui/components/walkthrough-hud.js +55 -0
  173. package/dist/ui/floorplan/floorplan-camera-sync.d.ts +22 -0
  174. package/dist/ui/floorplan/floorplan-camera-sync.d.ts.map +1 -0
  175. package/dist/ui/floorplan/floorplan-camera-sync.js +157 -0
  176. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts +61 -0
  177. package/dist/ui/floorplan/floorplan-dimension-renderer.d.ts.map +1 -0
  178. package/dist/ui/floorplan/floorplan-dimension-renderer.js +280 -0
  179. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts +54 -0
  180. package/dist/ui/floorplan/floorplan-geometry-renderer.d.ts.map +1 -0
  181. package/dist/ui/floorplan/floorplan-geometry-renderer.js +317 -0
  182. package/dist/ui/floorplan/floorplan-label-angle.d.ts +24 -0
  183. package/dist/ui/floorplan/floorplan-label-angle.d.ts.map +1 -0
  184. package/dist/ui/floorplan/floorplan-label-angle.js +60 -0
  185. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts +18 -0
  186. package/dist/ui/floorplan/floorplan-navigation-sync.d.ts.map +1 -0
  187. package/dist/ui/floorplan/floorplan-navigation-sync.js +41 -0
  188. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts +26 -0
  189. package/dist/ui/floorplan/floorplan-preview-geometry.d.ts.map +1 -0
  190. package/dist/ui/floorplan/floorplan-preview-geometry.js +173 -0
  191. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts +22 -0
  192. package/dist/ui/floorplan/floorplan-preview-navigation.d.ts.map +1 -0
  193. package/dist/ui/floorplan/floorplan-preview-navigation.js +41 -0
  194. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts +8 -0
  195. package/dist/ui/floorplan/floorplan-preview-visibility.d.ts.map +1 -0
  196. package/dist/ui/floorplan/floorplan-preview-visibility.js +21 -0
  197. package/dist/ui/floorplan/floorplan-preview.d.ts +19 -0
  198. package/dist/ui/floorplan/floorplan-preview.d.ts.map +1 -0
  199. package/dist/ui/floorplan/floorplan-preview.js +700 -0
  200. package/dist/ui/hooks/use-mobile.d.ts +2 -0
  201. package/dist/ui/hooks/use-mobile.d.ts.map +1 -0
  202. package/dist/ui/hooks/use-mobile.js +14 -0
  203. package/dist/ui/index.d.ts +49 -0
  204. package/dist/ui/index.d.ts.map +1 -0
  205. package/dist/ui/index.js +48 -0
  206. package/dist/ui/lib/below-grade.d.ts +38 -0
  207. package/dist/ui/lib/below-grade.d.ts.map +1 -0
  208. package/dist/ui/lib/below-grade.js +102 -0
  209. package/dist/ui/lib/camera-pose.d.ts +19 -0
  210. package/dist/ui/lib/camera-pose.d.ts.map +1 -0
  211. package/dist/ui/lib/camera-pose.js +113 -0
  212. package/dist/ui/lib/cn.d.ts +3 -0
  213. package/dist/ui/lib/cn.d.ts.map +1 -0
  214. package/dist/ui/lib/cn.js +5 -0
  215. package/dist/ui/lib/level-name.d.ts +10 -0
  216. package/dist/ui/lib/level-name.d.ts.map +1 -0
  217. package/dist/ui/lib/level-name.js +33 -0
  218. package/dist/ui/lib/node-display-label.d.ts +8 -0
  219. package/dist/ui/lib/node-display-label.d.ts.map +1 -0
  220. package/dist/ui/lib/node-display-label.js +36 -0
  221. package/dist/ui/lib/room-area.d.ts +3 -0
  222. package/dist/ui/lib/room-area.d.ts.map +1 -0
  223. package/dist/ui/lib/room-area.js +8 -0
  224. package/dist/ui/lib/room-assembly-overlay.d.ts +46 -0
  225. package/dist/ui/lib/room-assembly-overlay.d.ts.map +1 -0
  226. package/dist/ui/lib/room-assembly-overlay.js +222 -0
  227. package/dist/ui/lib/room-name-messages.d.ts +28 -0
  228. package/dist/ui/lib/room-name-messages.d.ts.map +1 -0
  229. package/dist/ui/lib/room-name-messages.js +112 -0
  230. package/dist/ui/lib/room-selection.d.ts +58 -0
  231. package/dist/ui/lib/room-selection.d.ts.map +1 -0
  232. package/dist/ui/lib/room-selection.js +297 -0
  233. package/dist/ui/lib/scene-theme-name.d.ts +4 -0
  234. package/dist/ui/lib/scene-theme-name.d.ts.map +1 -0
  235. package/dist/ui/lib/scene-theme-name.js +22 -0
  236. package/dist/ui/lib/selection-modifiers.d.ts +20 -0
  237. package/dist/ui/lib/selection-modifiers.d.ts.map +1 -0
  238. package/dist/ui/lib/selection-modifiers.js +15 -0
  239. package/dist/ui/lib/viewer-selection.d.ts +132 -0
  240. package/dist/ui/lib/viewer-selection.d.ts.map +1 -0
  241. package/dist/ui/lib/viewer-selection.js +462 -0
  242. package/dist/ui/lib/zone-content.d.ts +6 -0
  243. package/dist/ui/lib/zone-content.d.ts.map +1 -0
  244. package/dist/ui/lib/zone-content.js +96 -0
  245. package/dist/ui/primitives/button.d.ts +11 -0
  246. package/dist/ui/primitives/button.d.ts.map +1 -0
  247. package/dist/ui/primitives/button.js +35 -0
  248. package/dist/ui/primitives/dropdown-menu.d.ts +26 -0
  249. package/dist/ui/primitives/dropdown-menu.d.ts.map +1 -0
  250. package/dist/ui/primitives/dropdown-menu.js +53 -0
  251. package/dist/ui/primitives/popover.d.ts +8 -0
  252. package/dist/ui/primitives/popover.d.ts.map +1 -0
  253. package/dist/ui/primitives/popover.js +17 -0
  254. package/dist/ui/primitives/switch.d.ts +5 -0
  255. package/dist/ui/primitives/switch.d.ts.map +1 -0
  256. package/dist/ui/primitives/switch.js +8 -0
  257. package/dist/ui/primitives/tooltip.d.ts +8 -0
  258. package/dist/ui/primitives/tooltip.d.ts.map +1 -0
  259. package/dist/ui/primitives/tooltip.js +17 -0
  260. package/dist/ui/store/camera-pose-store.d.ts +17 -0
  261. package/dist/ui/store/camera-pose-store.d.ts.map +1 -0
  262. package/dist/ui/store/camera-pose-store.js +15 -0
  263. package/dist/ui/store/navigation-sync-pose-store.d.ts +26 -0
  264. package/dist/ui/store/navigation-sync-pose-store.d.ts.map +1 -0
  265. package/dist/ui/store/navigation-sync-pose-store.js +16 -0
  266. package/dist/ui/store/use-follow-pascal.d.ts +30 -0
  267. package/dist/ui/store/use-follow-pascal.d.ts.map +1 -0
  268. package/dist/ui/store/use-follow-pascal.js +28 -0
  269. package/dist/ui/theme.css +339 -0
  270. package/package.json +34 -7
@@ -1,7 +1,7 @@
1
1
  import { openElevatorDoor, requestElevatorLevel, resolveElevatorDispatchTarget, useInteractive, useScene, } from '@pascal-app/core';
2
2
  import { useThree } from '@react-three/fiber';
3
3
  import { useEffect, useRef } from 'react';
4
- import { Raycaster, Vector2 } from 'three';
4
+ import { Mesh, Raycaster, Vector2 } from 'three';
5
5
  function getElevatorButtonData(object) {
6
6
  let current = object;
7
7
  while (current) {
@@ -18,6 +18,8 @@ function getElevatorButtonData(object) {
18
18
  * whose meshes this capture-phase listener must not trip over.
19
19
  */
20
20
  export function collectElevatorButtons(object, into = []) {
21
+ if (!object.visible)
22
+ return into;
21
23
  if (object.userData.elevatorButton) {
22
24
  into.push(object);
23
25
  return into;
@@ -26,7 +28,41 @@ export function collectElevatorButtons(object, into = []) {
26
28
  collectElevatorButtons(child, into);
27
29
  return into;
28
30
  }
29
- export function ElevatorInteractionSystem() {
31
+ /** Ignore malformed material groups during transient paint updates, rather than raycasting invalid faces. */
32
+ export function pickElevatorButton(scene, raycaster) {
33
+ const buttons = collectElevatorButtons(scene);
34
+ const faces = [];
35
+ for (const button of buttons)
36
+ button.traverseVisible((object) => {
37
+ if (object instanceof Mesh)
38
+ faces.push(object);
39
+ });
40
+ const hit = raycaster.intersectObjects(faces, false)[0];
41
+ if (!hit)
42
+ return null;
43
+ const blockers = [];
44
+ scene.traverseVisible((object) => {
45
+ if (!(object instanceof Mesh) ||
46
+ getElevatorButtonData(object) ||
47
+ object.userData.pascalExport === 'strip')
48
+ return;
49
+ if (Array.isArray(object.material) &&
50
+ object.geometry.groups.some((group) => !object.material[group.materialIndex ?? 0] || !Number.isFinite(group.count)))
51
+ return;
52
+ blockers.push(object);
53
+ });
54
+ const occluded = raycaster.intersectObjects(blockers, false).some((intersection) => {
55
+ if (intersection.distance >= hit.distance - 0.001)
56
+ return false;
57
+ const mesh = intersection.object;
58
+ const material = Array.isArray(mesh.material)
59
+ ? mesh.material[intersection.face?.materialIndex ?? 0]
60
+ : mesh.material;
61
+ return material?.visible && (!material.transparent || material.opacity >= 0.95);
62
+ });
63
+ return occluded ? null : getElevatorButtonData(hit.object);
64
+ }
65
+ export function ElevatorInteractionSystem({ enabled = true }) {
30
66
  const camera = useThree((state) => state.camera);
31
67
  const gl = useThree((state) => state.gl);
32
68
  const scene = useThree((state) => state.scene);
@@ -35,7 +71,7 @@ export function ElevatorInteractionSystem() {
35
71
  useEffect(() => {
36
72
  const canvas = gl.domElement;
37
73
  const handlePointerDown = (event) => {
38
- if (event.button !== 0)
74
+ if (event.button !== 0 || !enabled)
39
75
  return;
40
76
  const buttons = collectElevatorButtons(scene);
41
77
  if (buttons.length === 0)
@@ -46,10 +82,7 @@ export function ElevatorInteractionSystem() {
46
82
  pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
47
83
  const raycaster = raycasterRef.current;
48
84
  raycaster.setFromCamera(pointer, camera);
49
- const button = raycaster
50
- .intersectObjects(buttons, true)
51
- .map((intersection) => getElevatorButtonData(intersection.object))
52
- .find((data) => data !== null);
85
+ const button = pickElevatorButton(scene, raycaster);
53
86
  if (!button || button.disabled)
54
87
  return;
55
88
  event.preventDefault();
@@ -74,6 +107,6 @@ export function ElevatorInteractionSystem() {
74
107
  return () => {
75
108
  canvas.removeEventListener('pointerdown', handlePointerDown, true);
76
109
  };
77
- }, [camera, gl, scene]);
110
+ }, [camera, gl, scene, enabled]);
78
111
  return null;
79
112
  }
@@ -0,0 +1,8 @@
1
+ import { type AnyNodeId } from '@pascal-app/core';
2
+ export type GeometryFailure = 'build-failed' | 'invalid-geometry';
3
+ /** Geometry failures describe the current rendering, never saved building data. */
4
+ export declare const useGeometryStatus: import("zustand").UseBoundStore<import("zustand").StoreApi<{
5
+ failures: ReadonlyMap<AnyNodeId, GeometryFailure>;
6
+ }>>;
7
+ export declare function setGeometryFailure(id: AnyNodeId, message: GeometryFailure | null): void;
8
+ //# sourceMappingURL=geometry-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"geometry-status.d.ts","sourceRoot":"","sources":["../../../src/systems/geometry/geometry-status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,kBAAkB,CAAA;AAG3D,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,kBAAkB,CAAA;AAEjE,mFAAmF;AACnF,eAAO,MAAM,iBAAiB;cAAsB,WAAW,CAAC,SAAS,EAAE,eAAe,CAAC;GAI1F,CAAA;AAED,wBAAgB,kBAAkB,CAAC,EAAE,EAAE,SAAS,EAAE,OAAO,EAAE,eAAe,GAAG,IAAI,QAOhF"}
@@ -0,0 +1,24 @@
1
+ import { useScene } from '@pascal-app/core';
2
+ import { create } from 'zustand';
3
+ /** Geometry failures describe the current rendering, never saved building data. */
4
+ export const useGeometryStatus = create(() => ({
5
+ failures: new Map(),
6
+ }));
7
+ export function setGeometryFailure(id, message) {
8
+ const current = useGeometryStatus.getState().failures;
9
+ if ((current.get(id) ?? null) === message)
10
+ return;
11
+ const failures = new Map(current);
12
+ if (message)
13
+ failures.set(id, message);
14
+ else
15
+ failures.delete(id);
16
+ useGeometryStatus.setState({ failures });
17
+ }
18
+ useScene.subscribe((scene, previous) => {
19
+ const failures = useGeometryStatus.getState().failures;
20
+ for (const id of failures.keys()) {
21
+ if (scene.hydrationId !== previous.hydrationId || !scene.nodes[id])
22
+ setGeometryFailure(id, null);
23
+ }
24
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"roof-system.d.ts","sourceRoot":"","sources":["../../../src/systems/roof/roof-system.tsx"],"names":[],"mappings":"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"}
@@ -9,6 +9,7 @@ import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
9
9
  import { ensureRenderableGeometryAttributes, subtractCsgBrush } from '../../lib/csg-utils';
10
10
  import useViewer from '../../store/use-viewer';
11
11
  import { isRevealAssembling } from '../construction-reveal/construction-reveal';
12
+ import { setGeometryFailure, useGeometryStatus } from '../geometry/geometry-status';
12
13
  import { ROOF_GABLE_LEFT_MATERIAL_INDEX, ROOF_GABLE_RIGHT_MATERIAL_INDEX, ROOF_HEEL_MATERIAL_INDEX, } from './roof-material-slots';
13
14
  function csgGeometry(brush) {
14
15
  return brush.geometry;
@@ -119,9 +120,14 @@ const MAX_ROOFS_PER_FRAME = 1;
119
120
  const MAX_SEGMENTS_PER_FRAME = 3;
120
121
  /** What each roof's shell was last merged from: its node and its segments, as the store held them. */
121
122
  const mergedFrom = new Map();
123
+ /** A roof's shell is merged from the roof and its segments; its faces of other kinds draw apart. */
124
+ const shellInputIds = (roof, nodes) => [
125
+ roof.id,
126
+ ...(roof.children ?? []).filter((id) => nodes[id]?.type === 'roof-segment'),
127
+ ];
122
128
  function rememberMergedInputs(roof, nodes) {
123
129
  const inputs = new Map();
124
- for (const id of [roof.id, ...(roof.children ?? [])]) {
130
+ for (const id of shellInputIds(roof, nodes)) {
125
131
  const node = nodes[id];
126
132
  if (node)
127
133
  inputs.set(id, node);
@@ -134,7 +140,7 @@ function mergedFromStillHolds(roofId, nodes) {
134
140
  const roof = nodes[roofId];
135
141
  if (!inputs || roof?.type !== 'roof')
136
142
  return false;
137
- const ids = [roofId, ...(roof.children ?? [])];
143
+ const ids = shellInputIds(roof, nodes);
138
144
  return ids.length === inputs.size && ids.every((id) => inputs.get(id) === nodes[id]);
139
145
  }
140
146
  /**
@@ -372,7 +378,13 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
372
378
  // Built even while hidden: a roof assembling from its segments (construction
373
379
  // reveal) hides its shell, and must show it whole the moment it is captured.
374
380
  rememberMergedInputs(node, nodes);
375
- updateMergedRoofGeometry(node, group, nodes, framing);
381
+ try {
382
+ updateMergedRoofGeometry(node, group, nodes, framing);
383
+ }
384
+ catch (error) {
385
+ setGeometryFailure(node.id, 'build-failed');
386
+ console.error('Roof rebuild failed:', error);
387
+ }
376
388
  roofsProcessed++;
377
389
  pendingRoofUpdates.delete(id);
378
390
  }
@@ -383,7 +395,19 @@ export const RoofSystem = ({ geometryAdapter } = {}) => {
383
395
  // GEOMETRY GENERATION
384
396
  // ============================================================================
385
397
  function updateRoofSegmentGeometry(node, mesh, nodes, adapter, framing = false) {
386
- const newGeo = generateRoofSegmentGeometry(node, nodes, { framing });
398
+ let newGeo;
399
+ try {
400
+ newGeo = generateRoofSegmentGeometry(node, nodes, { framing });
401
+ }
402
+ catch (error) {
403
+ setGeometryFailure(node.id, 'build-failed');
404
+ console.error('Roof segment rebuild failed:', error);
405
+ return;
406
+ }
407
+ if (useGeometryStatus.getState().failures.has(node.id)) {
408
+ newGeo.dispose();
409
+ return;
410
+ }
387
411
  mesh.geometry.dispose();
388
412
  mesh.geometry = newGeo;
389
413
  computeGeometryBoundsTree(newGeo);
@@ -405,69 +429,70 @@ function subtractAccessoryCuts(brushes, segment, nodes) {
405
429
  let workingShin = brushes.shinSlab;
406
430
  let workingDeck = brushes.deckSlab;
407
431
  let workingWall = brushes.wallBrush;
408
- for (const childElemId of segment.children ?? []) {
409
- const storedChild = nodes[childElemId];
410
- if (!storedChild)
411
- continue;
412
- const childElem = getEffectiveNode(storedChild);
413
- const meta = typeof childElem.metadata === 'object' && childElem.metadata !== null
414
- ? childElem.metadata
415
- : undefined;
416
- if (meta?.isTransient)
417
- continue;
418
- const childDef = nodeRegistry.get(childElem.type);
419
- const buildCut = childDef?.capabilities?.roofAccessory?.buildCut;
420
- if (!buildCut)
421
- continue;
422
- const cutGeo = buildCut(childElem, segment);
423
- if (!cutGeo)
424
- continue;
425
- // Wrap the kind-emitted geometry in a Brush. Kinds return raw
426
- // shapes; the viewer welds (mandatory after rotations leave
427
- // duplicated verts), attaches a single material group, and
428
- // builds the bounds tree — keeping kind code free of
429
- // three-bvh-csg / three-mesh-bvh imports.
430
- const welded = mergeVertices(cutGeo, 1e-4);
431
- cutGeo.dispose();
432
- const idxCount = welded.getIndex()?.count ?? 0;
433
- if (idxCount === 0) {
434
- welded.dispose();
435
- continue;
436
- }
437
- welded.clearGroups();
438
- welded.addGroup(0, idxCount, 0);
439
- welded.computeVertexNormals();
440
- ensureRenderableGeometryAttributes(welded);
441
- computeGeometryBoundsTree(welded);
442
- const cut = new Brush(welded, dummyMats[0]);
443
- cut.updateMatrixWorld();
444
- const cutScope = childDef?.capabilities?.roofAccessory?.cutScope ?? 'all';
445
- try {
446
- if (cutScope !== 'wall') {
447
- const nextShin = csgEvaluator.evaluate(workingShin, cut, SUBTRACTION);
448
- workingShin.geometry.dispose();
449
- prepareBrushForCSG(nextShin);
450
- workingShin = nextShin;
451
- const nextDeck = csgEvaluator.evaluate(workingDeck, cut, SUBTRACTION);
452
- workingDeck.geometry.dispose();
453
- prepareBrushForCSG(nextDeck);
454
- workingDeck = nextDeck;
432
+ try {
433
+ for (const childElemId of segment.children ?? []) {
434
+ const storedChild = nodes[childElemId];
435
+ if (!storedChild)
436
+ continue;
437
+ const childElem = getEffectiveNode(storedChild);
438
+ const meta = typeof childElem.metadata === 'object' && childElem.metadata !== null
439
+ ? childElem.metadata
440
+ : undefined;
441
+ if (meta?.isTransient)
442
+ continue;
443
+ const childDef = nodeRegistry.get(childElem.type);
444
+ const buildCut = childDef?.capabilities?.roofAccessory?.buildCut;
445
+ if (!buildCut)
446
+ continue;
447
+ const cutGeo = buildCut(childElem, segment);
448
+ if (!cutGeo)
449
+ continue;
450
+ // Wrap the kind-emitted geometry in a Brush. Kinds return raw
451
+ // shapes; the viewer welds (mandatory after rotations leave
452
+ // duplicated verts), attaches a single material group, and
453
+ // builds the bounds tree — keeping kind code free of
454
+ // three-bvh-csg / three-mesh-bvh imports.
455
+ const welded = mergeVertices(cutGeo, 1e-4);
456
+ cutGeo.dispose();
457
+ const idxCount = welded.getIndex()?.count ?? 0;
458
+ if (idxCount === 0) {
459
+ welded.dispose();
460
+ continue;
461
+ }
462
+ welded.clearGroups();
463
+ welded.addGroup(0, idxCount, 0);
464
+ welded.computeVertexNormals();
465
+ ensureRenderableGeometryAttributes(welded);
466
+ computeGeometryBoundsTree(welded);
467
+ const cut = new Brush(welded, dummyMats[0]);
468
+ cut.updateMatrixWorld();
469
+ const cutScope = childDef?.capabilities?.roofAccessory?.cutScope ?? 'all';
470
+ try {
471
+ if (cutScope !== 'wall') {
472
+ const nextShin = csgEvaluator.evaluate(workingShin, cut, SUBTRACTION);
473
+ workingShin.geometry.dispose();
474
+ prepareBrushForCSG(nextShin);
475
+ workingShin = nextShin;
476
+ const nextDeck = csgEvaluator.evaluate(workingDeck, cut, SUBTRACTION);
477
+ workingDeck.geometry.dispose();
478
+ prepareBrushForCSG(nextDeck);
479
+ workingDeck = nextDeck;
480
+ }
481
+ const nextWall = csgEvaluator.evaluate(workingWall, cut, SUBTRACTION);
482
+ workingWall.geometry.dispose();
483
+ prepareBrushForCSG(nextWall);
484
+ workingWall = nextWall;
485
+ }
486
+ finally {
487
+ cut.geometry.dispose();
455
488
  }
456
- const nextWall = csgEvaluator.evaluate(workingWall, cut, SUBTRACTION);
457
- workingWall.geometry.dispose();
458
- prepareBrushForCSG(nextWall);
459
- workingWall = nextWall;
460
- }
461
- catch (e) {
462
- console.error(`[${childElem.type}] cut CSG failed:`, e);
463
- }
464
- finally {
465
- cut.geometry.dispose();
466
489
  }
467
490
  }
468
- brushes.shinSlab = workingShin;
469
- brushes.deckSlab = workingDeck;
470
- brushes.wallBrush = workingWall;
491
+ finally {
492
+ brushes.shinSlab = workingShin;
493
+ brushes.deckSlab = workingDeck;
494
+ brushes.wallBrush = workingWall;
495
+ }
471
496
  }
472
497
  function getMergedRoofSegmentBrushes(roofNode, segment, nodes, framing) {
473
498
  const segmentId = segment.id;
@@ -478,7 +503,7 @@ function getMergedRoofSegmentBrushes(roofNode, segment, nodes, framing) {
478
503
  return cloneCachedMergedRoofSegmentBrushes(cached);
479
504
  }
480
505
  const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment, {
481
- fascia: wearsFascia(segment, roofNode),
506
+ fascia: wearsFascia(segment, roofNode, nodes),
482
507
  framing: framingDepth,
483
508
  }));
484
509
  if (!brushes) {
@@ -486,7 +511,13 @@ function getMergedRoofSegmentBrushes(roofNode, segment, nodes, framing) {
486
511
  mergedRoofSegmentGeometryCache.delete(segmentId);
487
512
  return null;
488
513
  }
489
- subtractAccessoryCuts(brushes, segment, nodes);
514
+ try {
515
+ subtractAccessoryCuts(brushes, segment, nodes);
516
+ }
517
+ catch (error) {
518
+ disposeRoofSegmentBrushSet(brushes);
519
+ throw error;
520
+ }
490
521
  _matrix.compose(_position.set(segment.position[0], segment.position[1], segment.position[2]), _quaternion.setFromAxisAngle(_yAxis, segment.rotation), _scale);
491
522
  const applyTransform = (brush) => {
492
523
  csgGeometry(brush).applyMatrix4(_matrix);
@@ -536,10 +567,11 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
536
567
  // Merge each child through `getEffectiveNode` so an in-flight handle
537
568
  // drag (live override on width / depth / wallHeight / pitch / rotation)
538
569
  // is reflected in the merged shell during the drag, not only on commit.
570
+ // Faces of other kinds (`capabilities.roofs`) render as their own kind, outside the shell.
539
571
  const children = (roofNode.children ?? [])
540
572
  .map((id) => {
541
573
  const scn = nodes[id];
542
- return scn ? getEffectiveNode(scn) : undefined;
574
+ return scn?.type === 'roof-segment' ? getEffectiveNode(scn) : undefined;
543
575
  })
544
576
  .filter((n) => n !== undefined && !hasSegmentMaterialOverride(n) && !isIndependentRoofSegment(n));
545
577
  if (children.length === 0) {
@@ -547,6 +579,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
547
579
  // Not BoxGeometry: its 6 groups against the merged mesh's 4-material array
548
580
  // crash GLTFExporter (materials[4] → undefined) when the roof bakes.
549
581
  mergedMesh.geometry = createDegenerateRoofPlaceholder();
582
+ setGeometryFailure(roofNode.id, null);
550
583
  return;
551
584
  }
552
585
  let totalShinSlab = null;
@@ -601,7 +634,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
601
634
  else {
602
635
  totalDeckSlab = brushes.deckSlab;
603
636
  }
604
- if (!shouldIncludeRoofSegmentWallShell(child, roofNode)) {
637
+ if (!shouldIncludeRoofSegmentWallShell(child, roofNode, nodes)) {
605
638
  brushes.wallBrush.geometry.dispose();
606
639
  brushes.innerBrush.geometry.dispose();
607
640
  }
@@ -643,6 +676,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
643
676
  console.warn('[RoofSystem] Skipping merged roof geometry with invalid attributes', roofNode.id);
644
677
  warnedMergedRoofNaNIds.add(roofNode.id);
645
678
  }
679
+ setGeometryFailure(roofNode.id, 'invalid-geometry');
646
680
  resultGeo.dispose();
647
681
  if (combined !== shinDeck)
648
682
  shinDeck.geometry.dispose();
@@ -667,7 +701,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
667
701
  }
668
702
  let finalGeo = addShedInsetEndPanels(resultGeo, csgChildren, true);
669
703
  splitRoofWallBandFaces(finalGeo, csgChildren
670
- .filter((child) => shouldIncludeRoofSegmentWallShell(child, roofNode))
704
+ .filter((child) => shouldIncludeRoofSegmentWallShell(child, roofNode, nodes))
671
705
  .map((child) => ({
672
706
  node: child,
673
707
  inverse: new THREE.Matrix4()
@@ -691,10 +725,12 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
691
725
  ensureRenderableGeometryAttributes(finalGeo);
692
726
  mergedMesh.geometry.dispose();
693
727
  mergedMesh.geometry = finalGeo;
728
+ setGeometryFailure(roofNode.id, null);
694
729
  if (combined !== shinDeck)
695
730
  shinDeck.geometry.dispose();
696
731
  }
697
732
  catch (e) {
733
+ setGeometryFailure(roofNode.id, 'build-failed');
698
734
  console.error('Merged roof CSG failed:', e);
699
735
  }
700
736
  totalShinSlab.geometry.dispose();
@@ -712,6 +748,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes, framing) {
712
748
  ensureRenderableGeometryAttributes(finalGeo);
713
749
  mergedMesh.geometry.dispose();
714
750
  mergedMesh.geometry = finalGeo;
751
+ setGeometryFailure(roofNode.id, null);
715
752
  }
716
753
  for (const geometry of directSegmentGeometries) {
717
754
  if (geometry !== finalGeo)
@@ -823,7 +860,7 @@ function getMergedRoofSegmentCacheKey(roofNode, segment, nodes, framing) {
823
860
  roofPosition: roofNode.position ?? [0, 0, 0],
824
861
  roofRotation: roofNode.rotation ?? 0,
825
862
  // a shed's fascia depends on whether its roof is a composite of pieces
826
- composite: (roofNode.children?.length ?? 0) > 1,
863
+ composite: isCompositeRoof(roofNode, nodes),
827
864
  segment,
828
865
  accessories: getMergedRoofAccessoryCachePayload(segment, nodes),
829
866
  framing,
@@ -955,7 +992,15 @@ function readShedOpenEndSides(node) {
955
992
  return new Set();
956
993
  return new Set(value.filter((side) => side === 'left' || side === 'right'));
957
994
  }
958
- function shouldIncludeRoofSegmentWallShell(node, parentRoof) {
995
+ /**
996
+ * Whether a roof's shell is pieced from several segments. Its faces of other kinds
997
+ * (`capabilities.roofs`) are no pieces of it; without the scene, every child counts.
998
+ */
999
+ function isCompositeRoof(roof, nodes) {
1000
+ const children = roof.children ?? [];
1001
+ return (nodes ? children.filter((id) => nodes[id]?.type === 'roof-segment') : children).length > 1;
1002
+ }
1003
+ function shouldIncludeRoofSegmentWallShell(node, parentRoof, nodes) {
959
1004
  if (node.wallShell === 'include')
960
1005
  return true;
961
1006
  if (node.wallShell === 'omit')
@@ -965,7 +1010,7 @@ function shouldIncludeRoofSegmentWallShell(node, parentRoof) {
965
1010
  // Older composite roofs use overlapping shed segments as deck pieces. Their
966
1011
  // wall volumes were never part of the rendered shell and make CSG grow
967
1012
  // exponentially when unioned together.
968
- return !parentRoof || (parentRoof.children?.length ?? 0) <= 1;
1013
+ return !parentRoof || !isCompositeRoof(parentRoof, nodes);
969
1014
  }
970
1015
  function hasSegmentTrim(node) {
971
1016
  const trim = normalizeRoofSegmentTrim(node);
@@ -1423,14 +1468,14 @@ function buildEaveCuts(node, reach, eave, seal) {
1423
1468
  * one deck (the same pieces whose wall shells are left out), so boards would
1424
1469
  * show between them.
1425
1470
  */
1426
- function wearsFascia(node, parentRoof) {
1471
+ function wearsFascia(node, parentRoof, nodes) {
1427
1472
  if (node.fascia !== true)
1428
1473
  return false;
1429
1474
  if (node.roofType !== 'shed')
1430
1475
  return true;
1431
1476
  if (isLeanToShedSegment(node))
1432
1477
  return false;
1433
- return !parentRoof || (parentRoof.children?.length ?? 0) <= 1;
1478
+ return !parentRoof || !isCompositeRoof(parentRoof, nodes);
1434
1479
  }
1435
1480
  /** What a framed roof's deck keeps over its open structure band at least: a 7/16 in sheet. */
1436
1481
  const ROOF_DECK_KEPT = 0.0111;
@@ -1932,18 +1977,25 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
1932
1977
  }
1933
1978
  result.computeVertexNormals();
1934
1979
  ensureRenderableGeometryAttributes(result);
1980
+ setGeometryFailure(node.id, null);
1935
1981
  return result;
1936
1982
  }
1937
1983
  const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, {
1938
- fascia: wearsFascia(node, parentRoof),
1984
+ fascia: wearsFascia(node, parentRoof, nodes),
1939
1985
  framing: options.framing ? roofFramingDepth(node) : 0,
1940
1986
  }));
1941
1987
  if (!brushes) {
1942
- // Fallback: simple box
1988
+ setGeometryFailure(node.id, 'build-failed');
1943
1989
  return new THREE.BoxGeometry(node.width, node.wallHeight, node.depth);
1944
1990
  }
1945
1991
  if (nodes) {
1946
- subtractAccessoryCuts(brushes, node, nodes);
1992
+ try {
1993
+ subtractAccessoryCuts(brushes, node, nodes);
1994
+ }
1995
+ catch (error) {
1996
+ disposeRoofSegmentBrushSet(brushes);
1997
+ throw error;
1998
+ }
1947
1999
  }
1948
2000
  const { deckSlab, shinSlab, wallBrush, innerBrush, rakeBoards } = brushes;
1949
2001
  let resultGeo = new THREE.BufferGeometry();
@@ -1952,7 +2004,7 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
1952
2004
  prepareBrushForCSG(shinDeck);
1953
2005
  let combined = shinDeck;
1954
2006
  let hollowWall = null;
1955
- if (shouldIncludeRoofSegmentWallShell(node, parentRoof)) {
2007
+ if (shouldIncludeRoofSegmentWallShell(node, parentRoof, nodes)) {
1956
2008
  hollowWall = csgEvaluator.evaluate(wallBrush, innerBrush, SUBTRACTION);
1957
2009
  prepareBrushForCSG(hollowWall);
1958
2010
  combined = csgEvaluator.evaluate(shinDeck, hollowWall, ADDITION);
@@ -1968,8 +2020,11 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
1968
2020
  resultGeo = csgGeometry(combined);
1969
2021
  if (geometryHasInvalidAttributes(resultGeo)) {
1970
2022
  resultGeo.dispose();
2023
+ setGeometryFailure(node.id, 'invalid-geometry');
1971
2024
  resultGeo = csgGeometry(wallBrush).clone();
1972
2025
  }
2026
+ else
2027
+ setGeometryFailure(node.id, null);
1973
2028
  const resultMaterials = csgMaterials(combined);
1974
2029
  const matToIndex = new Map([
1975
2030
  [dummyMats[0], 0],
@@ -1989,6 +2044,7 @@ export function generateRoofSegmentGeometry(node, nodes, options = {}) {
1989
2044
  shinDeck.geometry.dispose();
1990
2045
  }
1991
2046
  catch (e) {
2047
+ setGeometryFailure(node.id, 'build-failed');
1992
2048
  console.error('Roof CSG failed:', e);
1993
2049
  resultGeo = csgGeometry(wallBrush).clone();
1994
2050
  }
@@ -55,6 +55,17 @@ export declare function markWallsForZoneFinishChanges(): void;
55
55
  */
56
56
  export declare function wallGeometryChildren(node: WallNode, nodes: Readonly<Record<string, AnyNode>>): AnyNode[];
57
57
  type WallTerrainBottomSampler = (x: number, z: number) => number | null;
58
+ /** The wall's top in wall-local coordinates: `height` at the start, rising `slope` per metre of x. */
59
+ type WallTopLine = {
60
+ height: number;
61
+ slope: number;
62
+ };
63
+ /**
64
+ * Wall-local Y of the sloped top at `x`. Mitered corners sample `x` outside
65
+ * [0, L], where the line can fall below the wall base; the top never drops
66
+ * under {@link MIN_WALL_END_HEIGHT} (or the flat height, if lower).
67
+ */
68
+ export declare function wallTopYAt(wallTop: WallTopLine, x: number): number;
58
69
  export declare function generateExtrudedWall(wallNode: WallNode, childrenNodes: AnyNode[], miterData: WallMiterData, slabElevation?: number, baseElevation?: number, baseSegments?: readonly WallSlabSupportSegment[], storeyHeight?: number, terrainBottomAt?: WallTerrainBottomSampler, faceBase?: {
59
70
  a: readonly WallSlabSupportSegment[];
60
71
  b: readonly WallSlabSupportSegment[];
@@ -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,EACZ,KAAK,SAAS,EAqCd,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,QAAQ,EAEb,KAAK,sBAAsB,EAK5B,MAAM,kBAAkB,CAAA;AAGzB,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,EAAY,KAAK,EAAwC,MAAM,eAAe,CAAA;AAqBrF,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,oBAAoB,CAAA;AAK3B,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAA;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAA;AAyChE,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,SAAS,KAAK,EAAE,GAAG;IACjE,MAAM,EAAE,KAAK,GAAG,IAAI,CAAA;IACpB,eAAe,EAAE,KAAK,EAAE,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;CACrB,CA4FA;AAiJD;;;;GAIG;AACH,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,EAAE,gBAAgB,CAAA;IACxB,QAAQ,EAAE,MAAM,CAAC,GAAG,GAAG,GAAG,EAAE,eAAe,EAAE,CAAC,GAAG,IAAI,CAAA;IACrD,6FAA6F;IAC7F,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IACvB,wFAAwF;IACxF,UAAU,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAA;CAClD,CAAA;AAoUD,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,OAAO,CAAC,EACjC,gBAAgB,EAAE,MAAM,EACxB,SAAS,EAAE,MAAM,GAChB,OAAO,CAET;AAED,2EAA2E;AAC3E,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAID,MAAM,MAAM,0BAA0B,GAAG;IACvC,MAAM,EAAE,CAAC,EAAE,EAAE,SAAS,KAAK,OAAO,CAAA;CACnC,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,eAAe,CAAC,EAAE,CAChB,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,OAAO,EAAE,0BAA0B,KAChC;QAAE,gBAAgB,EAAE,OAAO,EAAE,CAAC;QAAC,cAAc,EAAE,OAAO,EAAE,CAAA;KAAE,CAAA;IAC/D,aAAa,CAAC,EAAE,CACd,IAAI,EAAE,QAAQ,EACd,QAAQ,EAAE,KAAK,CAAC,cAAc,EAC9B,QAAQ,EAAE,SAAS,OAAO,EAAE,KACzB,KAAK,CAAC,cAAc,CAAA;IACzB,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,cAAc,KAAK,IAAI,CAAA;CACnG,CAAA;AAED,eAAO,MAAM,UAAU,yBAAyB;IAAE,eAAe,CAAC,EAAE,mBAAmB,CAAA;CAAE,SAexF,CAAA;AAED,wBAAgB,iBAAiB,CAAC,eAAe,CAAC,EAAE,mBAAmB,QAatE;AAyCD;;;;GAIG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAoCpD;AAmZD;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,EACd,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACvC,OAAO,EAAE,CAUX;AA8ED,KAAK,wBAAwB,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAA;AA4OvE,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,EAC1C,QAAQ,CAAC,EAAE;IAAE,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;IAAC,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAA;CAAE,EACzF,YAAY,GAAE,gBAAsD,EACpE,UAAU,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAA6B,GACxE,KAAK,CAAC,cAAc,CAkUtB"}
1
+ {"version":3,"file":"wall-system.d.ts","sourceRoot":"","sources":["../../../src/systems/wall/wall-system.tsx"],"names":[],"mappings":"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"}