@pascal-app/core 0.9.1 → 0.9.2

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 (321) hide show
  1. package/dist/events/bus.d.ts +30 -2
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
  4. package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
  5. package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
  6. package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
  7. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  8. package/dist/hooks/scene-registry/scene-registry.js +27 -1
  9. package/dist/hooks/scene-registry/scene-registry.test.d.ts +2 -0
  10. package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
  11. package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +93 -0
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +69 -12
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  15. package/dist/hooks/spatial-grid/spatial-grid-manager.js +476 -195
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.js +19 -1
  18. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
  19. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
  20. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
  21. package/dist/index.d.ts +16 -6
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +15 -5
  24. package/dist/lib/level-name.d.ts +4 -0
  25. package/dist/lib/level-name.d.ts.map +1 -0
  26. package/dist/lib/level-name.js +10 -0
  27. package/dist/lib/measurement-geometry.d.ts +19 -0
  28. package/dist/lib/measurement-geometry.d.ts.map +1 -0
  29. package/dist/lib/measurement-geometry.js +232 -0
  30. package/dist/lib/measurement-geometry.test.d.ts +2 -0
  31. package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
  32. package/dist/lib/measurement-geometry.test.js +102 -0
  33. package/dist/lib/polygon-geometry.d.ts +0 -1
  34. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  35. package/dist/lib/polygon-geometry.js +0 -18
  36. package/dist/lib/selection-proxy.d.ts +4 -0
  37. package/dist/lib/selection-proxy.d.ts.map +1 -0
  38. package/dist/lib/selection-proxy.js +12 -0
  39. package/dist/lib/slab-polygon.d.ts +31 -2
  40. package/dist/lib/slab-polygon.d.ts.map +1 -1
  41. package/dist/lib/slab-polygon.js +525 -31
  42. package/dist/lib/slab-polygon.test.d.ts +2 -0
  43. package/dist/lib/slab-polygon.test.d.ts.map +1 -0
  44. package/dist/lib/slab-polygon.test.js +635 -0
  45. package/dist/lib/slots.d.ts +14 -0
  46. package/dist/lib/slots.d.ts.map +1 -0
  47. package/dist/lib/slots.js +26 -0
  48. package/dist/lib/space-detection.d.ts +25 -2
  49. package/dist/lib/space-detection.d.ts.map +1 -1
  50. package/dist/lib/space-detection.js +414 -118
  51. package/dist/lib/space-detection.test.js +301 -2
  52. package/dist/lib/wall-distance.d.ts +60 -0
  53. package/dist/lib/wall-distance.d.ts.map +1 -0
  54. package/dist/lib/wall-distance.js +85 -0
  55. package/dist/lib/zone-quantities.d.ts +21 -0
  56. package/dist/lib/zone-quantities.d.ts.map +1 -0
  57. package/dist/lib/zone-quantities.js +183 -0
  58. package/dist/lib/zone-quantities.test.d.ts +2 -0
  59. package/dist/lib/zone-quantities.test.d.ts.map +1 -0
  60. package/dist/lib/zone-quantities.test.js +82 -0
  61. package/dist/material-library.d.ts +20 -1
  62. package/dist/material-library.d.ts.map +1 -1
  63. package/dist/material-library.js +2181 -441
  64. package/dist/registry/handles.d.ts +75 -2
  65. package/dist/registry/handles.d.ts.map +1 -1
  66. package/dist/registry/index.d.ts +3 -3
  67. package/dist/registry/index.d.ts.map +1 -1
  68. package/dist/registry/index.js +1 -1
  69. package/dist/registry/registry.d.ts +46 -3
  70. package/dist/registry/registry.d.ts.map +1 -1
  71. package/dist/registry/registry.js +82 -1
  72. package/dist/registry/registry.test.js +14 -1
  73. package/dist/registry/subtree.d.ts.map +1 -1
  74. package/dist/registry/subtree.js +4 -0
  75. package/dist/registry/subtree.test.js +30 -0
  76. package/dist/registry/types.d.ts +717 -38
  77. package/dist/registry/types.d.ts.map +1 -1
  78. package/dist/schema/index.d.ts +21 -5
  79. package/dist/schema/index.d.ts.map +1 -1
  80. package/dist/schema/index.js +17 -3
  81. package/dist/schema/material.d.ts +3 -2
  82. package/dist/schema/material.d.ts.map +1 -1
  83. package/dist/schema/material.js +7 -5
  84. package/dist/schema/material.test.d.ts +2 -0
  85. package/dist/schema/material.test.d.ts.map +1 -0
  86. package/dist/schema/material.test.js +43 -0
  87. package/dist/schema/nodes/box-vent.d.ts +2 -2
  88. package/dist/schema/nodes/cabinet.d.ts +534 -0
  89. package/dist/schema/nodes/cabinet.d.ts.map +1 -0
  90. package/dist/schema/nodes/cabinet.js +137 -0
  91. package/dist/schema/nodes/ceiling.d.ts +3 -2
  92. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  93. package/dist/schema/nodes/ceiling.js +4 -0
  94. package/dist/schema/nodes/chimney.d.ts +4 -4
  95. package/dist/schema/nodes/column.d.ts +3 -2
  96. package/dist/schema/nodes/column.d.ts.map +1 -1
  97. package/dist/schema/nodes/column.js +3 -0
  98. package/dist/schema/nodes/cupola.d.ts +2 -2
  99. package/dist/schema/nodes/door.d.ts +13 -5
  100. package/dist/schema/nodes/door.d.ts.map +1 -1
  101. package/dist/schema/nodes/door.js +12 -0
  102. package/dist/schema/nodes/dormer.d.ts +8 -8
  103. package/dist/schema/nodes/dormer.js +1 -1
  104. package/dist/schema/nodes/downspout.d.ts +2 -2
  105. package/dist/schema/nodes/duct-fitting.d.ts +91 -0
  106. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
  107. package/dist/schema/nodes/duct-fitting.js +92 -0
  108. package/dist/schema/nodes/duct-segment.d.ts +61 -0
  109. package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
  110. package/dist/schema/nodes/duct-segment.js +73 -0
  111. package/dist/schema/nodes/duct-terminal.d.ts +57 -0
  112. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
  113. package/dist/schema/nodes/duct-terminal.js +51 -0
  114. package/dist/schema/nodes/elevator.d.ts +3 -2
  115. package/dist/schema/nodes/elevator.d.ts.map +1 -1
  116. package/dist/schema/nodes/elevator.js +3 -0
  117. package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
  118. package/dist/schema/nodes/fence.d.ts +18 -2
  119. package/dist/schema/nodes/fence.d.ts.map +1 -1
  120. package/dist/schema/nodes/fence.js +25 -2
  121. package/dist/schema/nodes/gutter.d.ts +2 -2
  122. package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
  123. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
  124. package/dist/schema/nodes/hvac-equipment.js +55 -0
  125. package/dist/schema/nodes/item.d.ts +8 -0
  126. package/dist/schema/nodes/item.d.ts.map +1 -1
  127. package/dist/schema/nodes/item.js +11 -0
  128. package/dist/schema/nodes/level.d.ts +1 -1
  129. package/dist/schema/nodes/level.d.ts.map +1 -1
  130. package/dist/schema/nodes/level.js +2 -0
  131. package/dist/schema/nodes/lineset.d.ts +42 -0
  132. package/dist/schema/nodes/lineset.d.ts.map +1 -0
  133. package/dist/schema/nodes/lineset.js +38 -0
  134. package/dist/schema/nodes/liquid-line.d.ts +34 -0
  135. package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
  136. package/dist/schema/nodes/liquid-line.js +24 -0
  137. package/dist/schema/nodes/measurement.d.ts +306 -0
  138. package/dist/schema/nodes/measurement.d.ts.map +1 -0
  139. package/dist/schema/nodes/measurement.js +87 -0
  140. package/dist/schema/nodes/measurement.test.d.ts +2 -0
  141. package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
  142. package/dist/schema/nodes/measurement.test.js +139 -0
  143. package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
  144. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
  145. package/dist/schema/nodes/pipe-fitting.js +45 -0
  146. package/dist/schema/nodes/pipe-segment.d.ts +48 -0
  147. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
  148. package/dist/schema/nodes/pipe-segment.js +36 -0
  149. package/dist/schema/nodes/pipe-trap.d.ts +43 -0
  150. package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
  151. package/dist/schema/nodes/pipe-trap.js +36 -0
  152. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +2 -0
  153. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
  154. package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
  155. package/dist/schema/nodes/ridge-vent.d.ts +18 -3
  156. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  157. package/dist/schema/nodes/ridge-vent.js +221 -3
  158. package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
  159. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
  160. package/dist/schema/nodes/roof-segment-shape.js +318 -0
  161. package/dist/schema/nodes/roof-segment-shape.test.d.ts +2 -0
  162. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
  163. package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
  164. package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
  165. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
  166. package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
  167. package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
  168. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
  169. package/dist/schema/nodes/roof-segment-trim.test.js +107 -0
  170. package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
  171. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
  172. package/dist/schema/nodes/roof-segment-walls.js +330 -0
  173. package/dist/schema/nodes/roof-segment-walls.test.d.ts +2 -0
  174. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
  175. package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
  176. package/dist/schema/nodes/roof-segment.d.ts +84 -8
  177. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  178. package/dist/schema/nodes/roof-segment.js +290 -5
  179. package/dist/schema/nodes/roof.d.ts +8 -8
  180. package/dist/schema/nodes/shelf.d.ts +3 -2
  181. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  182. package/dist/schema/nodes/shelf.js +6 -0
  183. package/dist/schema/nodes/skylight.d.ts +10 -10
  184. package/dist/schema/nodes/slab.d.ts +3 -2
  185. package/dist/schema/nodes/slab.d.ts.map +1 -1
  186. package/dist/schema/nodes/slab.js +4 -0
  187. package/dist/schema/nodes/solar-panel.d.ts +4 -4
  188. package/dist/schema/nodes/stair-segment.d.ts +4 -4
  189. package/dist/schema/nodes/stair.d.ts +13 -12
  190. package/dist/schema/nodes/stair.d.ts.map +1 -1
  191. package/dist/schema/nodes/stair.js +3 -0
  192. package/dist/schema/nodes/turbine-vent.d.ts +2 -2
  193. package/dist/schema/nodes/wall.d.ts +245 -6
  194. package/dist/schema/nodes/wall.d.ts.map +1 -1
  195. package/dist/schema/nodes/wall.js +257 -0
  196. package/dist/schema/nodes/wall.test.d.ts +2 -0
  197. package/dist/schema/nodes/wall.test.d.ts.map +1 -0
  198. package/dist/schema/nodes/wall.test.js +208 -0
  199. package/dist/schema/nodes/window.d.ts +11 -3
  200. package/dist/schema/nodes/window.d.ts.map +1 -1
  201. package/dist/schema/nodes/window.js +12 -0
  202. package/dist/schema/nodes/zone.d.ts +2 -0
  203. package/dist/schema/nodes/zone.d.ts.map +1 -1
  204. package/dist/schema/nodes/zone.js +6 -0
  205. package/dist/schema/scene-material.d.ts +41 -0
  206. package/dist/schema/scene-material.d.ts.map +1 -0
  207. package/dist/schema/scene-material.js +9 -0
  208. package/dist/schema/types.d.ts +1089 -94
  209. package/dist/schema/types.d.ts.map +1 -1
  210. package/dist/schema/types.js +23 -0
  211. package/dist/services/alignment-anchors.d.ts.map +1 -1
  212. package/dist/services/alignment-anchors.js +30 -1
  213. package/dist/services/alignment.d.ts +5 -0
  214. package/dist/services/alignment.d.ts.map +1 -1
  215. package/dist/services/alignment.js +2 -0
  216. package/dist/services/hosting.d.ts +8 -0
  217. package/dist/services/hosting.d.ts.map +1 -1
  218. package/dist/services/hosting.js +13 -4
  219. package/dist/services/hosting.test.js +32 -1
  220. package/dist/services/index.d.ts +8 -2
  221. package/dist/services/index.d.ts.map +1 -1
  222. package/dist/services/index.js +8 -2
  223. package/dist/services/level-height.d.ts +28 -0
  224. package/dist/services/level-height.d.ts.map +1 -0
  225. package/dist/services/level-height.js +62 -0
  226. package/dist/services/opening-guides.d.ts +141 -0
  227. package/dist/services/opening-guides.d.ts.map +1 -0
  228. package/dist/services/opening-guides.js +267 -0
  229. package/dist/services/opening-guides.test.d.ts +2 -0
  230. package/dist/services/opening-guides.test.d.ts.map +1 -0
  231. package/dist/services/opening-guides.test.js +208 -0
  232. package/dist/services/port-connectivity.d.ts +115 -0
  233. package/dist/services/port-connectivity.d.ts.map +1 -0
  234. package/dist/services/port-connectivity.js +300 -0
  235. package/dist/services/port-connectivity.test.d.ts +2 -0
  236. package/dist/services/port-connectivity.test.d.ts.map +1 -0
  237. package/dist/services/port-connectivity.test.js +297 -0
  238. package/dist/services/riser-diagram.d.ts +40 -0
  239. package/dist/services/riser-diagram.d.ts.map +1 -0
  240. package/dist/services/riser-diagram.js +110 -0
  241. package/dist/services/riser-diagram.test.d.ts +2 -0
  242. package/dist/services/riser-diagram.test.d.ts.map +1 -0
  243. package/dist/services/riser-diagram.test.js +66 -0
  244. package/dist/services/snap.d.ts +9 -1
  245. package/dist/services/snap.d.ts.map +1 -1
  246. package/dist/services/snap.js +21 -1
  247. package/dist/services/snap.test.js +43 -1
  248. package/dist/services/system-graph.d.ts +30 -0
  249. package/dist/services/system-graph.d.ts.map +1 -0
  250. package/dist/services/system-graph.js +164 -0
  251. package/dist/services/system-graph.test.d.ts +2 -0
  252. package/dist/services/system-graph.test.d.ts.map +1 -0
  253. package/dist/services/system-graph.test.js +138 -0
  254. package/dist/services/validate-dwv.d.ts +32 -0
  255. package/dist/services/validate-dwv.d.ts.map +1 -0
  256. package/dist/services/validate-dwv.js +118 -0
  257. package/dist/services/validate-dwv.test.d.ts +2 -0
  258. package/dist/services/validate-dwv.test.d.ts.map +1 -0
  259. package/dist/services/validate-dwv.test.js +109 -0
  260. package/dist/store/actions/node-actions.d.ts.map +1 -1
  261. package/dist/store/actions/node-actions.js +94 -1
  262. package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
  263. package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
  264. package/dist/store/actions/ridge-vent-update.test.js +264 -0
  265. package/dist/store/history-control.d.ts +11 -0
  266. package/dist/store/history-control.d.ts.map +1 -1
  267. package/dist/store/history-control.js +33 -0
  268. package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
  269. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
  270. package/dist/store/use-scene-dirty-tracking.test.js +68 -0
  271. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
  272. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
  273. package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
  274. package/dist/store/use-scene-plugins.test.d.ts +2 -0
  275. package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
  276. package/dist/store/use-scene-plugins.test.js +51 -0
  277. package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
  278. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
  279. package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
  280. package/dist/store/use-scene-window-migration.test.d.ts +2 -0
  281. package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
  282. package/dist/store/use-scene-window-migration.test.js +85 -0
  283. package/dist/store/use-scene.d.ts +17 -2
  284. package/dist/store/use-scene.d.ts.map +1 -1
  285. package/dist/store/use-scene.js +358 -51
  286. package/dist/systems/elevator/elevator-service.js +1 -1
  287. package/dist/systems/fence/fence-centerline.d.ts +12 -0
  288. package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
  289. package/dist/systems/fence/fence-centerline.js +37 -0
  290. package/dist/systems/fence/fence-spline.d.ts +39 -0
  291. package/dist/systems/fence/fence-spline.d.ts.map +1 -0
  292. package/dist/systems/fence/fence-spline.js +196 -0
  293. package/dist/systems/fence/fence-spline.test.d.ts +2 -0
  294. package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
  295. package/dist/systems/fence/fence-spline.test.js +136 -0
  296. package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
  297. package/dist/systems/stair/stair-footprint.js +21 -11
  298. package/dist/systems/stair/stair-opening-preview.d.ts +1139 -469
  299. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  300. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  301. package/dist/systems/stair/stair-opening-sync.js +1 -18
  302. package/dist/systems/stair/stair-opening-sync.test.js +39 -0
  303. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  304. package/dist/systems/wall/wall-mitering.js +38 -2
  305. package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
  306. package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
  307. package/dist/systems/wall/wall-mitering.test.js +63 -0
  308. package/dist/utils/clone-scene-graph.d.ts +1 -0
  309. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  310. package/dist/utils/clone-scene-graph.js +23 -3
  311. package/dist/utils/clone-scene-graph.test.js +3 -0
  312. package/dist/utils/heal-scene-graph.d.ts +18 -0
  313. package/dist/utils/heal-scene-graph.d.ts.map +1 -0
  314. package/dist/utils/heal-scene-graph.js +90 -0
  315. package/dist/utils/heal-scene-graph.test.d.ts +2 -0
  316. package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
  317. package/dist/utils/heal-scene-graph.test.js +88 -0
  318. package/dist/validation/validate-build-json.d.ts +1 -0
  319. package/dist/validation/validate-build-json.d.ts.map +1 -1
  320. package/dist/validation/validate-build-json.js +31 -2
  321. package/package.json +3 -3
@@ -0,0 +1,318 @@
1
+ import { ROOF_SHAPE_DEFAULTS } from './roof-segment';
2
+ export function getRoofShapeRatios(input) {
3
+ return {
4
+ gambrelLowerWidthRatio: input.gambrelLowerWidthRatio ?? ROOF_SHAPE_DEFAULTS.gambrelLowerWidthRatio,
5
+ mansardSteepWidthRatio: input.mansardSteepWidthRatio ?? ROOF_SHAPE_DEFAULTS.mansardSteepWidthRatio,
6
+ dutchHipWidthRatio: input.dutchHipWidthRatio ?? ROOF_SHAPE_DEFAULTS.dutchHipWidthRatio,
7
+ dutchHipHeightRatio: input.dutchHipHeightRatio ?? ROOF_SHAPE_DEFAULTS.dutchHipHeightRatio,
8
+ dutchWaistLengthRatio: input.dutchWaistLengthRatio ?? ROOF_SHAPE_DEFAULTS.dutchWaistLengthRatio,
9
+ dutchGabletRake: input.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake,
10
+ };
11
+ }
12
+ export function getDutchRoofShapeMetrics(input) {
13
+ const fallbackInset = Math.min(input.baseW, input.baseD) * input.shapeRatios.dutchHipWidthRatio;
14
+ const maxI = Math.max(0, Math.min(input.w, input.d) / 2 - 0.005);
15
+ const inset = Math.min(Math.max(0, input.dutchI ?? fallbackInset), maxI);
16
+ const peakHeight = input.wh + Math.max(0.001, input.rh);
17
+ const middleHeight = input.wh + input.rh * input.shapeRatios.dutchHipHeightRatio;
18
+ if (!(inset > 0.001) || !(peakHeight > middleHeight + 0.001))
19
+ return null;
20
+ if (input.w >= input.d) {
21
+ const innerWaistHalfX = Math.max(0, (input.w / 2 - inset) * input.shapeRatios.dutchWaistLengthRatio);
22
+ const innerWaistHalfZ = Math.max(0, input.d / 2 - inset);
23
+ if (!(innerWaistHalfX > 0.001 && innerWaistHalfZ > 0.001))
24
+ return null;
25
+ const rakeReach = Math.min(Math.max(0, input.shapeRatios.dutchGabletRake), Math.max(0, input.w / 2 - innerWaistHalfX) * 0.98);
26
+ return {
27
+ axis: 'width',
28
+ inset,
29
+ middleHeight,
30
+ peakHeight,
31
+ rakeReach,
32
+ innerWaistHalfX,
33
+ innerWaistHalfZ,
34
+ outerWaistHalfX: innerWaistHalfX + rakeReach,
35
+ outerWaistHalfZ: innerWaistHalfZ,
36
+ };
37
+ }
38
+ const innerWaistHalfX = Math.max(0, input.w / 2 - inset);
39
+ const innerWaistHalfZ = Math.max(0, (input.d / 2 - inset) * input.shapeRatios.dutchWaistLengthRatio);
40
+ if (!(innerWaistHalfX > 0.001 && innerWaistHalfZ > 0.001))
41
+ return null;
42
+ const rakeReach = Math.min(Math.max(0, input.shapeRatios.dutchGabletRake), Math.max(0, input.d / 2 - innerWaistHalfZ) * 0.98);
43
+ return {
44
+ axis: 'depth',
45
+ inset,
46
+ middleHeight,
47
+ peakHeight,
48
+ rakeReach,
49
+ innerWaistHalfX,
50
+ innerWaistHalfZ,
51
+ outerWaistHalfX: innerWaistHalfX,
52
+ outerWaistHalfZ: innerWaistHalfZ + rakeReach,
53
+ };
54
+ }
55
+ export function getRoofShapeInsets(input) {
56
+ let inset = (input.wh - input.baseY) * input.tanTheta;
57
+ const maxSafeInset = Math.min(input.brushW, input.brushD) / 2 - 0.005;
58
+ if (inset > maxSafeInset)
59
+ inset = maxSafeInset;
60
+ let iF = 0;
61
+ let iB = 0;
62
+ let iL = 0;
63
+ let iR = 0;
64
+ if (input.roofType === 'hip' || input.roofType === 'mansard' || input.roofType === 'dutch') {
65
+ iF = inset;
66
+ iB = inset;
67
+ iL = inset;
68
+ iR = inset;
69
+ }
70
+ else if (input.roofType === 'gable' || input.roofType === 'gambrel') {
71
+ iF = inset;
72
+ iB = inset;
73
+ }
74
+ else if (input.roofType === 'shed') {
75
+ iF = inset;
76
+ }
77
+ let dutchI = Math.min(input.width, input.depth) * input.dutchHipWidthRatio;
78
+ if (input.isVoid)
79
+ dutchI += input.shingleThickness;
80
+ return { iF, iB, iL, iR, dutchI };
81
+ }
82
+ export function getDutchEndSlopeFaces(input) {
83
+ const dutch = getDutchRoofShapeMetrics({
84
+ w: input.w,
85
+ d: input.d,
86
+ wh: input.wh,
87
+ rh: input.rh,
88
+ dutchI: input.insets.dutchI,
89
+ baseW: input.baseW,
90
+ baseD: input.baseD,
91
+ shapeRatios: input.shapeRatios,
92
+ });
93
+ if (!dutch)
94
+ return [];
95
+ const v = (x, y, z) => ({ x, y, z });
96
+ const e1 = v(-input.w / 2, input.wh, input.d / 2);
97
+ const e2 = v(input.w / 2, input.wh, input.d / 2);
98
+ const e3 = v(input.w / 2, input.wh, -input.d / 2);
99
+ const e4 = v(-input.w / 2, input.wh, -input.d / 2);
100
+ // The hip end slope is constructed only up to the outer waist (rake) line.
101
+ // Extend its top edge inward along the same hip rulings until it reaches the
102
+ // gablet's inner triangle (the inner waist line), continuing each
103
+ // eave→outer-waist ridge line so the face stays planar and the pitch is
104
+ // unchanged — the edge climbs past middleHeight and meets the gablet face.
105
+ const lerp = (a, b, t) => v(a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t, a.z + (b.z - a.z) * t);
106
+ const lowerAlongY = (point) => v(point.x, point.y - Math.max(0, input.dutchTopRakeThickness ?? 0), point.z);
107
+ if (dutch.axis === 'width') {
108
+ const o1 = v(-dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
109
+ const o2 = v(dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
110
+ const o3 = v(dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
111
+ const o4 = v(-dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
112
+ const m1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
113
+ const m2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
114
+ const m3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
115
+ const m4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
116
+ const r1 = v(-dutch.innerWaistHalfX, input.wh + input.rh, 0);
117
+ const r2 = v(dutch.innerWaistHalfX, input.wh + input.rh, 0);
118
+ const projectWidthAxisPointToEndSlope = (point, side) => {
119
+ const denomY = dutch.middleHeight - input.wh;
120
+ const tPlane = Math.abs(denomY) > 1e-9 ? (point.y - input.wh) / denomY : 0;
121
+ const x = side * (input.w / 2 + (dutch.outerWaistHalfX - input.w / 2) * tPlane);
122
+ return v(x, point.y, point.z);
123
+ };
124
+ const top2 = projectWidthAxisPointToEndSlope(lowerAlongY(lerp(m2, r2, 0.5)), 1);
125
+ const top3 = projectWidthAxisPointToEndSlope(lowerAlongY(lerp(m3, r2, 0.5)), 1);
126
+ const top1 = projectWidthAxisPointToEndSlope(lowerAlongY(lerp(m1, r1, 0.5)), -1);
127
+ const top4 = projectWidthAxisPointToEndSlope(lowerAlongY(lerp(m4, r1, 0.5)), -1);
128
+ return [
129
+ [e2, e3, o3, top3, top2, o2],
130
+ [e4, e1, o1, top1, top4, o4],
131
+ ];
132
+ }
133
+ const o1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
134
+ const o2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
135
+ const o3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
136
+ const o4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
137
+ const m1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
138
+ const m2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
139
+ const m3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
140
+ const m4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
141
+ const r1 = v(0, input.wh + input.rh, dutch.innerWaistHalfZ);
142
+ const r2 = v(0, input.wh + input.rh, -dutch.innerWaistHalfZ);
143
+ const projectDepthAxisPointToEndSlope = (point, side) => {
144
+ const denomY = dutch.middleHeight - input.wh;
145
+ const tPlane = Math.abs(denomY) > 1e-9 ? (point.y - input.wh) / denomY : 0;
146
+ const z = side * (input.d / 2 + (dutch.outerWaistHalfZ - input.d / 2) * tPlane);
147
+ return v(point.x, point.y, z);
148
+ };
149
+ const top2 = projectDepthAxisPointToEndSlope(lowerAlongY(lerp(m2, r1, 0.5)), 1);
150
+ const top1 = projectDepthAxisPointToEndSlope(lowerAlongY(lerp(m1, r1, 0.5)), 1);
151
+ const top4 = projectDepthAxisPointToEndSlope(lowerAlongY(lerp(m4, r2, 0.5)), -1);
152
+ const top3 = projectDepthAxisPointToEndSlope(lowerAlongY(lerp(m3, r2, 0.5)), -1);
153
+ return [
154
+ [e1, e2, o2, top2, top1, o1],
155
+ [e3, e4, o4, top4, top3, o3],
156
+ ];
157
+ }
158
+ export function getRoofModuleFaces(input) {
159
+ const v = (x, y, z) => ({ x, y, z });
160
+ const { iF = 0, iB = 0, iL = 0, iR = 0 } = input.insets;
161
+ const b1 = v(-input.w / 2 + iL, input.baseY, input.d / 2 - iF);
162
+ const b2 = v(input.w / 2 - iR, input.baseY, input.d / 2 - iF);
163
+ const b3 = v(input.w / 2 - iR, input.baseY, -input.d / 2 + iB);
164
+ const b4 = v(-input.w / 2 + iL, input.baseY, -input.d / 2 + iB);
165
+ const bottom = [b4, b3, b2, b1];
166
+ const e1 = v(-input.w / 2, input.wh, input.d / 2);
167
+ const e2 = v(input.w / 2, input.wh, input.d / 2);
168
+ const e3 = v(input.w / 2, input.wh, -input.d / 2);
169
+ const e4 = v(-input.w / 2, input.wh, -input.d / 2);
170
+ const faces = [];
171
+ faces.push([b1, b2, e2, e1], [b2, b3, e3, e2], [b3, b4, e4, e3], [b4, b1, e1, e4], bottom);
172
+ const h = input.wh + Math.max(0.001, input.rh);
173
+ if (input.type === 'flat' || input.rh === 0) {
174
+ faces.push([e1, e2, e3, e4]);
175
+ }
176
+ else if (input.type === 'gable') {
177
+ const r1 = v(-input.w / 2, h, 0);
178
+ const r2 = v(input.w / 2, h, 0);
179
+ faces.push([e4, e1, r1], [e2, e3, r2], [e1, e2, r2, r1], [e3, e4, r1, r2]);
180
+ }
181
+ else if (input.type === 'hip') {
182
+ if (Math.abs(input.w - input.d) < 0.01) {
183
+ const r = v(0, h, 0);
184
+ faces.push([e4, e1, r], [e1, e2, r], [e2, e3, r], [e3, e4, r]);
185
+ }
186
+ else if (input.w >= input.d) {
187
+ const r1 = v(-input.w / 2 + input.d / 2, h, 0);
188
+ const r2 = v(input.w / 2 - input.d / 2, h, 0);
189
+ faces.push([e4, e1, r1], [e2, e3, r2], [e1, e2, r2, r1], [e3, e4, r1, r2]);
190
+ }
191
+ else {
192
+ const r1 = v(0, h, input.d / 2 - input.w / 2);
193
+ const r2 = v(0, h, -input.d / 2 + input.w / 2);
194
+ faces.push([e1, e2, r1], [e3, e4, r2], [e2, e3, r2, r1], [e4, e1, r1, r2]);
195
+ }
196
+ }
197
+ else if (input.type === 'shed') {
198
+ const t1 = v(-input.w / 2, h, -input.d / 2);
199
+ const t2 = v(input.w / 2, h, -input.d / 2);
200
+ faces.push([e1, e2, t2, t1], [e2, e3, t2], [e3, e4, t1, t2], [e4, e1, t1]);
201
+ }
202
+ else if (input.type === 'gambrel') {
203
+ const mz = (input.baseD / 2) * input.shapeRatios.gambrelLowerWidthRatio;
204
+ const dist = input.d / 2 - mz;
205
+ const mh = input.wh + dist * (input.tanTheta || 0);
206
+ const m1 = v(-input.w / 2, mh, mz);
207
+ const m2 = v(input.w / 2, mh, mz);
208
+ const m3 = v(input.w / 2, mh, -mz);
209
+ const m4 = v(-input.w / 2, mh, -mz);
210
+ const r1 = v(-input.w / 2, h, 0);
211
+ const r2 = v(input.w / 2, h, 0);
212
+ faces.push([e4, e1, m1, r1, m4], [e2, e3, m3, r2, m2], [e1, e2, m2, m1], [m1, m2, r2, r1], [e3, e4, m4, m3], [m3, m4, r1, r2]);
213
+ }
214
+ else if (input.type === 'mansard') {
215
+ const i = Math.min(input.baseW, input.baseD) * input.shapeRatios.mansardSteepWidthRatio;
216
+ const mh = input.wh + i * (input.tanTheta || 0);
217
+ const m1 = v(-input.w / 2 + i, mh, input.d / 2 - i);
218
+ const m2 = v(input.w / 2 - i, mh, input.d / 2 - i);
219
+ const m3 = v(input.w / 2 - i, mh, -input.d / 2 + i);
220
+ const m4 = v(-input.w / 2 + i, mh, -input.d / 2 + i);
221
+ const topW = input.w - i * 2;
222
+ const topD = input.d - i * 2;
223
+ faces.push([e1, e2, m2, m1], [e2, e3, m3, m2], [e3, e4, m4, m3], [e4, e1, m1, m4]);
224
+ if (Math.abs(topW - topD) < 0.01) {
225
+ const r = v(0, h, 0);
226
+ faces.push([m4, m1, r], [m1, m2, r], [m2, m3, r], [m3, m4, r]);
227
+ }
228
+ else if (topW >= topD) {
229
+ const r1 = v(-topW / 2 + topD / 2, h, 0);
230
+ const r2 = v(topW / 2 - topD / 2, h, 0);
231
+ faces.push([m4, m1, r1], [m2, m3, r2], [m1, m2, r2, r1], [m3, m4, r1, r2]);
232
+ }
233
+ else {
234
+ const r1 = v(0, h, topD / 2 - topW / 2);
235
+ const r2 = v(0, h, -topD / 2 + topW / 2);
236
+ faces.push([m1, m2, r1], [m3, m4, r2], [m2, m3, r2, r1], [m4, m1, r1, r2]);
237
+ }
238
+ }
239
+ else if (input.type === 'dutch') {
240
+ const dutch = getDutchRoofShapeMetrics({
241
+ w: input.w,
242
+ d: input.d,
243
+ wh: input.wh,
244
+ rh: input.rh,
245
+ dutchI: input.insets.dutchI,
246
+ baseW: input.baseW,
247
+ baseD: input.baseD,
248
+ shapeRatios: input.shapeRatios,
249
+ });
250
+ if (!dutch)
251
+ return faces;
252
+ const m1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
253
+ const m2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
254
+ const m3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
255
+ const m4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
256
+ if (dutch.axis === 'width') {
257
+ const o1 = v(-dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
258
+ const o2 = v(dutch.outerWaistHalfX, dutch.middleHeight, dutch.innerWaistHalfZ);
259
+ const o3 = v(dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
260
+ const o4 = v(-dutch.outerWaistHalfX, dutch.middleHeight, -dutch.innerWaistHalfZ);
261
+ const r1 = v(-dutch.innerWaistHalfX, h, 0);
262
+ const r2 = v(dutch.innerWaistHalfX, h, 0);
263
+ const endSlopes = input.excludeDutchEndSlopes
264
+ ? []
265
+ : getDutchEndSlopeFaces({
266
+ w: input.w,
267
+ d: input.d,
268
+ wh: input.wh,
269
+ rh: input.rh,
270
+ insets: input.insets,
271
+ baseW: input.baseW,
272
+ baseD: input.baseD,
273
+ shapeRatios: input.shapeRatios,
274
+ dutchTopRakeThickness: input.dutchTopRakeThickness,
275
+ });
276
+ faces.push([e1, e2, o2, m2, m1, o1], [e3, e4, o4, m4, m3, o3]);
277
+ if (endSlopes.length === 2) {
278
+ faces.push(...endSlopes);
279
+ }
280
+ else if (!input.excludeDutchEndSlopes) {
281
+ faces.push([e2, e3, o3, o2], [e4, e1, o1, o4]);
282
+ }
283
+ faces.push([m1, m2, r2, r1], [m3, m4, r1, r2]);
284
+ faces.push([m4, m1, r1], [m2, m3, r2]);
285
+ }
286
+ else {
287
+ const o1 = v(-dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
288
+ const o2 = v(dutch.innerWaistHalfX, dutch.middleHeight, dutch.outerWaistHalfZ);
289
+ const o3 = v(dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
290
+ const o4 = v(-dutch.innerWaistHalfX, dutch.middleHeight, -dutch.outerWaistHalfZ);
291
+ const r1 = v(0, h, dutch.innerWaistHalfZ);
292
+ const r2 = v(0, h, -dutch.innerWaistHalfZ);
293
+ const endSlopes = input.excludeDutchEndSlopes
294
+ ? []
295
+ : getDutchEndSlopeFaces({
296
+ w: input.w,
297
+ d: input.d,
298
+ wh: input.wh,
299
+ rh: input.rh,
300
+ insets: input.insets,
301
+ baseW: input.baseW,
302
+ baseD: input.baseD,
303
+ shapeRatios: input.shapeRatios,
304
+ dutchTopRakeThickness: input.dutchTopRakeThickness,
305
+ });
306
+ faces.push([e2, e3, o3, m3, m2, o2], [e4, e1, o1, m1, m4, o4]);
307
+ if (endSlopes.length === 2) {
308
+ faces.push(...endSlopes);
309
+ }
310
+ else if (!input.excludeDutchEndSlopes) {
311
+ faces.push([e1, e2, o2, o1], [e3, e4, o4, o3]);
312
+ }
313
+ faces.push([m2, m3, r2, r1], [m4, m1, r1, r2]);
314
+ faces.push([m1, m2, r1], [m3, m4, r2]);
315
+ }
316
+ }
317
+ return faces;
318
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=roof-segment-shape.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roof-segment-shape.test.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/roof-segment-shape.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,175 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { getDutchEndSlopeFaces, getDutchRoofShapeMetrics, getRoofModuleFaces, getRoofShapeRatios, } from './roof-segment-shape';
3
+ describe('roof segment shape', () => {
4
+ test('dutch shell is built as one complete non-duplicated face set', () => {
5
+ const wh = 3;
6
+ const rh = 2;
7
+ const faces = getRoofModuleFaces({
8
+ type: 'dutch',
9
+ w: 8,
10
+ d: 6,
11
+ wh,
12
+ rh,
13
+ baseY: 0,
14
+ insets: { dutchI: 1.5 },
15
+ baseW: 8,
16
+ baseD: 6,
17
+ tanTheta: 1,
18
+ shapeRatios: getRoofShapeRatios({
19
+ dutchHipWidthRatio: 0.25,
20
+ dutchHipHeightRatio: 0.5,
21
+ dutchWaistLengthRatio: 1,
22
+ dutchGabletRake: 0.25,
23
+ }),
24
+ });
25
+ const peakY = wh + rh;
26
+ const peakFaces = faces.filter((face) => face.some((vertex) => Math.abs(vertex.y - peakY) < 1e-6));
27
+ const signatures = new Set(faces.map((face) => face
28
+ .map((vertex) => `${vertex.x.toFixed(3)},${vertex.y.toFixed(3)},${vertex.z.toFixed(3)}`)
29
+ .sort()
30
+ .join('|')));
31
+ expect(faces).toHaveLength(13);
32
+ expect(peakFaces).toHaveLength(4);
33
+ expect(signatures.size).toBe(faces.length);
34
+ const lowerRightHip = faces.find((face) => face.some((vertex) => Math.abs(vertex.x - 2.75) < 1e-6 &&
35
+ Math.abs(vertex.y - 4) < 1e-6 &&
36
+ Math.abs(vertex.z - 1.5) < 1e-6));
37
+ const gableTriangle = faces.find((face) => face.length === 3 &&
38
+ face.some((vertex) => vertex.x === 2.5 && vertex.y === 5) &&
39
+ face.some((vertex) => vertex.x === 2.5 && vertex.z === 1.5));
40
+ expect(lowerRightHip).toBeDefined();
41
+ expect(gableTriangle?.some((vertex) => vertex.x === 2.75)).toBe(false);
42
+ });
43
+ test('dutch depth-axis shell rotates the upper gable correctly', () => {
44
+ const faces = getRoofModuleFaces({
45
+ type: 'dutch',
46
+ w: 6,
47
+ d: 8,
48
+ wh: 3,
49
+ rh: 2,
50
+ baseY: 0,
51
+ insets: { dutchI: 1.5 },
52
+ baseW: 6,
53
+ baseD: 8,
54
+ tanTheta: 1,
55
+ shapeRatios: getRoofShapeRatios({
56
+ dutchHipWidthRatio: 0.25,
57
+ dutchHipHeightRatio: 0.5,
58
+ dutchWaistLengthRatio: 1,
59
+ dutchGabletRake: 0.25,
60
+ }),
61
+ });
62
+ const peakFaces = faces.filter((face) => face.some((vertex) => vertex.y === 5));
63
+ const ridgeOnlyOnZAxis = peakFaces
64
+ .flat()
65
+ .filter((vertex) => vertex.y === 5)
66
+ .every((vertex) => vertex.x === 0);
67
+ expect(peakFaces).toHaveLength(4);
68
+ expect(ridgeOnlyOnZAxis).toBe(true);
69
+ });
70
+ test('dutch end hip slope extends inward until it meets the gable triangle', () => {
71
+ const ratios = getRoofShapeRatios({
72
+ dutchHipWidthRatio: 0.25,
73
+ dutchHipHeightRatio: 0.5,
74
+ dutchWaistLengthRatio: 1,
75
+ dutchGabletRake: 0.75,
76
+ });
77
+ const metrics = getDutchRoofShapeMetrics({
78
+ w: 8,
79
+ d: 6,
80
+ wh: 3,
81
+ rh: 2,
82
+ dutchI: 1.5,
83
+ baseW: 8,
84
+ baseD: 6,
85
+ shapeRatios: ratios,
86
+ });
87
+ const faces = getRoofModuleFaces({
88
+ type: 'dutch',
89
+ w: 8,
90
+ d: 6,
91
+ wh: 3,
92
+ rh: 2,
93
+ baseY: 0,
94
+ insets: { dutchI: 1.5 },
95
+ baseW: 8,
96
+ baseD: 6,
97
+ tanTheta: 1,
98
+ shapeRatios: ratios,
99
+ dutchTopRakeThickness: 0.21,
100
+ });
101
+ expect(metrics?.innerWaistHalfX).toBe(2.5);
102
+ expect(metrics?.outerWaistHalfX).toBe(3.25);
103
+ // The end slope now clips against the rake's lower inner edge and is
104
+ // reprojected back onto the end-slope plane, so the shorter top edge stays
105
+ // planar instead of twisting.
106
+ const hipTopFace = faces.find((face) => face.some((vertex) => Math.abs(vertex.x - 4) < 1e-6 &&
107
+ Math.abs(vertex.y - 3) < 1e-6 &&
108
+ Math.abs(vertex.z - 3) < 1e-6) &&
109
+ face.some((vertex) => Math.abs(vertex.x - 3.0325) < 1e-6 &&
110
+ Math.abs(vertex.y - 4.29) < 1e-6 &&
111
+ Math.abs(vertex.z - 0.75) < 1e-6) &&
112
+ face.some((vertex) => Math.abs(vertex.x - 3.0325) < 1e-6 &&
113
+ Math.abs(vertex.y - 4.29) < 1e-6 &&
114
+ Math.abs(vertex.z + 0.75) < 1e-6));
115
+ const gableTriangle = faces.find((face) => face.length === 3 &&
116
+ face.some((vertex) => vertex.x === 2.5 && vertex.y === 5) &&
117
+ face.some((vertex) => vertex.x === 2.5 && vertex.z === 1.5));
118
+ expect(hipTopFace).toBeDefined();
119
+ expect(gableTriangle?.some((vertex) => vertex.x === 3.25)).toBe(false);
120
+ });
121
+ test('excludeDutchEndSlopes pulls the end slopes out into getDutchEndSlopeFaces', () => {
122
+ const ratios = getRoofShapeRatios({
123
+ dutchHipWidthRatio: 0.25,
124
+ dutchHipHeightRatio: 0.5,
125
+ dutchWaistLengthRatio: 1,
126
+ dutchGabletRake: 0.75,
127
+ });
128
+ const args = {
129
+ type: 'dutch',
130
+ w: 8,
131
+ d: 6,
132
+ wh: 3,
133
+ rh: 2,
134
+ baseY: 0,
135
+ insets: { dutchI: 1.5 },
136
+ baseW: 8,
137
+ baseD: 6,
138
+ tanTheta: 1,
139
+ shapeRatios: ratios,
140
+ dutchTopRakeThickness: 0.21,
141
+ };
142
+ const full = getRoofModuleFaces(args);
143
+ const shell = getRoofModuleFaces({ ...args, excludeDutchEndSlopes: true });
144
+ const endSlopes = getDutchEndSlopeFaces({
145
+ w: 8,
146
+ d: 6,
147
+ wh: 3,
148
+ rh: 2,
149
+ insets: { dutchI: 1.5 },
150
+ baseW: 8,
151
+ baseD: 6,
152
+ shapeRatios: ratios,
153
+ dutchTopRakeThickness: 0.21,
154
+ });
155
+ // The two end slopes leave the shell and reappear in the standalone set.
156
+ expect(shell).toHaveLength(full.length - 2);
157
+ expect(endSlopes).toHaveLength(2);
158
+ // The standalone end slopes are 6-point polygons whose shorter top edge is
159
+ // clipped to the rake's lower inner edge and then kept on the end-slope
160
+ // plane.
161
+ const endIsEndSlope = endSlopes.every((face) => face.length === 6 &&
162
+ face.some((vertex) => Math.abs(Math.abs(vertex.x) - 3.25) < 1e-6) &&
163
+ face.some((vertex) => Math.abs(Math.abs(vertex.x) - 3.0325) < 1e-6) &&
164
+ face.some((vertex) => Math.abs(Math.abs(vertex.x) - 4) < 1e-6));
165
+ expect(endIsEndSlope).toBe(true);
166
+ // The removed faces are exactly the end slopes — the shell keeps every
167
+ // other face the full module produced.
168
+ const signature = (face) => face
169
+ .map((vertex) => `${vertex.x.toFixed(3)},${vertex.y.toFixed(3)},${vertex.z.toFixed(3)}`)
170
+ .join('|');
171
+ const shellSigs = new Set(shell.map(signature));
172
+ const removed = full.filter((face) => !shellSigs.has(signature(face)));
173
+ expect(removed).toHaveLength(2);
174
+ });
175
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=roof-segment-surface.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roof-segment-surface.test.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/roof-segment-surface.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,48 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { getRoofSegmentSurfaceY, ROOF_SHAPE_DEFAULTS, RoofSegmentNode } from './roof-segment';
3
+ describe('getRoofSegmentSurfaceY', () => {
4
+ test('keeps the Dutch width-axis rake on the upper gable slope', () => {
5
+ const segment = RoofSegmentNode.parse({
6
+ roofType: 'dutch',
7
+ width: 8,
8
+ depth: 6,
9
+ wallHeight: 3,
10
+ pitch: 40,
11
+ });
12
+ const activeRh = getRoofSegmentSurfaceY(segment, 0, 0) - segment.wallHeight;
13
+ const upperRise = activeRh * (1 - segment.dutchHipHeightRatio);
14
+ const waistHalfZ = segment.depth / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio;
15
+ const availableRake = Math.max(0, segment.width / 2 -
16
+ (segment.width / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio) *
17
+ segment.dutchWaistLengthRatio);
18
+ const rakeReach = Math.min(segment.dutchGabletRake, availableRake * 0.98);
19
+ const localX = (segment.width / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio) *
20
+ segment.dutchWaistLengthRatio +
21
+ rakeReach * 0.5;
22
+ const localZ = waistHalfZ * 0.5;
23
+ const expected = segment.wallHeight + activeRh - localZ * (upperRise / waistHalfZ);
24
+ expect(localX).toBeGreaterThan((segment.width / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio) *
25
+ segment.dutchWaistLengthRatio);
26
+ expect(getRoofSegmentSurfaceY(segment, localX, localZ)).toBeCloseTo(expected, 6);
27
+ });
28
+ test('keeps the Dutch depth-axis rake on the upper gable slope', () => {
29
+ const segment = RoofSegmentNode.parse({
30
+ roofType: 'dutch',
31
+ width: 6,
32
+ depth: 8,
33
+ wallHeight: 3,
34
+ pitch: 40,
35
+ });
36
+ const activeRh = getRoofSegmentSurfaceY(segment, 0, 0) - segment.wallHeight;
37
+ const upperRise = activeRh * (1 - segment.dutchHipHeightRatio);
38
+ const waistHalfX = segment.width / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio;
39
+ const innerWaistHalfZ = (segment.depth / 2 - Math.min(segment.width, segment.depth) * segment.dutchHipWidthRatio) *
40
+ segment.dutchWaistLengthRatio;
41
+ const rakeReach = Math.min(segment.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake, Math.max(0, segment.depth / 2 - innerWaistHalfZ) * 0.98);
42
+ const localX = waistHalfX * 0.5;
43
+ const localZ = innerWaistHalfZ + rakeReach * 0.5;
44
+ const expected = segment.wallHeight + activeRh - localX * (upperRise / waistHalfX);
45
+ expect(localZ).toBeGreaterThan(innerWaistHalfZ);
46
+ expect(getRoofSegmentSurfaceY(segment, localX, localZ)).toBeCloseTo(expected, 6);
47
+ });
48
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=roof-segment-trim.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roof-segment-trim.test.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/roof-segment-trim.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,107 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { getRoofSegmentVisibleTopBounds, normalizeRoofSegmentTrim, RoofSegmentNode, } from './roof-segment';
3
+ describe('roof segment trim', () => {
4
+ test('defaults legacy segments to no trim', () => {
5
+ const segment = RoofSegmentNode.parse({
6
+ id: 'rseg_test',
7
+ type: 'roof-segment',
8
+ });
9
+ expect(segment.trim).toEqual({
10
+ left: 0,
11
+ right: 0,
12
+ front: 0,
13
+ back: 0,
14
+ frontLeft: 0,
15
+ frontRight: 0,
16
+ backLeft: 0,
17
+ backRight: 0,
18
+ frontLeftX: 0,
19
+ frontLeftZ: 0,
20
+ frontRightX: 0,
21
+ frontRightZ: 0,
22
+ backLeftX: 0,
23
+ backLeftZ: 0,
24
+ backRightX: 0,
25
+ backRightZ: 0,
26
+ });
27
+ });
28
+ test('normalizes impossible side totals without inverting the footprint', () => {
29
+ const trim = normalizeRoofSegmentTrim({
30
+ width: 4,
31
+ depth: 3,
32
+ trim: { left: 3, right: 3, front: 2, back: 2 },
33
+ });
34
+ expect(trim.left + trim.right).toBeCloseTo(3.9);
35
+ expect(trim.front + trim.back).toBeCloseTo(2.9);
36
+ });
37
+ test('visible top bounds respect asymmetric trims', () => {
38
+ const segment = RoofSegmentNode.parse({
39
+ id: 'rseg_test',
40
+ type: 'roof-segment',
41
+ width: 8,
42
+ depth: 6,
43
+ overhang: 0,
44
+ wallThickness: 0,
45
+ shingleThickness: 0,
46
+ trim: { left: 1, right: 2, front: 0, back: 0 },
47
+ });
48
+ const bounds = getRoofSegmentVisibleTopBounds(segment);
49
+ expect(bounds.minX).toBeCloseTo(-3);
50
+ expect(bounds.maxX).toBeCloseTo(2);
51
+ expect(bounds.width).toBeCloseTo(5);
52
+ });
53
+ test('visible top bounds stay finite for legacy partial segments', () => {
54
+ const bounds = getRoofSegmentVisibleTopBounds({
55
+ id: 'rseg_legacy',
56
+ type: 'roof-segment',
57
+ roofType: 'gable',
58
+ trim: { left: 1 },
59
+ });
60
+ expect(Number.isFinite(bounds.minX)).toBe(true);
61
+ expect(Number.isFinite(bounds.maxX)).toBe(true);
62
+ expect(Number.isFinite(bounds.minZ)).toBe(true);
63
+ expect(Number.isFinite(bounds.maxZ)).toBe(true);
64
+ expect(Number.isFinite(bounds.width)).toBe(true);
65
+ expect(Number.isFinite(bounds.depth)).toBe(true);
66
+ });
67
+ test('diagonal trims can shorten the ridge span', () => {
68
+ const segment = RoofSegmentNode.parse({
69
+ id: 'rseg_test',
70
+ type: 'roof-segment',
71
+ width: 8,
72
+ depth: 6,
73
+ overhang: 0,
74
+ wallThickness: 0,
75
+ shingleThickness: 0,
76
+ trim: { left: 0, right: 0, front: 0, back: 0, frontLeft: 4, backRight: 4 },
77
+ });
78
+ const bounds = getRoofSegmentVisibleTopBounds(segment);
79
+ expect(bounds.minX).toBeCloseTo(-3);
80
+ expect(bounds.maxX).toBeCloseTo(3);
81
+ expect(bounds.width).toBeCloseTo(6);
82
+ });
83
+ test('diagonal trims support independent width and depth endpoints', () => {
84
+ const segment = RoofSegmentNode.parse({
85
+ id: 'rseg_test',
86
+ type: 'roof-segment',
87
+ width: 8,
88
+ depth: 6,
89
+ overhang: 0,
90
+ wallThickness: 0,
91
+ shingleThickness: 0,
92
+ trim: {
93
+ frontLeftX: 5,
94
+ frontLeftZ: 2,
95
+ backRightX: 2,
96
+ backRightZ: 5,
97
+ },
98
+ });
99
+ const trim = normalizeRoofSegmentTrim(segment);
100
+ const bounds = getRoofSegmentVisibleTopBounds(segment);
101
+ expect(trim.frontLeft).toBeCloseTo(2);
102
+ expect(trim.frontLeftX).toBeCloseTo(5);
103
+ expect(trim.frontLeftZ).toBeCloseTo(2);
104
+ expect(bounds.maxZ).toBeCloseTo(2.6);
105
+ expect(bounds.maxX).toBeCloseTo(3.2);
106
+ });
107
+ });