@pascal-app/nodes 1.0.0-beta.5 → 1.0.0

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 (831) hide show
  1. package/LICENSE +21 -0
  2. package/dist/block/commands.d.ts +68 -0
  3. package/dist/block/commands.d.ts.map +1 -0
  4. package/dist/block/commands.js +1144 -0
  5. package/dist/block/contextual-help.d.ts +3 -0
  6. package/dist/block/contextual-help.d.ts.map +1 -0
  7. package/dist/block/contextual-help.js +38 -0
  8. package/dist/block/definition.d.ts +8 -0
  9. package/dist/block/definition.d.ts.map +1 -0
  10. package/dist/block/definition.js +126 -0
  11. package/dist/block/edit-session.d.ts +16 -0
  12. package/dist/block/edit-session.d.ts.map +1 -0
  13. package/dist/block/edit-session.js +32 -0
  14. package/dist/block/face-host.d.ts +3 -0
  15. package/dist/block/face-host.d.ts.map +1 -0
  16. package/dist/block/face-host.js +178 -0
  17. package/dist/block/floorplan.d.ts +3 -0
  18. package/dist/block/floorplan.d.ts.map +1 -0
  19. package/dist/block/floorplan.js +43 -0
  20. package/dist/block/geometry-snap.d.ts +18 -0
  21. package/dist/block/geometry-snap.d.ts.map +1 -0
  22. package/dist/block/geometry-snap.js +137 -0
  23. package/dist/block/geometry.d.ts +11 -0
  24. package/dist/block/geometry.d.ts.map +1 -0
  25. package/dist/block/geometry.js +146 -0
  26. package/dist/block/gesture-wheel.d.ts +8 -0
  27. package/dist/block/gesture-wheel.d.ts.map +1 -0
  28. package/dist/block/gesture-wheel.js +7 -0
  29. package/dist/block/interaction-sfx.d.ts +3 -0
  30. package/dist/block/interaction-sfx.d.ts.map +1 -0
  31. package/dist/block/interaction-sfx.js +16 -0
  32. package/dist/block/last-operation.d.ts +48 -0
  33. package/dist/block/last-operation.d.ts.map +1 -0
  34. package/dist/block/last-operation.js +120 -0
  35. package/dist/block/loop-cut-interaction.d.ts +5 -0
  36. package/dist/block/loop-cut-interaction.d.ts.map +1 -0
  37. package/dist/block/loop-cut-interaction.js +13 -0
  38. package/dist/block/material-slots.d.ts +61 -0
  39. package/dist/block/material-slots.d.ts.map +1 -0
  40. package/dist/block/material-slots.js +131 -0
  41. package/dist/block/modal-face-operation.d.ts +13 -0
  42. package/dist/block/modal-face-operation.d.ts.map +1 -0
  43. package/dist/block/modal-face-operation.js +40 -0
  44. package/dist/block/modal-session.d.ts +15 -0
  45. package/dist/block/modal-session.d.ts.map +1 -0
  46. package/dist/block/modal-session.js +40 -0
  47. package/dist/block/modal-transform.d.ts +30 -0
  48. package/dist/block/modal-transform.d.ts.map +1 -0
  49. package/dist/block/modal-transform.js +144 -0
  50. package/dist/block/paint.d.ts +3 -0
  51. package/dist/block/paint.d.ts.map +1 -0
  52. package/dist/block/paint.js +136 -0
  53. package/dist/block/panel.d.ts +2 -0
  54. package/dist/block/panel.d.ts.map +1 -0
  55. package/dist/block/panel.js +234 -0
  56. package/dist/block/parametrics.d.ts +4 -0
  57. package/dist/block/parametrics.d.ts.map +1 -0
  58. package/dist/block/parametrics.js +9 -0
  59. package/dist/block/preview.d.ts +6 -0
  60. package/dist/block/preview.d.ts.map +1 -0
  61. package/dist/block/preview.js +46 -0
  62. package/dist/block/rotation-drag.d.ts +5 -0
  63. package/dist/block/rotation-drag.d.ts.map +1 -0
  64. package/dist/block/rotation-drag.js +18 -0
  65. package/dist/block/schema.d.ts +2 -0
  66. package/dist/block/schema.d.ts.map +1 -0
  67. package/dist/block/schema.js +1 -0
  68. package/dist/block/selection-geometry.d.ts +9 -0
  69. package/dist/block/selection-geometry.d.ts.map +1 -0
  70. package/dist/block/selection-geometry.js +31 -0
  71. package/dist/block/selection-model.d.ts +17 -0
  72. package/dist/block/selection-model.d.ts.map +1 -0
  73. package/dist/block/selection-model.js +84 -0
  74. package/dist/block/selection.d.ts +4 -0
  75. package/dist/block/selection.d.ts.map +1 -0
  76. package/dist/block/selection.js +2570 -0
  77. package/dist/block/slots.d.ts +4 -0
  78. package/dist/block/slots.d.ts.map +1 -0
  79. package/dist/block/slots.js +17 -0
  80. package/dist/block/tool.d.ts +3 -0
  81. package/dist/block/tool.d.ts.map +1 -0
  82. package/dist/block/tool.js +174 -0
  83. package/dist/block/toolbar-state.d.ts +46 -0
  84. package/dist/block/toolbar-state.d.ts.map +1 -0
  85. package/dist/block/toolbar-state.js +67 -0
  86. package/dist/block/use-block-face-operation.d.ts +39 -0
  87. package/dist/block/use-block-face-operation.d.ts.map +1 -0
  88. package/dist/block/use-block-face-operation.js +218 -0
  89. package/dist/box-vent/definition.d.ts.map +1 -1
  90. package/dist/box-vent/definition.js +8 -5
  91. package/dist/box-vent/geometry.d.ts +4 -0
  92. package/dist/box-vent/geometry.d.ts.map +1 -1
  93. package/dist/box-vent/geometry.js +76 -44
  94. package/dist/box-vent/paint.d.ts +4 -0
  95. package/dist/box-vent/paint.d.ts.map +1 -0
  96. package/dist/box-vent/paint.js +40 -0
  97. package/dist/box-vent/panel.js +1 -1
  98. package/dist/box-vent/parametrics.js +3 -3
  99. package/dist/box-vent/renderer.d.ts.map +1 -1
  100. package/dist/box-vent/renderer.js +29 -10
  101. package/dist/cabinet/compartment-card.d.ts.map +1 -1
  102. package/dist/cabinet/compartment-card.js +11 -3
  103. package/dist/cabinet/continuous-placement.d.ts +3 -0
  104. package/dist/cabinet/continuous-placement.d.ts.map +1 -1
  105. package/dist/cabinet/continuous-placement.js +6 -0
  106. package/dist/cabinet/definition.d.ts.map +1 -1
  107. package/dist/cabinet/definition.js +400 -54
  108. package/dist/cabinet/floorplan-move.d.ts.map +1 -1
  109. package/dist/cabinet/floorplan-move.js +91 -22
  110. package/dist/cabinet/geometry/fridge.d.ts.map +1 -1
  111. package/dist/cabinet/geometry/fridge.js +25 -1
  112. package/dist/cabinet/geometry/fronts.d.ts +4 -0
  113. package/dist/cabinet/geometry/fronts.d.ts.map +1 -1
  114. package/dist/cabinet/geometry/fronts.js +1 -1
  115. package/dist/cabinet/geometry/run.d.ts.map +1 -1
  116. package/dist/cabinet/geometry/run.js +19 -1
  117. package/dist/cabinet/geometry.d.ts.map +1 -1
  118. package/dist/cabinet/geometry.js +63 -1
  119. package/dist/cabinet/index.d.ts +1 -0
  120. package/dist/cabinet/index.d.ts.map +1 -1
  121. package/dist/cabinet/index.js +1 -0
  122. package/dist/cabinet/insertion.d.ts +19 -0
  123. package/dist/cabinet/insertion.d.ts.map +1 -0
  124. package/dist/cabinet/insertion.js +49 -0
  125. package/dist/cabinet/move-frame.d.ts.map +1 -1
  126. package/dist/cabinet/move-frame.js +68 -2
  127. package/dist/cabinet/panel-context.d.ts +7 -0
  128. package/dist/cabinet/panel-context.d.ts.map +1 -0
  129. package/dist/cabinet/panel-context.js +16 -0
  130. package/dist/cabinet/panel-visibility.d.ts +9 -0
  131. package/dist/cabinet/panel-visibility.d.ts.map +1 -0
  132. package/dist/cabinet/panel-visibility.js +26 -0
  133. package/dist/cabinet/panel.d.ts.map +1 -1
  134. package/dist/cabinet/panel.js +221 -37
  135. package/dist/cabinet/placement-dimensions.d.ts +42 -0
  136. package/dist/cabinet/placement-dimensions.d.ts.map +1 -0
  137. package/dist/cabinet/placement-dimensions.js +227 -0
  138. package/dist/cabinet/placement-snap.d.ts +21 -1
  139. package/dist/cabinet/placement-snap.d.ts.map +1 -1
  140. package/dist/cabinet/placement-snap.js +48 -6
  141. package/dist/cabinet/preset-width-debt.d.ts +7 -0
  142. package/dist/cabinet/preset-width-debt.d.ts.map +1 -0
  143. package/dist/cabinet/preset-width-debt.js +45 -0
  144. package/dist/cabinet/presets.d.ts.map +1 -1
  145. package/dist/cabinet/presets.js +14 -13
  146. package/dist/cabinet/profiles.d.ts +14 -0
  147. package/dist/cabinet/profiles.d.ts.map +1 -0
  148. package/dist/cabinet/profiles.js +30 -0
  149. package/dist/cabinet/quick-actions.d.ts.map +1 -1
  150. package/dist/cabinet/quick-actions.js +8 -24
  151. package/dist/cabinet/reveals.d.ts +37 -0
  152. package/dist/cabinet/reveals.d.ts.map +1 -0
  153. package/dist/cabinet/reveals.js +13 -0
  154. package/dist/cabinet/run-layout.d.ts +90 -4
  155. package/dist/cabinet/run-layout.d.ts.map +1 -1
  156. package/dist/cabinet/run-layout.js +408 -23
  157. package/dist/cabinet/run-ops.d.ts +202 -10
  158. package/dist/cabinet/run-ops.d.ts.map +1 -1
  159. package/dist/cabinet/run-ops.js +520 -33
  160. package/dist/cabinet/run-panel.d.ts +6 -2
  161. package/dist/cabinet/run-panel.d.ts.map +1 -1
  162. package/dist/cabinet/run-panel.js +285 -108
  163. package/dist/cabinet/stack-transitions.d.ts.map +1 -1
  164. package/dist/cabinet/stack-transitions.js +35 -24
  165. package/dist/cabinet/stack.d.ts +169 -1
  166. package/dist/cabinet/stack.d.ts.map +1 -1
  167. package/dist/cabinet/stack.js +57 -18
  168. package/dist/cabinet/tool.d.ts.map +1 -1
  169. package/dist/cabinet/tool.js +615 -91
  170. package/dist/cabinet/validation.d.ts +27 -0
  171. package/dist/cabinet/validation.d.ts.map +1 -0
  172. package/dist/cabinet/validation.js +56 -0
  173. package/dist/cabinet/wall-height-presets.d.ts +12 -0
  174. package/dist/cabinet/wall-height-presets.d.ts.map +1 -0
  175. package/dist/cabinet/wall-height-presets.js +15 -0
  176. package/dist/cabinet/wall-snap.d.ts +27 -3
  177. package/dist/cabinet/wall-snap.d.ts.map +1 -1
  178. package/dist/cabinet/wall-snap.js +278 -62
  179. package/dist/cabinet/widths.d.ts +10 -0
  180. package/dist/cabinet/widths.d.ts.map +1 -0
  181. package/dist/cabinet/widths.js +13 -0
  182. package/dist/ceiling/definition.d.ts.map +1 -1
  183. package/dist/ceiling/definition.js +7 -1
  184. package/dist/ceiling/floorplan-affordances.d.ts +8 -8
  185. package/dist/ceiling/materials.d.ts.map +1 -1
  186. package/dist/ceiling/materials.js +0 -1
  187. package/dist/ceiling/panel.js +1 -1
  188. package/dist/ceiling/parametrics.js +1 -1
  189. package/dist/chimney/definition.js +1 -1
  190. package/dist/chimney/panel.js +1 -1
  191. package/dist/chimney/parametrics.js +3 -3
  192. package/dist/column/definition.d.ts.map +1 -1
  193. package/dist/column/definition.js +22 -4
  194. package/dist/column/panel.d.ts.map +1 -1
  195. package/dist/column/panel.js +37 -7
  196. package/dist/column/parametrics.d.ts.map +1 -1
  197. package/dist/column/parametrics.js +5 -3
  198. package/dist/column/tool.d.ts.map +1 -1
  199. package/dist/column/tool.js +50 -14
  200. package/dist/cupola/definition.d.ts.map +1 -1
  201. package/dist/cupola/definition.js +15 -6
  202. package/dist/cupola/geometry.d.ts +6 -0
  203. package/dist/cupola/geometry.d.ts.map +1 -1
  204. package/dist/cupola/geometry.js +60 -13
  205. package/dist/cupola/paint.d.ts +4 -0
  206. package/dist/cupola/paint.d.ts.map +1 -0
  207. package/dist/cupola/paint.js +39 -0
  208. package/dist/cupola/panel.js +1 -1
  209. package/dist/cupola/parametrics.js +3 -3
  210. package/dist/cupola/renderer.d.ts.map +1 -1
  211. package/dist/cupola/renderer.js +27 -8
  212. package/dist/door/floorplan-move.d.ts.map +1 -1
  213. package/dist/door/floorplan-move.js +1 -1
  214. package/dist/door/move-tool.d.ts.map +1 -1
  215. package/dist/door/move-tool.js +86 -32
  216. package/dist/door/panel.d.ts.map +1 -1
  217. package/dist/door/panel.js +1 -1
  218. package/dist/door/parametrics.js +2 -2
  219. package/dist/door/tool.d.ts.map +1 -1
  220. package/dist/door/tool.js +16 -4
  221. package/dist/dormer/csg-geometry.d.ts +6 -42
  222. package/dist/dormer/csg-geometry.d.ts.map +1 -1
  223. package/dist/dormer/csg-geometry.js +38 -209
  224. package/dist/dormer/definition.d.ts +4 -12
  225. package/dist/dormer/definition.d.ts.map +1 -1
  226. package/dist/dormer/definition.js +18 -193
  227. package/dist/dormer/floorplan.d.ts +3 -3
  228. package/dist/dormer/floorplan.d.ts.map +1 -1
  229. package/dist/dormer/floorplan.js +6 -17
  230. package/dist/dormer/geometry.d.ts +5 -24
  231. package/dist/dormer/geometry.d.ts.map +1 -1
  232. package/dist/dormer/geometry.js +64 -41
  233. package/dist/dormer/index.d.ts +1 -1
  234. package/dist/dormer/index.d.ts.map +1 -1
  235. package/dist/dormer/index.js +1 -1
  236. package/dist/dormer/move-tool.d.ts.map +1 -1
  237. package/dist/dormer/move-tool.js +31 -3
  238. package/dist/dormer/panel-windows-section.d.ts +9 -0
  239. package/dist/dormer/panel-windows-section.d.ts.map +1 -0
  240. package/dist/dormer/panel-windows-section.js +7 -0
  241. package/dist/dormer/panel.d.ts.map +1 -1
  242. package/dist/dormer/panel.js +100 -18
  243. package/dist/dormer/parametrics.d.ts.map +1 -1
  244. package/dist/dormer/parametrics.js +8 -77
  245. package/dist/dormer/preview.d.ts.map +1 -1
  246. package/dist/dormer/preview.js +9 -1
  247. package/dist/dormer/renderer.d.ts.map +1 -1
  248. package/dist/dormer/renderer.js +24 -26
  249. package/dist/dormer/tool.d.ts.map +1 -1
  250. package/dist/dormer/tool.js +3 -1
  251. package/dist/dormer/window-layout.d.ts +15 -0
  252. package/dist/dormer/window-layout.d.ts.map +1 -0
  253. package/dist/dormer/window-layout.js +59 -0
  254. package/dist/downspout/definition.d.ts.map +1 -1
  255. package/dist/downspout/definition.js +17 -4
  256. package/dist/downspout/geometry.d.ts.map +1 -1
  257. package/dist/downspout/geometry.js +10 -0
  258. package/dist/downspout/inspector-editors.d.ts.map +1 -1
  259. package/dist/downspout/inspector-editors.js +9 -1
  260. package/dist/downspout/parametrics.d.ts.map +1 -1
  261. package/dist/downspout/parametrics.js +2 -1
  262. package/dist/downspout/renderer.d.ts.map +1 -1
  263. package/dist/downspout/renderer.js +19 -3
  264. package/dist/downspout/system.d.ts +7 -0
  265. package/dist/downspout/system.d.ts.map +1 -0
  266. package/dist/downspout/system.js +114 -0
  267. package/dist/duct-fitting/accessory-geometry.d.ts +4 -0
  268. package/dist/duct-fitting/accessory-geometry.d.ts.map +1 -0
  269. package/dist/duct-fitting/accessory-geometry.js +47 -0
  270. package/dist/duct-fitting/definition.d.ts.map +1 -1
  271. package/dist/duct-fitting/definition.js +11 -1
  272. package/dist/duct-fitting/floorplan.d.ts.map +1 -1
  273. package/dist/duct-fitting/floorplan.js +8 -0
  274. package/dist/duct-fitting/geometry.d.ts.map +1 -1
  275. package/dist/duct-fitting/geometry.js +52 -38
  276. package/dist/duct-fitting/parametrics.d.ts.map +1 -1
  277. package/dist/duct-fitting/parametrics.js +78 -15
  278. package/dist/duct-fitting/ports.d.ts +1 -0
  279. package/dist/duct-fitting/ports.d.ts.map +1 -1
  280. package/dist/duct-fitting/ports.js +46 -0
  281. package/dist/duct-fitting/tool.d.ts +19 -0
  282. package/dist/duct-fitting/tool.d.ts.map +1 -1
  283. package/dist/duct-fitting/tool.js +126 -34
  284. package/dist/duct-segment/continuation.d.ts +21 -0
  285. package/dist/duct-segment/continuation.d.ts.map +1 -0
  286. package/dist/duct-segment/continuation.js +167 -0
  287. package/dist/duct-segment/definition.d.ts.map +1 -1
  288. package/dist/duct-segment/definition.js +19 -18
  289. package/dist/duct-segment/floorplan.d.ts.map +1 -1
  290. package/dist/duct-segment/floorplan.js +43 -0
  291. package/dist/duct-segment/geometry.d.ts.map +1 -1
  292. package/dist/duct-segment/geometry.js +3 -0
  293. package/dist/duct-segment/move-tool.d.ts.map +1 -1
  294. package/dist/duct-segment/move-tool.js +33 -6
  295. package/dist/duct-segment/parametrics.d.ts.map +1 -1
  296. package/dist/duct-segment/parametrics.js +45 -1
  297. package/dist/duct-segment/selection.d.ts +3 -4
  298. package/dist/duct-segment/selection.d.ts.map +1 -1
  299. package/dist/duct-segment/selection.js +186 -158
  300. package/dist/duct-segment/tool.d.ts +37 -0
  301. package/dist/duct-segment/tool.d.ts.map +1 -1
  302. package/dist/duct-segment/tool.js +305 -678
  303. package/dist/duct-terminal/parametrics.js +2 -2
  304. package/dist/duct-terminal/tool.d.ts.map +1 -1
  305. package/dist/duct-terminal/tool.js +37 -7
  306. package/dist/elevator/panel.d.ts.map +1 -1
  307. package/dist/elevator/panel.js +7 -7
  308. package/dist/elevator/system.d.ts.map +1 -1
  309. package/dist/elevator/system.js +10 -1
  310. package/dist/eyebrow-vent/definition.d.ts.map +1 -1
  311. package/dist/eyebrow-vent/definition.js +8 -5
  312. package/dist/eyebrow-vent/geometry.d.ts +4 -0
  313. package/dist/eyebrow-vent/geometry.d.ts.map +1 -1
  314. package/dist/eyebrow-vent/geometry.js +60 -15
  315. package/dist/eyebrow-vent/paint.d.ts +4 -0
  316. package/dist/eyebrow-vent/paint.d.ts.map +1 -0
  317. package/dist/eyebrow-vent/paint.js +33 -0
  318. package/dist/eyebrow-vent/panel.js +1 -1
  319. package/dist/eyebrow-vent/parametrics.js +3 -3
  320. package/dist/eyebrow-vent/renderer.d.ts.map +1 -1
  321. package/dist/eyebrow-vent/renderer.js +27 -8
  322. package/dist/fence/definition.d.ts.map +1 -1
  323. package/dist/fence/definition.js +2 -1
  324. package/dist/fence/floorplan-affordances.d.ts +1 -0
  325. package/dist/fence/floorplan-affordances.d.ts.map +1 -1
  326. package/dist/fence/floorplan-affordances.js +30 -1
  327. package/dist/fence/floorplan.d.ts.map +1 -1
  328. package/dist/fence/floorplan.js +14 -0
  329. package/dist/fence/parametrics.js +3 -3
  330. package/dist/fence/tool.d.ts.map +1 -1
  331. package/dist/fence/tool.js +47 -20
  332. package/dist/gutter/corner-mitre.d.ts.map +1 -1
  333. package/dist/gutter/corner-mitre.js +47 -7
  334. package/dist/gutter/definition.d.ts.map +1 -1
  335. package/dist/gutter/definition.js +23 -13
  336. package/dist/gutter/eave-align.d.ts.map +1 -1
  337. package/dist/gutter/eave-align.js +10 -0
  338. package/dist/gutter/eave-snap.d.ts +1 -1
  339. package/dist/gutter/eave-snap.d.ts.map +1 -1
  340. package/dist/gutter/eave-snap.js +10 -23
  341. package/dist/gutter/geometry.d.ts.map +1 -1
  342. package/dist/gutter/geometry.js +126 -8
  343. package/dist/gutter/outlet-lookup.d.ts.map +1 -1
  344. package/dist/gutter/outlet-lookup.js +22 -1
  345. package/dist/gutter/paint.d.ts +3 -0
  346. package/dist/gutter/paint.d.ts.map +1 -0
  347. package/dist/gutter/paint.js +6 -0
  348. package/dist/gutter/parametrics.d.ts.map +1 -1
  349. package/dist/gutter/parametrics.js +4 -1
  350. package/dist/gutter/renderer.d.ts.map +1 -1
  351. package/dist/gutter/renderer.js +63 -7
  352. package/dist/gutter/trim-clip.d.ts +3 -0
  353. package/dist/gutter/trim-clip.d.ts.map +1 -0
  354. package/dist/gutter/trim-clip.js +5 -0
  355. package/dist/hvac-equipment/parametrics.js +3 -3
  356. package/dist/hvac-equipment/tool.d.ts.map +1 -1
  357. package/dist/hvac-equipment/tool.js +12 -2
  358. package/dist/index.d.ts +5 -2
  359. package/dist/index.d.ts.map +1 -1
  360. package/dist/index.js +9 -2
  361. package/dist/item/definition.d.ts.map +1 -1
  362. package/dist/item/definition.js +10 -2
  363. package/dist/item/floorplan-move.d.ts.map +1 -1
  364. package/dist/item/floorplan-move.js +20 -1
  365. package/dist/item/floorplan.d.ts.map +1 -1
  366. package/dist/item/floorplan.js +22 -1
  367. package/dist/item/move-tool.d.ts +28 -1
  368. package/dist/item/move-tool.d.ts.map +1 -1
  369. package/dist/item/move-tool.js +14 -2
  370. package/dist/item/renderer.d.ts.map +1 -1
  371. package/dist/item/renderer.js +16 -6
  372. package/dist/item/system.d.ts +5 -0
  373. package/dist/item/system.d.ts.map +1 -1
  374. package/dist/item/system.js +7 -1
  375. package/dist/lean-to-extension/arc.d.ts +14 -0
  376. package/dist/lean-to-extension/arc.d.ts.map +1 -0
  377. package/dist/lean-to-extension/arc.js +56 -0
  378. package/dist/lean-to-extension/assembly.d.ts +61 -0
  379. package/dist/lean-to-extension/assembly.d.ts.map +1 -0
  380. package/dist/lean-to-extension/assembly.js +761 -0
  381. package/dist/lean-to-extension/canopy-joint.d.ts +35 -0
  382. package/dist/lean-to-extension/canopy-joint.d.ts.map +1 -0
  383. package/dist/lean-to-extension/canopy-joint.js +234 -0
  384. package/dist/lean-to-extension/conical-host.d.ts +15 -0
  385. package/dist/lean-to-extension/conical-host.d.ts.map +1 -0
  386. package/dist/lean-to-extension/conical-host.js +113 -0
  387. package/dist/lean-to-extension/corner-joint.d.ts +29 -0
  388. package/dist/lean-to-extension/corner-joint.d.ts.map +1 -0
  389. package/dist/lean-to-extension/corner-joint.js +1202 -0
  390. package/dist/lean-to-extension/definition.d.ts +4 -0
  391. package/dist/lean-to-extension/definition.d.ts.map +1 -0
  392. package/dist/lean-to-extension/definition.js +433 -0
  393. package/dist/lean-to-extension/floorplan-affordances.d.ts +4 -0
  394. package/dist/lean-to-extension/floorplan-affordances.d.ts.map +1 -0
  395. package/dist/lean-to-extension/floorplan-affordances.js +144 -0
  396. package/dist/lean-to-extension/floorplan-move.d.ts +3 -0
  397. package/dist/lean-to-extension/floorplan-move.d.ts.map +1 -0
  398. package/dist/lean-to-extension/floorplan-move.js +157 -0
  399. package/dist/lean-to-extension/floorplan-tool.d.ts +4 -0
  400. package/dist/lean-to-extension/floorplan-tool.d.ts.map +1 -0
  401. package/dist/lean-to-extension/floorplan-tool.js +385 -0
  402. package/dist/lean-to-extension/floorplan.d.ts +3 -0
  403. package/dist/lean-to-extension/floorplan.d.ts.map +1 -0
  404. package/dist/lean-to-extension/floorplan.js +373 -0
  405. package/dist/lean-to-extension/geometry.d.ts +7 -0
  406. package/dist/lean-to-extension/geometry.d.ts.map +1 -0
  407. package/dist/lean-to-extension/geometry.js +662 -0
  408. package/dist/lean-to-extension/index.d.ts +8 -0
  409. package/dist/lean-to-extension/index.d.ts.map +1 -0
  410. package/dist/lean-to-extension/index.js +7 -0
  411. package/dist/lean-to-extension/layout.d.ts +108 -0
  412. package/dist/lean-to-extension/layout.d.ts.map +1 -0
  413. package/dist/lean-to-extension/layout.js +477 -0
  414. package/dist/lean-to-extension/managed-preview.d.ts +3 -0
  415. package/dist/lean-to-extension/managed-preview.d.ts.map +1 -0
  416. package/dist/lean-to-extension/managed-preview.js +93 -0
  417. package/dist/lean-to-extension/move-tool.d.ts +8 -0
  418. package/dist/lean-to-extension/move-tool.d.ts.map +1 -0
  419. package/dist/lean-to-extension/move-tool.js +202 -0
  420. package/dist/lean-to-extension/paint.d.ts +2 -0
  421. package/dist/lean-to-extension/paint.d.ts.map +1 -0
  422. package/dist/lean-to-extension/paint.js +16 -0
  423. package/dist/lean-to-extension/parametrics.d.ts +4 -0
  424. package/dist/lean-to-extension/parametrics.d.ts.map +1 -0
  425. package/dist/lean-to-extension/parametrics.js +570 -0
  426. package/dist/lean-to-extension/placement-scope.d.ts +3 -0
  427. package/dist/lean-to-extension/placement-scope.d.ts.map +1 -0
  428. package/dist/lean-to-extension/placement-scope.js +4 -0
  429. package/dist/lean-to-extension/placement-validation.d.ts +4 -0
  430. package/dist/lean-to-extension/placement-validation.d.ts.map +1 -0
  431. package/dist/lean-to-extension/placement-validation.js +324 -0
  432. package/dist/lean-to-extension/placement.d.ts +59 -0
  433. package/dist/lean-to-extension/placement.d.ts.map +1 -0
  434. package/dist/lean-to-extension/placement.js +331 -0
  435. package/dist/lean-to-extension/plan-footprint.d.ts +10 -0
  436. package/dist/lean-to-extension/plan-footprint.d.ts.map +1 -0
  437. package/dist/lean-to-extension/plan-footprint.js +27 -0
  438. package/dist/lean-to-extension/preview-geometry.d.ts +7 -0
  439. package/dist/lean-to-extension/preview-geometry.d.ts.map +1 -0
  440. package/dist/lean-to-extension/preview-geometry.js +40 -0
  441. package/dist/lean-to-extension/preview.d.ts +7 -0
  442. package/dist/lean-to-extension/preview.d.ts.map +1 -0
  443. package/dist/lean-to-extension/preview.js +20 -0
  444. package/dist/lean-to-extension/renderer.d.ts +6 -0
  445. package/dist/lean-to-extension/renderer.d.ts.map +1 -0
  446. package/dist/lean-to-extension/renderer.js +34 -0
  447. package/dist/lean-to-extension/roof-attachment.d.ts +28 -0
  448. package/dist/lean-to-extension/roof-attachment.d.ts.map +1 -0
  449. package/dist/lean-to-extension/roof-attachment.js +376 -0
  450. package/dist/lean-to-extension/schema.d.ts +2 -0
  451. package/dist/lean-to-extension/schema.d.ts.map +1 -0
  452. package/dist/lean-to-extension/schema.js +1 -0
  453. package/dist/lean-to-extension/slots.d.ts +5 -0
  454. package/dist/lean-to-extension/slots.d.ts.map +1 -0
  455. package/dist/lean-to-extension/slots.js +15 -0
  456. package/dist/lean-to-extension/system.d.ts +7 -0
  457. package/dist/lean-to-extension/system.d.ts.map +1 -0
  458. package/dist/lean-to-extension/system.js +726 -0
  459. package/dist/lean-to-extension/tool.d.ts +3 -0
  460. package/dist/lean-to-extension/tool.d.ts.map +1 -0
  461. package/dist/lean-to-extension/tool.js +580 -0
  462. package/dist/lean-to-extension/wall-target.d.ts +10 -0
  463. package/dist/lean-to-extension/wall-target.d.ts.map +1 -0
  464. package/dist/lean-to-extension/wall-target.js +29 -0
  465. package/dist/lineset/definition.d.ts.map +1 -1
  466. package/dist/lineset/definition.js +1 -0
  467. package/dist/liquid-line/definition.d.ts.map +1 -1
  468. package/dist/liquid-line/definition.js +1 -0
  469. package/dist/pipe-fitting/definition.d.ts.map +1 -1
  470. package/dist/pipe-fitting/definition.js +16 -2
  471. package/dist/pipe-fitting/floorplan.d.ts.map +1 -1
  472. package/dist/pipe-fitting/floorplan.js +4 -0
  473. package/dist/pipe-fitting/geometry.d.ts +3 -5
  474. package/dist/pipe-fitting/geometry.d.ts.map +1 -1
  475. package/dist/pipe-fitting/geometry.js +154 -21
  476. package/dist/pipe-fitting/inline-insertion.d.ts +13 -0
  477. package/dist/pipe-fitting/inline-insertion.d.ts.map +1 -0
  478. package/dist/pipe-fitting/inline-insertion.js +96 -0
  479. package/dist/pipe-fitting/parametrics.d.ts.map +1 -1
  480. package/dist/pipe-fitting/parametrics.js +17 -3
  481. package/dist/pipe-fitting/ports.d.ts.map +1 -1
  482. package/dist/pipe-fitting/ports.js +8 -1
  483. package/dist/pipe-fitting/tool.d.ts +26 -0
  484. package/dist/pipe-fitting/tool.d.ts.map +1 -1
  485. package/dist/pipe-fitting/tool.js +182 -37
  486. package/dist/pipe-segment/continuation.d.ts +22 -0
  487. package/dist/pipe-segment/continuation.d.ts.map +1 -0
  488. package/dist/pipe-segment/continuation.js +146 -0
  489. package/dist/pipe-segment/definition.d.ts.map +1 -1
  490. package/dist/pipe-segment/definition.js +23 -7
  491. package/dist/pipe-segment/floorplan.d.ts.map +1 -1
  492. package/dist/pipe-segment/floorplan.js +42 -0
  493. package/dist/pipe-segment/geometry.d.ts +2 -1
  494. package/dist/pipe-segment/geometry.d.ts.map +1 -1
  495. package/dist/pipe-segment/geometry.js +4 -1
  496. package/dist/pipe-segment/move-tool.d.ts.map +1 -1
  497. package/dist/pipe-segment/move-tool.js +25 -5
  498. package/dist/pipe-segment/parametrics.d.ts.map +1 -1
  499. package/dist/pipe-segment/parametrics.js +45 -0
  500. package/dist/pipe-segment/selection.d.ts +3 -4
  501. package/dist/pipe-segment/selection.d.ts.map +1 -1
  502. package/dist/pipe-segment/selection.js +169 -105
  503. package/dist/pipe-segment/slope.d.ts +5 -0
  504. package/dist/pipe-segment/slope.d.ts.map +1 -0
  505. package/dist/pipe-segment/slope.js +10 -0
  506. package/dist/pipe-segment/tool.d.ts.map +1 -1
  507. package/dist/pipe-segment/tool.js +325 -545
  508. package/dist/pipe-trap/tool.d.ts.map +1 -1
  509. package/dist/pipe-trap/tool.js +15 -2
  510. package/dist/ridge-vent/definition.js +1 -1
  511. package/dist/ridge-vent/panel.d.ts.map +1 -1
  512. package/dist/ridge-vent/panel.js +2 -2
  513. package/dist/ridge-vent/parametrics.js +1 -1
  514. package/dist/roof/conical-roof-placement.d.ts +36 -0
  515. package/dist/roof/conical-roof-placement.d.ts.map +1 -0
  516. package/dist/roof/conical-roof-placement.js +134 -0
  517. package/dist/roof/conical-roof.d.ts +3 -0
  518. package/dist/roof/conical-roof.d.ts.map +1 -0
  519. package/dist/roof/conical-roof.js +53 -0
  520. package/dist/roof/definition.d.ts +3 -10
  521. package/dist/roof/definition.d.ts.map +1 -1
  522. package/dist/roof/definition.js +83 -13
  523. package/dist/roof/floorplan.d.ts +3 -10
  524. package/dist/roof/floorplan.d.ts.map +1 -1
  525. package/dist/roof/floorplan.js +139 -165
  526. package/dist/roof/index.d.ts +1 -0
  527. package/dist/roof/index.d.ts.map +1 -1
  528. package/dist/roof/index.js +1 -0
  529. package/dist/roof/panel.d.ts.map +1 -1
  530. package/dist/roof/panel.js +22 -23
  531. package/dist/roof/renderer.d.ts.map +1 -1
  532. package/dist/roof/roof-footprint-source.d.ts +16 -0
  533. package/dist/roof/roof-footprint-source.d.ts.map +1 -0
  534. package/dist/roof/roof-footprint-source.js +14 -0
  535. package/dist/roof/roof-footprint.d.ts +25 -0
  536. package/dist/roof/roof-footprint.d.ts.map +1 -0
  537. package/dist/roof/roof-footprint.js +94 -0
  538. package/dist/roof/roof-placement-mode.d.ts +18 -0
  539. package/dist/roof/roof-placement-mode.d.ts.map +1 -0
  540. package/dist/roof/roof-placement-mode.js +24 -0
  541. package/dist/roof/tool.d.ts +10 -0
  542. package/dist/roof/tool.d.ts.map +1 -0
  543. package/dist/roof/tool.js +922 -0
  544. package/dist/roof-segment/definition.d.ts.map +1 -1
  545. package/dist/roof-segment/definition.js +52 -8
  546. package/dist/roof-segment/floorplan-affordances.d.ts +5 -6
  547. package/dist/roof-segment/floorplan-affordances.d.ts.map +1 -1
  548. package/dist/roof-segment/floorplan-affordances.js +64 -18
  549. package/dist/roof-segment/floorplan.d.ts +3 -2
  550. package/dist/roof-segment/floorplan.d.ts.map +1 -1
  551. package/dist/roof-segment/floorplan.js +95 -46
  552. package/dist/roof-segment/panel.d.ts.map +1 -1
  553. package/dist/roof-segment/panel.js +122 -27
  554. package/dist/scan/definition.d.ts +2 -3
  555. package/dist/scan/definition.d.ts.map +1 -1
  556. package/dist/scan/definition.js +9 -7
  557. package/dist/scan/parametrics.d.ts.map +1 -1
  558. package/dist/scan/parametrics.js +13 -1
  559. package/dist/scan/renderer.d.ts.map +1 -1
  560. package/dist/scan/renderer.js +8 -2
  561. package/dist/shared/accessory-cursor.d.ts +7 -0
  562. package/dist/shared/accessory-cursor.d.ts.map +1 -0
  563. package/dist/shared/accessory-cursor.js +83 -0
  564. package/dist/shared/accessory-floorplan.d.ts +4 -0
  565. package/dist/shared/accessory-floorplan.d.ts.map +1 -0
  566. package/dist/shared/accessory-floorplan.js +51 -0
  567. package/dist/shared/accessory-geometry.d.ts +7 -0
  568. package/dist/shared/accessory-geometry.d.ts.map +1 -0
  569. package/dist/shared/accessory-geometry.js +79 -0
  570. package/dist/shared/accessory-placement.d.ts +10 -0
  571. package/dist/shared/accessory-placement.d.ts.map +1 -0
  572. package/dist/shared/accessory-placement.js +126 -0
  573. package/dist/shared/accessory-snapping.d.ts +5 -0
  574. package/dist/shared/accessory-snapping.d.ts.map +1 -0
  575. package/dist/shared/accessory-snapping.js +40 -0
  576. package/dist/shared/automatic-run-end-cap.d.ts +11 -0
  577. package/dist/shared/automatic-run-end-cap.d.ts.map +1 -0
  578. package/dist/shared/automatic-run-end-cap.js +159 -0
  579. package/dist/shared/block-face-host.d.ts +15 -0
  580. package/dist/shared/block-face-host.d.ts.map +1 -0
  581. package/dist/shared/block-face-host.js +36 -0
  582. package/dist/shared/connection-compatibility.d.ts +8 -0
  583. package/dist/shared/connection-compatibility.d.ts.map +1 -0
  584. package/dist/shared/connection-compatibility.js +28 -0
  585. package/dist/shared/connection-feedback.d.ts +10 -0
  586. package/dist/shared/connection-feedback.d.ts.map +1 -0
  587. package/dist/shared/connection-feedback.js +17 -0
  588. package/dist/shared/distribution-run-contract.d.ts +49 -0
  589. package/dist/shared/distribution-run-contract.d.ts.map +1 -0
  590. package/dist/shared/distribution-run-contract.js +30 -0
  591. package/dist/shared/distribution-run-tool.d.ts +107 -0
  592. package/dist/shared/distribution-run-tool.d.ts.map +1 -0
  593. package/dist/shared/distribution-run-tool.js +941 -0
  594. package/dist/shared/dormer-wall-opening-placement.d.ts +21 -0
  595. package/dist/shared/dormer-wall-opening-placement.d.ts.map +1 -0
  596. package/dist/shared/dormer-wall-opening-placement.js +101 -0
  597. package/dist/shared/duct-adapter.d.ts +10 -0
  598. package/dist/shared/duct-adapter.d.ts.map +1 -0
  599. package/dist/shared/duct-adapter.js +43 -0
  600. package/dist/shared/elbow-branch-continuation.d.ts +20 -0
  601. package/dist/shared/elbow-branch-continuation.d.ts.map +1 -0
  602. package/dist/shared/elbow-branch-continuation.js +200 -0
  603. package/dist/shared/fitting-clearance.d.ts +5 -0
  604. package/dist/shared/fitting-clearance.d.ts.map +1 -0
  605. package/dist/shared/fitting-clearance.js +11 -0
  606. package/dist/shared/fitting-deletion-cleanup.d.ts +14 -0
  607. package/dist/shared/fitting-deletion-cleanup.d.ts.map +1 -0
  608. package/dist/shared/fitting-deletion-cleanup.js +297 -0
  609. package/dist/shared/fitting-surface-support.d.ts +5 -0
  610. package/dist/shared/fitting-surface-support.d.ts.map +1 -0
  611. package/dist/shared/fitting-surface-support.js +42 -0
  612. package/dist/shared/fitting-tool-options.d.ts +4 -0
  613. package/dist/shared/fitting-tool-options.d.ts.map +1 -0
  614. package/dist/shared/fitting-tool-options.js +175 -0
  615. package/dist/shared/floor-placement.d.ts +1 -1
  616. package/dist/shared/floor-placement.d.ts.map +1 -1
  617. package/dist/shared/floor-placement.js +13 -33
  618. package/dist/shared/ghost-materials.d.ts.map +1 -1
  619. package/dist/shared/ghost-materials.js +2 -1
  620. package/dist/shared/lean-to-post-omissions.d.ts +8 -0
  621. package/dist/shared/lean-to-post-omissions.d.ts.map +1 -0
  622. package/dist/shared/lean-to-post-omissions.js +51 -0
  623. package/dist/shared/mep-fitting-actions.d.ts +5 -0
  624. package/dist/shared/mep-fitting-actions.d.ts.map +1 -0
  625. package/dist/shared/mep-fitting-actions.js +16 -0
  626. package/dist/shared/mep-presets.d.ts +11 -0
  627. package/dist/shared/mep-presets.d.ts.map +1 -0
  628. package/dist/shared/mep-presets.js +34 -0
  629. package/dist/shared/node-batch/candidates.d.ts +29 -0
  630. package/dist/shared/node-batch/candidates.d.ts.map +1 -0
  631. package/dist/shared/node-batch/candidates.js +260 -0
  632. package/dist/shared/node-batch/store.d.ts +26 -0
  633. package/dist/shared/node-batch/store.d.ts.map +1 -0
  634. package/dist/shared/node-batch/store.js +319 -0
  635. package/dist/shared/node-batch/system.d.ts +8 -0
  636. package/dist/shared/node-batch/system.d.ts.map +1 -0
  637. package/dist/shared/node-batch/system.js +569 -0
  638. package/dist/shared/node-batch/types.d.ts +99 -0
  639. package/dist/shared/node-batch/types.d.ts.map +1 -0
  640. package/dist/shared/node-batch/types.js +4 -0
  641. package/dist/shared/opening-move-history.d.ts +47 -0
  642. package/dist/shared/opening-move-history.d.ts.map +1 -0
  643. package/dist/shared/opening-move-history.js +18 -0
  644. package/dist/shared/opening-move-wall-gate.d.ts +55 -0
  645. package/dist/shared/opening-move-wall-gate.d.ts.map +1 -0
  646. package/dist/shared/opening-move-wall-gate.js +44 -0
  647. package/dist/shared/path-point-affordance.d.ts +2 -2
  648. package/dist/shared/path-point-affordance.d.ts.map +1 -1
  649. package/dist/shared/path-point-affordance.js +30 -6
  650. package/dist/shared/placeholder-geometry.d.ts.map +1 -1
  651. package/dist/shared/placeholder-geometry.js +4 -0
  652. package/dist/shared/ports.d.ts +20 -3
  653. package/dist/shared/ports.d.ts.map +1 -1
  654. package/dist/shared/ports.js +148 -7
  655. package/dist/shared/primitive-uv.d.ts +17 -0
  656. package/dist/shared/primitive-uv.d.ts.map +1 -0
  657. package/dist/shared/primitive-uv.js +107 -0
  658. package/dist/shared/reducer-size.d.ts +2 -0
  659. package/dist/shared/reducer-size.d.ts.map +1 -0
  660. package/dist/shared/reducer-size.js +12 -0
  661. package/dist/shared/ridge-snap.js +1 -1
  662. package/dist/shared/roof-surface.d.ts.map +1 -1
  663. package/dist/shared/roof-surface.js +17 -2
  664. package/dist/shared/run-cursor.d.ts +22 -0
  665. package/dist/shared/run-cursor.d.ts.map +1 -0
  666. package/dist/shared/run-cursor.js +19 -0
  667. package/dist/shared/run-direction-feedback.d.ts +20 -0
  668. package/dist/shared/run-direction-feedback.d.ts.map +1 -0
  669. package/dist/shared/run-direction-feedback.js +139 -0
  670. package/dist/shared/run-hanger-controls.d.ts +7 -0
  671. package/dist/shared/run-hanger-controls.d.ts.map +1 -0
  672. package/dist/shared/run-hanger-controls.js +44 -0
  673. package/dist/shared/run-hanger-inspector.d.ts +5 -0
  674. package/dist/shared/run-hanger-inspector.d.ts.map +1 -0
  675. package/dist/shared/run-hanger-inspector.js +43 -0
  676. package/dist/shared/run-hanger-mode.d.ts +11 -0
  677. package/dist/shared/run-hanger-mode.d.ts.map +1 -0
  678. package/dist/shared/run-hanger-mode.js +32 -0
  679. package/dist/shared/run-hanger-system.d.ts +3 -0
  680. package/dist/shared/run-hanger-system.d.ts.map +1 -0
  681. package/dist/shared/run-hanger-system.js +39 -0
  682. package/dist/shared/run-hangers.d.ts +25 -0
  683. package/dist/shared/run-hangers.d.ts.map +1 -0
  684. package/dist/shared/run-hangers.js +261 -0
  685. package/dist/shared/run-port-snap.d.ts +11 -0
  686. package/dist/shared/run-port-snap.d.ts.map +1 -0
  687. package/dist/shared/run-port-snap.js +19 -0
  688. package/dist/shared/run-screen-direction.d.ts +5 -0
  689. package/dist/shared/run-screen-direction.d.ts.map +1 -0
  690. package/dist/shared/run-screen-direction.js +24 -0
  691. package/dist/shared/selection-handles.d.ts +4 -0
  692. package/dist/shared/selection-handles.d.ts.map +1 -1
  693. package/dist/shared/selection-handles.js +10 -0
  694. package/dist/shared/slot-paint.d.ts +8 -1
  695. package/dist/shared/slot-paint.d.ts.map +1 -1
  696. package/dist/shared/slot-paint.js +120 -27
  697. package/dist/shared/surface-paint.d.ts +1 -2
  698. package/dist/shared/surface-paint.d.ts.map +1 -1
  699. package/dist/shared/surface-paint.js +9 -20
  700. package/dist/shared/system-check-panel.d.ts +6 -0
  701. package/dist/shared/system-check-panel.d.ts.map +1 -0
  702. package/dist/shared/system-check-panel.js +62 -0
  703. package/dist/shared/system-checks.d.ts +9 -0
  704. package/dist/shared/system-checks.d.ts.map +1 -0
  705. package/dist/shared/system-checks.js +131 -0
  706. package/dist/shared/wall-attach-target.d.ts +14 -2
  707. package/dist/shared/wall-attach-target.d.ts.map +1 -1
  708. package/dist/shared/wall-attach-target.js +108 -4
  709. package/dist/shared/wall-opening-clearance.d.ts +19 -0
  710. package/dist/shared/wall-opening-clearance.d.ts.map +1 -0
  711. package/dist/shared/wall-opening-clearance.js +28 -0
  712. package/dist/shared/wall-run-move.d.ts +17 -0
  713. package/dist/shared/wall-run-move.d.ts.map +1 -0
  714. package/dist/shared/wall-run-move.js +56 -0
  715. package/dist/shelf/parametrics.js +4 -4
  716. package/dist/site/renderer.d.ts.map +1 -1
  717. package/dist/site/renderer.js +40 -28
  718. package/dist/site/terrain-geometry.d.ts +1 -1
  719. package/dist/site/terrain-geometry.d.ts.map +1 -1
  720. package/dist/site/terrain-geometry.js +35 -9
  721. package/dist/site/terrain-mesh.d.ts +5 -0
  722. package/dist/site/terrain-mesh.d.ts.map +1 -1
  723. package/dist/site/terrain-mesh.js +63 -34
  724. package/dist/skylight/definition.js +1 -1
  725. package/dist/skylight/panel.js +1 -1
  726. package/dist/skylight/parametrics.js +2 -2
  727. package/dist/slab/definition.d.ts.map +1 -1
  728. package/dist/slab/definition.js +6 -1
  729. package/dist/slab/dependency-tracker.d.ts +3 -0
  730. package/dist/slab/dependency-tracker.d.ts.map +1 -0
  731. package/dist/slab/dependency-tracker.js +104 -0
  732. package/dist/slab/floorplan-affordances.d.ts +8 -8
  733. package/dist/slab/geometry.d.ts.map +1 -1
  734. package/dist/slab/geometry.js +16 -2
  735. package/dist/slab/panel.js +1 -1
  736. package/dist/slab/parametrics.js +1 -1
  737. package/dist/slab/system.d.ts.map +1 -1
  738. package/dist/slab/system.js +8 -64
  739. package/dist/solar-panel/definition.js +1 -1
  740. package/dist/solar-panel/panel.js +1 -1
  741. package/dist/solar-panel/parametrics.js +2 -2
  742. package/dist/stair/panel.d.ts.map +1 -1
  743. package/dist/stair/panel.js +18 -13
  744. package/dist/stair/stair-type.d.ts +18 -0
  745. package/dist/stair/stair-type.d.ts.map +1 -0
  746. package/dist/stair/stair-type.js +25 -0
  747. package/dist/stair-segment/panel.d.ts.map +1 -1
  748. package/dist/stair-segment/panel.js +44 -5
  749. package/dist/turbine-vent/definition.d.ts.map +1 -1
  750. package/dist/turbine-vent/definition.js +8 -5
  751. package/dist/turbine-vent/geometry.d.ts.map +1 -1
  752. package/dist/turbine-vent/geometry.js +48 -18
  753. package/dist/turbine-vent/paint.d.ts +4 -0
  754. package/dist/turbine-vent/paint.d.ts.map +1 -0
  755. package/dist/turbine-vent/paint.js +35 -0
  756. package/dist/turbine-vent/panel.js +1 -1
  757. package/dist/turbine-vent/parametrics.js +2 -2
  758. package/dist/turbine-vent/renderer.d.ts.map +1 -1
  759. package/dist/turbine-vent/renderer.js +31 -9
  760. package/dist/wall/curve-eligibility.d.ts +3 -0
  761. package/dist/wall/curve-eligibility.d.ts.map +1 -0
  762. package/dist/wall/curve-eligibility.js +10 -0
  763. package/dist/wall/curve-tool.d.ts.map +1 -1
  764. package/dist/wall/curve-tool.js +8 -11
  765. package/dist/wall/definition.d.ts +1 -1
  766. package/dist/wall/definition.d.ts.map +1 -1
  767. package/dist/wall/definition.js +36 -13
  768. package/dist/wall/floorplan-affordances.d.ts +1 -0
  769. package/dist/wall/floorplan-affordances.d.ts.map +1 -1
  770. package/dist/wall/floorplan-affordances.js +46 -7
  771. package/dist/wall/floorplan-move.d.ts +3 -4
  772. package/dist/wall/floorplan-move.d.ts.map +1 -1
  773. package/dist/wall/floorplan-move.js +122 -9
  774. package/dist/wall/floorplan.d.ts.map +1 -1
  775. package/dist/wall/floorplan.js +19 -23
  776. package/dist/wall/move-shared.d.ts.map +1 -1
  777. package/dist/wall/move-shared.js +10 -7
  778. package/dist/wall/paint.d.ts.map +1 -1
  779. package/dist/wall/paint.js +2 -0
  780. package/dist/wall/panel.d.ts.map +1 -1
  781. package/dist/wall/panel.js +10 -17
  782. package/dist/wall/parametrics.js +2 -2
  783. package/dist/wall/pointer-transparency.d.ts +128 -0
  784. package/dist/wall/pointer-transparency.d.ts.map +1 -0
  785. package/dist/wall/pointer-transparency.js +147 -0
  786. package/dist/wall/renderer.d.ts.map +1 -1
  787. package/dist/wall/renderer.js +65 -8
  788. package/dist/wall/selection-hit-owner.d.ts +53 -0
  789. package/dist/wall/selection-hit-owner.d.ts.map +1 -0
  790. package/dist/wall/selection-hit-owner.js +79 -0
  791. package/dist/wall/system.d.ts +2 -0
  792. package/dist/wall/system.d.ts.map +1 -1
  793. package/dist/wall/system.js +66 -25
  794. package/dist/wall/tool.d.ts.map +1 -1
  795. package/dist/wall/tool.js +5 -0
  796. package/dist/wall/treatment-level-data.d.ts +6 -1
  797. package/dist/wall/treatment-level-data.d.ts.map +1 -1
  798. package/dist/wall/treatment-level-data.js +82 -5
  799. package/dist/wall/treatments.d.ts +1 -0
  800. package/dist/wall/treatments.d.ts.map +1 -1
  801. package/dist/wall/treatments.js +5 -0
  802. package/dist/wall/wall-batch-system.d.ts +38 -6
  803. package/dist/wall/wall-batch-system.d.ts.map +1 -1
  804. package/dist/wall/wall-batch-system.js +122 -25
  805. package/dist/wall/wall-batch.js +2 -2
  806. package/dist/window/definition.d.ts +2 -1
  807. package/dist/window/definition.d.ts.map +1 -1
  808. package/dist/window/definition.js +51 -24
  809. package/dist/window/floorplan-move.d.ts.map +1 -1
  810. package/dist/window/floorplan-move.js +1 -1
  811. package/dist/window/move-tool.d.ts +7 -5
  812. package/dist/window/move-tool.d.ts.map +1 -1
  813. package/dist/window/move-tool.js +344 -58
  814. package/dist/window/panel.d.ts.map +1 -1
  815. package/dist/window/panel.js +2 -0
  816. package/dist/window/parametrics.js +2 -2
  817. package/dist/window/renderer.d.ts.map +1 -1
  818. package/dist/window/renderer.js +5 -7
  819. package/dist/window/tool.d.ts.map +1 -1
  820. package/dist/window/tool.js +227 -13
  821. package/dist/zone/floorplan-affordances.d.ts +12 -12
  822. package/package.json +11 -9
  823. package/dist/dormer/panel-window-section.d.ts +0 -14
  824. package/dist/dormer/panel-window-section.d.ts.map +0 -1
  825. package/dist/dormer/panel-window-section.js +0 -59
  826. package/dist/dormer/window-assembly.d.ts +0 -22
  827. package/dist/dormer/window-assembly.d.ts.map +0 -1
  828. package/dist/dormer/window-assembly.js +0 -111
  829. package/dist/dormer/window-frame.d.ts +0 -24
  830. package/dist/dormer/window-frame.d.ts.map +0 -1
  831. package/dist/dormer/window-frame.js +0 -100
@@ -1,19 +1,25 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { DuctSegmentNode, emitter, getCeilingAt, getCeilingHeightAt, resolveCeilingHeight, useScene, } from '@pascal-app/core';
4
- import { CursorSphere, DimensionPill, EDITOR_LAYER, isAngleSnapActive, isGridSnapActive, isMagneticSnapActive, markToolCancelConsumed, triggerSFX, useEditor, usePathDraftPreview, } from '@pascal-app/editor';
5
- import { useViewer } from '@pascal-app/viewer';
6
- import { Html } from '@react-three/drei';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { DuctSegmentNode } from '@pascal-app/core';
4
+ import { EDITOR_LAYER, triggerSFX, useEditor, usePathDraftPreview, useRegistryToolContext, } from '@pascal-app/editor';
7
5
  import { useEffect, useMemo, useRef, useState } from 'react';
8
- import { DoubleSide, Matrix4, Path, Shape, ShapeGeometry, Vector3, } from 'three';
6
+ import { Euler, Vector3 } from 'three';
9
7
  import { getDuctFittingPorts } from '../duct-fitting/ports';
10
8
  import { planCrossAtRunBody, planElbowAtPort, planElbowRealign, planTeeAtRunBody, } from '../shared/auto-fitting';
11
- import { alignDrawPoint, clearDrawAlignment } from '../shared/draw-alignment';
9
+ import { createDuctRunEndCap, findMatedRunEndCapIds, isRunEndCapPort, } from '../shared/automatic-run-end-cap';
10
+ import { ConnectionFeedback } from '../shared/connection-feedback';
11
+ import { createRunWallAttachment } from '../shared/distribution-run-contract';
12
+ import { DistributionRunCursor, runDistanceSquared as dist2, runSectionHalfSizeM, stepNominalRunSize, useDistributionRunTool, } from '../shared/distribution-run-tool';
13
+ import { ductProfilesMatch, planDuctAdapter } from '../shared/duct-adapter';
14
+ import { FITTING_CLEARANCE_MESSAGE, hasFittingClearance } from '../shared/fitting-clearance';
12
15
  import { LevelOffsetGroup } from '../shared/level-offset-group';
13
- import { FittingGhost } from '../shared/mep-ghost';
14
- import { collectScenePorts, DUCT_PORT_SYSTEMS, findNearestPortXZ, findNearestRunBodyXZ, findRunBodyCrossingXZ, } from '../shared/ports';
16
+ import { DuctSegmentGhost, FittingGhost } from '../shared/mep-ghost';
17
+ import { collectScenePorts, DUCT_PORT_SYSTEMS, findNearestRunBody3D, findRunBodyCrossingSurface, } from '../shared/ports';
18
+ import { RunHangerPreview } from '../shared/run-hanger-controls';
19
+ import { useRunHangerMode } from '../shared/run-hanger-mode';
20
+ import { currentDuctContinuationSeed, ductEndpointPort } from './continuation';
15
21
  import { ductSegmentDefinition } from './definition';
16
- import { ductPortDiameterIn, rectSectionAxes, rollToContinueAcrossElbow } from './geometry';
22
+ import { rectSectionAxes, rollToContinueAcrossElbow } from './geometry';
17
23
  /**
18
24
  * Continuous placement tool for duct segments.
19
25
  *
@@ -42,46 +48,15 @@ import { ductPortDiameterIn, rectSectionAxes, rollToContinueAcrossElbow } from '
42
48
  * vertical mouse motion drives Y. Click commits the riser segment.
43
49
  * - **[ / ]** step the duct diameter through nominal US sizes; the
44
50
  * ghost preview and the committed node both use it.
45
- * - **C** toggles ceiling-level placement: each point lands just below
46
- * the ceiling actually covering it (duct top hugging that ceiling)
47
- * instead of the floor, so a run tracks per-room ceiling heights.
48
- * Points not under any ceiling fall back to the floor.
49
- * - Esc clears an anchored start point.
51
+ * - Esc exits drawing.
50
52
  */
51
- const PREVIEW_OPACITY = 0.55;
52
53
  /**
53
54
  * Nominal US round-duct sizes (inches): 4"–10" in 1" steps, 12"+ in 2"
54
55
  * steps — matches what flex and rigid round actually ship in.
55
56
  */
56
57
  const DUCT_DIAMETERS_IN = [4, 5, 6, 7, 8, 9, 10, 12, 14, 16, 18, 20];
57
- /** Snap radius (meters) for joining onto an existing duct's start/end. */
58
- const ENDPOINT_SNAP_RADIUS_M = 0.5;
59
- /** Snap radius (meters) for tapping the SIDE of an existing run — a tee
60
- * is minted there. Tighter than the port radius so run ends keep
61
- * priority near their last stretch. */
62
58
  const BODY_SNAP_RADIUS_M = 0.35;
63
59
  /** Angle step (radians) for the XZ angle lock — 45°. */
64
- const ANGLE_STEP_RAD = Math.PI / 4;
65
- /** Mouse pixels → meters mapping for Alt-vertical drag. 100 px ≈ 1 m. */
66
- const ALT_PIXELS_PER_METER = 100;
67
- /** Bounds on Alt-driven Y so a wild fling doesn't fly off. */
68
- const ALT_Y_MIN_M = -3;
69
- const ALT_Y_MAX_M = 10;
70
- /** green-500 — the project's bounding-box / placeable accent. The cursor
71
- * ring + vertical line recolour to this while the point is snapped onto an
72
- * existing run, so the coincidence reads with the familiar snap green. */
73
- const SNAP_CURSOR_COLOR = '#22c55e';
74
- function snap(value, step) {
75
- if (step <= 0)
76
- return value;
77
- return Math.round(value / step) * step;
78
- }
79
- function dist2(a, b) {
80
- const dx = a[0] - b[0];
81
- const dy = a[1] - b[1];
82
- const dz = a[2] - b[2];
83
- return dx * dx + dy * dy + dz * dz;
84
- }
85
60
  /**
86
61
  * Cross-section roll for a new rect run leaving `port` along `newDir`,
87
62
  * so its profile stays continuous with whatever it joined: a turn
@@ -92,11 +67,15 @@ function dist2(a, b) {
92
67
  * there). Null when the port doesn't carry a rect orientation. Shared
93
68
  * by the ghost preview and the commit so what you see is what lands.
94
69
  */
95
- function continuityRollFrom(port, newDir) {
70
+ function continuityRollFrom(port, newDir, nodes) {
96
71
  if (!port)
97
72
  return null;
98
- const nodes = useScene.getState().nodes;
99
73
  const owner = nodes[port.nodeId];
74
+ if (owner?.type === 'duct-fitting' && ['reducer', 'transition'].includes(owner.fittingType)) {
75
+ const width = new Vector3(0, 0, 1).applyEuler(new Euler(...owner.rotation));
76
+ const basis = rectSectionAxes(newDir);
77
+ return Math.atan2(width.dot(basis.height), width.dot(basis.width));
78
+ }
100
79
  let srcDir = null;
101
80
  let srcRoll = 0;
102
81
  if ((owner?.type === 'hvac-equipment' || owner?.type === 'duct-terminal') &&
@@ -134,26 +113,26 @@ function continuityRollFrom(port, newDir) {
134
113
  if (!srcDir)
135
114
  return null;
136
115
  const cross = new Vector3().crossVectors(srcDir, newDir);
137
- if (cross.lengthSq() < 1e-8)
116
+ if (cross.lengthSq() < 1e-8) {
117
+ if (owner?.type === 'duct-segment') {
118
+ const i = port.id === 'start' ? 0 : owner.path.length - 2;
119
+ const direction = new Vector3(...owner.path[i + 1]).sub(new Vector3(...owner.path[i]));
120
+ const width = rectSectionAxes(direction, owner.roll).width;
121
+ const basis = rectSectionAxes(newDir);
122
+ return Math.atan2(width.dot(basis.height), width.dot(basis.width));
123
+ }
138
124
  return srcRoll;
125
+ }
139
126
  return rollToContinueAcrossElbow(srcDir, srcRoll, srcDir, newDir);
140
127
  }
141
- function continuityRollForRun(startPort, endPort, dir) {
142
- return continuityRollFrom(startPort, dir) ?? continuityRollFrom(endPort, dir) ?? 0;
143
- }
144
- /**
145
- * Nearest typed port — duct run ends, fitting collars, anything whose
146
- * kind registers `def.ports` — within snap range of `point` on the XZ
147
- * plane. Y is ignored for the distance check (grid events ride the floor
148
- * while ports hang at duct height); the snap adopts the port's full 3D
149
- * position. The full port is returned so the commit knows what it joined
150
- * (auto-elbow insertion needs the port's direction and owner).
151
- */
152
- function findNearbyPort(point) {
153
- return findNearestPortXZ(point, collectScenePorts({ systems: DUCT_PORT_SYSTEMS }), ENDPOINT_SNAP_RADIUS_M);
128
+ function continuityRollForRun(startPort, endPort, dir, nodes) {
129
+ return continuityRollFrom(startPort, dir, nodes) ?? continuityRollFrom(endPort, dir, nodes) ?? 0;
154
130
  }
155
- function portPoint(port) {
156
- return [port.position[0], port.position[1], port.position[2]];
131
+ function getConnectionPorts(levelId, nodes) {
132
+ return collectScenePorts({
133
+ systems: DUCT_PORT_SYSTEMS,
134
+ levelId: levelId ?? undefined,
135
+ }).filter((port) => !isRunEndCapPort(port, nodes));
157
136
  }
158
137
  /**
159
138
  * Profile to inherit when the segment start snaps onto `port` — joining
@@ -161,16 +140,16 @@ function portPoint(port) {
161
140
  * run / fitting collar keeps its diameter. Equipment and terminal
162
141
  * collars are round at the port's advertised size.
163
142
  */
164
- function inheritProfile(port) {
165
- const owner = useScene.getState().nodes[port.nodeId];
143
+ function inheritProfile(port, nodes) {
144
+ const owner = nodes[port.nodeId];
166
145
  if (!owner)
167
146
  return null;
168
147
  if (owner.type === 'duct-segment' || owner.type === 'duct-fitting') {
169
148
  return {
170
- shape: owner.shape,
149
+ shape: port.shape ?? owner.shape,
171
150
  diameter: Math.min(48, Math.max(2, owner.type === 'duct-segment' ? owner.diameter : port.diameter)),
172
- width: owner.width,
173
- height: owner.height,
151
+ width: port.width ?? owner.width,
152
+ height: port.height ?? owner.height,
174
153
  };
175
154
  }
176
155
  if (owner.type === 'hvac-equipment' || owner.type === 'duct-terminal') {
@@ -195,52 +174,38 @@ function inheritProfile(port) {
195
174
  }
196
175
  return null;
197
176
  }
198
- /**
199
- * Project `raw` onto the nearest of the eight 45° rays emanating from
200
- * `from` in the XZ plane. Y is preserved from `from`. The projection
201
- * keeps the cursor's *distance* along the chosen ray so the user feels
202
- * the segment grow with their mouse motion rather than snap to a fixed
203
- * length.
204
- */
205
- function projectToAngleLock(from, raw) {
206
- const dx = raw[0] - from[0];
207
- const dz = raw[2] - from[2];
208
- const len = Math.hypot(dx, dz);
209
- if (len < 1e-4)
210
- return [from[0], from[1], from[2]];
211
- const theta = Math.atan2(dz, dx);
212
- const snapped = Math.round(theta / ANGLE_STEP_RAD) * ANGLE_STEP_RAD;
213
- // Distance along the chosen ray = projection of raw onto that direction.
214
- const proj = dx * Math.cos(snapped) + dz * Math.sin(snapped);
215
- const d = Math.max(0, proj);
216
- return [from[0] + Math.cos(snapped) * d, from[1], from[2] + Math.sin(snapped) * d];
217
- }
218
- const elbowPlanFor = (port, awayDir, profile) => {
177
+ const elbowPlanFor = (port, awayDir, profile, nodes) => {
219
178
  if (!port)
220
179
  return null;
221
- const owner = useScene.getState().nodes[port.nodeId];
180
+ const owner = nodes[port.nodeId];
222
181
  if (owner?.type !== 'duct-segment')
223
182
  return null;
224
- const plan = planElbowAtPort(port, awayDir, profile);
183
+ const source = inheritProfile(port, nodes) ?? profile;
184
+ const plan = planElbowAtPort(port, awayDir, source);
225
185
  if (!plan)
226
186
  return null;
227
187
  // Trim the run's snapped endpoint back to the elbow's inlet collar.
228
188
  const path = owner.path.map((p) => [...p]);
229
189
  const index = port.id === 'start' ? 0 : path.length - 1;
230
190
  const neighbor = path[index === 0 ? 1 : index - 1];
231
- const remaining = Math.hypot(plan.trimmedPortPoint[0] - neighbor[0], plan.trimmedPortPoint[1] - neighbor[1], plan.trimmedPortPoint[2] - neighbor[2]);
232
- // The trim must leave a real piece of the existing run AND not flip it.
233
191
  const original = path[index];
234
- const originalLen = Math.hypot(original[0] - neighbor[0], original[1] - neighbor[1], original[2] - neighbor[2]);
235
- if (remaining < 0.08 || remaining >= originalLen)
236
- return null;
192
+ const direction = [
193
+ original[0] - neighbor[0],
194
+ original[1] - neighbor[1],
195
+ original[2] - neighbor[2],
196
+ ];
197
+ const hasClearance = hasFittingClearance(neighbor, plan.trimmedPortPoint, direction, 0.08);
237
198
  path[index] = plan.trimmedPortPoint;
238
- return { ...plan, trim: { id: port.nodeId, data: { path } } };
199
+ return {
200
+ ...plan,
201
+ hasClearance,
202
+ trim: { id: port.nodeId, data: { path } },
203
+ };
239
204
  };
240
- const realignPlanFor = (port, awayDir) => {
205
+ const realignPlanFor = (port, awayDir, nodes) => {
241
206
  if (!port)
242
207
  return null;
243
- const owner = useScene.getState().nodes[port.nodeId];
208
+ const owner = nodes[port.nodeId];
244
209
  if (owner?.type !== 'duct-fitting')
245
210
  return null;
246
211
  return planElbowRealign(owner, port.id, awayDir);
@@ -251,10 +216,10 @@ const realignPlanFor = (port, awayDir) => {
251
216
  * tap), decide every node the commit creates / updates — auto-inserted
252
217
  * elbows / tees / crosses, the drawn run (split in two when it crosses a
253
218
  * trunk), trunk tails, and trim / realign updates. Reads the live scene
254
- * graph but mutates nothing, so the live preview can call it each frame
255
- * to ghost the fittings before the commit applies the identical plan.
219
+ * graph snapshot but mutates nothing, so the live preview can call it each
220
+ * frame to ghost the fittings before the commit applies the identical plan.
256
221
  */
257
- function planDuctDraw(start, end, startPort, startBody, endPort, endBody, profile) {
222
+ export function planDuctDraw(start, end, startPort, startBody, endPort, endBody, profile, nodes, surface, autoHangers = false, toolDefaults = {}, hangerStyle = 'single') {
258
223
  const length = Math.hypot(end[0] - start[0], end[1] - start[1], end[2] - start[2]);
259
224
  if (length < 1e-4)
260
225
  return null;
@@ -263,56 +228,107 @@ function planDuctDraw(start, end, startPort, startBody, endPort, endBody, profil
263
228
  (end[1] - start[1]) / length,
264
229
  (end[2] - start[2]) / length,
265
230
  ];
266
- const startPlan = elbowPlanFor(startPort, dir, profile);
267
- const endPlan = elbowPlanFor(endPort, [-dir[0], -dir[1], -dir[2]], profile);
268
- const startRealign = startPlan ? null : realignPlanFor(startPort, dir);
269
- const endRealign = endPlan ? null : realignPlanFor(endPort, [-dir[0], -dir[1], -dir[2]]);
231
+ const startPlan = elbowPlanFor(startPort, dir, profile, nodes);
232
+ const endPlan = elbowPlanFor(endPort, [-dir[0], -dir[1], -dir[2]], profile, nodes);
233
+ const invalidPlan = () => ({
234
+ validationMessage: FITTING_CLEARANCE_MESSAGE,
235
+ fittings: [],
236
+ ducts: [],
237
+ tails: [],
238
+ updates: [],
239
+ delete: [],
240
+ });
241
+ if (startPlan?.hasClearance === false || endPlan?.hasClearance === false)
242
+ return invalidPlan();
243
+ const startRealign = startPlan ? null : realignPlanFor(startPort, dir, nodes);
244
+ const endRealign = endPlan ? null : realignPlanFor(endPort, [-dir[0], -dir[1], -dir[2]], nodes);
270
245
  const trunkBody = startPlan ? null : startBody;
271
- const trunkOwner = trunkBody ? useScene.getState().nodes[trunkBody.nodeId] : null;
246
+ const trunkOwner = trunkBody ? nodes[trunkBody.nodeId] : null;
272
247
  const teePlan = trunkBody && trunkOwner?.type === 'duct-segment'
273
248
  ? planTeeAtRunBody(trunkOwner, trunkBody, dir, profile)
274
249
  : null;
275
250
  const endTrunkBody = endPlan || endRealign ? null : endBody;
276
- const endTrunkOwner = endTrunkBody ? useScene.getState().nodes[endTrunkBody.nodeId] : null;
251
+ const endTrunkOwner = endTrunkBody ? nodes[endTrunkBody.nodeId] : null;
277
252
  const endTeePlan = endTrunkBody && endTrunkOwner?.type === 'duct-segment'
278
253
  ? planTeeAtRunBody(endTrunkOwner, endTrunkBody, [-dir[0], -dir[1], -dir[2]], profile)
279
254
  : null;
280
- let ductStart = startPlan?.collarPoint ?? teePlan?.branchCollar ?? startRealign?.collarPoint ?? start;
281
- let ductEnd = endPlan?.collarPoint ?? endTeePlan?.branchCollar ?? endRealign?.collarPoint ?? end;
282
- const remaining = Math.hypot(ductEnd[0] - ductStart[0], ductEnd[1] - ductStart[1], ductEnd[2] - ductStart[2]);
283
- let plans = [startPlan, endPlan].filter((p) => p !== null);
284
- let tee = teePlan;
255
+ const adapterFor = (port, corner, away) => {
256
+ if (!port)
257
+ return null;
258
+ const source = inheritProfile(port, nodes);
259
+ if (!source)
260
+ return null;
261
+ const axis = new Vector3(...away);
262
+ if (!corner && axis.dot(new Vector3(...port.direction).normalize()) < 0.9999)
263
+ return null;
264
+ const owner = nodes[port.nodeId];
265
+ const roll = continuityRollFrom(port, axis, nodes) ?? 0;
266
+ const width = rectSectionAxes(axis, roll).width;
267
+ if (owner?.type === 'duct-fitting' && !corner) {
268
+ width.set(0, 0, 1).applyEuler(new Euler(...owner.rotation));
269
+ }
270
+ return planDuctAdapter({ ...port, position: corner?.collarPoint ?? port.position, direction: away }, source, profile, width);
271
+ };
272
+ const startAdapter = adapterFor(startPort, startPlan, dir);
273
+ const endAdapter = adapterFor(endPort, endPlan, [-dir[0], -dir[1], -dir[2]]);
274
+ for (const [port, adapter] of [
275
+ [startPort, startAdapter],
276
+ [endPort, endAdapter],
277
+ ]) {
278
+ const source = port ? inheritProfile(port, nodes) : null;
279
+ if (source && !ductProfilesMatch(source, profile) && !adapter) {
280
+ return {
281
+ ...invalidPlan(),
282
+ validationMessage: 'Align the connection or draw a 15–90° bend to fit the profile change.',
283
+ };
284
+ }
285
+ }
286
+ const ductStart = startAdapter?.collarPoint ??
287
+ startPlan?.collarPoint ??
288
+ teePlan?.branchCollar ??
289
+ startRealign?.collarPoint ??
290
+ start;
291
+ let ductEnd = endAdapter?.collarPoint ??
292
+ endPlan?.collarPoint ??
293
+ endTeePlan?.branchCollar ??
294
+ endRealign?.collarPoint ??
295
+ end;
296
+ const plans = [startPlan, endPlan].filter((p) => p !== null);
297
+ const tee = teePlan;
285
298
  let endTee = endTeePlan && endTrunkBody?.nodeId === trunkBody?.nodeId ? null : endTeePlan;
286
299
  if (!endTee && endTeePlan)
287
300
  ductEnd = endRealign?.collarPoint ?? end;
288
- let realigns = [startRealign, endRealign].filter((p) => p !== null);
289
- const crossHit = findRunBodyCrossingXZ(start, end, BODY_SNAP_RADIUS_M);
290
- const crossOwner = crossHit ? useScene.getState().nodes[crossHit.nodeId] : null;
301
+ const realigns = [startRealign, endRealign].filter((p) => p !== null);
302
+ const crossHit = surface
303
+ ? findRunBodyCrossingSurface(start, end, BODY_SNAP_RADIUS_M, surface)
304
+ : null;
305
+ const crossOwner = crossHit ? nodes[crossHit.nodeId] : null;
291
306
  const crossTappedElsewhere = crossHit?.nodeId === trunkBody?.nodeId || crossHit?.nodeId === endTrunkBody?.nodeId;
292
- let cross = crossHit && !crossTappedElsewhere && crossOwner?.type === 'duct-segment'
307
+ const cross = crossHit && !crossTappedElsewhere && crossOwner?.type === 'duct-segment'
293
308
  ? planCrossAtRunBody(crossOwner, crossHit, dir, profile)
294
309
  : null;
295
- if (remaining <= 0.08) {
296
- plans = [];
297
- tee = null;
298
- endTee = null;
299
- realigns = [];
300
- cross = null;
301
- ductStart = start;
302
- ductEnd = end;
303
- }
310
+ if (!hasFittingClearance(ductStart, ductEnd, dir, 0.08) ||
311
+ (trunkBody && !teePlan) ||
312
+ (endTrunkBody && !endTeePlan) ||
313
+ (crossHit && !crossTappedElsewhere && !cross))
314
+ return invalidPlan();
315
+ if (cross &&
316
+ (!hasFittingClearance(ductStart, cross.branchCollarNear, dir, 0.08) ||
317
+ !hasFittingClearance(cross.branchCollarFar, ductEnd, dir, 0.08)))
318
+ return invalidPlan();
304
319
  // Rect / oval continuity: roll the new run's cross-section so its
305
320
  // profile stays continuous with whatever either end joined.
306
321
  let roll = 0;
307
322
  if (profile.shape !== 'round') {
308
323
  const newDir = new Vector3(...dir);
309
- roll = continuityRollForRun(startPort, endPort, newDir);
324
+ roll = continuityRollForRun(startPort, endPort, newDir, nodes);
310
325
  }
311
326
  const defaults = ductSegmentDefinition.defaults();
312
- const toolDefaults = useEditor.getState().toolDefaults['duct-segment'] ?? {};
313
327
  const makeDuct = (from, to) => DuctSegmentNode.parse({
314
328
  ...defaults,
315
329
  ...toolDefaults,
330
+ autoHangers,
331
+ hangerStyle,
316
332
  name: profile.shape === 'rect' ? 'Trunk' : 'Duct run',
317
333
  path: [from, to],
318
334
  shape: profile.shape,
@@ -333,6 +349,7 @@ function planDuctDraw(start, end, startPort, startBody, endPort, endBody, profil
333
349
  : [makeDuct(ductStart, ductEnd)];
334
350
  const fittings = [
335
351
  ...plans.map((p) => p.fitting),
352
+ ...[startAdapter, endAdapter].flatMap((p) => (p ? [p.fitting] : [])),
336
353
  ...(tee ? [tee.fitting] : []),
337
354
  ...(endTee ? [endTee.fitting] : []),
338
355
  ...(cross ? [cross.fitting] : []),
@@ -349,587 +366,197 @@ function planDuctDraw(start, end, startPort, startBody, endPort, endBody, profil
349
366
  ...(cross ? [cross.trunkUpdate] : []),
350
367
  ...realigns.map((p) => p.update),
351
368
  ];
352
- return { fittings, ducts, tails, updates };
353
- }
354
- function ductEndPort(duct, id) {
355
- if (duct.path.length < 2)
356
- return null;
357
- const index = id === 'start' ? 0 : duct.path.length - 1;
358
- const neighborIndex = id === 'start' ? 1 : duct.path.length - 2;
359
- const position = duct.path[index];
360
- const neighbor = duct.path[neighborIndex];
361
- const dx = position[0] - neighbor[0];
362
- const dy = position[1] - neighbor[1];
363
- const dz = position[2] - neighbor[2];
364
- const len = Math.hypot(dx, dy, dz);
365
- const direction = len < 1e-9 ? [1, 0, 0] : [dx / len, dy / len, dz / len];
366
- return {
367
- id,
368
- nodeId: duct.id,
369
- position,
370
- direction,
371
- diameter: ductPortDiameterIn(duct),
372
- system: duct.system,
373
- };
369
+ const deleteIds = Array.from(new Set([
370
+ ...findMatedRunEndCapIds(startPort, nodes, 'duct-fitting'),
371
+ ...findMatedRunEndCapIds(endPort, nodes, 'duct-fitting'),
372
+ ]));
373
+ const firstDuct = ducts[0];
374
+ const nextDuct = ducts.at(-1);
375
+ const startEndCap = !startPort && !startBody && firstDuct ? createDuctRunEndCap(firstDuct, 'start') : null;
376
+ const nextEndCap = !endPort && !endBody && nextDuct ? createDuctRunEndCap(nextDuct) : null;
377
+ if (startEndCap)
378
+ fittings.push(startEndCap);
379
+ if (nextEndCap)
380
+ fittings.push(nextEndCap);
381
+ return { validationMessage: null, fittings, ducts, tails, updates, delete: deleteIds };
374
382
  }
375
383
  const DuctSegmentTool = () => {
376
- const activeLevelId = useViewer((s) => s.selection.levelId);
377
- const unit = useViewer((s) => s.unit);
384
+ const { activeLevelId, sceneApi, unit } = useRegistryToolContext();
378
385
  const cursorRef = useRef(null);
379
- // Cross-section profile for the next committed segment. Q toggles
380
- // round/rect, [ / ] steps the round diameter, and snapping the start
381
- // onto an existing run / fitting INHERITS that node's profile — so
382
- // continuing a 14×8 trunk keeps drawing 14×8, and branching off a
383
- // round collar keeps its diameter. Seeded from `toolDefaults`.
386
+ const continuationSeedRef = useRef(currentDuctContinuationSeed());
387
+ const continuationSeed = continuationSeedRef.current;
388
+ const hangerDefaults = useEditor((state) => state.toolDefaults['duct-segment']);
389
+ const initialAutoHangersRef = useRef(Boolean(hangerDefaults?.autoHangers ?? continuationSeed?.duct.autoHangers ?? false));
390
+ const autoHangers = useRunHangerMode((state) => state.enabled['duct-segment']);
391
+ const hangerStyle = (hangerDefaults?.hangerStyle ?? continuationSeed?.duct.hangerStyle) === 'double'
392
+ ? 'double'
393
+ : 'single';
394
+ const hangerStyleRef = useRef(hangerStyle);
395
+ hangerStyleRef.current = hangerStyle;
396
+ const autoHangersRef = useRef(autoHangers);
397
+ autoHangersRef.current = autoHangers;
398
+ const pendingPromotionRef = useRef(continuationSeed?.promotedFitting ?? null);
384
399
  const [profile, setProfile] = useState(() => {
385
400
  const defaults = ductSegmentDefinition.defaults();
386
401
  const seeded = useEditor.getState().toolDefaults['duct-segment'];
387
402
  return {
388
- shape: seeded?.shape ?? defaults.shape,
389
- diameter: seeded?.diameter ?? defaults.diameter,
390
- width: seeded?.width ?? defaults.width,
391
- height: seeded?.height ?? defaults.height,
403
+ shape: continuationSeed?.duct.shape ?? seeded?.shape ?? defaults.shape,
404
+ diameter: continuationSeed?.duct.diameter ?? seeded?.diameter ?? defaults.diameter,
405
+ width: continuationSeed?.duct.width ?? seeded?.width ?? defaults.width,
406
+ height: continuationSeed?.duct.height ?? seeded?.height ?? defaults.height,
392
407
  };
393
408
  });
394
- const [draftPoints, setDraftPoints] = useState([]);
395
- const [cursorPos, setCursorPos] = useState(null);
396
- // Ceiling mode (toggle with C): the first point lands at the level's
397
- // ceiling height (duct top hugging the ceiling) instead of the floor.
398
- const [ceilingMode, setCeilingMode] = useState(false);
399
- // The shared coordinate when the cursor is within snap range of an existing
400
- // duct (null = free placement). Drives the green cursor highlight so the
401
- // user sees the next click will join an existing run, not freeform-place.
402
- const [snapTarget, setSnapTarget] = useState(null);
403
- // In ceiling mode, the ceiling the cursor is currently under — rendered as
404
- // a translucent overlay so the duct reads as hung against a real surface
405
- // rather than a dot floating in space. Null when off-ceiling.
406
- const [hoverCeiling, setHoverCeiling] = useState(null);
407
- // True while Alt is held with a last point on the draft — drives the
408
- // vertical-cylinder ghost and the cursor HUD label.
409
- const [altActive, setAltActive] = useState(false);
410
- // What the in-flight cursor end currently snaps onto (port end, or a
411
- // run body for a tee / cross tap). Drives the auto-fitting GHOST so the
412
- // user sees the elbow / tee / cross the next click will mint.
413
- const [endSnap, setEndSnap] = useState({
414
- port: null,
415
- body: null,
416
- });
417
- // Mirror into refs so emitter callbacks (closing over the first render's
418
- // setState) read the latest values without re-subscribing.
419
- const draftRef = useRef(draftPoints);
420
- draftRef.current = draftPoints;
421
- const cursorPosRef = useRef(cursorPos);
422
- cursorPosRef.current = cursorPos;
423
409
  const profileRef = useRef(profile);
424
410
  profileRef.current = profile;
425
- const ceilingModeRef = useRef(ceilingMode);
426
- ceilingModeRef.current = ceilingMode;
427
- // Port the anchored START point snapped onto (null = free placement).
428
- // Read at commit so a turn off an existing run mints an elbow there.
429
- const startPortRef = useRef(null);
430
- // Centerline hit the anchored START point snapped onto (null = none).
431
- // Read at commit so a branch off a trunk's side mints a tee there.
432
- const startBodyRef = useRef(null);
433
- // Anchor captured when Alt is pressed: screen Y at that moment and the
434
- // base elevation (= last point's Y). Cleared on Alt release.
435
- const altAnchorRef = useRef(null);
436
- // Latest mouse clientY from grid:move; used so the Alt anchor knows where
437
- // the cursor was at key-press time.
438
- const lastClientYRef = useRef(null);
439
- const ghostFittings = useMemo(() => {
440
- const last = draftPoints.at(-1);
441
- if (!(activeLevelId && last && cursorPos) || altActive)
442
- return [];
443
- const fittings = planDuctDraw(last, cursorPos, startPortRef.current, startBodyRef.current, endSnap.port, endSnap.body, profile)?.fittings ?? [];
444
- return fittings.map((fitting, index) => ({
445
- ...fitting,
446
- id: `duct-fitting_live-draft-${index}`,
447
- parentId: activeLevelId,
448
- }));
449
- }, [activeLevelId, altActive, cursorPos, draftPoints, endSnap, profile]);
450
411
  useEffect(() => {
451
- usePathDraftPreview
452
- .getState()
453
- .setDraft('duct-segment', draftPoints, cursorPos, profile, ghostFittings);
454
- }, [cursorPos, draftPoints, ghostFittings, profile]);
455
- useEffect(() => () => usePathDraftPreview.getState().clear('duct-segment'), []);
456
- useEffect(() => {
457
- if (!activeLevelId)
458
- return;
459
- // Continuous chain: first click anchors the start, each following
460
- // click commits one two-point duct and uses that duct's far end as
461
- // the next anchor. No selection switch or finish gesture.
462
- //
463
- // All the auto-fitting decisions (elbow / tee / cross) live in the
464
- // shared `planDuctDraw` so the live ghost previews exactly what this
465
- // commit applies.
466
- const commitSegment = (start, end, endPort = null, endBody = null) => {
467
- const plan = planDuctDraw(start, end, startPortRef.current, startBodyRef.current, endPort, endBody, profileRef.current);
468
- if (!plan)
412
+ const mode = useRunHangerMode.getState();
413
+ mode.setEnabled('duct-segment', initialAutoHangersRef.current);
414
+ return () => mode.setEnabled('duct-segment', false);
415
+ }, []);
416
+ const run = useDistributionRunTool({
417
+ active: !!activeLevelId,
418
+ levelId: activeLevelId,
419
+ toolName: 'duct-segment',
420
+ initialStart: continuationSeed
421
+ ? [...(continuationSeed.port?.position ?? continuationSeed.body?.point ?? [0, 0, 0])]
422
+ : null,
423
+ initialConnection: continuationSeed
424
+ ? { port: continuationSeed.port, body: continuationSeed.body }
425
+ : null,
426
+ getPorts: () => getConnectionPorts(activeLevelId, sceneApi.nodes()),
427
+ findBody: (point) => findNearestRunBody3D(point, BODY_SNAP_RADIUS_M, { levelId: activeLevelId ?? undefined }),
428
+ surfaceClearance: (surface) => surface
429
+ ? runSectionHalfSizeM(profileRef.current.shape === 'round'
430
+ ? profileRef.current.diameter
431
+ : profileRef.current.height)
432
+ : 0,
433
+ minimumSegmentLength: 0.08,
434
+ inheritFromConnection: ({ port }) => {
435
+ if (!port)
469
436
  return;
470
- // One atomic change: trim / split the joined runs, create the
471
- // fittings + the new duct. Single undo step.
472
- useScene.getState().applyNodeChanges({
437
+ const inherited = inheritProfile(port, sceneApi.nodes());
438
+ if (inherited)
439
+ setProfile(inherited);
440
+ },
441
+ commit: ({ start, end, startConnection, endConnection, surfaceTarget }) => {
442
+ if (!activeLevelId)
443
+ return null;
444
+ const promotedFitting = pendingPromotionRef.current;
445
+ const plan = planDuctDraw(start, end, promotedFitting ? null : startConnection.port, startConnection.body, endConnection.port, endConnection.body, profileRef.current, sceneApi.nodes(), surfaceTarget, autoHangersRef.current, hangerDefaults, hangerStyleRef.current);
446
+ if (!plan || plan.validationMessage)
447
+ return null;
448
+ const attachDuct = (node) => {
449
+ const wallAttachment = surfaceTarget?.kind === 'wall'
450
+ ? createRunWallAttachment(surfaceTarget.hostId, surfaceTarget.side, node.path[0], node.path.at(-1), surfaceTarget, profileRef.current.shape === 'round'
451
+ ? runSectionHalfSizeM(profileRef.current.diameter)
452
+ : runSectionHalfSizeM(profileRef.current.height))
453
+ : undefined;
454
+ return { ...node, wallAttachment };
455
+ };
456
+ const ducts = plan.ducts.map(attachDuct);
457
+ const tails = plan.tails;
458
+ if (!sceneApi.applyChanges)
459
+ throw new Error('Registry SceneApi must support atomic changes');
460
+ sceneApi.applyChanges({
473
461
  create: [
474
462
  ...plan.fittings.map((node) => ({ node, parentId: activeLevelId })),
475
- ...plan.tails.map((node) => ({ node, parentId: activeLevelId })),
476
- ...plan.ducts.map((node) => ({ node, parentId: activeLevelId })),
463
+ ...tails.map((node) => ({ node, parentId: activeLevelId })),
464
+ ...ducts.map((node) => ({ node, parentId: activeLevelId })),
477
465
  ],
478
- update: plan.updates,
466
+ update: [
467
+ ...(promotedFitting
468
+ ? [
469
+ {
470
+ id: promotedFitting.id,
471
+ data: {
472
+ name: promotedFitting.name,
473
+ fittingType: promotedFitting.fittingType,
474
+ rotation: promotedFitting.rotation,
475
+ branchAngle: promotedFitting.branchAngle,
476
+ shape2: promotedFitting.shape2,
477
+ width2: promotedFitting.width2,
478
+ height2: promotedFitting.height2,
479
+ diameter2: promotedFitting.diameter2,
480
+ },
481
+ },
482
+ ]
483
+ : []),
484
+ ...plan.updates,
485
+ ],
486
+ delete: plan.delete,
479
487
  });
488
+ pendingPromotionRef.current = null;
480
489
  const nextDuct = plan.ducts.at(-1);
481
490
  const nextStart = nextDuct ? nextDuct.path[nextDuct.path.length - 1] : end;
482
- const nextPort = nextDuct ? ductEndPort(nextDuct, 'end') : endPort;
483
- triggerSFX('sfx:item-place');
484
- setDraftPoints([nextStart]);
485
- setSnapTarget(null);
486
- setEndSnap({ port: null, body: null });
487
- startPortRef.current = nextPort;
488
- startBodyRef.current = nextPort ? null : endBody;
489
- altAnchorRef.current = null;
490
- setAltActive(false);
491
- };
492
- // Y for a point at level-local `[x, z]`. Floor (0) when ceiling mode is
493
- // off. In ceiling mode, query the ceiling actually covering that point
494
- // and hang the duct just below it (centerline = ceiling underside −
495
- // half the duct's vertical dimension) so its top hugs the ceiling. Each
496
- // point follows its own ceiling, so a run stepping into a room with a
497
- // different ceiling height tracks that change. Points not under any
498
- // ceiling fall back to the floor.
499
- const resolveCeilingY = (x, z) => {
500
- if (!ceilingModeRef.current)
501
- return 0;
502
- const ceiling = getCeilingHeightAt(activeLevelId, useScene.getState().nodes, x, z);
503
- if (ceiling === null)
504
- return 0;
505
- const p = profileRef.current;
506
- const verticalIn = p.shape === 'round' ? p.diameter : p.height;
507
- return Math.max(0, ceiling - (verticalIn * 0.0254) / 2);
508
- };
509
- const resolveSnappedPoint = (event) => {
510
- // Port / body mating is the run's primary affordance; it stays on in
511
- // every snapping mode except `off` (the raw-cursor bypass).
512
- const snapEnabled = isGridSnapActive() || isMagneticSnapActive() || isAngleSnapActive();
513
- const last = draftRef.current.at(-1);
514
- // First point of the run: grid-snapped placement. Y follows the
515
- // ceiling under the cursor in ceiling mode (floor otherwise).
516
- // Endpoint snap can still join an existing run.
517
- if (!last) {
518
- const baseY = resolveCeilingY(event.localPosition[0], event.localPosition[2]);
519
- const raw = [
520
- event.localPosition[0],
521
- baseY,
522
- event.localPosition[2],
523
- ];
524
- const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0;
525
- if (event.nativeEvent?.altKey !== true && snapEnabled) {
526
- const target = findNearbyPort(raw);
527
- if (target)
528
- return {
529
- point: portPoint(target),
530
- snapped: portPoint(target),
531
- port: target,
532
- body: null,
533
- };
534
- // No open end nearby — try the side of a run (tee tap). Probe
535
- // with a grid-snapped cursor so the tap steps along the duct
536
- // like every other placement; `off` mode (step 0) rides smoothly.
537
- const probe = [snap(raw[0], step), baseY, snap(raw[2], step)];
538
- const body = findNearestRunBodyXZ(probe, BODY_SNAP_RADIUS_M);
539
- if (body)
540
- return { point: body.point, snapped: body.point, port: null, body };
541
- }
542
- const sx = snap(raw[0], step);
543
- const sz = snap(raw[2], step);
544
- return {
545
- point: [sx, resolveCeilingY(sx, sz), sz],
546
- snapped: null,
547
- port: null,
548
- body: null,
549
- };
550
- }
551
- // Subsequent points: angle-locked to 45° from `last` in `angles` mode.
552
- // Y inherits `last[1]` for the angle/probe math; the free placement below
553
- // re-resolves it from the ceiling under the point in ceiling mode, so a run
554
- // stepping into a room with a different ceiling height tracks that change.
555
- // Depth changes otherwise come from Alt-vertical risers.
556
- const rawXZ = [
557
- event.localPosition[0],
558
- last[1],
559
- event.localPosition[2],
560
- ];
561
- // The 45° lock is now the `angles` snapping mode (Shift cycles to it),
562
- // not a held key.
563
- const angled = isAngleSnapActive() ? projectToAngleLock(last, rawXZ) : rawXZ;
564
- const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0;
565
- // Port snap (Alt bypass) — checked against the RAW cursor, not the
566
- // angle-locked projection, so a port slightly off the 45° ray can
567
- // still capture the cursor. Joining beats the lock.
568
- if (event.nativeEvent?.altKey !== true && snapEnabled) {
569
- const target = findNearbyPort(rawXZ);
570
- if (target)
571
- return { point: portPoint(target), snapped: portPoint(target), port: target, body: null };
572
- // No open end nearby — landing on the side of a run taps a tee
573
- // there (mirror of the first-point tee tap). Probe with a
574
- // grid-snapped cursor so the tap steps along the duct instead of
575
- // sliding smoothly (Shift above frees it). Checked against the
576
- // cursor, not the 45° projection, so a slightly-off trunk captures.
577
- const probe = [
578
- snap(rawXZ[0], step),
579
- rawXZ[1],
580
- snap(rawXZ[2], step),
581
- ];
582
- const body = findNearestRunBodyXZ(probe, BODY_SNAP_RADIUS_M);
583
- if (body)
584
- return { point: body.point, snapped: body.point, port: null, body };
585
- }
586
- const fx = snap(angled[0], step);
587
- const fz = snap(angled[2], step);
588
- const fy = ceilingModeRef.current ? resolveCeilingY(fx, fz) : angled[1];
491
+ const nextPort = nextDuct ? ductEndpointPort(nextDuct, 'end') : endConnection.port;
589
492
  return {
590
- point: [fx, fy, fz],
591
- snapped: null,
592
- port: null,
593
- body: null,
493
+ nextStart,
494
+ nextConnection: {
495
+ port: nextPort,
496
+ body: nextPort ? null : endConnection.body,
497
+ },
594
498
  };
595
- };
596
- /**
597
- * Compute the Alt-mode cursor position: XZ locked to the last point,
598
- * Y driven by how far the mouse has moved vertically on screen since
599
- * Alt was pressed. Returns null if there's no anchor (Alt not active).
600
- */
601
- const resolveAltVerticalPoint = (clientY) => {
602
- const anchor = altAnchorRef.current;
603
- const last = draftRef.current.at(-1);
604
- if (!anchor || !last)
605
- return null;
606
- const step = isGridSnapActive() ? useEditor.getState().gridSnapStep : 0;
607
- // Screen +Y points down, so subtract to map "drag up = raise Y".
608
- const dy = (anchor.clientY - clientY) / ALT_PIXELS_PER_METER;
609
- const snappedDy = snap(dy, step);
610
- const y = Math.min(ALT_Y_MAX_M, Math.max(ALT_Y_MIN_M, anchor.baseY + snappedDy));
611
- return [last[0], y, last[2]];
612
- };
613
- // Resolve the cursor point (port / body / grid / angle snap) and then
614
- // layer Figma-style alignment on top so a run lines up with other runs,
615
- // fittings, and items as it's drawn. A free point (first vertex, or no
616
- // angle lock) snaps; an angle-locked continuation shows the guide passively
617
- // without leaving its 45° ray. Alignment follows the `lines` mode; a
618
- // port / body snap or Alt-vertical bypasses it.
619
- const resolveAlignedPoint = (event) => {
620
- const r = resolveSnappedPoint(event);
621
- const hasStart = draftRef.current.length > 0;
622
- const alt = event.nativeEvent?.altKey === true;
623
- const point = alignDrawPoint(r.point, {
624
- applySnap: isMagneticSnapActive() && (!hasStart || !isAngleSnapActive()),
625
- bypass: alt || r.snapped !== null,
626
- });
627
- return { ...r, point };
628
- };
629
- // The ceiling the cursor is under (ceiling mode only) — drives the
630
- // translucent surface overlay so the in-flight point reads as hung
631
- // against a real ceiling. Cleared when off-ceiling or out of mode.
632
- const updateHoverCeiling = (x, z) => {
633
- if (!ceilingModeRef.current) {
634
- setHoverCeiling(null);
635
- return;
636
- }
637
- setHoverCeiling(getCeilingAt(activeLevelId, useScene.getState().nodes, x, z));
638
- };
639
- const onMove = (event) => {
640
- const clientY = event.nativeEvent?.clientY;
641
- if (typeof clientY === 'number')
642
- lastClientYRef.current = clientY;
643
- // Alt vertical mode wins over the XZ logic.
644
- if (altAnchorRef.current && typeof clientY === 'number') {
645
- const point = resolveAltVerticalPoint(clientY);
646
- if (point) {
647
- clearDrawAlignment();
648
- setCursorPos(point);
649
- setSnapTarget(null);
650
- setEndSnap({ port: null, body: null });
651
- updateHoverCeiling(point[0], point[2]);
652
- return;
499
+ },
500
+ onShortcut: (event) => {
501
+ if (event.key === '[' || event.key === ']') {
502
+ event.preventDefault();
503
+ const next = stepNominalRunSize(DUCT_DIAMETERS_IN, profileRef.current.diameter, event.key === ']' ? 1 : -1);
504
+ if (next !== profileRef.current.diameter) {
505
+ setProfile((current) => ({ ...current, diameter: next }));
506
+ triggerSFX('sfx:grid-snap');
653
507
  }
654
508
  }
655
- const { point, snapped, port, body } = resolveAlignedPoint(event);
656
- setCursorPos(point);
657
- setSnapTarget(snapped);
658
- setEndSnap({ port, body: port ? null : body });
659
- updateHoverCeiling(point[0], point[2]);
660
- };
661
- const onClick = (event) => {
662
- const start = draftRef.current.at(-1);
663
- // Vertical mode with a start anchored: the click commits the riser
664
- // segment right there. Never falls through to the XZ logic — a
665
- // no-op Alt click (height unchanged) must not place anything.
666
- if (altAnchorRef.current && start) {
667
- const clientY = event.nativeEvent?.clientY ?? lastClientYRef.current;
668
- if (typeof clientY === 'number') {
669
- const point = resolveAltVerticalPoint(clientY);
670
- if (point && Math.abs(point[1] - start[1]) >= 1e-4) {
671
- commitSegment(start, point);
672
- }
673
- }
674
- return;
675
- }
676
- const { point, port, body } = resolveAlignedPoint(event);
677
- if (!start) {
678
- // First click: anchor the segment start, remembering the port or
679
- // run body it snapped to so the commit can mint an elbow / tee.
680
- // Joining a port INHERITS the source's cross-section — continuing
681
- // a rect trunk keeps drawing rect at its W×H, a round collar its
682
- // diameter. Body taps (tee branches) keep the tool's own profile.
683
- triggerSFX('sfx:grid-snap');
684
- startPortRef.current = port;
685
- startBodyRef.current = port ? null : body;
686
- if (port) {
687
- const inherited = inheritProfile(port);
688
- if (inherited)
689
- setProfile(inherited);
690
- }
691
- setDraftPoints([point]);
692
- return;
693
- }
694
- // Second click: commit the segment and re-arm. A body hit on the end
695
- // (no end port) taps a tee into that run's side.
696
- commitSegment(start, point, port, port ? null : body);
697
- };
698
- const enterAltMode = () => {
699
- const last = draftRef.current.at(-1);
700
- if (!last || lastClientYRef.current === null)
701
- return;
702
- if (altAnchorRef.current)
703
- return;
704
- altAnchorRef.current = { clientY: lastClientYRef.current, baseY: last[1] };
705
- setAltActive(true);
706
- };
707
- const exitAltMode = () => {
708
- if (!altAnchorRef.current)
709
- return;
710
- altAnchorRef.current = null;
711
- setAltActive(false);
712
- };
713
- const stepDiameter = (step) => {
714
- const sizes = DUCT_DIAMETERS_IN;
715
- const current = profileRef.current.diameter;
716
- // Nearest catalogue index, then step — handles seeded off-catalogue
717
- // values (e.g. a preset's 7.5") gracefully.
718
- let nearest = 0;
719
- for (let i = 1; i < sizes.length; i++) {
720
- if (Math.abs(sizes[i] - current) < Math.abs(sizes[nearest] - current))
721
- nearest = i;
722
- }
723
- const next = sizes[Math.min(sizes.length - 1, Math.max(0, nearest + step))];
724
- if (next === current)
725
- return;
726
- setProfile((p) => ({ ...p, diameter: next }));
727
- triggerSFX('sfx:grid-snap');
728
- };
729
- const onKeyDown = (e) => {
730
- const tag = e.target?.tagName;
731
- if (tag === 'INPUT' || tag === 'TEXTAREA')
732
- return;
733
- if (e.key === 'Alt') {
734
- e.preventDefault();
735
- enterAltMode();
736
- }
737
- else if (e.key === '[') {
738
- e.preventDefault();
739
- stepDiameter(-1);
740
- }
741
- else if (e.key === ']') {
742
- e.preventDefault();
743
- stepDiameter(1);
744
- }
745
- else if (e.key === 'q' || e.key === 'Q') {
746
- e.preventDefault();
747
- setProfile((p) => ({ ...p, shape: p.shape === 'round' ? 'rect' : 'round' }));
509
+ else if (event.key === 'q' || event.key === 'Q') {
510
+ event.preventDefault();
511
+ setProfile((current) => ({
512
+ ...current,
513
+ shape: current.shape === 'round' ? 'rect' : current.shape === 'rect' ? 'oval' : 'round',
514
+ }));
748
515
  triggerSFX('sfx:grid-snap');
749
516
  }
750
- else if (e.key === 'c' || e.key === 'C') {
751
- // Toggle ceiling mode: points hang from the ceiling above them
752
- // (duct top hugging the ceiling) instead of sitting on the floor.
753
- // Only flip while unanchored — already-placed points keep their Y,
754
- // so a mid-run toggle would split a run across two height regimes.
755
- if (draftRef.current.length > 0)
756
- return;
757
- e.preventDefault();
758
- setCeilingMode((m) => !m);
759
- setHoverCeiling(null);
517
+ else if (event.key === 'h' || event.key === 'H') {
518
+ event.preventDefault();
519
+ useRunHangerMode.getState().toggle('duct-segment');
760
520
  triggerSFX('sfx:grid-snap');
761
521
  }
762
- };
763
- const onKeyUp = (e) => {
764
- if (e.key === 'Alt') {
765
- e.preventDefault();
766
- exitAltMode();
767
- }
768
- };
769
- const onCancel = () => {
770
- clearDrawAlignment();
771
- if (draftRef.current.length === 0)
772
- return;
773
- markToolCancelConsumed();
774
- setDraftPoints([]);
775
- setCursorPos(null);
776
- setSnapTarget(null);
777
- setEndSnap({ port: null, body: null });
778
- setHoverCeiling(null);
779
- startPortRef.current = null;
780
- startBodyRef.current = null;
781
- };
782
- emitter.on('grid:move', onMove);
783
- emitter.on('grid:click', onClick);
784
- emitter.on('tool:cancel', onCancel);
785
- window.addEventListener('keydown', onKeyDown);
786
- window.addEventListener('keyup', onKeyUp);
787
- return () => {
788
- emitter.off('grid:move', onMove);
789
- emitter.off('grid:click', onClick);
790
- emitter.off('tool:cancel', onCancel);
791
- window.removeEventListener('keydown', onKeyDown);
792
- window.removeEventListener('keyup', onKeyUp);
793
- altAnchorRef.current = null;
794
- clearDrawAlignment();
795
- };
796
- }, [activeLevelId]);
522
+ },
523
+ });
524
+ const previewPlan = useMemo(() => {
525
+ if (!(activeLevelId && run.start && run.cursor))
526
+ return null;
527
+ return planDuctDraw(run.start, run.cursor, pendingPromotionRef.current ? null : run.startConnection.port, run.startConnection.body, run.endConnection.port, run.endConnection.body, profile, sceneApi.nodes(), run.surfaceTarget, autoHangers, hangerDefaults, hangerStyle);
528
+ }, [
529
+ activeLevelId,
530
+ autoHangers,
531
+ hangerDefaults,
532
+ hangerStyle,
533
+ profile,
534
+ run.start,
535
+ run.cursor,
536
+ run.startConnection,
537
+ run.endConnection,
538
+ run.surfaceTarget,
539
+ sceneApi,
540
+ ]);
541
+ const ghostFittings = useMemo(() => previewPlan?.fittings ?? [], [previewPlan]);
542
+ useEffect(() => {
543
+ usePathDraftPreview
544
+ .getState()
545
+ .setDraft('duct-segment', run.start ? [run.start] : [], run.cursor, { ...profile, autoHangers, hangerStyle }, ghostFittings);
546
+ }, [autoHangers, hangerStyle, ghostFittings, profile, run.cursor, run.start]);
547
+ useEffect(() => () => usePathDraftPreview.getState().clear('duct-segment'), []);
548
+ useEffect(() => () => useEditor.getState().setToolDefaults('duct-segment', null), []);
797
549
  if (!activeLevelId)
798
550
  return null;
799
- const previewSegments = [];
800
- for (let i = 0; i < draftPoints.length - 1; i++) {
801
- previewSegments.push({ a: draftPoints[i], b: draftPoints[i + 1] });
802
- }
803
- const last = draftPoints.at(-1);
804
- if (last && cursorPos) {
805
- previewSegments.push({ a: last, b: cursorPos });
806
- }
807
- // Wall-style dimension pill above the cursor: absolute world coords before
808
- // the first point, signed per-axis deltas from the last placed point while
809
- // a segment is in flight. The actively-driven axis is emphasised — Y in
810
- // Alt-vertical mode, otherwise whichever horizontal axis dominates. A
811
- // trailing Ø readout shows the diameter the next click commits ([ / ]).
812
- const pillParts = cursorPos
813
- ? [
814
- ...['x', 'y', 'z'].map((axis, i) => ({
815
- key: axis,
816
- prefix: axis.toUpperCase(),
817
- value: last ? cursorPos[i] - last[i] : cursorPos[i],
818
- signed: !!last,
819
- })),
820
- ...(profile.shape === 'round'
821
- ? [{ key: 'diameter', prefix: 'Ø', value: profile.diameter * 0.0254, signed: false }]
822
- : [
823
- { key: 'trunk-w', prefix: 'W', value: profile.width * 0.0254, signed: false },
824
- { key: 'trunk-h', prefix: 'H', value: profile.height * 0.0254, signed: false },
825
- ]),
826
- ]
827
- : null;
828
- const pillPrimary = last && cursorPos
829
- ? altActive
830
- ? 'y'
831
- : Math.abs(cursorPos[0] - last[0]) >= Math.abs(cursorPos[2] - last[2])
832
- ? 'x'
833
- : 'z'
834
- : undefined;
835
- // When the in-flight point hangs above the floor (ceiling mode, or an
836
- // Alt riser), the cursor marker itself rides AT the point (where the
837
- // mouse is aiming and the next click commits), and a plumb line drops
838
- // straight down to a faint ground ring on the floor below — so the plan
839
- // position stays legible from any angle. A floor-level point keeps the
840
- // standard fixed-height cursor look.
841
- const cursorElevation = cursorPos ? cursorPos[1] : 0;
842
- const isElevated = cursorElevation > 0.001;
843
- const cursorGround = cursorPos
844
- ? [cursorPos[0], 0, cursorPos[2]]
845
- : null;
846
- return (_jsxs(LevelOffsetGroup, { children: [ceilingMode && hoverCeiling && _jsx(CeilingHighlight, { ceiling: hoverCeiling }), cursorPos && cursorGround && (_jsxs(_Fragment, { children: [isElevated ? (_jsx(CursorSphere, { color: snapTarget ? SNAP_CURSOR_COLOR : undefined, dotAtTip: true, height: cursorElevation, position: cursorGround, ref: cursorRef })) : (_jsx(CursorSphere, { color: snapTarget ? SNAP_CURSOR_COLOR : undefined, position: cursorPos, ref: cursorRef })), pillParts && (_jsx("group", { position: cursorPos, children: _jsx(Html, { center: true, position: [0, 1.45, 0], style: { pointerEvents: 'none', userSelect: 'none' }, zIndexRange: [100, 0], children: _jsxs("div", { className: "flex flex-col items-center gap-2", children: [ceilingMode && !last && (_jsx("div", { className: "whitespace-nowrap rounded-full border border-border/60 bg-background/90 px-3 py-0.5 text-[10px] text-muted-foreground shadow-sm backdrop-blur", children: "Ceiling \u00B7 C to toggle" })), _jsx(DimensionPill, { parts: pillParts, primary: pillPrimary, unit: unit })] }) }) }))] })), draftPoints.map((p, i) => (_jsxs("mesh", { layers: EDITOR_LAYER, position: p, children: [_jsx("sphereGeometry", { args: [0.07, 16, 12] }), _jsx("meshBasicMaterial", { color: "#818cf8", depthTest: false })] }, `pt-${i}`))), previewSegments.map((seg, i) => (_jsx(PreviewSegment, { a: seg.a, b: seg.b, endPort: endSnap.port, profile: profile, startPort: startPortRef.current }, `seg-${i}`))), ghostFittings.map((fitting) => (_jsx(FittingGhost, { fitting: fitting }, fitting.id)))] }));
551
+ const extraParts = profile.shape === 'round'
552
+ ? [{ key: 'diameter', prefix: 'Ø', value: profile.diameter * 0.0254 }]
553
+ : [
554
+ { key: 'trunk-w', prefix: 'W', value: profile.width * 0.0254 },
555
+ { key: 'trunk-h', prefix: 'H', value: profile.height * 0.0254 },
556
+ ];
557
+ return (_jsxs(LevelOffsetGroup, { children: [_jsx(ConnectionFeedback, { point: run.cursor, target: run.endConnection.port, levelId: activeLevelId, profile: {
558
+ ...profile,
559
+ system: String(hangerDefaults?.system ?? ductSegmentDefinition.defaults().system),
560
+ } }), _jsx(DistributionRunCursor, { altActive: run.altActive, cursor: run.cursor, cursorRef: cursorRef, directionMode: run.directionMode, extraParts: extraParts, lengthInput: run.lengthInput, onLengthInputChange: run.onLengthInputChange, onDirectionSelect: run.onDirectionSelect, validationMessage: previewPlan?.validationMessage ?? run.validationMessage, snapTarget: run.snapTarget, snapScreen: run.snapScreen, start: run.start, startDirection: run.startConnection.port?.direction ?? null, unit: unit }), run.start && (_jsxs("mesh", { layers: EDITOR_LAYER, position: run.start, children: [_jsx("sphereGeometry", { args: [0.07, 16, 12] }), _jsx("meshBasicMaterial", { color: "#818cf8", depthTest: false })] })), previewPlan?.ducts.map((duct, index) => (_jsx(DuctSegmentGhost, { duct: duct }, index))), previewPlan?.ducts.map((duct, index) => (_jsx(RunHangerPreview, { run: duct, levelId: activeLevelId }, `hanger-${index}`))), ghostFittings.map((fitting) => (_jsx(FittingGhost, { fitting: fitting }, fitting.id)))] }));
847
561
  };
848
- /**
849
- * Build a horizontal `ShapeGeometry` for a ceiling polygon (with holes) in
850
- * level-local XZ, laid flat in the XZ plane. Mirrors the ceiling renderer /
851
- * move-tool convention (Z negated, then rotated onto the floor plane).
852
- */
853
- function buildCeilingShape(polygon, holes) {
854
- if (polygon.length < 3)
855
- return null;
856
- const shape = new Shape();
857
- const first = polygon[0];
858
- shape.moveTo(first[0], -first[1]);
859
- for (let i = 1; i < polygon.length; i++) {
860
- const pt = polygon[i];
861
- shape.lineTo(pt[0], -pt[1]);
862
- }
863
- shape.closePath();
864
- for (const holePolygon of holes) {
865
- if (holePolygon.length < 3)
866
- continue;
867
- const hole = new Path();
868
- const hf = holePolygon[0];
869
- hole.moveTo(hf[0], -hf[1]);
870
- for (let i = 1; i < holePolygon.length; i++) {
871
- const pt = holePolygon[i];
872
- hole.lineTo(pt[0], -pt[1]);
873
- }
874
- hole.closePath();
875
- shape.holes.push(hole);
876
- }
877
- const geometry = new ShapeGeometry(shape);
878
- geometry.rotateX(-Math.PI / 2);
879
- return geometry;
880
- }
881
- /**
882
- * Translucent overlay of the ceiling the cursor is under, drawn at the
883
- * ceiling's own height. Gives the in-flight duct point a real surface to
884
- * read against, so "hung against the ceiling" is visible from any angle
885
- * instead of being a dot floating in space.
886
- */
887
- function CeilingHighlight({ ceiling }) {
888
- const geometry = useMemo(() => buildCeilingShape(ceiling.polygon, ceiling.holes), [ceiling.polygon, ceiling.holes]);
889
- const outline = useMemo(() => {
890
- if (ceiling.polygon.length < 2)
891
- return null;
892
- const pts = ceiling.polygon.map(([x, z]) => new Vector3(x, 0, z));
893
- const f = ceiling.polygon[0];
894
- pts.push(new Vector3(f[0], 0, f[1]));
895
- return pts;
896
- }, [ceiling.polygon]);
897
- if (!geometry)
898
- return null;
899
- const y = resolveCeilingHeight(ceiling, useScene.getState().nodes);
900
- return (_jsxs("group", { position: [0, y, 0], children: [_jsx("mesh", { geometry: geometry, layers: EDITOR_LAYER, renderOrder: 1, children: _jsx("meshBasicMaterial", { color: "#818cf8", depthWrite: false, opacity: 0.15, side: DoubleSide, transparent: true }) }), outline && (_jsxs("line", { children: [_jsx("bufferGeometry", { ref: (g) => {
901
- if (g)
902
- g.setFromPoints(outline);
903
- } }), _jsx("lineBasicMaterial", { color: "#818cf8", opacity: 0.6, transparent: true })] }))] }));
904
- }
905
- function PreviewSegment({ a, b, profile, startPort, endPort, }) {
906
- const start = new Vector3(...a);
907
- const end = new Vector3(...b);
908
- const dir = new Vector3().subVectors(end, start);
909
- const length = dir.length();
910
- if (length < 1e-4)
911
- return null;
912
- dir.normalize();
913
- const mid = new Vector3().addVectors(start, end).multiplyScalar(0.5);
914
- // Rect AND oval ghost as a box — close enough for a translucent guide.
915
- if (profile.shape !== 'round') {
916
- const w = profile.width * 0.0254;
917
- const h = profile.height * 0.0254;
918
- return (_jsxs("mesh", { layers: EDITOR_LAYER, position: mid.toArray(), ref: (m) => {
919
- if (!m)
920
- return;
921
- // Same basis AND roll as the commit will use, so the ghost
922
- // shows the orientation that actually lands.
923
- const roll = continuityRollForRun(startPort, endPort, dir);
924
- const { width: x, height: z } = rectSectionAxes(dir, roll);
925
- m.quaternion.setFromRotationMatrix(new Matrix4().makeBasis(x, dir, z));
926
- }, children: [_jsx("boxGeometry", { args: [w, length, h] }), _jsx("meshBasicMaterial", { color: "#818cf8", depthTest: false, opacity: PREVIEW_OPACITY, transparent: true })] }));
927
- }
928
- const radius = (profile.diameter * 0.0254) / 2;
929
- return (_jsxs("mesh", { layers: EDITOR_LAYER, position: mid.toArray(), ref: (m) => {
930
- if (!m)
931
- return;
932
- m.quaternion.setFromUnitVectors(new Vector3(0, 1, 0), dir);
933
- }, children: [_jsx("cylinderGeometry", { args: [radius, radius, length, 24, 1, false] }), _jsx("meshBasicMaterial", { color: "#818cf8", depthTest: false, opacity: PREVIEW_OPACITY, transparent: true })] }));
934
- }
935
562
  export default DuctSegmentTool;