@pascal-app/viewer 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (276) hide show
  1. package/dist/capture/capture-runtime.js +1 -1
  2. package/dist/components/error-boundary.d.ts.map +1 -1
  3. package/dist/components/renderers/node-renderer.d.ts +1 -1
  4. package/dist/components/renderers/node-renderer.d.ts.map +1 -1
  5. package/dist/components/renderers/node-renderer.js +12 -4
  6. package/dist/components/renderers/parametric-node-renderer.d.ts.map +1 -1
  7. package/dist/components/viewer/bvh-ecctrl.d.ts.map +1 -1
  8. package/dist/components/viewer/frame-limiter.d.ts +1 -0
  9. package/dist/components/viewer/frame-limiter.d.ts.map +1 -1
  10. package/dist/components/viewer/frame-limiter.js +22 -3
  11. package/dist/components/viewer/glb-interactive.d.ts +25 -1
  12. package/dist/components/viewer/glb-interactive.d.ts.map +1 -1
  13. package/dist/components/viewer/glb-interactive.js +253 -86
  14. package/dist/components/viewer/glb-mechanisms.d.ts +10 -0
  15. package/dist/components/viewer/glb-mechanisms.d.ts.map +1 -0
  16. package/dist/components/viewer/glb-mechanisms.js +19 -0
  17. package/dist/components/viewer/glb-reference-nodes.js +1 -1
  18. package/dist/components/viewer/glb-replace-instances.js +1 -1
  19. package/dist/components/viewer/glb-scene.d.ts +45 -27
  20. package/dist/components/viewer/glb-scene.d.ts.map +1 -1
  21. package/dist/components/viewer/glb-scene.js +421 -508
  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 +13 -2
  25. package/dist/components/viewer/index.d.ts +24 -1
  26. package/dist/components/viewer/index.d.ts.map +1 -1
  27. package/dist/components/viewer/index.js +111 -37
  28. package/dist/components/viewer/lights.d.ts.map +1 -1
  29. package/dist/components/viewer/lights.js +113 -22
  30. package/dist/components/viewer/perf-monitor.d.ts.map +1 -1
  31. package/dist/components/viewer/perf-monitor.js +6 -0
  32. package/dist/components/viewer/perf-panel.d.ts.map +1 -1
  33. package/dist/components/viewer/perf-panel.js +1 -1
  34. package/dist/components/viewer/post-processing.d.ts.map +1 -1
  35. package/dist/components/viewer/post-processing.js +104 -36
  36. package/dist/components/viewer/registered-systems.d.ts.map +1 -1
  37. package/dist/components/viewer/registered-systems.js +17 -3
  38. package/dist/components/viewer/render-error.d.ts +18 -0
  39. package/dist/components/viewer/render-error.d.ts.map +1 -0
  40. package/dist/components/viewer/render-error.js +29 -0
  41. package/dist/components/viewer/scene-bvh.d.ts.map +1 -1
  42. package/dist/components/viewer/scene-bvh.js +1 -1
  43. package/dist/components/viewer/scene-environment.d.ts.map +1 -1
  44. package/dist/components/viewer/scene-environment.js +31 -11
  45. package/dist/components/viewer/scene-ground-replacement.d.ts +6 -0
  46. package/dist/components/viewer/scene-ground-replacement.d.ts.map +1 -1
  47. package/dist/components/viewer/scene-ground-replacement.js +15 -0
  48. package/dist/components/viewer/selection-manager.d.ts.map +1 -1
  49. package/dist/components/viewer/selection-manager.js +25 -7
  50. package/dist/components/viewer/underground-ambiance.d.ts +9 -0
  51. package/dist/components/viewer/underground-ambiance.d.ts.map +1 -0
  52. package/dist/components/viewer/underground-ambiance.js +105 -0
  53. package/dist/components/viewer/viewer-camera.d.ts +4 -0
  54. package/dist/components/viewer/viewer-camera.d.ts.map +1 -1
  55. package/dist/components/viewer/viewer-camera.js +12 -2
  56. package/dist/components/viewer/viewer-presentations.d.ts.map +1 -1
  57. package/dist/hooks/use-gltf-ktx2.d.ts.map +1 -1
  58. package/dist/hooks/use-gltf-ktx2.js +2 -0
  59. package/dist/hooks/use-node-events.d.ts.map +1 -1
  60. package/dist/index.d.ts +36 -16
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +34 -19
  63. package/dist/lib/asset-url.d.ts.map +1 -1
  64. package/dist/lib/asset-url.js +7 -1
  65. package/dist/lib/backdrop.d.ts +17 -0
  66. package/dist/lib/backdrop.d.ts.map +1 -1
  67. package/dist/lib/backdrop.js +18 -1
  68. package/dist/lib/construction-dust.d.ts +57 -0
  69. package/dist/lib/construction-dust.d.ts.map +1 -0
  70. package/dist/lib/construction-dust.js +187 -0
  71. package/dist/lib/csg-utils.js +1 -1
  72. package/dist/lib/display-state.d.ts +25 -0
  73. package/dist/lib/display-state.d.ts.map +1 -0
  74. package/dist/lib/display-state.js +1 -0
  75. package/dist/lib/foundation-materials.d.ts +26 -0
  76. package/dist/lib/foundation-materials.d.ts.map +1 -0
  77. package/dist/lib/foundation-materials.js +65 -0
  78. package/dist/lib/gltf-unlit.d.ts +10 -0
  79. package/dist/lib/gltf-unlit.d.ts.map +1 -0
  80. package/dist/lib/gltf-unlit.js +23 -0
  81. package/dist/lib/ground-window.d.ts +33 -0
  82. package/dist/lib/ground-window.d.ts.map +1 -0
  83. package/dist/lib/ground-window.js +114 -0
  84. package/dist/lib/hero-pose.d.ts +13 -3
  85. package/dist/lib/hero-pose.d.ts.map +1 -1
  86. package/dist/lib/hero-pose.js +72 -5
  87. package/dist/lib/ink-edges.d.ts +6 -3
  88. package/dist/lib/ink-edges.d.ts.map +1 -1
  89. package/dist/lib/ink-edges.js +17 -7
  90. package/dist/lib/key-light-override.d.ts +11 -0
  91. package/dist/lib/key-light-override.d.ts.map +1 -0
  92. package/dist/lib/key-light-override.js +14 -0
  93. package/dist/lib/ktx2-loader.d.ts +7 -0
  94. package/dist/lib/ktx2-loader.d.ts.map +1 -1
  95. package/dist/lib/ktx2-loader.js +35 -0
  96. package/dist/lib/layer-pass.d.ts.map +1 -1
  97. package/dist/lib/layers.d.ts +11 -7
  98. package/dist/lib/layers.d.ts.map +1 -1
  99. package/dist/lib/layers.js +16 -6
  100. package/dist/lib/live-frame-hold.d.ts +14 -0
  101. package/dist/lib/live-frame-hold.d.ts.map +1 -0
  102. package/dist/lib/live-frame-hold.js +28 -0
  103. package/dist/lib/materials.d.ts +19 -7
  104. package/dist/lib/materials.d.ts.map +1 -1
  105. package/dist/lib/materials.js +65 -9
  106. package/dist/lib/merged-outline-node.d.ts +5 -0
  107. package/dist/lib/merged-outline-node.d.ts.map +1 -1
  108. package/dist/lib/merged-outline-node.js +80 -2
  109. package/dist/lib/node-top-surface-height.d.ts +6 -0
  110. package/dist/lib/node-top-surface-height.d.ts.map +1 -0
  111. package/dist/lib/node-top-surface-height.js +74 -0
  112. package/dist/lib/perf-probe-state.d.ts +39 -0
  113. package/dist/lib/perf-probe-state.d.ts.map +1 -0
  114. package/dist/lib/perf-probe-state.js +17 -0
  115. package/dist/lib/pointer-events.d.ts +26 -2
  116. package/dist/lib/pointer-events.d.ts.map +1 -1
  117. package/dist/lib/pointer-events.js +99 -12
  118. package/dist/lib/post-processing-resources.d.ts.map +1 -1
  119. package/dist/lib/procedural-emission.d.ts +7 -0
  120. package/dist/lib/procedural-emission.d.ts.map +1 -0
  121. package/dist/lib/procedural-emission.js +54 -0
  122. package/dist/lib/reveal-candidates.d.ts +80 -0
  123. package/dist/lib/reveal-candidates.d.ts.map +1 -0
  124. package/dist/lib/reveal-candidates.js +524 -0
  125. package/dist/lib/reveal-ghost.d.ts +9 -0
  126. package/dist/lib/reveal-ghost.d.ts.map +1 -0
  127. package/dist/lib/reveal-ghost.js +62 -0
  128. package/dist/lib/reveal-pending.d.ts +12 -0
  129. package/dist/lib/reveal-pending.d.ts.map +1 -0
  130. package/dist/lib/reveal-pending.js +12 -0
  131. package/dist/lib/reveal-pose.d.ts +70 -0
  132. package/dist/lib/reveal-pose.d.ts.map +1 -0
  133. package/dist/lib/reveal-pose.js +248 -0
  134. package/dist/lib/reveal-schedule.d.ts +208 -0
  135. package/dist/lib/reveal-schedule.d.ts.map +1 -0
  136. package/dist/lib/reveal-schedule.js +410 -0
  137. package/dist/lib/reveal-texture-scale.d.ts +5 -0
  138. package/dist/lib/reveal-texture-scale.d.ts.map +1 -0
  139. package/dist/lib/reveal-texture-scale.js +113 -0
  140. package/dist/lib/reveal-windows.d.ts +23 -0
  141. package/dist/lib/reveal-windows.d.ts.map +1 -0
  142. package/dist/lib/reveal-windows.js +31 -0
  143. package/dist/lib/reveal-wipe.d.ts +71 -0
  144. package/dist/lib/reveal-wipe.d.ts.map +1 -0
  145. package/dist/lib/reveal-wipe.js +105 -0
  146. package/dist/lib/scene-pass-mrt.d.ts +12 -0
  147. package/dist/lib/scene-pass-mrt.d.ts.map +1 -0
  148. package/dist/lib/scene-pass-mrt.js +23 -0
  149. package/dist/lib/scene-support-height.d.ts +4 -0
  150. package/dist/lib/scene-support-height.d.ts.map +1 -0
  151. package/dist/lib/scene-support-height.js +34 -0
  152. package/dist/lib/scene-themes.d.ts +30 -8
  153. package/dist/lib/scene-themes.d.ts.map +1 -1
  154. package/dist/lib/scene-themes.js +36 -0
  155. package/dist/lib/scene-visibility.d.ts +17 -1
  156. package/dist/lib/scene-visibility.d.ts.map +1 -1
  157. package/dist/lib/scene-visibility.js +53 -3
  158. package/dist/lib/scripted-opening.d.ts +11 -0
  159. package/dist/lib/scripted-opening.d.ts.map +1 -0
  160. package/dist/lib/scripted-opening.js +20 -0
  161. package/dist/lib/site-surfaces.d.ts +32 -0
  162. package/dist/lib/site-surfaces.d.ts.map +1 -0
  163. package/dist/lib/site-surfaces.js +70 -0
  164. package/dist/lib/snapshot-pipeline.d.ts +34 -3
  165. package/dist/lib/snapshot-pipeline.d.ts.map +1 -1
  166. package/dist/lib/snapshot-pipeline.js +247 -117
  167. package/dist/lib/terrain-perimeter-fill.d.ts.map +1 -1
  168. package/dist/lib/terrain-perimeter-fill.js +22 -14
  169. package/dist/lib/tsl-compat.d.ts +2 -2
  170. package/dist/lib/tsl-compat.d.ts.map +1 -1
  171. package/dist/lib/underground-ambiance.d.ts +168 -0
  172. package/dist/lib/underground-ambiance.d.ts.map +1 -0
  173. package/dist/lib/underground-ambiance.js +334 -0
  174. package/dist/lib/zone-geometry.d.ts +11 -0
  175. package/dist/lib/zone-geometry.d.ts.map +1 -0
  176. package/dist/lib/zone-geometry.js +46 -0
  177. package/dist/store/use-item-light-pool.d.ts +14 -10
  178. package/dist/store/use-item-light-pool.d.ts.map +1 -1
  179. package/dist/store/use-item-light-pool.js +52 -26
  180. package/dist/store/use-viewer.d.ts +194 -26
  181. package/dist/store/use-viewer.d.ts.map +1 -1
  182. package/dist/store/use-viewer.js +164 -21
  183. package/dist/systems/ceiling/ceiling-system.d.ts +15 -1
  184. package/dist/systems/ceiling/ceiling-system.d.ts.map +1 -1
  185. package/dist/systems/ceiling/ceiling-system.js +88 -6
  186. package/dist/systems/construction-reveal/construction-dust.d.ts +16 -0
  187. package/dist/systems/construction-reveal/construction-dust.d.ts.map +1 -0
  188. package/dist/systems/construction-reveal/construction-dust.js +92 -0
  189. package/dist/systems/construction-reveal/construction-reveal.d.ts +276 -0
  190. package/dist/systems/construction-reveal/construction-reveal.d.ts.map +1 -0
  191. package/dist/systems/construction-reveal/construction-reveal.js +2383 -0
  192. package/dist/systems/construction-reveal/dust-material.d.ts +10 -0
  193. package/dist/systems/construction-reveal/dust-material.d.ts.map +1 -0
  194. package/dist/systems/construction-reveal/dust-material.js +17 -0
  195. package/dist/systems/construction-reveal/ghost-dress.d.ts +10 -0
  196. package/dist/systems/construction-reveal/ghost-dress.d.ts.map +1 -0
  197. package/dist/systems/construction-reveal/ghost-dress.js +24 -0
  198. package/dist/systems/door/door-system.d.ts.map +1 -1
  199. package/dist/systems/door/door-system.js +55 -17
  200. package/dist/systems/elevator/elevator-interaction-system.d.ts +7 -0
  201. package/dist/systems/elevator/elevator-interaction-system.d.ts.map +1 -1
  202. package/dist/systems/elevator/elevator-interaction-system.js +18 -1
  203. package/dist/systems/floor-elevation/floor-elevation-system.d.ts +3 -1
  204. package/dist/systems/floor-elevation/floor-elevation-system.d.ts.map +1 -1
  205. package/dist/systems/floor-elevation/floor-elevation-system.js +33 -5
  206. package/dist/systems/geometry/geometry-system.d.ts.map +1 -1
  207. package/dist/systems/geometry/geometry-system.js +71 -11
  208. package/dist/systems/interactive/control-widget.d.ts.map +1 -1
  209. package/dist/systems/interactive/interactive-system.d.ts.map +1 -1
  210. package/dist/systems/interactive/interactive-system.js +52 -17
  211. package/dist/systems/interactive/procedural-controls.d.ts +13 -0
  212. package/dist/systems/interactive/procedural-controls.d.ts.map +1 -0
  213. package/dist/systems/interactive/procedural-controls.js +17 -0
  214. package/dist/systems/interactive/scripted-clips.d.ts +19 -0
  215. package/dist/systems/interactive/scripted-clips.d.ts.map +1 -0
  216. package/dist/systems/interactive/scripted-clips.js +134 -0
  217. package/dist/systems/item/item-system.d.ts.map +1 -1
  218. package/dist/systems/item/item-system.js +9 -3
  219. package/dist/systems/item/object-cut-invalidation.d.ts +3 -0
  220. package/dist/systems/item/object-cut-invalidation.d.ts.map +1 -0
  221. package/dist/systems/item/object-cut-invalidation.js +68 -0
  222. package/dist/systems/item-light/item-light-system.d.ts +1 -1
  223. package/dist/systems/item-light/item-light-system.d.ts.map +1 -1
  224. package/dist/systems/item-light/item-light-system.js +48 -53
  225. package/dist/systems/level/level-hold.d.ts +4 -0
  226. package/dist/systems/level/level-hold.d.ts.map +1 -0
  227. package/dist/systems/level/level-hold.js +14 -0
  228. package/dist/systems/level/level-system.d.ts.map +1 -1
  229. package/dist/systems/level/level-system.js +19 -6
  230. package/dist/systems/level/level-utils.d.ts +19 -6
  231. package/dist/systems/level/level-utils.d.ts.map +1 -1
  232. package/dist/systems/level/level-utils.js +68 -9
  233. package/dist/systems/level/shadow-only-levels.d.ts +9 -0
  234. package/dist/systems/level/shadow-only-levels.d.ts.map +1 -0
  235. package/dist/systems/level/shadow-only-levels.js +7 -0
  236. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts +103 -0
  237. package/dist/systems/roof/__fixtures__/roof-plane-cases.d.ts.map +1 -0
  238. package/dist/systems/roof/__fixtures__/roof-plane-cases.js +317 -0
  239. package/dist/systems/roof/roof-materials.d.ts +16 -2
  240. package/dist/systems/roof/roof-materials.d.ts.map +1 -1
  241. package/dist/systems/roof/roof-materials.js +64 -31
  242. package/dist/systems/roof/roof-system.d.ts +21 -4
  243. package/dist/systems/roof/roof-system.d.ts.map +1 -1
  244. package/dist/systems/roof/roof-system.js +374 -49
  245. package/dist/systems/slab/slab-system.d.ts +1 -1
  246. package/dist/systems/slab/slab-system.d.ts.map +1 -1
  247. package/dist/systems/slab/slab-system.js +49 -19
  248. package/dist/systems/stair/stair-system.d.ts.map +1 -1
  249. package/dist/systems/stair/stair-system.js +133 -354
  250. package/dist/systems/wall/wall-cutout-cache.d.ts +39 -3
  251. package/dist/systems/wall/wall-cutout-cache.d.ts.map +1 -1
  252. package/dist/systems/wall/wall-cutout-cache.js +141 -19
  253. package/dist/systems/wall/wall-cutout.d.ts +6 -0
  254. package/dist/systems/wall/wall-cutout.d.ts.map +1 -1
  255. package/dist/systems/wall/wall-cutout.js +42 -28
  256. package/dist/systems/wall/wall-finish-data.d.ts +22 -0
  257. package/dist/systems/wall/wall-finish-data.d.ts.map +1 -0
  258. package/dist/systems/wall/wall-finish-data.js +17 -0
  259. package/dist/systems/wall/wall-material-variant.d.ts.map +1 -1
  260. package/dist/systems/wall/wall-materials.d.ts +9 -4
  261. package/dist/systems/wall/wall-materials.d.ts.map +1 -1
  262. package/dist/systems/wall/wall-materials.js +135 -153
  263. package/dist/systems/wall/wall-placeholder-sweep.d.ts.map +1 -1
  264. package/dist/systems/wall/wall-system.d.ts +33 -2
  265. package/dist/systems/wall/wall-system.d.ts.map +1 -1
  266. package/dist/systems/wall/wall-system.js +621 -159
  267. package/dist/systems/window/corner-frame.d.ts +10 -0
  268. package/dist/systems/window/corner-frame.d.ts.map +1 -0
  269. package/dist/systems/window/corner-frame.js +17 -0
  270. package/dist/systems/window/window-system.d.ts +1 -1
  271. package/dist/systems/window/window-system.d.ts.map +1 -1
  272. package/dist/systems/window/window-system.js +71 -27
  273. package/package.json +10 -10
  274. package/dist/systems/fence/fence-system.d.ts +0 -22
  275. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  276. package/dist/systems/fence/fence-system.js +0 -357
@@ -1,4 +1,4 @@
1
- import { getConicalRoofCoverage, getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getEffectiveNode, getRoofModuleFaces, getRoofPlanBounds, getRoofSegmentSurfaceY, getRoofShapeInsets, getRoofShapeRatios, getSegmentSlopeFrame, hasSegmentMaterialOverride, isBandedShedSegment, nodeRegistry, normalizeRoofSegmentTrim, pointInPolygon2D, ROOF_SHAPE_DEFAULTS, roofOverlapEntryOwns, roofPlanBoundsOverlap, sceneRegistry, unionPolygons, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
1
+ import { getBarrelRoofFaces, getBarrelRoofSolidPieces, getConicalRoofCoverage, getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getEffectiveNode, getGlazedRoofFaces, getGlazedRoofPieceFaces, getGlazedRoofPieces, getRoofModuleFaces, getRoofPlanBounds, getRoofSegmentDeckReach, getRoofSegmentSurfaceY, getRoofShapeInsets, getRoofShapeRatios, getSegmentSlopeFrame, hasSegmentMaterialOverride, isBandedShedSegment, isGlazedRoofSegment, isIndependentRoofSegment, nodeRegistry, normalizeRoofSegmentTrim, pointInPolygon2D, ROOF_SHAPE_DEFAULTS, roofOverlapEntryOwns, roofPlanBoundsOverlap, sceneRegistry, unionPolygons, useLiveNodeOverrides, useScene, } from '@pascal-app/core';
2
2
  import { useFrame } from '@react-three/fiber';
3
3
  import * as THREE from 'three';
4
4
  import { mergeGeometries, mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
@@ -6,6 +6,7 @@ import { ADDITION, Brush, Evaluator, SUBTRACTION } from 'three-bvh-csg';
6
6
  import { computeBoundsTree } from 'three-mesh-bvh';
7
7
  import { applyWorldScaleBoxUVs } from '../../lib/box-uv';
8
8
  import { ensureRenderableGeometryAttributes, subtractCsgBrush } from '../../lib/csg-utils';
9
+ import { isRevealAssembling } from '../construction-reveal/construction-reveal';
9
10
  function csgGeometry(brush) {
10
11
  return brush.geometry;
11
12
  }
@@ -20,7 +21,7 @@ csgEvaluator.attributes = ['position', 'normal', 'uv', 'uv2'];
20
21
  function computeGeometryBoundsTree(geometry) {
21
22
  ;
22
23
  geometry.computeBoundsTree = computeBoundsTree;
23
- geometry.computeBoundsTree({ maxLeafSize: 10 });
24
+ geometry.computeBoundsTree({ targetLeafSize: 10 });
24
25
  }
25
26
  function prepareBrushForCSG(brush) {
26
27
  ensureRenderableGeometryAttributes(brush.geometry);
@@ -108,11 +109,60 @@ function createDegenerateRoofPlaceholder() {
108
109
  }
109
110
  // Pending merged-roof updates carried across frames (for throttling)
110
111
  const pendingRoofUpdates = new Set();
112
+ const pendingIndependentSegments = new Set();
111
113
  const previousRoofPlanBounds = new Map();
112
114
  const warnedMergedRoofNaNIds = new Set();
113
115
  const MAX_ROOFS_PER_FRAME = 1;
114
116
  const MAX_SEGMENTS_PER_FRAME = 3;
115
- function queueSiblingRoofUpdates(roofId, nodes) {
117
+ /** What each roof's shell was last merged from: its node and its segments, as the store held them. */
118
+ const mergedFrom = new Map();
119
+ function rememberMergedInputs(roof, nodes) {
120
+ const inputs = new Map();
121
+ for (const id of [roof.id, ...(roof.children ?? [])]) {
122
+ const node = nodes[id];
123
+ if (node)
124
+ inputs.set(id, node);
125
+ }
126
+ mergedFrom.set(roof.id, inputs);
127
+ }
128
+ /** Whether nothing a roof's shell is made of has changed since it was merged. */
129
+ function mergedFromStillHolds(roofId, nodes) {
130
+ const inputs = mergedFrom.get(roofId);
131
+ const roof = nodes[roofId];
132
+ if (!inputs || roof?.type !== 'roof')
133
+ return false;
134
+ const ids = [roofId, ...(roof.children ?? [])];
135
+ return ids.length === inputs.size && ids.every((id) => inputs.get(id) === nodes[id]);
136
+ }
137
+ /**
138
+ * Roofs whose merged shell is still to build: until it is, the roof shows its segments apart (or
139
+ * an old shell), and a capture would show that. Only roofs on screen count, a first merge (a
140
+ * mounted group with no shell yet) included: one never mounted stays queued and would hold every
141
+ * capture.
142
+ */
143
+ export function getPendingRoofMergeCount() {
144
+ return countPendingRoofShells([...pendingRoofUpdates, ...pendingIndependentSegments], (id) => sceneRegistry.nodes.get(id));
145
+ }
146
+ export function countPendingRoofShells(pending, mounted) {
147
+ let count = 0;
148
+ for (const id of pending)
149
+ if (mounted(id))
150
+ count++;
151
+ return count;
152
+ }
153
+ function queueSiblingRoofUpdates(roofId, nodes, changedSegmentId) {
154
+ const queueIndependent = (roof) => {
155
+ for (const id of roof.children) {
156
+ const segment = nodes[id];
157
+ if (segment?.type === 'roof-segment' &&
158
+ segment.id !== changedSegmentId &&
159
+ isIndependentRoofSegment(getEffectiveNode(segment)))
160
+ pendingIndependentSegments.add(segment.id);
161
+ }
162
+ };
163
+ const currentRoof = nodes[roofId];
164
+ if (currentRoof?.type === 'roof')
165
+ queueIndependent(currentRoof);
116
166
  pendingRoofUpdates.add(roofId);
117
167
  const roof = nodes[roofId]?.type === 'roof' ? getEffectiveNode(nodes[roofId]) : undefined;
118
168
  if (roof?.type !== 'roof' || !roof.parentId)
@@ -170,13 +220,11 @@ function queueSiblingRoofUpdates(roofId, nodes) {
170
220
  if ((currentBounds && roofPlanBoundsOverlap(currentBounds, siblingBounds)) ||
171
221
  (oldBounds && roofPlanBoundsOverlap(oldBounds, siblingBounds))) {
172
222
  pendingRoofUpdates.add(sibling.id);
223
+ queueIndependent(sibling);
173
224
  }
174
225
  }
175
226
  }
176
- // ============================================================================
177
- // ROOF SYSTEM
178
- // ============================================================================
179
- export const RoofSystem = () => {
227
+ export const RoofSystem = ({ geometryAdapter } = {}) => {
180
228
  const dirtyNodes = useScene((state) => state.dirtyNodes);
181
229
  const clearDirty = useScene((state) => state.clearDirty);
182
230
  const rootNodeIds = useScene((state) => state.rootNodeIds);
@@ -189,23 +237,30 @@ export const RoofSystem = () => {
189
237
  // Clear stale pending updates when the scene is unloaded
190
238
  if (rootNodeIds.length === 0) {
191
239
  pendingRoofUpdates.clear();
240
+ pendingIndependentSegments.clear();
192
241
  previousRoofPlanBounds.clear();
193
242
  warnedMergedRoofNaNIds.clear();
243
+ mergedFrom.clear();
194
244
  for (const cached of mergedRoofSegmentGeometryCache.values()) {
195
245
  disposeCachedMergedRoofSegmentGeometrySet(cached);
196
246
  }
197
247
  mergedRoofSegmentGeometryCache.clear();
198
248
  return;
199
249
  }
200
- if (dirtyNodes.size === 0 && pendingRoofUpdates.size === 0)
250
+ if (dirtyNodes.size === 0 &&
251
+ pendingRoofUpdates.size === 0 &&
252
+ pendingIndependentSegments.size === 0)
201
253
  return;
202
254
  const nodes = useScene.getState().nodes;
203
255
  // --- Pass 1: Process dirty roof-segments (throttled) ---
204
256
  let segmentsProcessed = 0;
205
- dirtyNodes.forEach((id) => {
257
+ new Set([...dirtyNodes, ...pendingIndependentSegments]).forEach((id) => {
258
+ const independentCascade = pendingIndependentSegments.has(id) && !dirtyNodes.has(id);
206
259
  const node = nodes[id];
207
- if (!node)
260
+ if (!node) {
261
+ pendingIndependentSegments.delete(id);
208
262
  return;
263
+ }
209
264
  // Cutting roof accessories cascade their dirty mark to the host
210
265
  // segment's parent roof so the merged shell re-CSGs with the new
211
266
  // cut. Non-cutting accessories (vents, panels, gutters, etc.) sit on
@@ -220,6 +275,8 @@ export const RoofSystem = () => {
220
275
  const seg = segId ? nodes[segId] : undefined;
221
276
  if (roofAccessory.buildCut && seg?.parentId) {
222
277
  pendingRoofUpdates.add(seg.parentId);
278
+ if (isIndependentRoofSegment(seg))
279
+ useScene.getState().markDirty(seg.id);
223
280
  }
224
281
  clearDirty(id);
225
282
  return;
@@ -245,7 +302,7 @@ export const RoofSystem = () => {
245
302
  const revealOnly = mesh.parent?.name === 'segments-wrapper' &&
246
303
  mesh.parent?.parent?.getObjectByName('merged-roof')?.visible === true;
247
304
  if (isVisible && !revealOnly && segmentsProcessed < MAX_SEGMENTS_PER_FRAME) {
248
- updateRoofSegmentGeometry(effectiveSegment, mesh, nodes);
305
+ updateRoofSegmentGeometry(effectiveSegment, mesh, nodes, geometryAdapter);
249
306
  segmentsProcessed++;
250
307
  }
251
308
  else if (isVisible && !revealOnly) {
@@ -268,9 +325,12 @@ export const RoofSystem = () => {
268
325
  else {
269
326
  clearDirty(id);
270
327
  }
271
- // Queue the parent roof for a merged geometry update
272
- if (effectiveSegment.parentId) {
273
- queueSiblingRoofUpdates(effectiveSegment.parentId, nodes);
328
+ pendingIndependentSegments.delete(id);
329
+ const parentId = effectiveSegment.parentId;
330
+ if (parentId &&
331
+ !independentCascade &&
332
+ !(isRevealAssembling(parentId) && mergedFromStillHolds(parentId, nodes))) {
333
+ queueSiblingRoofUpdates(parentId, nodes, id);
274
334
  }
275
335
  }
276
336
  else if (node.type === 'roof') {
@@ -286,6 +346,7 @@ export const RoofSystem = () => {
286
346
  const node = nodes[id];
287
347
  if (node?.type !== 'roof') {
288
348
  pendingRoofUpdates.delete(id);
349
+ mergedFrom.delete(id);
289
350
  continue;
290
351
  }
291
352
  const group = sceneRegistry.nodes.get(id);
@@ -294,11 +355,11 @@ export const RoofSystem = () => {
294
355
  const mergedMesh = group.getObjectByName('merged-roof');
295
356
  if (!mergedMesh)
296
357
  continue;
297
- if (mergedMesh.visible !== false) {
298
- // Only rebuild when visible — RoofEditSystem re-triggers via markDirty on edit mode exit
299
- updateMergedRoofGeometry(node, group, nodes);
300
- roofsProcessed++;
301
- }
358
+ // Built even while hidden: a roof assembling from its segments (construction
359
+ // reveal) hides its shell, and must show it whole the moment it is captured.
360
+ rememberMergedInputs(node, nodes);
361
+ updateMergedRoofGeometry(node, group, nodes);
362
+ roofsProcessed++;
302
363
  pendingRoofUpdates.delete(id);
303
364
  }
304
365
  }, 5); // Priority 5: run after all other systems have settled
@@ -307,13 +368,14 @@ export const RoofSystem = () => {
307
368
  // ============================================================================
308
369
  // GEOMETRY GENERATION
309
370
  // ============================================================================
310
- function updateRoofSegmentGeometry(node, mesh, nodes) {
371
+ function updateRoofSegmentGeometry(node, mesh, nodes, adapter) {
311
372
  const newGeo = generateRoofSegmentGeometry(node, nodes);
312
373
  mesh.geometry.dispose();
313
374
  mesh.geometry = newGeo;
314
375
  computeGeometryBoundsTree(newGeo);
315
376
  mesh.position.set(node.position[0], node.position[1], node.position[2]);
316
377
  mesh.rotation.y = node.rotation;
378
+ adapter?.syncGeometry(node, mesh);
317
379
  }
318
380
  /**
319
381
  * Subtract every hosted accessory cut (`capabilities.roofAccessory.
@@ -400,7 +462,7 @@ function getMergedRoofSegmentBrushes(roofNode, segment, nodes) {
400
462
  if (cached?.key === cacheKey) {
401
463
  return cloneCachedMergedRoofSegmentBrushes(cached);
402
464
  }
403
- const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment));
465
+ const brushes = withSegmentUvMatrix(composeSegmentWorldMatrix(roofNode.position, roofNode.rotation ?? 0, segment.position, segment.rotation ?? 0), () => getRoofSegmentBrushes(segment, { fascia: wearsFascia(segment, roofNode) }));
404
466
  if (!brushes) {
405
467
  disposeCachedMergedRoofSegmentGeometrySet(cached);
406
468
  mergedRoofSegmentGeometryCache.delete(segmentId);
@@ -461,7 +523,7 @@ function updateMergedRoofGeometry(roofNode, group, nodes) {
461
523
  const scn = nodes[id];
462
524
  return scn ? getEffectiveNode(scn) : undefined;
463
525
  })
464
- .filter((n) => n !== undefined && !hasSegmentMaterialOverride(n));
526
+ .filter((n) => n !== undefined && !hasSegmentMaterialOverride(n) && !isIndependentRoofSegment(n));
465
527
  if (children.length === 0) {
466
528
  mergedMesh.geometry.dispose();
467
529
  // Not BoxGeometry: its 6 groups against the merged mesh's 4-material array
@@ -734,6 +796,8 @@ function getMergedRoofSegmentCacheKey(roofNode, segment, nodes) {
734
796
  return JSON.stringify({
735
797
  roofPosition: roofNode.position ?? [0, 0, 0],
736
798
  roofRotation: roofNode.rotation ?? 0,
799
+ // a shed's fascia depends on whether its roof is a composite of pieces
800
+ composite: (roofNode.children?.length ?? 0) > 1,
737
801
  segment,
738
802
  accessories: getMergedRoofAccessoryCachePayload(segment, nodes),
739
803
  });
@@ -819,6 +883,22 @@ const TRIM_CUT_EPSILON = 0.002;
819
883
  const ROOF_EDGE_MATERIAL_INDEX = 0;
820
884
  const ROOF_INSET_WALL_MATERIAL_INDEX = 2;
821
885
  const DUTCH_RAKE_SIDE_MATERIAL_INDEX = 1;
886
+ /**
887
+ * A lean-to's shed segment (the lean-to assembly writes the side-infill
888
+ * fields) has no wall band of its own: its high side is the host wall and
889
+ * its ends are the inset infill panels. Every other shed — a house's
890
+ * mono-pitch roof, hand-placed or generated — carries the same hollow wall
891
+ * band a gable does (the high wall and the raked side walls above the
892
+ * plate), sided in the wall/trim slot like a gable's pediment.
893
+ */
894
+ function isLeanToShedSegment(node) {
895
+ return (node.roofType === 'shed' &&
896
+ (node.shedSideInfillSpan !== undefined ||
897
+ node.shedSideInfillMinX !== undefined ||
898
+ node.shedSideInfillMaxX !== undefined ||
899
+ node.shedFootprintPieces !== undefined ||
900
+ node.shedOpenEndSides !== undefined));
901
+ }
822
902
  const DUTCH_RAKE_TOP_MATERIAL_INDEX = 3;
823
903
  const DUTCH_RAKE_SLOPE_SEAT_OFFSET = 0.0002;
824
904
  function pushDoubleSidedFace(targetFaces, face) {
@@ -981,7 +1061,12 @@ function buildSegmentTrimCutBrushes(segment) {
981
1061
  return [];
982
1062
  }
983
1063
  const { activeRh } = getSegmentSlopeFrame(segment);
984
- const extra = segment.wallThickness + segment.overhang + segment.deckThickness + segment.shingleThickness + 2;
1064
+ const extra = segment.wallThickness +
1065
+ segment.overhang +
1066
+ (segment.gableOverhang ?? 0) +
1067
+ segment.deckThickness +
1068
+ segment.shingleThickness +
1069
+ 2;
985
1070
  const minX = -segment.width / 2 - extra;
986
1071
  const maxX = segment.width / 2 + extra;
987
1072
  const minZ = -segment.depth / 2 - extra;
@@ -1075,6 +1160,9 @@ export function clipGeometryBySegmentTrim(geometry, segment) {
1075
1160
  prepareBrushForCSG(brush);
1076
1161
  const next = csgEvaluator.evaluate(brush, cut, SUBTRACTION);
1077
1162
  const trimmed = csgGeometry(next).clone();
1163
+ const materialIndices = new Map(dummyMats.map((material, index) => [material, index]));
1164
+ for (const group of trimmed.groups)
1165
+ group.materialIndex = mapRoofGroupMaterialIndex(group.materialIndex, csgMaterials(next), materialIndices);
1078
1166
  currentGeometry.dispose();
1079
1167
  next.geometry.dispose();
1080
1168
  ensureRenderableGeometryAttributes(trimmed);
@@ -1093,8 +1181,124 @@ export function clipGeometryBySegmentTrim(geometry, segment) {
1093
1181
  * Generate complete hollow-shell geometry for a roof segment.
1094
1182
  * Ports the prototype's CSG approach using three-bvh-csg.
1095
1183
  */
1096
- export function getRoofSegmentBrushes(node) {
1097
- const { roofType, width, depth, wallHeight, wallThickness, deckThickness, overhang, shingleThickness, } = node;
1184
+ /** A 1x8 fascia / rake board: actual 3/4 × 7-1/4 in. */
1185
+ const FASCIA_BOARD = { thickness: 0.019, height: 0.184 };
1186
+ /** A board's back sits this far inside the deck edge, so the union is one solid with no coplanar faces. */
1187
+ const FASCIA_EMBED = 0.002;
1188
+ /**
1189
+ * Slot 2 takes the trim finish like the rake faces, and the shell remap
1190
+ * leaves it alone: the remap would send a plumb eave board to the wall band
1191
+ * and its top edge to the shingles.
1192
+ */
1193
+ const FASCIA_MATERIAL_INDEX = 2;
1194
+ /**
1195
+ * The fascia and rake boards a segment wears, in its own frame: a plumb 1x8
1196
+ * on every eave with its top on the deck's top edge, and one up every gable
1197
+ * or shed rake, from the eave line the deck itself is built on. Being part
1198
+ * of the segment, they follow its pitch, size, overhang and type, and the
1199
+ * trims, cuts and sibling roofs clip them like the deck. Gable, hip, shed
1200
+ * and flat; null for the other forms.
1201
+ */
1202
+ function buildFasciaBoards(node, reach, eaveTopY, tanTheta) {
1203
+ const { roofType, width, depth } = node;
1204
+ const bt = node.fasciaDepth ?? FASCIA_BOARD.thickness;
1205
+ const bh = node.fasciaHeight ?? FASCIA_BOARD.height;
1206
+ // A flat roof's boards sized on purpose reach its top, over the membrane's edge: a coping band.
1207
+ const flatTopY = node.fasciaHeight !== undefined ? eaveTopY + node.shingleThickness : eaveTopY;
1208
+ const out = bt / 2 - FASCIA_EMBED;
1209
+ const halfD = depth / 2 + reach.z;
1210
+ const zE = halfD + out;
1211
+ const xE = width / 2 + reach.x + out;
1212
+ // the eave boards run through the corners to the rake / side boards'
1213
+ // outer faces (a lapped return, no notch at the corner)
1214
+ const xL = xE + bt / 2;
1215
+ const faces = [];
1216
+ const board = (a, b, across) => {
1217
+ const ax = (across[0] * bt) / 2;
1218
+ const az = (across[1] * bt) / 2;
1219
+ const p = [
1220
+ [a[0] - ax, a[1] - bh, a[2] - az],
1221
+ [b[0] - ax, b[1] - bh, b[2] - az],
1222
+ [b[0] + ax, b[1] - bh, b[2] + az],
1223
+ [a[0] + ax, a[1] - bh, a[2] + az],
1224
+ [a[0] - ax, a[1], a[2] - az],
1225
+ [b[0] - ax, b[1], b[2] - az],
1226
+ [b[0] + ax, b[1], b[2] + az],
1227
+ [a[0] + ax, a[1], a[2] + az],
1228
+ ].map(([x, y, z]) => new THREE.Vector3(x, y, z));
1229
+ const centre = p.reduce((sum, v) => sum.add(v), new THREE.Vector3()).multiplyScalar(1 / 8);
1230
+ const quads = [
1231
+ [0, 1, 2, 3],
1232
+ [4, 5, 6, 7],
1233
+ [0, 4, 5, 1],
1234
+ [1, 5, 6, 2],
1235
+ [2, 6, 7, 3],
1236
+ [3, 7, 4, 0],
1237
+ ];
1238
+ for (const quad of quads) {
1239
+ const face = quad.map((k) => p[k].clone());
1240
+ const normal = new THREE.Vector3()
1241
+ .subVectors(face[1], face[0])
1242
+ .cross(new THREE.Vector3().subVectors(face[2], face[0]));
1243
+ const faceCentre = face
1244
+ .reduce((sum, v) => sum.add(v), new THREE.Vector3())
1245
+ .multiplyScalar(1 / 4);
1246
+ if (normal.dot(faceCentre.sub(centre)) < 0)
1247
+ face.reverse();
1248
+ faces.push(face);
1249
+ }
1250
+ };
1251
+ if (roofType === 'gable') {
1252
+ const ridgeY = eaveTopY + halfD * tanTheta;
1253
+ board([-xL, eaveTopY, zE], [xL, eaveTopY, zE], [0, 1]);
1254
+ board([-xL, eaveTopY, -zE], [xL, eaveTopY, -zE], [0, 1]);
1255
+ for (const sx of [-1, 1]) {
1256
+ board([sx * xE, eaveTopY, zE], [sx * xE, ridgeY, 0], [1, 0]);
1257
+ board([sx * xE, ridgeY, 0], [sx * xE, eaveTopY, -zE], [1, 0]);
1258
+ }
1259
+ }
1260
+ else if (roofType === 'hip' || roofType === 'flat') {
1261
+ const y = roofType === 'flat' ? flatTopY : eaveTopY;
1262
+ for (const sz of [-1, 1])
1263
+ board([-xL, y, sz * zE], [xL, y, sz * zE], [0, 1]);
1264
+ // the side boards butt the eave boards' backs
1265
+ for (const sx of [-1, 1])
1266
+ board([sx * xE, y, -zE + bt / 2], [sx * xE, y, zE - bt / 2], [1, 0]);
1267
+ }
1268
+ else if (roofType === 'shed') {
1269
+ // the low eave at +z, the slope rising to −z
1270
+ const highY = eaveTopY + 2 * halfD * tanTheta;
1271
+ board([-xL, eaveTopY, zE], [xL, eaveTopY, zE], [0, 1]);
1272
+ if (node.fasciaHighEdge ?? true)
1273
+ board([-xL, highY, -zE], [xL, highY, -zE], [0, 1]);
1274
+ for (const sx of [-1, 1])
1275
+ board([sx * xE, eaveTopY, zE], [sx * xE, highY, -zE], [1, 0]);
1276
+ }
1277
+ else
1278
+ return null;
1279
+ return createGeometryFromFaces(faces, FASCIA_MATERIAL_INDEX);
1280
+ }
1281
+ /**
1282
+ * Whether a drawn segment wears fascia boards. A lean-to carries its own
1283
+ * trim, and a legacy composite shed roof stacks overlapping shed pieces as
1284
+ * one deck (the same pieces whose wall shells are left out), so boards would
1285
+ * show between them.
1286
+ */
1287
+ function wearsFascia(node, parentRoof) {
1288
+ if (node.fascia !== true)
1289
+ return false;
1290
+ if (node.roofType !== 'shed')
1291
+ return true;
1292
+ if (isLeanToShedSegment(node))
1293
+ return false;
1294
+ return !parentRoof || (parentRoof.children?.length ?? 0) <= 1;
1295
+ }
1296
+ /**
1297
+ * `fascia`: build the segment's fascia boards into its deck. Only the roof
1298
+ * renderer asks; a dormer, a chimney or a sibling's clip takes the bare shell.
1299
+ */
1300
+ export function getRoofSegmentBrushes(node, options = {}) {
1301
+ const { roofType, width, depth, wallHeight, wallThickness, deckThickness, shingleThickness } = node;
1098
1302
  const conicalCoverage = getConicalRoofCoverage(node);
1099
1303
  const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(node);
1100
1304
  const shapeRatios = getRoofShapeRatios({
@@ -1110,20 +1314,28 @@ export function getRoofSegmentBrushes(node) {
1110
1314
  // the 2D floorplan and the slope frame (which derives activeRh from the
1111
1315
  // same ratio). A hardcoded 0.25 desyncs the gablet from the parameter.
1112
1316
  const baseI = Math.min(width, depth) * node.dutchHipWidthRatio;
1113
- const getVol = (wExt, vOffset, baseY, matIndex, isVoid, materialRule) => {
1114
- const wV = Math.max(0.01, width + 2 * wExt);
1317
+ const plateSeated = wallHeight <= 0;
1318
+ // `xExt`: how far the volume reaches along x, where a gable's rakes are; the eaves' `wExt` else.
1319
+ const getVol = (wExt, vOffset, baseY, matIndex, isVoid, materialRule, onPlate = false, xExt = wExt) => {
1320
+ const wV = Math.max(0.01, width + 2 * xExt);
1115
1321
  const dV = Math.max(0.01, depth + 2 * wExt);
1116
1322
  const autoDrop = wExt * tanTheta;
1117
1323
  // Floor every prism at 5 cm so CSG never sees a degenerate volume — by
1118
1324
  // raising the top, never by sinking the base (the base is the wall top).
1119
1325
  // One floor for all volumes keeps each cutter level with the shell it carves.
1120
- const whV = Math.max(0.05, wallHeight - autoDrop + vOffset);
1326
+ // A plate-seated roof (wallHeight 0) is the exception for its sloped deck
1327
+ // volumes: their eave hangs below the plate, and raising it lifted the
1328
+ // whole roof off its walls, so their base sinks instead.
1329
+ const eaveY = wallHeight - autoDrop + vOffset;
1330
+ const sinkBase = plateSeated && !onPlate && autoDrop !== 0;
1331
+ const whV = sinkBase ? eaveY : Math.max(0.05, eaveY);
1121
1332
  let rhV = activeRh;
1122
1333
  if (activeRh > 0) {
1123
1334
  rhV = activeRh + autoDrop;
1124
1335
  if (roofType === 'shed')
1125
1336
  rhV = activeRh + 2 * autoDrop;
1126
1337
  }
1338
+ const safeBaseY = sinkBase ? Math.min(baseY, whV - 0.05) : baseY;
1127
1339
  let structuralI = baseI;
1128
1340
  if (isVoid) {
1129
1341
  structuralI += deckThickness;
@@ -1134,7 +1346,7 @@ export function getRoofSegmentBrushes(node) {
1134
1346
  d: dV,
1135
1347
  wh: whV,
1136
1348
  rh: rhV,
1137
- baseY,
1349
+ baseY: safeBaseY,
1138
1350
  insets: { dutchI: structuralI },
1139
1351
  baseW: width,
1140
1352
  baseD: depth,
@@ -1146,18 +1358,27 @@ export function getRoofSegmentBrushes(node) {
1146
1358
  }).map((face) => face.map((point) => new THREE.Vector3(point.x, point.y, point.z)));
1147
1359
  return createGeometryFromFaces(faces, materialRule ?? matIndex);
1148
1360
  };
1149
- const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false);
1150
- const innerGeo = getVol(-wallThickness / 2, 0, -5, 2, false);
1151
- const horizontalOverhang = overhang * cosTheta;
1152
- const deckExt = wallThickness / 2 + horizontalOverhang;
1153
- const shedRoofSideMaterialRule = roofType === 'shed'
1154
- ? (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX
1361
+ const wallGeo = getVol(wallThickness / 2, 0, 0, 0, false, undefined, true);
1362
+ const innerGeo = getVol(-wallThickness / 2, 0, -5, 2, false, undefined, true);
1363
+ const reach = getRoofSegmentDeckReach(node);
1364
+ const deckExt = reach.z;
1365
+ // A shed's deck and shingle edges take the wall/trim slot, unless it wears
1366
+ // fascia boards: then they are fascia like a gable's, in the deck slot.
1367
+ const shedEdgesInTrimSlot = roofType === 'shed' && !options.fascia;
1368
+ const shedRoofSideMaterialRule = shedEdgesInTrimSlot
1369
+ ? (normal) => (normal.y > SHINGLE_SURFACE_EPSILON ? 3 : ROOF_EDGE_MATERIAL_INDEX)
1155
1370
  : undefined;
1156
- const deckTopGeo = getVol(deckExt, verticalRt, 0, 1, false, shedRoofSideMaterialRule);
1157
- const deckBotGeo = getVol(deckExt, 0, -5, 0, true);
1371
+ const deckTopGeo = getVol(deckExt, verticalRt, 0, 1, false, shedRoofSideMaterialRule, false, reach.x);
1372
+ const deckBotGeo = getVol(deckExt, 0, -5, 0, true, undefined, false, reach.x);
1373
+ // the deck's top edge at its outer plane, as `getVol` puts it
1374
+ const deckEaveDrop = deckExt * tanTheta;
1375
+ const deckEaveTopY = plateSeated && deckEaveDrop > 0
1376
+ ? wallHeight - deckEaveDrop + verticalRt
1377
+ : Math.max(0.05, wallHeight - deckEaveDrop + verticalRt);
1378
+ const fasciaGeo = options.fascia ? buildFasciaBoards(node, reach, deckEaveTopY, tanTheta) : null;
1158
1379
  const stSin = shingleThickness * sinTheta;
1159
1380
  const stCos = shingleThickness * cosTheta;
1160
- const shinBotW = Math.max(0.01, width + 2 * deckExt);
1381
+ const shinBotW = Math.max(0.01, width + 2 * reach.x);
1161
1382
  const shinBotD = Math.max(0.01, depth + 2 * deckExt);
1162
1383
  const deckDrop = deckExt * tanTheta;
1163
1384
  const shinBotWh = wallHeight - deckDrop + verticalRt;
@@ -1254,7 +1475,7 @@ export function getRoofSegmentBrushes(node) {
1254
1475
  if (roofType === 'dutch' && insetsTop.dutchI !== undefined) {
1255
1476
  rakeBoards = buildDutchRakeBoards(shinTopW, shinTopD, shinTopWh, shinTopRh, insetsTop.dutchI, shapeRatios, node.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake, node.dutchTopRakeThickness ?? ROOF_SHAPE_DEFAULTS.dutchTopRakeThickness);
1256
1477
  }
1257
- const shedRoofSideMaterialIndex = roofType === 'shed' ? ROOF_EDGE_MATERIAL_INDEX : 1;
1478
+ const shedRoofSideMaterialIndex = shedEdgesInTrimSlot ? ROOF_EDGE_MATERIAL_INDEX : 1;
1258
1479
  const shinBotGeo = createGeometryFromFaces(botFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : shedRoofSideMaterialIndex);
1259
1480
  const shinTopGeo = createGeometryFromFaces(topFaces, (normal) => normal.y > SHINGLE_SURFACE_EPSILON ? 3 : shedRoofSideMaterialIndex);
1260
1481
  if (transZ !== 0) {
@@ -1291,13 +1512,16 @@ export function getRoofSegmentBrushes(node) {
1291
1512
  const deckTopBrush = toBrush(deckTopGeo);
1292
1513
  const deckBotBrush = toBrush(deckBotGeo);
1293
1514
  if (deckBotBrush) {
1294
- const wV = Math.max(0.01, width + 2 * deckExt);
1515
+ const wV = Math.max(0.01, width + 2 * reach.x);
1295
1516
  const dV = Math.max(0.01, depth + 2 * deckExt);
1296
1517
  deckBotBrush.scale.set(1 + eps / wV, 1, 1 + eps / dV);
1297
1518
  deckBotBrush.updateMatrixWorld();
1298
1519
  }
1299
1520
  const shinTopBrush = toBrush(shinTopGeo);
1300
1521
  const shinBotBrush = toBrush(shinBotGeo);
1522
+ const fasciaBrush = fasciaGeo ? toBrush(fasciaGeo) : null;
1523
+ if (fasciaGeo && !fasciaBrush)
1524
+ fasciaGeo.dispose();
1301
1525
  if (shinBotBrush) {
1302
1526
  const wV = shinBotW;
1303
1527
  const dV = shinBotD;
@@ -1312,8 +1536,15 @@ export function getRoofSegmentBrushes(node) {
1312
1536
  shinBotGeo.dispose();
1313
1537
  if (deckTopBrush && deckBotBrush && wallBrush && innerBrush && shinTopBrush && shinBotBrush) {
1314
1538
  try {
1315
- const deckSlab = csgEvaluator.evaluate(deckTopBrush, deckBotBrush, SUBTRACTION);
1539
+ let deckSlab = csgEvaluator.evaluate(deckTopBrush, deckBotBrush, SUBTRACTION);
1316
1540
  prepareBrushForCSG(deckSlab);
1541
+ if (fasciaBrush) {
1542
+ const withFascia = csgEvaluator.evaluate(deckSlab, fasciaBrush, ADDITION);
1543
+ prepareBrushForCSG(withFascia);
1544
+ deckSlab.geometry.dispose();
1545
+ fasciaBrush.geometry.dispose();
1546
+ deckSlab = withFascia;
1547
+ }
1317
1548
  const shinSlab = csgEvaluator.evaluate(shinTopBrush, shinBotBrush, SUBTRACTION);
1318
1549
  prepareBrushForCSG(shinSlab);
1319
1550
  deckTopBrush.geometry.dispose();
@@ -1356,16 +1587,94 @@ export function getRoofSegmentBrushes(node) {
1356
1587
  wallBrush.geometry.dispose();
1357
1588
  if (innerBrush)
1358
1589
  innerBrush.geometry.dispose();
1590
+ fasciaBrush?.geometry.dispose();
1359
1591
  rakeBoards?.dispose();
1360
1592
  return null;
1361
1593
  }
1594
+ function buildIndependentRoofSegmentGeometry(node, nodes) {
1595
+ if (!isIndependentRoofSegment(node))
1596
+ return null;
1597
+ const pieces = isGlazedRoofSegment(node)
1598
+ ? getGlazedRoofPieces(node)
1599
+ : getBarrelRoofSolidPieces(node);
1600
+ const build = (pieces) => {
1601
+ const parts = pieces.map((piece) => createGeometryFromFaces(getGlazedRoofPieceFaces(piece).map((face) => face.map(({ x, y, z }) => new THREE.Vector3(x, y, z))), piece.slot));
1602
+ const geometry = (parts.length ? mergeGeometries(parts, true) : null) ?? new THREE.BufferGeometry();
1603
+ geometry.groups.forEach((group, index) => {
1604
+ group.materialIndex = parts[index]?.groups[0]?.materialIndex ?? 0;
1605
+ });
1606
+ for (const part of parts)
1607
+ part.dispose();
1608
+ ensureRenderableGeometryAttributes(geometry);
1609
+ return geometry;
1610
+ };
1611
+ let covering = build(pieces.filter((piece) => piece.face.normal[1] > 0));
1612
+ let walls = build(pieces.filter((piece) => piece.face.normal[1] <= 0));
1613
+ const subtract = (geometry, cutter) => {
1614
+ if (!geometry.getAttribute('position')?.count)
1615
+ return geometry;
1616
+ const brush = new Brush(geometry, dummyMats);
1617
+ prepareBrushForCSG(brush);
1618
+ const result = csgEvaluator.evaluate(brush, cutter, SUBTRACTION);
1619
+ const next = result.geometry.clone();
1620
+ const indices = new Map(dummyMats.map((material, index) => [material, index]));
1621
+ for (const group of next.groups)
1622
+ group.materialIndex = mapRoofGroupMaterialIndex(group.materialIndex, csgMaterials(result), indices);
1623
+ result.geometry.dispose();
1624
+ geometry.dispose();
1625
+ return next;
1626
+ };
1627
+ for (const id of node.children ?? []) {
1628
+ const stored = nodes?.[id];
1629
+ if (!stored)
1630
+ continue;
1631
+ const child = getEffectiveNode(stored);
1632
+ if (child.metadata?.isTransient)
1633
+ continue;
1634
+ const capability = nodeRegistry.get(child.type)?.capabilities.roofAccessory;
1635
+ const cutGeometry = capability?.buildCut?.(child, node);
1636
+ if (!cutGeometry)
1637
+ continue;
1638
+ const cutter = new Brush(cutGeometry, dummyMats[0]);
1639
+ try {
1640
+ prepareBrushForCSG(cutter);
1641
+ if (capability?.cutScope !== 'wall')
1642
+ covering = subtract(covering, cutter);
1643
+ walls = subtract(walls, cutter);
1644
+ }
1645
+ finally {
1646
+ cutGeometry.dispose();
1647
+ }
1648
+ }
1649
+ const parts = [covering, walls]
1650
+ .filter((part) => part.getAttribute('position')?.count)
1651
+ .map((part) => (part.index ? part.toNonIndexed() : part.clone()));
1652
+ let geometry = (parts.length ? mergeGeometries(parts, false) : null) ?? new THREE.BufferGeometry();
1653
+ let offset = 0;
1654
+ for (const part of parts) {
1655
+ for (const group of part.groups)
1656
+ geometry.addGroup(offset + group.start, group.count, group.materialIndex);
1657
+ offset += part.getAttribute('position').count;
1658
+ part.dispose();
1659
+ }
1660
+ covering.dispose();
1661
+ walls.dispose();
1662
+ geometry = clipGeometryBySegmentTrim(geometry, node) ?? geometry;
1663
+ ensureRenderableGeometryAttributes(geometry);
1664
+ return geometry;
1665
+ }
1362
1666
  export function generateRoofSegmentGeometry(node, nodes) {
1363
1667
  const parentNode = node.parentId ? nodes?.[node.parentId] : undefined;
1364
1668
  const parentRoof = parentNode?.type === 'roof' ? parentNode : undefined;
1365
1669
  const parentRoofPosition = parentRoof?.position;
1366
1670
  const parentRoofRotation = parentRoof?.rotation ?? 0;
1367
1671
  const segmentWorldMatrix = composeSegmentWorldMatrix(parentRoofPosition, parentRoofRotation, node.position, node.rotation ?? 0);
1368
- const directSegmentGeometry = withSegmentUvMatrix(segmentWorldMatrix, () => buildCustomShedGeometry(node, nodes) ?? buildDirectConicalSectorGeometry(node));
1672
+ const directSegmentGeometry = withSegmentUvMatrix(segmentWorldMatrix, () => {
1673
+ const independent = buildIndependentRoofSegmentGeometry(node, nodes);
1674
+ if (independent)
1675
+ return independent;
1676
+ return buildCustomShedGeometry(node, nodes) ?? buildDirectConicalSectorGeometry(node);
1677
+ });
1369
1678
  if (directSegmentGeometry) {
1370
1679
  let result = addShedInsetEndPanels(directSegmentGeometry, [node], false);
1371
1680
  if (nodes) {
@@ -1375,7 +1684,7 @@ export function generateRoofSegmentGeometry(node, nodes) {
1375
1684
  ensureRenderableGeometryAttributes(result);
1376
1685
  return result;
1377
1686
  }
1378
- const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node));
1687
+ const brushes = withSegmentUvMatrix(segmentWorldMatrix, () => getRoofSegmentBrushes(node, { fascia: wearsFascia(node, parentRoof) }));
1379
1688
  if (!brushes) {
1380
1689
  // Fallback: simple box
1381
1690
  return new THREE.BoxGeometry(node.width, node.wallHeight, node.depth);
@@ -1444,7 +1753,7 @@ export function generateRoofSegmentGeometry(node, nodes) {
1444
1753
  ensureRenderableGeometryAttributes(resultGeo);
1445
1754
  return resultGeo;
1446
1755
  }
1447
- function clipDirectRoofGeometryAgainstSiblings(geometry, node, nodes, space) {
1756
+ export function clipDirectRoofGeometryAgainstSiblings(geometry, node, nodes, space) {
1448
1757
  const siblingInterior = buildOccludingRoofInterior(node, nodes, space);
1449
1758
  if (!siblingInterior)
1450
1759
  return geometry;
@@ -2757,7 +3066,9 @@ function buildDutchRakeBoards(W, D, wh, rh, i, shapeRatios, rake, thickness) {
2757
3066
  return merged;
2758
3067
  }
2759
3068
  function createShedInsetEndPanelGeometry(node) {
2760
- if (node.roofType !== 'shed')
3069
+ // a house shed has real end walls in its wall band; only a lean-to
3070
+ // needs the inset infill panels
3071
+ if (!isLeanToShedSegment(node))
2761
3072
  return null;
2762
3073
  const trim = normalizeRoofSegmentTrim(node);
2763
3074
  const openEndSides = readShedOpenEndSides(node);
@@ -2996,7 +3307,21 @@ function pushRoofUv(uvs, point, normal) {
2996
3307
  * to the true roof surface even on multi-slope roofs (gambrel/mansard/dutch).
2997
3308
  */
2998
3309
  export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
2999
- const { roofType, width, depth, wallHeight, wallThickness, deckThickness, overhang, shingleThickness, } = segment;
3310
+ if (isGlazedRoofSegment(segment) || segment.roofType === 'barrel') {
3311
+ const face = (isGlazedRoofSegment(segment) ? getGlazedRoofFaces(segment) : getBarrelRoofFaces(segment)).find((face) => face.normal[1] > 0 &&
3312
+ lx >= face.origin[0] &&
3313
+ lx <= face.origin[0] + face.width &&
3314
+ (lz - face.origin[2]) / face.v[2] >= -1e-6 &&
3315
+ (lz - face.origin[2]) / face.v[2] <= face.length + 1e-6);
3316
+ if (face) {
3317
+ const v = (lz - face.origin[2]) / face.v[2];
3318
+ return {
3319
+ point: new THREE.Vector3(lx, face.origin[1] + face.v[1] * v, lz),
3320
+ normal: new THREE.Vector3(...face.normal),
3321
+ };
3322
+ }
3323
+ }
3324
+ const { roofType, width, depth, wallHeight, deckThickness, shingleThickness } = segment;
3000
3325
  const { activeRh, tanTheta, cosTheta, sinTheta } = getSegmentSlopeFrame(segment);
3001
3326
  if (roofType === 'flat' || activeRh === 0) {
3002
3327
  return {
@@ -3005,11 +3330,11 @@ export function getRoofOuterSurfaceFrameAtPoint(segment, lx, lz) {
3005
3330
  };
3006
3331
  }
3007
3332
  const verticalRt = deckThickness / cosTheta;
3008
- const horizontalOverhang = overhang * cosTheta;
3009
- const deckExt = wallThickness / 2 + horizontalOverhang;
3333
+ const reach = getRoofSegmentDeckReach(segment);
3334
+ const deckExt = reach.z;
3010
3335
  const stSin = shingleThickness * sinTheta;
3011
3336
  const stCos = shingleThickness * cosTheta;
3012
- const shinBotW = Math.max(0.01, width + 2 * deckExt);
3337
+ const shinBotW = Math.max(0.01, width + 2 * reach.x);
3013
3338
  const shinBotD = Math.max(0.01, depth + 2 * deckExt);
3014
3339
  const deckDrop = deckExt * tanTheta;
3015
3340
  const shinBotWh = wallHeight - deckDrop + verticalRt;