@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,139 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { MeasurementNode } from './measurement';
3
+ const parseMeasurement = (measurement) => MeasurementNode.safeParse({
4
+ id: 'measurement_test',
5
+ type: 'measurement',
6
+ measurement,
7
+ });
8
+ describe('MeasurementNode', () => {
9
+ test('accepts exactly two finite distance points', () => {
10
+ expect(parseMeasurement({
11
+ kind: 'distance',
12
+ points: [
13
+ [0, 1, 2],
14
+ [3, 4, 5],
15
+ ],
16
+ }).success).toBe(true);
17
+ expect(parseMeasurement({
18
+ kind: 'distance',
19
+ points: [
20
+ [0, 1, 2],
21
+ [3, 4, 5],
22
+ [6, 7, 8],
23
+ ],
24
+ }).success).toBe(false);
25
+ expect(parseMeasurement({
26
+ kind: 'distance',
27
+ points: [
28
+ [0, 1, 2],
29
+ [Number.POSITIVE_INFINITY, 4, 5],
30
+ ],
31
+ }).success).toBe(false);
32
+ });
33
+ test('accepts semantic feature anchors with finite fallbacks', () => {
34
+ expect(parseMeasurement({
35
+ kind: 'distance',
36
+ points: [
37
+ {
38
+ kind: 'feature',
39
+ reference: {
40
+ nodeId: 'wall_a',
41
+ featureId: 'wall:centerline',
42
+ parameters: { t: 0.25, side: 'center' },
43
+ },
44
+ fallback: [1, 0, 2],
45
+ },
46
+ [3, 0, 2],
47
+ ],
48
+ }).success).toBe(true);
49
+ });
50
+ test('accepts angle and perimeter measurements', () => {
51
+ expect(parseMeasurement({
52
+ kind: 'angle',
53
+ points: [
54
+ [1, 0, 0],
55
+ [0, 0, 0],
56
+ [0, 0, 1],
57
+ ],
58
+ }).success).toBe(true);
59
+ expect(parseMeasurement({
60
+ kind: 'perimeter',
61
+ base: [
62
+ [0, 0, 0],
63
+ [1, 0, 0],
64
+ [0, 0, 1],
65
+ ],
66
+ }).success).toBe(true);
67
+ });
68
+ test('requires at least three area base points', () => {
69
+ expect(parseMeasurement({
70
+ kind: 'area',
71
+ base: [
72
+ [0, 0, 0],
73
+ [1, 0, 0],
74
+ [0, 0, 1],
75
+ ],
76
+ }).success).toBe(true);
77
+ expect(parseMeasurement({
78
+ kind: 'area',
79
+ base: [
80
+ [0, 0, 0],
81
+ [1, 0, 0],
82
+ ],
83
+ }).success).toBe(false);
84
+ expect(parseMeasurement({
85
+ kind: 'area',
86
+ base: [
87
+ [0, 0, 0],
88
+ [1, 0, 0],
89
+ [2, 0, 0],
90
+ ],
91
+ }).success).toBe(false);
92
+ expect(parseMeasurement({
93
+ kind: 'area',
94
+ base: [
95
+ [0, 0, 0],
96
+ [1, 0, 0],
97
+ [1, 0, 1],
98
+ [0, 0.1, 1],
99
+ ],
100
+ }).success).toBe(false);
101
+ });
102
+ test('requires a finite extrusion and at least three volume base points', () => {
103
+ expect(parseMeasurement({
104
+ kind: 'volume',
105
+ base: [
106
+ [0, 0, 0],
107
+ [1, 0, 0],
108
+ [0, 0, 1],
109
+ ],
110
+ extrusion: [0, 2, 0],
111
+ }).success).toBe(true);
112
+ expect(parseMeasurement({
113
+ kind: 'volume',
114
+ base: [
115
+ [0, 0, 0],
116
+ [1, 0, 0],
117
+ ],
118
+ extrusion: [0, 2, 0],
119
+ }).success).toBe(false);
120
+ expect(parseMeasurement({
121
+ kind: 'volume',
122
+ base: [
123
+ [0, 0, 0],
124
+ [1, 0, 0],
125
+ [0, 0, 1],
126
+ ],
127
+ extrusion: [0, Number.NaN, 0],
128
+ }).success).toBe(false);
129
+ expect(parseMeasurement({
130
+ kind: 'volume',
131
+ base: [
132
+ [0, 0, 0],
133
+ [1, 0, 0],
134
+ [0, 0, 1],
135
+ ],
136
+ extrusion: [2, 0, 0],
137
+ }).success).toBe(false);
138
+ });
139
+ });
@@ -0,0 +1,56 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * DWV pipe fitting — the joints drain systems are actually built from:
4
+ * elbows (bends), wyes (45° branch entries, the code-preferred way to
5
+ * join horizontal drains), sanitary tees (square branch entries), and
6
+ * crosses (two opposed branches where a run passes straight through).
7
+ *
8
+ * Local-frame conventions (before `rotation`):
9
+ * - elbow: inlet faces -X, outlet turned `angle`° in XZ.
10
+ * - wye: run along X (inlet -X, outlet +X), branch collar at
11
+ * 45° between +X and +Z.
12
+ * - sanitary-tee: run along X, branch collar faces +Z.
13
+ * - cross: run along X, two opposed branch collars on ±Z.
14
+ */
15
+ export declare const PipeFittingNode: z.ZodObject<{
16
+ object: z.ZodDefault<z.ZodLiteral<"node">>;
17
+ name: z.ZodOptional<z.ZodString>;
18
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
19
+ visible: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
20
+ camera: z.ZodOptional<z.ZodObject<{
21
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
22
+ target: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
23
+ mode: z.ZodDefault<z.ZodEnum<{
24
+ perspective: "perspective";
25
+ orthographic: "orthographic";
26
+ }>>;
27
+ fov: z.ZodOptional<z.ZodNumber>;
28
+ zoom: z.ZodOptional<z.ZodNumber>;
29
+ }, z.core.$strip>>;
30
+ metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
31
+ id: z.ZodDefault<z.ZodTemplateLiteral<`pipe-fitting_${string}`>>;
32
+ type: z.ZodDefault<z.ZodLiteral<"pipe-fitting">>;
33
+ position: z.ZodDefault<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
34
+ rotation: z.ZodDefault<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
35
+ fittingType: z.ZodDefault<z.ZodEnum<{
36
+ elbow: "elbow";
37
+ cross: "cross";
38
+ wye: "wye";
39
+ "sanitary-tee": "sanitary-tee";
40
+ }>>;
41
+ angle: z.ZodDefault<z.ZodNumber>;
42
+ diameter: z.ZodDefault<z.ZodNumber>;
43
+ diameter2: z.ZodDefault<z.ZodNumber>;
44
+ pipeMaterial: z.ZodDefault<z.ZodEnum<{
45
+ pvc: "pvc";
46
+ abs: "abs";
47
+ "cast-iron": "cast-iron";
48
+ }>>;
49
+ system: z.ZodDefault<z.ZodEnum<{
50
+ waste: "waste";
51
+ vent: "vent";
52
+ }>>;
53
+ }, z.core.$strip>;
54
+ export type PipeFittingNode = z.infer<typeof PipeFittingNode>;
55
+ export type PipeFittingNodeId = PipeFittingNode['id'];
56
+ //# sourceMappingURL=pipe-fitting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipe-fitting.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/pipe-fitting.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8B3B,CAAA;AACD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AAC7D,MAAM,MAAM,iBAAiB,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA"}
@@ -0,0 +1,45 @@
1
+ import dedent from 'dedent';
2
+ import { z } from 'zod';
3
+ import { BaseNode, nodeType, objectId } from '../base';
4
+ /**
5
+ * DWV pipe fitting — the joints drain systems are actually built from:
6
+ * elbows (bends), wyes (45° branch entries, the code-preferred way to
7
+ * join horizontal drains), sanitary tees (square branch entries), and
8
+ * crosses (two opposed branches where a run passes straight through).
9
+ *
10
+ * Local-frame conventions (before `rotation`):
11
+ * - elbow: inlet faces -X, outlet turned `angle`° in XZ.
12
+ * - wye: run along X (inlet -X, outlet +X), branch collar at
13
+ * 45° between +X and +Z.
14
+ * - sanitary-tee: run along X, branch collar faces +Z.
15
+ * - cross: run along X, two opposed branch collars on ±Z.
16
+ */
17
+ export const PipeFittingNode = BaseNode.extend({
18
+ id: objectId('pipe-fitting'),
19
+ type: nodeType('pipe-fitting'),
20
+ // Level-local meters.
21
+ position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
22
+ // XYZ euler radians.
23
+ rotation: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
24
+ fittingType: z.enum(['elbow', 'wye', 'sanitary-tee', 'cross']).default('elbow'),
25
+ // Elbow turn in degrees — DWV bends ship as 22.5 / 45 / 90 ("long
26
+ // sweep" for drains); adjustable range matches the duct elbow. 0° is a
27
+ // straight coupling — what an elbow flattens to when its run is dragged
28
+ // into line with the fixed collar.
29
+ angle: z.number().min(0).max(90).default(90),
30
+ // Run nominal size in inches.
31
+ diameter: z.number().min(1.25).max(8).default(2),
32
+ // Branch collar size (wye / sanitary-tee).
33
+ diameter2: z.number().min(1.25).max(8).default(2),
34
+ pipeMaterial: z.enum(['pvc', 'abs', 'cast-iron']).default('pvc'),
35
+ system: z.enum(['waste', 'vent']).default('waste'),
36
+ }).describe(dedent `
37
+ DWV pipe fitting - elbow (bend), wye (45° branch), sanitary tee (square branch), or cross (two opposed branches).
38
+ - position: [x, y, z] level-local meters
39
+ - rotation: [x, y, z] euler radians
40
+ - fittingType: elbow | wye | sanitary-tee | cross
41
+ - angle: elbow turn in degrees (22.5 / 45 / 90 typical)
42
+ - diameter: run size in inches; diameter2: branch collar size (both branches for a cross)
43
+ - pipeMaterial: pvc | abs | cast-iron
44
+ - system: waste | vent
45
+ `);
@@ -0,0 +1,48 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * DWV pipe segment — drain / waste / vent runs in US residential
4
+ * plumbing. Phase 2 of the distribution-system effort: the plumbing
5
+ * sibling of `duct-segment`, sharing the polyline model and the typed
6
+ * port machinery.
7
+ *
8
+ * The defining difference from ducts is SLOPE: drains must fall
9
+ * (IPC: ¼" per foot for pipes under 3", ⅛" allowed at 3"+). Slope is
10
+ * stored implicitly in the path's Y coordinates — the draw tool drops
11
+ * Y as you draw a waste run; vents run level or vertical.
12
+ *
13
+ * Path coordinates are level-local meters. Y may be negative (drains
14
+ * drop below the floor into the joist / crawl space).
15
+ */
16
+ export declare const PipeSegmentNode: z.ZodObject<{
17
+ object: z.ZodDefault<z.ZodLiteral<"node">>;
18
+ name: z.ZodOptional<z.ZodString>;
19
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
20
+ visible: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
21
+ camera: z.ZodOptional<z.ZodObject<{
22
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
23
+ target: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
24
+ mode: z.ZodDefault<z.ZodEnum<{
25
+ perspective: "perspective";
26
+ orthographic: "orthographic";
27
+ }>>;
28
+ fov: z.ZodOptional<z.ZodNumber>;
29
+ zoom: z.ZodOptional<z.ZodNumber>;
30
+ }, z.core.$strip>>;
31
+ metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
32
+ id: z.ZodDefault<z.ZodTemplateLiteral<`pipe-segment_${string}`>>;
33
+ type: z.ZodDefault<z.ZodLiteral<"pipe-segment">>;
34
+ path: z.ZodArray<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
35
+ diameter: z.ZodDefault<z.ZodNumber>;
36
+ pipeMaterial: z.ZodDefault<z.ZodEnum<{
37
+ pvc: "pvc";
38
+ abs: "abs";
39
+ "cast-iron": "cast-iron";
40
+ }>>;
41
+ system: z.ZodDefault<z.ZodEnum<{
42
+ waste: "waste";
43
+ vent: "vent";
44
+ }>>;
45
+ }, z.core.$strip>;
46
+ export type PipeSegmentNode = z.infer<typeof PipeSegmentNode>;
47
+ export type PipeSegmentNodeId = PipeSegmentNode['id'];
48
+ //# sourceMappingURL=pipe-segment.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipe-segment.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/pipe-segment.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoB3B,CAAA;AACD,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAA;AAC7D,MAAM,MAAM,iBAAiB,GAAG,eAAe,CAAC,IAAI,CAAC,CAAA"}
@@ -0,0 +1,36 @@
1
+ import dedent from 'dedent';
2
+ import { z } from 'zod';
3
+ import { BaseNode, nodeType, objectId } from '../base';
4
+ /**
5
+ * DWV pipe segment — drain / waste / vent runs in US residential
6
+ * plumbing. Phase 2 of the distribution-system effort: the plumbing
7
+ * sibling of `duct-segment`, sharing the polyline model and the typed
8
+ * port machinery.
9
+ *
10
+ * The defining difference from ducts is SLOPE: drains must fall
11
+ * (IPC: ¼" per foot for pipes under 3", ⅛" allowed at 3"+). Slope is
12
+ * stored implicitly in the path's Y coordinates — the draw tool drops
13
+ * Y as you draw a waste run; vents run level or vertical.
14
+ *
15
+ * Path coordinates are level-local meters. Y may be negative (drains
16
+ * drop below the floor into the joist / crawl space).
17
+ */
18
+ export const PipeSegmentNode = BaseNode.extend({
19
+ id: objectId('pipe-segment'),
20
+ type: nodeType('pipe-segment'),
21
+ // Polyline path in level-local meters. Minimum two points.
22
+ path: z.array(z.tuple([z.number(), z.number(), z.number()])).min(2),
23
+ // Nominal pipe size in inches. Residential DWV: 1¼ (lav tailpiece) to
24
+ // 4 (building drain); 6 covers oversized mains.
25
+ diameter: z.number().min(1.25).max(8).default(2),
26
+ pipeMaterial: z.enum(['pvc', 'abs', 'cast-iron']).default('pvc'),
27
+ // Which DWV role the run plays. Waste carries water (sloped); vent
28
+ // carries air (level or vertical, dashed in plan).
29
+ system: z.enum(['waste', 'vent']).default('waste'),
30
+ }).describe(dedent `
31
+ DWV pipe segment - drain / waste / vent run as a polyline of 3D points.
32
+ - path: list of [x, y, z] points in level-local meters (min 2; y may go below the floor)
33
+ - diameter: nominal size in inches (1.5 / 2 / 3 / 4 typical residential)
34
+ - pipeMaterial: pvc | abs | cast-iron
35
+ - system: waste (sloped drains) | vent (level / vertical air pipes)
36
+ `);
@@ -0,0 +1,43 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * DWV trap — the P-trap between a fixture and the waste system. Holds a
4
+ * water seal that blocks sewer gas; every drained fixture has exactly
5
+ * one. Modeled as an explicit fitting (not folded into the fixture) so
6
+ * the trap-arm rule (IPC 909.1 max developed length to the vent) has a
7
+ * node to attach to and the inspector can edit size + arm length.
8
+ *
9
+ * Local-frame convention (before `rotation`): inlet faces +Y (up, to
10
+ * the fixture tailpiece), outlet faces +X (the horizontal trap arm
11
+ * toward the vented waste line).
12
+ */
13
+ export declare const PipeTrapNode: z.ZodObject<{
14
+ object: z.ZodDefault<z.ZodLiteral<"node">>;
15
+ name: z.ZodOptional<z.ZodString>;
16
+ parentId: z.ZodDefault<z.ZodNullable<z.ZodString>>;
17
+ visible: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
18
+ camera: z.ZodOptional<z.ZodObject<{
19
+ position: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
20
+ target: z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>;
21
+ mode: z.ZodDefault<z.ZodEnum<{
22
+ perspective: "perspective";
23
+ orthographic: "orthographic";
24
+ }>>;
25
+ fov: z.ZodOptional<z.ZodNumber>;
26
+ zoom: z.ZodOptional<z.ZodNumber>;
27
+ }, z.core.$strip>>;
28
+ metadata: z.ZodDefault<z.ZodOptional<z.ZodJSONSchema>>;
29
+ id: z.ZodDefault<z.ZodTemplateLiteral<`pipe-trap_${string}`>>;
30
+ type: z.ZodDefault<z.ZodLiteral<"pipe-trap">>;
31
+ position: z.ZodDefault<z.ZodTuple<[z.ZodNumber, z.ZodNumber, z.ZodNumber], null>>;
32
+ rotation: z.ZodDefault<z.ZodNumber>;
33
+ diameter: z.ZodDefault<z.ZodNumber>;
34
+ pipeMaterial: z.ZodDefault<z.ZodEnum<{
35
+ pvc: "pvc";
36
+ abs: "abs";
37
+ "cast-iron": "cast-iron";
38
+ }>>;
39
+ armLengthM: z.ZodDefault<z.ZodNumber>;
40
+ }, z.core.$strip>;
41
+ export type PipeTrapNode = z.infer<typeof PipeTrapNode>;
42
+ export type PipeTrapNodeId = PipeTrapNode['id'];
43
+ //# sourceMappingURL=pipe-trap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipe-trap.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/pipe-trap.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAGvB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuBxB,CAAA;AACD,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAA;AACvD,MAAM,MAAM,cAAc,GAAG,YAAY,CAAC,IAAI,CAAC,CAAA"}
@@ -0,0 +1,36 @@
1
+ import dedent from 'dedent';
2
+ import { z } from 'zod';
3
+ import { BaseNode, nodeType, objectId } from '../base';
4
+ /**
5
+ * DWV trap — the P-trap between a fixture and the waste system. Holds a
6
+ * water seal that blocks sewer gas; every drained fixture has exactly
7
+ * one. Modeled as an explicit fitting (not folded into the fixture) so
8
+ * the trap-arm rule (IPC 909.1 max developed length to the vent) has a
9
+ * node to attach to and the inspector can edit size + arm length.
10
+ *
11
+ * Local-frame convention (before `rotation`): inlet faces +Y (up, to
12
+ * the fixture tailpiece), outlet faces +X (the horizontal trap arm
13
+ * toward the vented waste line).
14
+ */
15
+ export const PipeTrapNode = BaseNode.extend({
16
+ id: objectId('pipe-trap'),
17
+ type: nodeType('pipe-trap'),
18
+ // Level-local meters.
19
+ position: z.tuple([z.number(), z.number(), z.number()]).default([0, 0, 0]),
20
+ // Yaw in radians (the arm direction in plan).
21
+ rotation: z.number().default(0),
22
+ // Trap size in inches — matches the fixture drain it serves.
23
+ diameter: z.number().min(1.25).max(4).default(2),
24
+ pipeMaterial: z.enum(['pvc', 'abs', 'cast-iron']).default('pvc'),
25
+ // Developed length of the trap arm (trap weir → vent) in meters. The
26
+ // draw tool measures it when the arm is drawn; editable in the
27
+ // inspector. Drives the IPC 909.1 max-trap-arm check.
28
+ armLengthM: z.number().min(0).default(0),
29
+ }).describe(dedent `
30
+ DWV trap (P-trap) - the water-seal fitting between a fixture and the waste line.
31
+ - position: [x, y, z] level-local meters
32
+ - rotation: yaw radians (trap-arm direction in plan)
33
+ - diameter: trap size in inches (matches the fixture drain)
34
+ - pipeMaterial: pvc | abs | cast-iron
35
+ - armLengthM: developed length from trap to vent in meters (IPC 909.1 limited by size)
36
+ `);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ridge-vent-defaults.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ridge-vent-defaults.test.d.ts","sourceRoot":"","sources":["../../../src/schema/nodes/ridge-vent-defaults.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,205 @@
1
+ import { describe, expect, test } from 'bun:test';
2
+ import { createDefaultRidgeVentsForSegment, getRidgeVentLinesForSegment, isAutoRidgeVentEnabled, isDefaultRidgeVentNode, RidgeVentNode, } from './ridge-vent';
3
+ import { getDutchRoofMetrics, getRoofSegmentVisibleTopBounds, ROOF_SHAPE_DEFAULTS, RoofSegmentNode, } from './roof-segment';
4
+ describe('createDefaultRidgeVentsForSegment', () => {
5
+ test('creates one shingled default ridge vent for gable roofs', () => {
6
+ const segment = RoofSegmentNode.parse({
7
+ roofType: 'gable',
8
+ width: 8,
9
+ depth: 6,
10
+ });
11
+ const vents = createDefaultRidgeVentsForSegment(segment);
12
+ expect(vents).toHaveLength(1);
13
+ expect(vents[0]?.name).toBe('Ridge Vent');
14
+ expect(vents[0]?.style).toBe('shingled');
15
+ expect(vents[0]?.roofSegmentId).toBe(segment.id);
16
+ expect(isDefaultRidgeVentNode(vents[0], segment.id)).toBe(true);
17
+ });
18
+ test('keeps generated gable ridge vents anchored to the untrimmed ridge', () => {
19
+ const segment = RoofSegmentNode.parse({
20
+ roofType: 'gable',
21
+ width: 8,
22
+ depth: 6,
23
+ overhang: 0,
24
+ wallThickness: 0,
25
+ shingleThickness: 0,
26
+ trim: { left: 1, right: 2, front: 0, back: 0 },
27
+ });
28
+ const vents = createDefaultRidgeVentsForSegment(segment);
29
+ expect(vents).toHaveLength(1);
30
+ expect(vents[0]?.length).toBeCloseTo(8);
31
+ expect(vents[0]?.position[0]).toBeCloseTo(0);
32
+ });
33
+ test('creates top ridge plus four hip vents for rectangular hip roofs', () => {
34
+ const segment = RoofSegmentNode.parse({
35
+ roofType: 'hip',
36
+ width: 8,
37
+ depth: 6,
38
+ });
39
+ const vents = createDefaultRidgeVentsForSegment(segment);
40
+ expect(vents).toHaveLength(5);
41
+ expect(vents.filter((vent) => vent.name === 'Ridge Vent')).toHaveLength(1);
42
+ expect(vents.filter((vent) => vent.name === 'Hip Ridge Vent')).toHaveLength(4);
43
+ for (const vent of vents) {
44
+ expect(vent.style).toBe('shingled');
45
+ expect(vent.length).toBeGreaterThan(0.4);
46
+ expect(isDefaultRidgeVentNode(vent, segment.id)).toBe(true);
47
+ }
48
+ });
49
+ test('omits the collapsed top ridge on square hip roofs', () => {
50
+ const segment = RoofSegmentNode.parse({
51
+ roofType: 'hip',
52
+ width: 6,
53
+ depth: 6,
54
+ });
55
+ const vents = createDefaultRidgeVentsForSegment(segment);
56
+ expect(vents).toHaveLength(4);
57
+ expect(vents.every((vent) => vent.name === 'Hip Ridge Vent')).toBe(true);
58
+ });
59
+ test('creates a top ridge plus four hip vents for width-axis Dutch roofs', () => {
60
+ const segment = RoofSegmentNode.parse({
61
+ roofType: 'dutch',
62
+ width: 8,
63
+ depth: 6,
64
+ overhang: 0,
65
+ wallThickness: 0,
66
+ shingleThickness: 0,
67
+ });
68
+ const vents = createDefaultRidgeVentsForSegment(segment);
69
+ expect(vents.filter((vent) => vent.name === 'Ridge Vent')).toHaveLength(1);
70
+ expect(vents.filter((vent) => vent.name === 'Hip Ridge Vent')).toHaveLength(4);
71
+ // Width-axis Dutch ridge runs along X (constant Z = 0).
72
+ const metrics = getDutchRoofMetrics(segment);
73
+ const ridge = vents.find((vent) => vent.name === 'Ridge Vent');
74
+ const frontRightHip = vents.find((vent) => vent.name === 'Hip Ridge Vent' &&
75
+ (vent.position[0] ?? 0) > 0 &&
76
+ (vent.position[2] ?? 0) > 0);
77
+ const expectedRakeReach = Math.min(segment.dutchGabletRake, Math.max(0, segment.width / 2 - metrics.waistHalfX) * 0.98);
78
+ expect(ridge?.position[2]).toBeCloseTo(0);
79
+ expect(ridge?.length).toBeCloseTo((metrics.waistHalfX + expectedRakeReach) * 2, 2);
80
+ expect(frontRightHip?.position[0]).toBeCloseTo((4 + 2.93) / 2, 2);
81
+ expect(frontRightHip?.position[2]).toBeCloseTo((3 + 1.5) / 2, 2);
82
+ for (const vent of vents) {
83
+ expect(vent.style).toBe('shingled');
84
+ expect(vent.length).toBeGreaterThan(0.4);
85
+ expect(isDefaultRidgeVentNode(vent, segment.id)).toBe(true);
86
+ }
87
+ });
88
+ test('treats legacy segments with generated ridge vents as auto-enabled', () => {
89
+ const segment = RoofSegmentNode.parse({
90
+ roofType: 'gable',
91
+ width: 8,
92
+ depth: 6,
93
+ });
94
+ const vents = createDefaultRidgeVentsForSegment(segment);
95
+ const nodes = Object.fromEntries(vents.map((vent) => [vent.id, vent]));
96
+ expect(isAutoRidgeVentEnabled({
97
+ id: segment.id,
98
+ children: vents.map((vent) => vent.id),
99
+ metadata: {},
100
+ }, nodes)).toBe(true);
101
+ });
102
+ test('treats legacy preset-white ridge vents as generated defaults', () => {
103
+ const segment = RoofSegmentNode.parse({
104
+ id: 'rseg_test',
105
+ roofType: 'gable',
106
+ width: 8,
107
+ depth: 6,
108
+ });
109
+ const legacyVent = RidgeVentNode.parse({
110
+ id: 'rvent_legacy',
111
+ roofSegmentId: segment.id,
112
+ name: 'Ridge Vent',
113
+ style: 'shingled',
114
+ materialPreset: 'preset-white',
115
+ position: [0, 0, 0],
116
+ length: 8,
117
+ });
118
+ expect(isDefaultRidgeVentNode(legacyVent, segment.id)).toBe(true);
119
+ });
120
+ test('creates a Z-oriented ridge plus four hip lines for depth-axis Dutch roofs', () => {
121
+ const segment = RoofSegmentNode.parse({
122
+ roofType: 'dutch',
123
+ width: 6,
124
+ depth: 8,
125
+ overhang: 0,
126
+ wallThickness: 0,
127
+ shingleThickness: 0,
128
+ });
129
+ const lines = getRidgeVentLinesForSegment(segment);
130
+ const metrics = getDutchRoofMetrics(segment);
131
+ const expectedRakeReach = Math.min(segment.dutchGabletRake, Math.max(0, segment.depth / 2 - metrics.waistHalfZ) * 0.98);
132
+ const ridges = lines.filter((line) => line.name === 'Ridge Vent');
133
+ const hips = lines.filter((line) => line.name === 'Hip Ridge Vent');
134
+ expect(ridges).toHaveLength(1);
135
+ expect(hips).toHaveLength(4);
136
+ // Depth-axis Dutch ridge runs along Z (constant X = 0).
137
+ expect(ridges[0]?.start[0]).toBeCloseTo(0);
138
+ expect(ridges[0]?.end[0]).toBeCloseTo(0);
139
+ expect(Math.abs(ridges[0]?.start[1] ?? 0)).toBeCloseTo(metrics.waistHalfZ + expectedRakeReach, 2);
140
+ });
141
+ test('keeps Dutch hip lines on the rendered arris when the roof has overhang', () => {
142
+ // Overhang + shingle thickness expand the rendered roof; the eave corners
143
+ // and the waist must share that expanded frame, otherwise the hip lines
144
+ // tilt off the arris and the vents sink into the slope.
145
+ const segment = RoofSegmentNode.parse({
146
+ roofType: 'dutch',
147
+ width: 8,
148
+ depth: 6,
149
+ overhang: 0.5,
150
+ wallThickness: 0.2,
151
+ shingleThickness: 0.05,
152
+ });
153
+ const lines = getRidgeVentLinesForSegment(segment);
154
+ const hips = lines.filter((line) => line.name === 'Hip Ridge Vent');
155
+ const ridge = lines.find((line) => line.name === 'Ridge Vent');
156
+ expect(hips).toHaveLength(4);
157
+ expect(ridge).toBeDefined();
158
+ const bounds = getRoofSegmentVisibleTopBounds(segment);
159
+ const { axis, inset } = getDutchRoofMetrics(segment);
160
+ const waistLengthRatio = segment.dutchWaistLengthRatio;
161
+ // width 8 >= depth 6 -> axis 'x': the ridge runs along X (waist scaled by
162
+ // waistLengthRatio), and Z is the clean hipped axis (inset exactly).
163
+ expect(axis).toBe('x');
164
+ const halfWExpanded = bounds.maxX;
165
+ const halfDExpanded = bounds.maxZ;
166
+ const expectedWaistX = (halfWExpanded - inset) * waistLengthRatio;
167
+ const expectedWaistZ = halfDExpanded - inset;
168
+ const expectedRakeReach = Math.min(segment.dutchGabletRake ?? ROOF_SHAPE_DEFAULTS.dutchGabletRake, Math.max(0, halfWExpanded - expectedWaistX) * 0.98);
169
+ for (const hip of hips) {
170
+ const [ex, ez] = hip.start;
171
+ const [wx, wz] = hip.end;
172
+ // Eave end on an expanded-bounds corner; upper end at the rendered rake
173
+ // termination where the lower slope starts, derived from the SAME
174
+ // expanded frame (not the base-dim inner waist).
175
+ expect(Math.abs(ex)).toBeCloseTo(halfWExpanded);
176
+ expect(Math.abs(ez)).toBeCloseTo(halfDExpanded);
177
+ expect(Math.abs(wx)).toBeCloseTo(expectedWaistX + expectedRakeReach);
178
+ expect(Math.abs(wz)).toBeCloseTo(expectedWaistZ);
179
+ }
180
+ });
181
+ test('creates top ridge plus four upper hip vents plus four lower-slope vents for mansard roofs', () => {
182
+ const segment = RoofSegmentNode.parse({
183
+ roofType: 'mansard',
184
+ width: 8,
185
+ depth: 6,
186
+ });
187
+ const vents = createDefaultRidgeVentsForSegment(segment);
188
+ expect(vents).toHaveLength(9);
189
+ expect(vents.filter((vent) => vent.name === 'Ridge Vent')).toHaveLength(1);
190
+ expect(vents.filter((vent) => vent.name === 'Hip Ridge Vent')).toHaveLength(4);
191
+ expect(vents.filter((vent) => vent.name === 'Slope Ridge Vent')).toHaveLength(4);
192
+ expect(vents.find((vent) => vent.name === 'Ridge Vent')?.length).toBeLessThan(segment.width);
193
+ const slopeVents = vents.filter((vent) => vent.name === 'Slope Ridge Vent');
194
+ const slopeRotations = slopeVents.map((vent) => Math.abs(vent.rotation));
195
+ expect(slopeRotations.every((rotation) => rotation > 0.1)).toBe(true);
196
+ expect(slopeRotations.every((rotation) => Math.abs(rotation - Math.PI / 2) > 0.1)).toBe(true);
197
+ expect(slopeVents.every((vent) => Math.abs(vent.position[0]) > segment.width / 2 - 0.8 &&
198
+ Math.abs(vent.position[2]) > segment.depth / 2 - 0.8)).toBe(true);
199
+ expect(new Set(slopeVents.map((vent) => `${Math.sign(vent.position[0])},${Math.sign(vent.position[2])}`)).size).toBe(4);
200
+ for (const vent of vents) {
201
+ expect(vent.style).toBe('shingled');
202
+ expect(isDefaultRidgeVentNode(vent, segment.id)).toBe(true);
203
+ }
204
+ });
205
+ });