@pascal-app/core 0.8.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/dist/events/bus.d.ts +67 -13
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
  4. package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
  5. package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
  6. package/dist/hooks/scene-registry/scene-registry.d.ts +14 -23
  7. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  8. package/dist/hooks/scene-registry/scene-registry.js +59 -26
  9. package/dist/hooks/scene-registry/scene-registry.test.d.ts +2 -0
  10. package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
  11. package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
  13. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
  14. package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
  15. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
  16. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
  17. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +366 -0
  18. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +79 -11
  19. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  20. package/dist/hooks/spatial-grid/spatial-grid-manager.js +508 -141
  21. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
  22. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  23. package/dist/hooks/spatial-grid/spatial-grid-sync.js +109 -18
  24. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
  25. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
  26. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
  27. package/dist/index.d.ts +36 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +33 -6
  30. package/dist/lib/level-name.d.ts +4 -0
  31. package/dist/lib/level-name.d.ts.map +1 -0
  32. package/dist/lib/level-name.js +10 -0
  33. package/dist/lib/measurement-geometry.d.ts +19 -0
  34. package/dist/lib/measurement-geometry.d.ts.map +1 -0
  35. package/dist/lib/measurement-geometry.js +232 -0
  36. package/dist/lib/measurement-geometry.test.d.ts +2 -0
  37. package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
  38. package/dist/lib/measurement-geometry.test.js +102 -0
  39. package/dist/lib/polygon-geometry.d.ts +0 -1
  40. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  41. package/dist/lib/polygon-geometry.js +0 -18
  42. package/dist/lib/polygon-relations.d.ts +10 -0
  43. package/dist/lib/polygon-relations.d.ts.map +1 -0
  44. package/dist/lib/polygon-relations.js +73 -0
  45. package/dist/lib/selection-proxy.d.ts +4 -0
  46. package/dist/lib/selection-proxy.d.ts.map +1 -0
  47. package/dist/lib/selection-proxy.js +12 -0
  48. package/dist/lib/slab-polygon.d.ts +31 -2
  49. package/dist/lib/slab-polygon.d.ts.map +1 -1
  50. package/dist/lib/slab-polygon.js +525 -31
  51. package/dist/lib/slab-polygon.test.d.ts +2 -0
  52. package/dist/lib/slab-polygon.test.d.ts.map +1 -0
  53. package/dist/lib/slab-polygon.test.js +635 -0
  54. package/dist/lib/slots.d.ts +14 -0
  55. package/dist/lib/slots.d.ts.map +1 -0
  56. package/dist/lib/slots.js +26 -0
  57. package/dist/lib/space-detection-pause.test.d.ts +2 -0
  58. package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
  59. package/dist/lib/space-detection-pause.test.js +42 -0
  60. package/dist/lib/space-detection.d.ts +54 -2
  61. package/dist/lib/space-detection.d.ts.map +1 -1
  62. package/dist/lib/space-detection.js +599 -124
  63. package/dist/lib/space-detection.test.d.ts +2 -0
  64. package/dist/lib/space-detection.test.d.ts.map +1 -0
  65. package/dist/lib/space-detection.test.js +376 -0
  66. package/dist/lib/wall-distance.d.ts +60 -0
  67. package/dist/lib/wall-distance.d.ts.map +1 -0
  68. package/dist/lib/wall-distance.js +85 -0
  69. package/dist/lib/zone-quantities.d.ts +21 -0
  70. package/dist/lib/zone-quantities.d.ts.map +1 -0
  71. package/dist/lib/zone-quantities.js +183 -0
  72. package/dist/lib/zone-quantities.test.d.ts +2 -0
  73. package/dist/lib/zone-quantities.test.d.ts.map +1 -0
  74. package/dist/lib/zone-quantities.test.js +82 -0
  75. package/dist/material-library.d.ts +20 -1
  76. package/dist/material-library.d.ts.map +1 -1
  77. package/dist/material-library.js +3737 -200
  78. package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
  79. package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
  80. package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
  81. package/dist/registry/handles.d.ts +394 -0
  82. package/dist/registry/handles.d.ts.map +1 -0
  83. package/dist/registry/handles.js +24 -0
  84. package/dist/registry/index.d.ts +7 -0
  85. package/dist/registry/index.d.ts.map +1 -0
  86. package/dist/registry/index.js +4 -0
  87. package/dist/registry/registry.d.ts +136 -0
  88. package/dist/registry/registry.d.ts.map +1 -0
  89. package/dist/registry/registry.js +282 -0
  90. package/dist/registry/registry.test.d.ts +2 -0
  91. package/dist/registry/registry.test.d.ts.map +1 -0
  92. package/dist/registry/registry.test.js +196 -0
  93. package/dist/registry/relations-resolver.d.ts +42 -0
  94. package/dist/registry/relations-resolver.d.ts.map +1 -0
  95. package/dist/registry/relations-resolver.js +77 -0
  96. package/dist/registry/relations-resolver.test.d.ts +2 -0
  97. package/dist/registry/relations-resolver.test.d.ts.map +1 -0
  98. package/dist/registry/relations-resolver.test.js +183 -0
  99. package/dist/registry/scene-api.d.ts +44 -0
  100. package/dist/registry/scene-api.d.ts.map +1 -0
  101. package/dist/registry/scene-api.js +111 -0
  102. package/dist/registry/scene-api.test.d.ts +2 -0
  103. package/dist/registry/scene-api.test.d.ts.map +1 -0
  104. package/dist/registry/scene-api.test.js +183 -0
  105. package/dist/registry/subtree.d.ts +67 -0
  106. package/dist/registry/subtree.d.ts.map +1 -0
  107. package/dist/registry/subtree.js +123 -0
  108. package/dist/registry/subtree.test.d.ts +2 -0
  109. package/dist/registry/subtree.test.d.ts.map +1 -0
  110. package/dist/registry/subtree.test.js +163 -0
  111. package/dist/registry/types.d.ts +2099 -0
  112. package/dist/registry/types.d.ts.map +1 -0
  113. package/dist/registry/types.js +1 -0
  114. package/dist/schema/index.d.ts +35 -4
  115. package/dist/schema/index.d.ts.map +1 -1
  116. package/dist/schema/index.js +32 -4
  117. package/dist/schema/material.d.ts +13 -2
  118. package/dist/schema/material.d.ts.map +1 -1
  119. package/dist/schema/material.js +17 -5
  120. package/dist/schema/material.test.d.ts +2 -0
  121. package/dist/schema/material.test.d.ts.map +1 -0
  122. package/dist/schema/material.test.js +43 -0
  123. package/dist/schema/nodes/box-vent.d.ts +74 -0
  124. package/dist/schema/nodes/box-vent.d.ts.map +1 -0
  125. package/dist/schema/nodes/box-vent.js +63 -0
  126. package/dist/schema/nodes/building.d.ts +1 -1
  127. package/dist/schema/nodes/building.d.ts.map +1 -1
  128. package/dist/schema/nodes/building.js +3 -2
  129. package/dist/schema/nodes/cabinet.d.ts +534 -0
  130. package/dist/schema/nodes/cabinet.d.ts.map +1 -0
  131. package/dist/schema/nodes/cabinet.js +137 -0
  132. package/dist/schema/nodes/ceiling.d.ts +5 -2
  133. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  134. package/dist/schema/nodes/ceiling.js +5 -1
  135. package/dist/schema/nodes/chimney.d.ts +158 -0
  136. package/dist/schema/nodes/chimney.d.ts.map +1 -0
  137. package/dist/schema/nodes/chimney.js +67 -0
  138. package/dist/schema/nodes/column.d.ts +600 -7
  139. package/dist/schema/nodes/column.d.ts.map +1 -1
  140. package/dist/schema/nodes/column.js +589 -6
  141. package/dist/schema/nodes/cupola.d.ts +66 -0
  142. package/dist/schema/nodes/cupola.d.ts.map +1 -0
  143. package/dist/schema/nodes/cupola.js +28 -0
  144. package/dist/schema/nodes/door.d.ts +13 -5
  145. package/dist/schema/nodes/door.d.ts.map +1 -1
  146. package/dist/schema/nodes/door.js +12 -0
  147. package/dist/schema/nodes/dormer.d.ts +228 -0
  148. package/dist/schema/nodes/dormer.d.ts.map +1 -0
  149. package/dist/schema/nodes/dormer.js +119 -0
  150. package/dist/schema/nodes/downspout.d.ts +75 -0
  151. package/dist/schema/nodes/downspout.d.ts.map +1 -0
  152. package/dist/schema/nodes/downspout.js +63 -0
  153. package/dist/schema/nodes/duct-fitting.d.ts +91 -0
  154. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
  155. package/dist/schema/nodes/duct-fitting.js +92 -0
  156. package/dist/schema/nodes/duct-segment.d.ts +61 -0
  157. package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
  158. package/dist/schema/nodes/duct-segment.js +73 -0
  159. package/dist/schema/nodes/duct-terminal.d.ts +57 -0
  160. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
  161. package/dist/schema/nodes/duct-terminal.js +51 -0
  162. package/dist/schema/nodes/elevator.d.ts +106 -0
  163. package/dist/schema/nodes/elevator.d.ts.map +1 -0
  164. package/dist/schema/nodes/elevator.js +57 -0
  165. package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
  166. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
  167. package/dist/schema/nodes/eyebrow-vent.js +40 -0
  168. package/dist/schema/nodes/fence.d.ts +19 -2
  169. package/dist/schema/nodes/fence.d.ts.map +1 -1
  170. package/dist/schema/nodes/fence.js +27 -2
  171. package/dist/schema/nodes/gutter.d.ts +84 -0
  172. package/dist/schema/nodes/gutter.d.ts.map +1 -0
  173. package/dist/schema/nodes/gutter.js +76 -0
  174. package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
  175. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
  176. package/dist/schema/nodes/hvac-equipment.js +55 -0
  177. package/dist/schema/nodes/item.d.ts +12 -0
  178. package/dist/schema/nodes/item.d.ts.map +1 -1
  179. package/dist/schema/nodes/item.js +20 -0
  180. package/dist/schema/nodes/level.d.ts +1 -1
  181. package/dist/schema/nodes/level.d.ts.map +1 -1
  182. package/dist/schema/nodes/level.js +4 -0
  183. package/dist/schema/nodes/lineset.d.ts +42 -0
  184. package/dist/schema/nodes/lineset.d.ts.map +1 -0
  185. package/dist/schema/nodes/lineset.js +38 -0
  186. package/dist/schema/nodes/liquid-line.d.ts +34 -0
  187. package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
  188. package/dist/schema/nodes/liquid-line.js +24 -0
  189. package/dist/schema/nodes/measurement.d.ts +306 -0
  190. package/dist/schema/nodes/measurement.d.ts.map +1 -0
  191. package/dist/schema/nodes/measurement.js +87 -0
  192. package/dist/schema/nodes/measurement.test.d.ts +2 -0
  193. package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
  194. package/dist/schema/nodes/measurement.test.js +139 -0
  195. package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
  196. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
  197. package/dist/schema/nodes/pipe-fitting.js +45 -0
  198. package/dist/schema/nodes/pipe-segment.d.ts +48 -0
  199. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
  200. package/dist/schema/nodes/pipe-segment.js +36 -0
  201. package/dist/schema/nodes/pipe-trap.d.ts +43 -0
  202. package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
  203. package/dist/schema/nodes/pipe-trap.js +36 -0
  204. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +2 -0
  205. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
  206. package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
  207. package/dist/schema/nodes/ridge-vent.d.ts +82 -0
  208. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
  209. package/dist/schema/nodes/ridge-vent.js +248 -0
  210. package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
  211. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
  212. package/dist/schema/nodes/roof-segment-shape.js +318 -0
  213. package/dist/schema/nodes/roof-segment-shape.test.d.ts +2 -0
  214. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
  215. package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
  216. package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
  217. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
  218. package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
  219. package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
  220. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
  221. package/dist/schema/nodes/roof-segment-trim.test.js +107 -0
  222. package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
  223. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
  224. package/dist/schema/nodes/roof-segment-walls.js +330 -0
  225. package/dist/schema/nodes/roof-segment-walls.test.d.ts +2 -0
  226. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
  227. package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
  228. package/dist/schema/nodes/roof-segment.d.ts +273 -3
  229. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  230. package/dist/schema/nodes/roof-segment.js +539 -3
  231. package/dist/schema/nodes/roof.d.ts +8 -8
  232. package/dist/schema/nodes/roof.d.ts.map +1 -1
  233. package/dist/schema/nodes/roof.js +4 -16
  234. package/dist/schema/nodes/shelf.d.ts +106 -0
  235. package/dist/schema/nodes/shelf.d.ts.map +1 -0
  236. package/dist/schema/nodes/shelf.js +90 -0
  237. package/dist/schema/nodes/site.d.ts +1 -122
  238. package/dist/schema/nodes/site.d.ts.map +1 -1
  239. package/dist/schema/nodes/site.js +2 -6
  240. package/dist/schema/nodes/skylight.d.ts +279 -0
  241. package/dist/schema/nodes/skylight.d.ts.map +1 -0
  242. package/dist/schema/nodes/skylight.js +152 -0
  243. package/dist/schema/nodes/slab.d.ts +5 -2
  244. package/dist/schema/nodes/slab.d.ts.map +1 -1
  245. package/dist/schema/nodes/slab.js +5 -1
  246. package/dist/schema/nodes/solar-panel.d.ts +117 -0
  247. package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
  248. package/dist/schema/nodes/solar-panel.js +47 -0
  249. package/dist/schema/nodes/stair-segment.d.ts +4 -4
  250. package/dist/schema/nodes/stair.d.ts +13 -12
  251. package/dist/schema/nodes/stair.d.ts.map +1 -1
  252. package/dist/schema/nodes/stair.js +3 -0
  253. package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
  254. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
  255. package/dist/schema/nodes/surface-hole-metadata.js +3 -1
  256. package/dist/schema/nodes/turbine-vent.d.ts +68 -0
  257. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
  258. package/dist/schema/nodes/turbine-vent.js +41 -0
  259. package/dist/schema/nodes/wall.d.ts +245 -6
  260. package/dist/schema/nodes/wall.d.ts.map +1 -1
  261. package/dist/schema/nodes/wall.js +257 -0
  262. package/dist/schema/nodes/wall.test.d.ts +2 -0
  263. package/dist/schema/nodes/wall.test.d.ts.map +1 -0
  264. package/dist/schema/nodes/wall.test.js +208 -0
  265. package/dist/schema/nodes/window.d.ts +11 -3
  266. package/dist/schema/nodes/window.d.ts.map +1 -1
  267. package/dist/schema/nodes/window.js +12 -0
  268. package/dist/schema/nodes/zone.d.ts +2 -0
  269. package/dist/schema/nodes/zone.d.ts.map +1 -1
  270. package/dist/schema/nodes/zone.js +6 -0
  271. package/dist/schema/scene-material.d.ts +41 -0
  272. package/dist/schema/scene-material.d.ts.map +1 -0
  273. package/dist/schema/scene-material.js +9 -0
  274. package/dist/schema/types.d.ts +2850 -655
  275. package/dist/schema/types.d.ts.map +1 -1
  276. package/dist/schema/types.js +49 -0
  277. package/dist/services/alignment-anchors.d.ts +100 -0
  278. package/dist/services/alignment-anchors.d.ts.map +1 -0
  279. package/dist/services/alignment-anchors.js +312 -0
  280. package/dist/services/alignment-anchors.test.d.ts +2 -0
  281. package/dist/services/alignment-anchors.test.d.ts.map +1 -0
  282. package/dist/services/alignment-anchors.test.js +383 -0
  283. package/dist/services/alignment.d.ts +131 -0
  284. package/dist/services/alignment.d.ts.map +1 -0
  285. package/dist/services/alignment.js +171 -0
  286. package/dist/services/alignment.test.d.ts +2 -0
  287. package/dist/services/alignment.test.d.ts.map +1 -0
  288. package/dist/services/alignment.test.js +92 -0
  289. package/dist/services/drag-session.d.ts +47 -0
  290. package/dist/services/drag-session.d.ts.map +1 -0
  291. package/dist/services/drag-session.js +100 -0
  292. package/dist/services/drag-session.test.d.ts +2 -0
  293. package/dist/services/drag-session.test.d.ts.map +1 -0
  294. package/dist/services/drag-session.test.js +219 -0
  295. package/dist/services/hosting.d.ts +88 -0
  296. package/dist/services/hosting.d.ts.map +1 -0
  297. package/dist/services/hosting.js +131 -0
  298. package/dist/services/hosting.test.d.ts +2 -0
  299. package/dist/services/hosting.test.d.ts.map +1 -0
  300. package/dist/services/hosting.test.js +230 -0
  301. package/dist/services/index.d.ts +13 -0
  302. package/dist/services/index.d.ts.map +1 -0
  303. package/dist/services/index.js +12 -0
  304. package/dist/services/level-height.d.ts +28 -0
  305. package/dist/services/level-height.d.ts.map +1 -0
  306. package/dist/services/level-height.js +62 -0
  307. package/dist/services/movement.d.ts +48 -0
  308. package/dist/services/movement.d.ts.map +1 -0
  309. package/dist/services/movement.js +65 -0
  310. package/dist/services/movement.test.d.ts +2 -0
  311. package/dist/services/movement.test.d.ts.map +1 -0
  312. package/dist/services/movement.test.js +133 -0
  313. package/dist/services/opening-guides.d.ts +141 -0
  314. package/dist/services/opening-guides.d.ts.map +1 -0
  315. package/dist/services/opening-guides.js +267 -0
  316. package/dist/services/opening-guides.test.d.ts +2 -0
  317. package/dist/services/opening-guides.test.d.ts.map +1 -0
  318. package/dist/services/opening-guides.test.js +208 -0
  319. package/dist/services/port-connectivity.d.ts +115 -0
  320. package/dist/services/port-connectivity.d.ts.map +1 -0
  321. package/dist/services/port-connectivity.js +300 -0
  322. package/dist/services/port-connectivity.test.d.ts +2 -0
  323. package/dist/services/port-connectivity.test.d.ts.map +1 -0
  324. package/dist/services/port-connectivity.test.js +297 -0
  325. package/dist/services/riser-diagram.d.ts +40 -0
  326. package/dist/services/riser-diagram.d.ts.map +1 -0
  327. package/dist/services/riser-diagram.js +110 -0
  328. package/dist/services/riser-diagram.test.d.ts +2 -0
  329. package/dist/services/riser-diagram.test.d.ts.map +1 -0
  330. package/dist/services/riser-diagram.test.js +66 -0
  331. package/dist/services/single-undo-dance.test.d.ts +2 -0
  332. package/dist/services/single-undo-dance.test.d.ts.map +1 -0
  333. package/dist/services/single-undo-dance.test.js +210 -0
  334. package/dist/services/snap.d.ts +80 -0
  335. package/dist/services/snap.d.ts.map +1 -0
  336. package/dist/services/snap.js +140 -0
  337. package/dist/services/snap.test.d.ts +2 -0
  338. package/dist/services/snap.test.d.ts.map +1 -0
  339. package/dist/services/snap.test.js +137 -0
  340. package/dist/services/system-graph.d.ts +30 -0
  341. package/dist/services/system-graph.d.ts.map +1 -0
  342. package/dist/services/system-graph.js +164 -0
  343. package/dist/services/system-graph.test.d.ts +2 -0
  344. package/dist/services/system-graph.test.d.ts.map +1 -0
  345. package/dist/services/system-graph.test.js +138 -0
  346. package/dist/services/validate-dwv.d.ts +32 -0
  347. package/dist/services/validate-dwv.d.ts.map +1 -0
  348. package/dist/services/validate-dwv.js +118 -0
  349. package/dist/services/validate-dwv.test.d.ts +2 -0
  350. package/dist/services/validate-dwv.test.d.ts.map +1 -0
  351. package/dist/services/validate-dwv.test.js +109 -0
  352. package/dist/solar-panel-presets.d.ts +17 -0
  353. package/dist/solar-panel-presets.d.ts.map +1 -0
  354. package/dist/solar-panel-presets.js +43 -0
  355. package/dist/store/actions/node-actions.d.ts +14 -2
  356. package/dist/store/actions/node-actions.d.ts.map +1 -1
  357. package/dist/store/actions/node-actions.js +585 -15
  358. package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
  359. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
  360. package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
  361. package/dist/store/actions/reparent.test.d.ts +2 -0
  362. package/dist/store/actions/reparent.test.d.ts.map +1 -0
  363. package/dist/store/actions/reparent.test.js +212 -0
  364. package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
  365. package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
  366. package/dist/store/actions/ridge-vent-update.test.js +264 -0
  367. package/dist/store/history-control.d.ts +11 -0
  368. package/dist/store/history-control.d.ts.map +1 -1
  369. package/dist/store/history-control.js +33 -0
  370. package/dist/store/use-interactive.d.ts +39 -0
  371. package/dist/store/use-interactive.d.ts.map +1 -1
  372. package/dist/store/use-interactive.js +75 -0
  373. package/dist/store/use-live-node-overrides.d.ts +22 -0
  374. package/dist/store/use-live-node-overrides.d.ts.map +1 -0
  375. package/dist/store/use-live-node-overrides.js +59 -0
  376. package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
  377. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
  378. package/dist/store/use-scene-dirty-tracking.test.js +68 -0
  379. package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
  380. package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
  381. package/dist/store/use-scene-elevator-migration.test.js +109 -0
  382. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
  383. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
  384. package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
  385. package/dist/store/use-scene-plugins.test.d.ts +2 -0
  386. package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
  387. package/dist/store/use-scene-plugins.test.js +51 -0
  388. package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
  389. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
  390. package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
  391. package/dist/store/use-scene-window-migration.test.d.ts +2 -0
  392. package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
  393. package/dist/store/use-scene-window-migration.test.js +85 -0
  394. package/dist/store/use-scene.d.ts +28 -2
  395. package/dist/store/use-scene.d.ts.map +1 -1
  396. package/dist/store/use-scene.js +546 -49
  397. package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
  398. package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
  399. package/dist/systems/elevator/elevator-dispatch.js +57 -0
  400. package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
  401. package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
  402. package/dist/systems/elevator/elevator-geometry.js +50 -0
  403. package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
  404. package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
  405. package/dist/systems/elevator/elevator-opening-sync.js +218 -0
  406. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
  407. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
  408. package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
  409. package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
  410. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
  411. package/dist/systems/elevator/elevator-opening-system.js +48 -0
  412. package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
  413. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
  414. package/dist/systems/elevator/elevator-runtime-system.js +8 -0
  415. package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
  416. package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
  417. package/dist/systems/elevator/elevator-runtime.js +225 -0
  418. package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
  419. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
  420. package/dist/systems/elevator/elevator-runtime.test.js +64 -0
  421. package/dist/systems/elevator/elevator-service.d.ts +19 -0
  422. package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
  423. package/dist/systems/elevator/elevator-service.js +111 -0
  424. package/dist/systems/fence/fence-centerline.d.ts +12 -0
  425. package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
  426. package/dist/systems/fence/fence-centerline.js +37 -0
  427. package/dist/systems/fence/fence-spline.d.ts +39 -0
  428. package/dist/systems/fence/fence-spline.d.ts.map +1 -0
  429. package/dist/systems/fence/fence-spline.js +196 -0
  430. package/dist/systems/fence/fence-spline.test.d.ts +2 -0
  431. package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
  432. package/dist/systems/fence/fence-spline.test.js +136 -0
  433. package/dist/systems/stair/stair-footprint.d.ts +44 -0
  434. package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
  435. package/dist/systems/stair/stair-footprint.js +178 -0
  436. package/dist/systems/stair/stair-opening-preview.d.ts +2447 -0
  437. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
  438. package/dist/systems/stair/stair-opening-preview.js +93 -0
  439. package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
  440. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
  441. package/dist/systems/stair/stair-opening-preview.test.js +80 -0
  442. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  443. package/dist/systems/stair/stair-opening-sync.js +155 -147
  444. package/dist/systems/stair/stair-opening-sync.test.js +424 -2
  445. package/dist/systems/stair/stair-opening-system.d.ts +2 -0
  446. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
  447. package/dist/systems/stair/stair-opening-system.js +104 -0
  448. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  449. package/dist/systems/wall/wall-mitering.js +38 -2
  450. package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
  451. package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
  452. package/dist/systems/wall/wall-mitering.test.js +63 -0
  453. package/dist/systems/wall/wall-move.d.ts +48 -0
  454. package/dist/systems/wall/wall-move.d.ts.map +1 -0
  455. package/dist/systems/wall/wall-move.js +193 -0
  456. package/dist/utils/clone-scene-graph.d.ts +7 -4
  457. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  458. package/dist/utils/clone-scene-graph.js +29 -7
  459. package/dist/utils/clone-scene-graph.test.d.ts +2 -0
  460. package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
  461. package/dist/utils/clone-scene-graph.test.js +62 -0
  462. package/dist/utils/heal-scene-graph.d.ts +18 -0
  463. package/dist/utils/heal-scene-graph.d.ts.map +1 -0
  464. package/dist/utils/heal-scene-graph.js +90 -0
  465. package/dist/utils/heal-scene-graph.test.d.ts +2 -0
  466. package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
  467. package/dist/utils/heal-scene-graph.test.js +88 -0
  468. package/dist/validation/validate-build-json.d.ts +44 -0
  469. package/dist/validation/validate-build-json.d.ts.map +1 -0
  470. package/dist/validation/validate-build-json.js +256 -0
  471. package/package.json +11 -4
@@ -0,0 +1,2099 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { AnimationClip, BufferGeometry, Object3D, Ray } from 'three';
3
+ import type { ZodObject, z } from 'zod';
4
+ import type { MaterialSchema, MaterialTarget } from '../schema/material';
5
+ import type { MeasurementFeatureReference, MeasurementPoint } from '../schema/nodes/measurement';
6
+ import type { SceneMaterial, SceneMaterialId } from '../schema/scene-material';
7
+ import type { AnyNode, AnyNodeId } from '../schema/types';
8
+ import type { HandleList } from './handles';
9
+ import type { CloneNodesIntoOptions, Subtree } from './subtree';
10
+ export type GeometryContext = {
11
+ /** Look up any node by ID. Returns undefined if the node doesn't exist. */
12
+ resolve: <N = AnyNode>(id: AnyNodeId) => N | undefined;
13
+ /** Resolved children of this node (filters out unresolvable IDs). */
14
+ children: AnyNode[];
15
+ /** Same kind, same parent — drives wall mitering / endpoint-match. */
16
+ siblings: AnyNode[];
17
+ /** Resolved parent (null for root-level nodes). */
18
+ parent: AnyNode | null;
19
+ /**
20
+ * Pre-computed level-batch data, populated by the dispatcher when the
21
+ * kind declares `def.computeLevelData` (3D) or
22
+ * `def.computeFloorplanLevelData` (2D). Shared across every builder call
23
+ * in the same level batch within a single frame/render pass, so kinds
24
+ * whose geometry depends on cross-sibling data (wall mitering, gradient
25
+ * sky uniforms across a zone, etc.) don't pay an O(N²) recomputation cost.
26
+ *
27
+ * Typed as `unknown` at the framework boundary — kinds cast to their
28
+ * own `LevelData` shape inside `def.geometry` / `def.floorplan` (the
29
+ * same kind owns both the compute hook's return shape and the builder
30
+ * consumer, so the cast is internal).
31
+ */
32
+ levelData?: unknown;
33
+ /**
34
+ * The scene's shared material library (`useScene.materials`), passed so a
35
+ * pure geometry builder can resolve `scene:<id>` slot refs without importing
36
+ * `useScene`. Populated by `<GeometrySystem>` for every `def.geometry` call;
37
+ * undefined for `def.floorplan`. `library:<id>` refs resolve against the
38
+ * static catalog and need no store, so builders only consult this for
39
+ * `scene:` refs.
40
+ */
41
+ materials?: Record<SceneMaterialId, SceneMaterial>;
42
+ /**
43
+ * Optional view state — only populated for `def.floorplan` builders. The
44
+ * 2D floor-plan layer surfaces selection / hover here so kinds can vary
45
+ * their output (themed stroke when selected, endpoint dots when
46
+ * selected, hatch overlay, hover-side highlight). For `def.geometry`
47
+ * (3D) this is always undefined — the 3D selection outline is handled
48
+ * by the merged-outline post-process pass instead.
49
+ */
50
+ viewState?: {
51
+ selected: boolean;
52
+ unit: 'metric' | 'imperial';
53
+ /** Marquee or programmatic highlight — shows selected chrome without keyboard focus. */
54
+ highlighted: boolean;
55
+ /** Pointer-hovered. */
56
+ hovered: boolean;
57
+ /**
58
+ * True while this node is the target of an active 2D move (i.e.
59
+ * `useEditor.movingNode === node`). Used by kinds whose move
60
+ * preview includes extra chrome — e.g. door / window emit
61
+ * dimension lines showing the distance to adjacent openings or
62
+ * wall ends only during the move.
63
+ */
64
+ moving: boolean;
65
+ /**
66
+ * The kind's theme palette. Theme-aware colors (selection stroke,
67
+ * endpoint handle fill, hatch color) live here so kinds don't need
68
+ * to import `useViewer.theme` themselves.
69
+ */
70
+ palette: FloorplanPalette;
71
+ };
72
+ };
73
+ export type MeasurementSnapKind = 'endpoint' | 'midpoint' | 'edge' | 'center' | 'face' | 'ridge' | 'height';
74
+ export type MeasurementFeatureGeometry = {
75
+ kind: 'point';
76
+ point: MeasurementPoint;
77
+ } | {
78
+ kind: 'segment';
79
+ start: MeasurementPoint;
80
+ end: MeasurementPoint;
81
+ } | {
82
+ kind: 'path';
83
+ points: MeasurementPoint[];
84
+ closed?: boolean;
85
+ } | {
86
+ kind: 'polygon';
87
+ points: MeasurementPoint[];
88
+ };
89
+ export type MeasurementFeature = {
90
+ /** Stable within the node kind; presentation labels must not be used as IDs. */
91
+ id: string;
92
+ label: string;
93
+ snapKind: MeasurementSnapKind;
94
+ geometry: MeasurementFeatureGeometry;
95
+ /**
96
+ * Level-local surface normal for contact markers. Continuous features may
97
+ * provide the normal from `resolve(...)` after applying reference parameters.
98
+ */
99
+ normal?: MeasurementPoint;
100
+ /** Higher values win when multiple candidates occupy the same screen-space radius. */
101
+ priority?: number;
102
+ };
103
+ export type MeasurementFeatureBinding = {
104
+ featureId: string;
105
+ point: MeasurementPoint;
106
+ parameters?: Record<string, string | number | boolean>;
107
+ distance: number;
108
+ };
109
+ export type QuickMeasurementQuantity = 'length' | 'area' | 'volume';
110
+ export type QuickMeasurementMetric = {
111
+ key: string;
112
+ label: string;
113
+ abbreviation: string;
114
+ quantity: QuickMeasurementQuantity;
115
+ /** Canonical metres, square metres, or cubic metres according to `quantity`. */
116
+ value: number;
117
+ };
118
+ export type QuickMeasurementReport = {
119
+ title: string;
120
+ kindLabel: string;
121
+ /** Stable level-local label anchor chosen by the node kind. */
122
+ anchor: MeasurementPoint;
123
+ metrics: QuickMeasurementMetric[];
124
+ note?: string;
125
+ };
126
+ export type MeasurementContribution<N = AnyNode> = {
127
+ /** Enumerates semantic candidates for hover, quick measure, and snapping. */
128
+ features: (node: N, ctx: GeometryContext) => MeasurementFeature[];
129
+ /** Resolve IDs that cannot be fully enumerated by `features`. */
130
+ resolve?: (node: N, ctx: GeometryContext, reference: MeasurementFeatureReference) => MeasurementFeature | null;
131
+ /** Kind-aware nearest semantic binding for a level-local surface hit. */
132
+ match?: (node: N, ctx: GeometryContext, point: MeasurementPoint, maxDistance: number) => MeasurementFeatureBinding | null;
133
+ /** Live, non-persistent quantities shown by the smart measurement tool. */
134
+ quickMeasure?: (node: N, ctx: GeometryContext) => QuickMeasurementReport | null;
135
+ };
136
+ export type FloorplanPalette = {
137
+ selectedStroke: string;
138
+ selectedFill: string;
139
+ /** Hatch / cross-stroke color used for selected fills with patterns. */
140
+ selectedHatch: string;
141
+ /**
142
+ * Stroke colour applied to a wall (and fence by analogy) when the
143
+ * pointer hovers it. Light blue in the legacy palette — distinct from
144
+ * the orange endpoint-handle hover so the body and its handles can
145
+ * both glow independently. Pass through `viewState.palette.wall
146
+ * HoverStroke` in `def.floorplan` when `viewState.hovered === true`
147
+ * and the node isn't selected.
148
+ */
149
+ wallHoverStroke: string;
150
+ endpointHandleFill: string;
151
+ endpointHandleStroke: string;
152
+ endpointHandleHoverStroke: string;
153
+ endpointHandleActiveFill: string;
154
+ endpointHandleActiveStroke: string;
155
+ /**
156
+ * Curve sagitta handle slot — distinct teal colour-set the legacy
157
+ * `FloorplanWallCurveLayer` uses so users can tell endpoint dots
158
+ * (orange) and curve dots (teal) apart at a glance.
159
+ */
160
+ curveHandleFill: string;
161
+ curveHandleStroke: string;
162
+ curveHandleHoverStroke: string;
163
+ measurementStroke: string;
164
+ measurementLabelBackground: string;
165
+ measurementLabelText: string;
166
+ };
167
+ export type FloorplanPoint = readonly [x: number, y: number];
168
+ export type FloorplanStyle = {
169
+ stroke?: string;
170
+ fill?: string;
171
+ strokeWidth?: number;
172
+ strokeDasharray?: string;
173
+ opacity?: number;
174
+ /**
175
+ * When `'non-scaling-stroke'`, the SVG renderer interprets `strokeWidth`
176
+ * as a constant screen-pixel width regardless of viewport zoom. Maps
177
+ * straight to the SVG `vector-effect` attribute. Default (undefined)
178
+ * treats `strokeWidth` as plan-unit metres.
179
+ *
180
+ * Kinds that emit hand-drawn-looking strokes (fence body, wall hairlines,
181
+ * post markers) want non-scaling so the visual weight stays stable as
182
+ * the user zooms. Kinds whose stroke represents a real-world thickness
183
+ * (wall body in floor plan, slab outline) leave it undefined.
184
+ */
185
+ vectorEffect?: 'non-scaling-stroke';
186
+ strokeLinecap?: 'butt' | 'round' | 'square';
187
+ strokeLinejoin?: 'miter' | 'round' | 'bevel';
188
+ strokeOpacity?: number;
189
+ fillOpacity?: number;
190
+ /**
191
+ * SVG `pointer-events`. Default (undefined) lets the renderer pick its
192
+ * normal behaviour — `visiblePainted` for filled shapes, `stroke` for
193
+ * line / hit-line. Set `'none'` to make a primitive completely
194
+ * passthrough — useful for chrome that should be visible but never
195
+ * trigger selection or drag (e.g. a wall's body once it's already
196
+ * selected, where only the side-arrows / corner handles should grab
197
+ * the pointer).
198
+ */
199
+ pointerEvents?: 'none' | 'auto' | 'all' | 'stroke' | 'fill' | 'visible' | 'visiblePainted';
200
+ /**
201
+ * CSS `cursor` for the rendered primitive. Defaults to inheriting the
202
+ * registry entry wrapper's `cursor: 'pointer'`. Override to neutralise
203
+ * a hover affordance — e.g. a selected wall body that catches the
204
+ * pointer (to block fall-through to the slab below) but should not
205
+ * advertise itself as a drag target.
206
+ */
207
+ cursor?: string;
208
+ };
209
+ export type NodePort = {
210
+ /** Stable identifier within the node, e.g. 'start', 'end', 'branch'. */
211
+ id: string;
212
+ /** Level-local meters. */
213
+ position: readonly [number, number, number];
214
+ /** Unit vector pointing OUT of the port (away from the node body). */
215
+ direction: readonly [number, number, number];
216
+ /** Nominal connection diameter in inches. For a rect / oval port this is
217
+ * the area-equivalent round size, so a round run still mates sensibly. */
218
+ diameter: number;
219
+ /** Which distribution loop the port belongs to, e.g. 'supply' | 'return'. */
220
+ system?: string;
221
+ /** Cross-section of the connection. Omitted = round at `diameter`. A duct
222
+ * run joining a rect / oval port adopts this shape and rolls its
223
+ * cross-section to line up with the collar. */
224
+ shape?: 'round' | 'rect' | 'oval';
225
+ /** Rect / oval cross-section in inches: width is the collar's horizontal
226
+ * face at roll 0, height the vertical one. */
227
+ width?: number;
228
+ height?: number;
229
+ };
230
+ export type ToolHint = {
231
+ /** Key combo or input label, e.g. 'Left click', 'Shift', 'Esc'. */
232
+ key: string;
233
+ /** Description of what the input does. Sentence case. */
234
+ label: string;
235
+ /**
236
+ * Only show this hint once the in-progress draft has at least this many
237
+ * vertices (reads `useEditor.draftVertexCount`). Lets a polygon tool's
238
+ * "Finish" hint appear only when finishing is actually possible (≥ 3 points),
239
+ * so the HUD reflects reality. Omit for always-shown hints.
240
+ */
241
+ minDraftVertices?: number;
242
+ /**
243
+ * Render this hint as a live mode chip — like the snapping / continuation
244
+ * chips — instead of a static key row: the HUD shows the current value's
245
+ * label and clicking the row (or pressing `key`) cycles it. The kind owns
246
+ * the state (typically its own ephemeral store); `label` above becomes the
247
+ * fallback when the current value has no entry in `chip.labels`.
248
+ */
249
+ chip?: ToolHintChip;
250
+ };
251
+ export type ToolHintChip = {
252
+ /** Subscribe to live value changes (Zustand-store-like); returns unsubscribe. */
253
+ subscribe: (onChange: () => void) => () => void;
254
+ /** Current value token, resolved through `labels` / `icons` for display. */
255
+ value: () => string;
256
+ /** Advance to the next value — the chip's click action. The keyboard path is
257
+ * the tool's own handler for the hint's `key`; both must hit the same store. */
258
+ cycle: () => void;
259
+ /** value → chip row label, e.g. `{ cabinet: 'Type: Cabinet', island: 'Type: Island' }`. */
260
+ labels: Record<string, string>;
261
+ /** value → iconify icon name. */
262
+ icons?: Record<string, string>;
263
+ /** Hover tooltip, e.g. 'Placement type — click or press I to cycle'. */
264
+ tooltip?: string;
265
+ };
266
+ export type FloorplanGeometry = ({
267
+ kind: 'path';
268
+ d: string;
269
+ } & FloorplanStyle) | ({
270
+ kind: 'polygon';
271
+ points: readonly FloorplanPoint[];
272
+ } & FloorplanStyle) | ({
273
+ kind: 'polyline';
274
+ points: readonly FloorplanPoint[];
275
+ } & FloorplanStyle) | ({
276
+ kind: 'rect';
277
+ x: number;
278
+ y: number;
279
+ width: number;
280
+ height: number;
281
+ rx?: number;
282
+ ry?: number;
283
+ } & FloorplanStyle) | ({
284
+ kind: 'circle';
285
+ cx: number;
286
+ cy: number;
287
+ r: number;
288
+ } & FloorplanStyle) | ({
289
+ kind: 'line';
290
+ x1: number;
291
+ y1: number;
292
+ x2: number;
293
+ y2: number;
294
+ } & FloorplanStyle)
295
+ /**
296
+ * Plain SVG text in plan space. Used for short labels that need to
297
+ * sit at a specific plan coordinate — e.g. the elevator served-level
298
+ * chips' floor numbers. Rotates with the floor plan's transform
299
+ * (same as polygon coordinates) so it shares the building's
300
+ * orientation. For text that needs to stay screen-upright regardless
301
+ * of plan rotation, use `dimension-label` instead (it auto-flips
302
+ * upside-down labels).
303
+ *
304
+ * `fontSize` is in plan metres — typical values are 0.1–0.2m. The
305
+ * registry layer doesn't apply any text-rendering chrome (no plate,
306
+ * no rotation auto-flip) — it's just a styled `<text>` element.
307
+ */
308
+ | {
309
+ kind: 'text';
310
+ x: number;
311
+ y: number;
312
+ text: string;
313
+ fontSize: number;
314
+ fill?: string;
315
+ fontWeight?: number | string;
316
+ fontFamily?: string;
317
+ textAnchor?: 'start' | 'middle' | 'end';
318
+ dominantBaseline?: 'auto' | 'middle' | 'central' | 'hanging' | 'alphabetic';
319
+ opacity?: number;
320
+ /**
321
+ * Outlined-text styling — when `stroke` is set the renderer applies
322
+ * `stroke` / `strokeWidth` plus `paintOrder='stroke'` so the stroke
323
+ * is drawn under the fill. Used by zone name labels for the
324
+ * "white text inside a colored outline" look that stays legible
325
+ * against any fill color.
326
+ */
327
+ stroke?: string;
328
+ strokeWidth?: number;
329
+ paintOrder?: 'stroke' | 'fill' | 'normal';
330
+ /**
331
+ * When true, the registry layer counter-rotates the label by
332
+ * `sceneRotationDeg` so it reads horizontally on screen regardless
333
+ * of the floor-plan's scene rotation (default 90°).
334
+ */
335
+ upright?: boolean;
336
+ }
337
+ /**
338
+ * Bitmap overlay — captured top-down asset thumbnail, AI-generated
339
+ * floor-plan symbol, scan slice, etc. `url` is passed through the
340
+ * editor's `loadAssetUrl` resolver (handles CDN / Supabase storage),
341
+ * so kinds emit the raw `asset.floorPlanUrl` and don't worry about
342
+ * fetching.
343
+ *
344
+ * `rotation` is in radians around `center`. The image is drawn at
345
+ * `center` with size `width × height` in plan-local metres;
346
+ * `preserveAspectRatio` controls letterboxing (default
347
+ * `'xMidYMid meet'`).
348
+ */
349
+ | {
350
+ kind: 'image';
351
+ url: string;
352
+ center: FloorplanPoint;
353
+ width: number;
354
+ height: number;
355
+ rotation?: number;
356
+ preserveAspectRatio?: string;
357
+ opacity?: number;
358
+ } | {
359
+ kind: 'group';
360
+ children: FloorplanGeometry[];
361
+ /** Optional transform applied to all children. Rotation in radians. */
362
+ transform?: {
363
+ translate?: FloorplanPoint;
364
+ rotate?: number;
365
+ };
366
+ }
367
+ /**
368
+ * Hatched fill overlay — same polygon shape as the kind's main fill but
369
+ * stroked with diagonal lines on top. Used for the selected-wall hatch
370
+ * effect from the legacy floor-plan panel. The 2D layer mounts a
371
+ * shared `<pattern>` in `<defs>` and references it via `fill=url(...)`.
372
+ */
373
+ | {
374
+ kind: 'hatch';
375
+ points: readonly FloorplanPoint[];
376
+ color: string;
377
+ opacity?: number;
378
+ }
379
+ /**
380
+ * Transparent click-detection segment. Sits on top of the kind's main
381
+ * geometry with a wide stroke so the user doesn't need to pixel-hunt
382
+ * the polygon. `select` is the only affordance for now — clicking
383
+ * triggers selection of the owning node.
384
+ */
385
+ | {
386
+ kind: 'hit-line';
387
+ x1: number;
388
+ y1: number;
389
+ x2: number;
390
+ y2: number;
391
+ /** Stroke width in screen pixels — converted to plan units by the dispatcher. */
392
+ strokeWidthPx: number;
393
+ cursor?: string;
394
+ /**
395
+ * Override the default `pointer-events="stroke"`. Use `'none'` when
396
+ * a kind wants to keep the line painted (for hit-debugging or layout
397
+ * stability) but route grabs through other affordances instead.
398
+ */
399
+ pointerEvents?: 'none' | 'stroke' | 'auto';
400
+ }
401
+ /**
402
+ * Endpoint manipulation handle — the 5-circle stack from the legacy
403
+ * floor-plan: outer hover glow ring + hover ring + filled outer +
404
+ * inner dot + transparent hit. Rendered with theme-aware colors from
405
+ * `viewState.palette`. `affordance` keys into a kind-owned drag flow
406
+ * the dispatcher invokes; `payload` is opaque kind data the
407
+ * affordance handler unpacks.
408
+ */
409
+ | {
410
+ kind: 'endpoint-handle';
411
+ point: FloorplanPoint;
412
+ /** `active` = currently being dragged; `idle` = visible but inert. */
413
+ state: 'idle' | 'active';
414
+ /**
415
+ * Visual colour-set. `'endpoint'` (default) → orange — wall /
416
+ * fence endpoints, polygon vertices. `'curve'` → teal — the
417
+ * sagitta midpoint handle. Other values are reserved for future
418
+ * affordances (rotation, scale) without expanding the union.
419
+ */
420
+ variant?: 'endpoint' | 'curve';
421
+ affordance: string;
422
+ payload: unknown;
423
+ }
424
+ /**
425
+ * Smaller "insert here" handle drawn between two polygon vertices.
426
+ * Visually a small white dot with a `+` icon; hover-expanded. Triggers
427
+ * an affordance that typically inserts a new vertex at the midpoint
428
+ * and then drags it (matches the legacy slab / ceiling boundary
429
+ * editor's edge-midpoint behaviour).
430
+ */
431
+ | {
432
+ kind: 'midpoint-handle';
433
+ point: FloorplanPoint;
434
+ affordance: string;
435
+ payload: unknown;
436
+ }
437
+ /**
438
+ * Hit-target along an entire polygon edge. Renders as a transparent
439
+ * wide stroke for click detection; the dispatcher overlays a glow +
440
+ * solid stroke when hovered or actively being dragged. Used by the
441
+ * slab / ceiling boundary editor's "drag whole edge perpendicular"
442
+ * affordance — both endpoints translate together along the edge
443
+ * normal.
444
+ */
445
+ | {
446
+ kind: 'edge-handle';
447
+ x1: number;
448
+ y1: number;
449
+ x2: number;
450
+ y2: number;
451
+ affordance: string;
452
+ payload: unknown;
453
+ }
454
+ /**
455
+ * "Grab to move" handle drawn at a node's centroid — the orange dot
456
+ * users click-and-drag to move a door / window / item in the
457
+ * floorplan without going through the inspector's Move button.
458
+ *
459
+ * Pointer-down on the handle sets `useEditor.movingNode` to the
460
+ * owning node, which `FloorplanRegistryMoveOverlay` picks up and
461
+ * routes through the kind's `def.floorplanMoveTarget`. So both
462
+ * entry points (Move button + dot grab) share the same move
463
+ * pipeline — no parallel kind-side logic.
464
+ */
465
+ | {
466
+ kind: 'move-handle';
467
+ point: FloorplanPoint;
468
+ }
469
+ /**
470
+ * Directional move handle drawn as an arrow pointing AWAY from the
471
+ * owning node, rotated by `angle` (radians; 0 = +x). Used by wall to
472
+ * place two arrows on perpendicular sides at the wall midpoint —
473
+ * mirrors the 3D `WallMoveSideHandles`. Routes through the same
474
+ * `onMoveHandlePointerDown` → `setMovingNode` path as `move-handle`.
475
+ */
476
+ | {
477
+ kind: 'move-arrow';
478
+ point: FloorplanPoint;
479
+ /** Rotation in radians; 0 points along +x in plan coords. */
480
+ angle: number;
481
+ /**
482
+ * Optional affordance routing. When set, pointer-down on the arrow
483
+ * starts a `def.floorplanAffordances?.[affordance]` session with the
484
+ * given `payload` (same dispatch path as `edge-handle`) instead of
485
+ * the default `setMovingNode` flow. Used by doors for the in-plane
486
+ * width-resize handles that visually mirror the move arrow shape but
487
+ * drive a different mutation.
488
+ */
489
+ affordance?: string;
490
+ payload?: unknown;
491
+ }
492
+ /**
493
+ * Curved two-headed rotation arrow — the 2D counterpart of the 3D
494
+ * `arc-resize` handle's `shape: 'rotate'` gizmo. Visually a short arc
495
+ * with arrowheads at each end pointing tangentially in opposite
496
+ * directions, so it reads as "rotate either way" rather than "drag
497
+ * along a line." Always routes through an affordance (rotation has no
498
+ * sensible default Move semantics).
499
+ *
500
+ * `angle` is the radial-outward direction in plan coords — the icon's
501
+ * local +X axis points away from the pivot, with the arc curving
502
+ * around it. Emitters typically compute this as
503
+ * `atan2(handle.y − pivot.y, handle.x − pivot.x)`.
504
+ */
505
+ | {
506
+ kind: 'rotate-arrow';
507
+ point: FloorplanPoint;
508
+ /** Radial-outward direction from the rotation pivot, in radians. */
509
+ angle: number;
510
+ affordance: string;
511
+ payload?: unknown;
512
+ /**
513
+ * Rotation pivot (plan coords) this handle turns the node around.
514
+ * When present, the floor-plan layer draws a live angle wedge + degree
515
+ * readout swept from grab to the current pointer bearing during the
516
+ * drag — the 2D twin of the 3D rotate gizmo's readout. Emitters that
517
+ * already compute the pivot to place the handle should pass it through.
518
+ */
519
+ pivot?: FloorplanPoint;
520
+ }
521
+ /**
522
+ * Centered length / distance label. Renders as a small rounded
523
+ * background plate by default, or as outlined text when `appearance`
524
+ * is `'outlined'`, oriented along `angle` (radians). The
525
+ * 2D layer flips the label upright when it would otherwise be upside
526
+ * down. Use this for simple "what length am I?" badges (fence, item
527
+ * width, draft preview).
528
+ */
529
+ | {
530
+ kind: 'dimension-label';
531
+ cx: number;
532
+ cy: number;
533
+ text: string;
534
+ /** Rotation in radians. The renderer auto-flips to keep text upright. */
535
+ angle: number;
536
+ /** Keep the plate horizontal on screen instead of following a segment. */
537
+ screenUpright?: boolean;
538
+ /** Perpendicular screen-pixel offset from the anchor segment. */
539
+ offsetPx?: number;
540
+ /** Match map-style labels without changing the default editing badge. */
541
+ appearance?: 'plate' | 'outlined';
542
+ }
543
+ /**
544
+ * Equal-spacing badge — a small accent pill marking one gap in a run of
545
+ * (near-)equally-spaced openings (the 2D counterpart of Figma's "=" distance
546
+ * chips). Emitted once per equal gap so the repeated value reads as a rhythm.
547
+ * `text` is the shared gap distance; `angle` orients the pill along the wall
548
+ * (the renderer auto-flips it upright).
549
+ */
550
+ | {
551
+ kind: 'equal-spacing-badge';
552
+ point: FloorplanPoint;
553
+ text: string;
554
+ /** Rotation in radians. */
555
+ angle: number;
556
+ }
557
+ /**
558
+ * Architect's dimension overlay — extension lines from the edge
559
+ * endpoints out past the dimension line, two dimension line halves
560
+ * with the label sitting in the gap, end ticks perpendicular to the
561
+ * line. Used for the selected wall's full measurement; the rounded
562
+ * plate label is the wrong shape when you want plan-drawing chrome.
563
+ *
564
+ * The renderer computes the segment geometry from these inputs so the
565
+ * kind only needs to know "where is the edge and which way does the
566
+ * dimension line offset." `offsetNormal` is a unit vector
567
+ * perpendicular to the edge; pass the *outward* normal so the line
568
+ * sits on the side facing away from the wall interior.
569
+ */
570
+ | {
571
+ kind: 'dimension';
572
+ start: FloorplanPoint;
573
+ end: FloorplanPoint;
574
+ /** Outward-pointing unit normal — the dimension line offsets along this. */
575
+ offsetNormal: FloorplanPoint;
576
+ /** Distance (plan units) from the edge to the dimension line. */
577
+ offsetDistance: number;
578
+ /** How far past the offset point the extension line continues. */
579
+ extensionOvershoot: number;
580
+ text: string;
581
+ /** Optional override for the line/text colour. Defaults to the palette accent. */
582
+ stroke?: string;
583
+ };
584
+ export type FloorplanAffordancePoint = readonly [x: number, y: number];
585
+ export type FloorplanAffordanceModifiers = {
586
+ shiftKey: boolean;
587
+ altKey: boolean;
588
+ ctrlKey: boolean;
589
+ metaKey: boolean;
590
+ };
591
+ export type FloorplanAffordanceSession = {
592
+ /** Node IDs the drag may mutate. Used by the dispatcher for the snapshot. */
593
+ affectedIds: AnyNodeId[];
594
+ /**
595
+ * Run a single drag tick. New implementations publish per-frame overrides to
596
+ * `useLiveNodeOverrides` / `useLiveTransforms` (or another preview store);
597
+ * `useScene` stays untouched during the drag. Legacy sessions that still
598
+ * write preview state into `useScene` are supported only by the dispatcher's
599
+ * snapshot-diff compatibility path.
600
+ *
601
+ * Snap logic, linked-node cascade, and angle locking live here.
602
+ */
603
+ apply(args: {
604
+ planPoint: FloorplanAffordancePoint;
605
+ modifiers: FloorplanAffordanceModifiers;
606
+ }): void;
607
+ /**
608
+ * Called on pointer-up. Return `true` if the drag should commit;
609
+ * `false` reverts to the snapshot (e.g. wall too short, vertex
610
+ * collapsed onto neighbour).
611
+ */
612
+ canCommit(): boolean;
613
+ /**
614
+ * Optional atomic commit hook — mirror of the same field on
615
+ * `FloorplanMoveTargetSession`. New live-preview sessions should provide
616
+ * this so the dispatcher can revert to the pre-drag baseline, resume
617
+ * history, then call `commit()`. The session owns the full final write
618
+ * (typically `applyNodeChanges` or `updateNodes`) plus clearing any live
619
+ * overrides it published in `apply()`.
620
+ */
621
+ commit?(): void;
622
+ };
623
+ export type FloorplanAffordance<N> = {
624
+ start(args: {
625
+ node: N;
626
+ /** Opaque kind-specific payload from the handle primitive. */
627
+ payload: unknown;
628
+ /** Current scene snapshot at drag start. */
629
+ nodes: Record<AnyNodeId, AnyNode>;
630
+ /** Initial pointer position in plan coordinates. */
631
+ initialPlanPoint: FloorplanAffordancePoint;
632
+ /** Active editor grid step in meters. */
633
+ gridSnapStep: number;
634
+ }): FloorplanAffordanceSession;
635
+ };
636
+ export type FloorplanMoveTargetSession = {
637
+ /** Node IDs the move may mutate. Used by the dispatcher for snapshot capture. */
638
+ affectedIds: AnyNodeId[];
639
+ /**
640
+ * Single move-preview tick. Implementations publish per-frame overrides to
641
+ * `useLiveNodeOverrides` / `useLiveTransforms`; the scene store stays
642
+ * untouched during the drag. Provide `commit()` below so the final scene
643
+ * write happens once at the end.
644
+ */
645
+ apply(args: {
646
+ planPoint: FloorplanAffordancePoint;
647
+ modifiers: FloorplanAffordanceModifiers;
648
+ }): void;
649
+ /**
650
+ * Called on pointer-up. Return `true` to commit the current scene
651
+ * state; `false` reverts to the snapshot (e.g. dropped in invalid
652
+ * area, overlap detected, ...).
653
+ */
654
+ canCommit(): boolean;
655
+ /**
656
+ * Optional atomic-commit hook. The default overlay path snapshots
657
+ * each affected node before drag and writes a diff back on commit —
658
+ * fine for kinds whose commit is a pure position update, but
659
+ * insufficient when commit needs to also create or delete nodes
660
+ * (e.g. wall move emits bridge wall creates + collapsed wall deletes
661
+ * via `planWallMoveJunctions`).
662
+ *
663
+ * When present, the overlay reverts to the pre-drag baseline,
664
+ * resumes history, and calls `commit()` instead of the default
665
+ * `updateNodes(finalUpdates)`. The session is responsible for the
666
+ * full final write (typically `applyNodeChanges`) plus any
667
+ * post-commit selection / metadata. The overlay still emits the
668
+ * standard place SFX and clears `movingNode` after `commit()`
669
+ * returns.
670
+ */
671
+ commit?(): void;
672
+ /**
673
+ * Optional R-key flip toggle. Kinds with a directional facing
674
+ * (door / window: front ↔ back) implement this so the overlay can flip
675
+ * the orientation mid-placement before commit. Toggling just records the
676
+ * intent; the visible change lands when the overlay re-runs `apply()` with
677
+ * the last pointer position. Kinds with no facing leave it unset.
678
+ */
679
+ flipSide?(): void;
680
+ };
681
+ export type FloorplanMoveTarget<N> = (args: {
682
+ node: N;
683
+ nodes: Record<AnyNodeId, AnyNode>;
684
+ }) => FloorplanMoveTargetSession;
685
+ export type Plugin = {
686
+ id: string;
687
+ apiVersion: 1;
688
+ nodes?: AnyNodeDefinition[];
689
+ };
690
+ export type AnyNodeDefinition = NodeDefinition<ZodObject<any>>;
691
+ export type SurfaceRole = 'wall' | 'floor' | 'ceiling' | 'roof' | 'joinery' | 'glazing' | 'furnishing';
692
+ /** Role a kind plays in a duct / pipe / lineset distribution system. */
693
+ export type DistributionRole = 'run' | 'fitting' | 'terminal' | 'equipment';
694
+ /**
695
+ * A kind's snapping profile (see `NodeDefinition.snapProfile`).
696
+ * - `'item'` free object (furniture/fixtures): lines-default, no grid lattice, no angle.
697
+ * - `'structural'` walls / fences / slabs / ceilings / roofs / zones: grid-default, and an
698
+ * angle lock while *setting direction* (drafting a run/polygon, dragging an endpoint or a
699
+ * polygon vertex). A plain translate or a curve of a structural node has no angle.
700
+ */
701
+ export type SnapProfile = 'item' | 'structural';
702
+ /**
703
+ * How a kind is treated by the GLB bake and the baked `/viewer`. See
704
+ * plans/editor-plugin-trees-example.md → Part D.
705
+ * - `'static'` (default) — baked as geometry; the viewer shows the baked mesh.
706
+ * - `'strip'` — excluded from the bake; the viewer rebuilds it live from
707
+ * `scene_graph` via the registry renderer (heavy reference assets: scans, guides).
708
+ * - `'replace'` — baked as *static* geometry (a plain glTF viewer still shows it),
709
+ * but our viewer removes the baked meshes for this kind and re-renders the node
710
+ * live from `scene_graph` — for dynamic content whose runtime look differs from a
711
+ * frozen snapshot (shader wind, interactivity), rendered through its own path.
712
+ */
713
+ export type BakePolicy = 'static' | 'strip' | 'replace';
714
+ export type ExportAnimationContext<N = AnyNode> = {
715
+ node: N;
716
+ object: Object3D;
717
+ };
718
+ export type NodeDefinition<S extends ZodObject<any>> = {
719
+ kind: string;
720
+ schemaVersion: number;
721
+ schema: S;
722
+ category: NodeCategory;
723
+ surfaceRole?: SurfaceRole;
724
+ /**
725
+ * Show a floor direction-triangle while placing/moving — the kind has a
726
+ * meaningful front. `true` points along the node's local +Z (forward).
727
+ * `{ reversed: true }` points along local -Z, for kinds whose front is the
728
+ * -Z side (a stair faces *out* of its run: you approach from the low end,
729
+ * which sits on the -Z side of the footprint).
730
+ */
731
+ facingIndicator?: boolean | {
732
+ reversed?: boolean;
733
+ };
734
+ /**
735
+ * Role this kind plays in a distribution system (HVAC duct / DWV pipe /
736
+ * refrigerant lineset). Lets the system-graph summary classify a
737
+ * component without branching on `node.type`:
738
+ * - `'run'` — a duct / pipe / lineset segment (carries `path`).
739
+ * - `'fitting'` — an inline fitting (elbow / tee / reducer / trap).
740
+ * - `'terminal'` — a grille / register / diffuser endpoint.
741
+ * - `'equipment'` — a furnace / air handler / condenser source.
742
+ * Kinds outside any distribution system leave this unset.
743
+ */
744
+ distributionRole?: DistributionRole;
745
+ /**
746
+ * When `distributionRole` is `'fitting'`, controls whether this fitting
747
+ * is dragged as a rigid follower when a connected run endpoint moves.
748
+ *
749
+ * - `true` (default for `distributionRole === 'fitting'`): the fitting
750
+ * translates rigidly so its mated collar stays on the moved port — the
751
+ * right behaviour for in-line fittings (elbows, tees, wyes, crosses).
752
+ * - `false`: the fitting is anchored in space; moving a connected run
753
+ * endpoint stretches the run arm, not the fitting. Use this for
754
+ * fixed-position fixtures like `pipe-trap`.
755
+ *
756
+ * Has no effect when `distributionRole` is not `'fitting'`.
757
+ */
758
+ portConnectivityFollow?: boolean;
759
+ defaults: () => Omit<z.infer<S>, 'id' | 'type'>;
760
+ migrate?: Record<number, (old: unknown) => unknown>;
761
+ capabilities: Capabilities;
762
+ relations?: Relations;
763
+ parametrics?: ParametricDescriptor<z.infer<S>>;
764
+ /**
765
+ * Whether scene mutations add this kind to `dirtyNodes` (the per-frame
766
+ * rebuild queue). Default true. Set `false` for structural/organizational
767
+ * kinds (site, building, level, zone, guide) that no dirty consumer ever
768
+ * rebuilds — no `def.geometry`, no legacy viewer system, no
769
+ * `capabilities.floorPlaced`. Their marks are never cleared, so they
770
+ * accumulate for the whole session, defeat every consumer's empty-set
771
+ * early exit each frame, and pollute the perf overlay's DIRTY readout.
772
+ * If a kind later gains a dirty consumer, delete the flag.
773
+ */
774
+ dirtyTracking?: boolean;
775
+ /** GLB bake treatment for this kind (default `'static'`). See {@link BakePolicy}. */
776
+ bake?: BakePolicy;
777
+ /**
778
+ * Renderer for this kind. Optional under the three-checkbox composition
779
+ * model (see `wiki/architecture/node-definitions.md`): when omitted, the
780
+ * framework mounts a generic empty-group renderer that the per-kind
781
+ * geometry/system fills. Required today only because the generic
782
+ * renderer is not yet implemented — Phase 4 lands it, then this field
783
+ * becomes truly optional at runtime too. Making the type optional now so
784
+ * milestone-A skeletons (like wall) can compile before their runtime
785
+ * port; downstream consumers (`<NodeRenderer>`, `RegisteredSystems`)
786
+ * already null-guard on `def.renderer` so omitting it is safe.
787
+ */
788
+ renderer?: RendererSource<z.infer<S>>;
789
+ /**
790
+ * Collective renderer the baked `/viewer` uses to re-render this kind live when
791
+ * `bake === 'replace'`. It receives every node of this kind under one baked
792
+ * level and is portaled into that level's `Object3D`, so an instanced kind can
793
+ * draw them as instanced meshes in level-local space (riding level stacking for
794
+ * free) instead of the frozen baked meshes (which the viewer hides). Needed when
795
+ * the normal per-node `renderer` can't stand alone in a baked scene (e.g. an
796
+ * instanced kind whose `renderer` is an invisible selection proxy and whose real
797
+ * geometry comes from a `system`). See plans/editor-plugin-trees-example.md → Part D.
798
+ */
799
+ bakeReplaceRenderer?: BakeReplaceRenderer<z.infer<S>>;
800
+ /**
801
+ * Pure geometry builder. When set, the framework's generic
802
+ * `<GeometrySystem>` calls this on every dirty mark — `nodes` keyed by
803
+ * `def.geometry`'s presence are picked up; the returned `Object3D`'s
804
+ * children replace the registered group's children. Together with
805
+ * `<ParametricNodeRenderer>` this lets a kind ship without per-kind
806
+ * `renderer.tsx` or `system.tsx` files (see
807
+ * `wiki/architecture/node-definitions.md`). Combine with `renderer` if
808
+ * you want JSX-side composition (drei, `<Html>`, GLB) AND parametric
809
+ * rebuilds; combine with `system` if you also need per-frame imperative
810
+ * work (animations, named-mesh material poking).
811
+ */
812
+ geometry?: (node: z.infer<S>, ctx: GeometryContext) => Object3D;
813
+ /**
814
+ * Optional GLB export animation hook for kind-owned moving parts. The
815
+ * exporter calls this against the cloned export subtree after material/mesh
816
+ * cleanup; implementations should leave `object` in its intended rest pose
817
+ * and return engine-agnostic Three.js clips that target objects inside that
818
+ * subtree.
819
+ */
820
+ exportAnimation?: (ctx: ExportAnimationContext<z.infer<S>>) => AnimationClip | AnimationClip[] | null | undefined;
821
+ /**
822
+ * Optional cache key over the geometry-relevant inputs of `node`. When
823
+ * set, `<GeometrySystem>` skips the rebuild (dispose + re-create the
824
+ * group's children) if the key is unchanged since the last build for
825
+ * this node — even though the node was marked dirty. Use for kinds whose
826
+ * geometry depends *only* on their own fields (not on `children`,
827
+ * `position`, neighbours, or `ctx`): a hosted child reparenting onto a
828
+ * shelf, say, dirties the shelf but doesn't change its boards, so without
829
+ * this the boards needlessly remount and any pointer hover churns
830
+ * (enter/leave) as the meshes are swapped. Must NOT be set for kinds with
831
+ * neighbour-dependent geometry (e.g. wall/fence miters via `ctx`), whose
832
+ * inputs aren't captured by the node alone.
833
+ */
834
+ geometryKey?: (node: z.infer<S>) => string;
835
+ /**
836
+ * Level-batch precompute hook. Called by `<GeometrySystem>` once per
837
+ * level per frame, **before** the per-node `def.geometry` calls in
838
+ * that batch. The result lands in `ctx.levelData` for every node in
839
+ * the same level.
840
+ *
841
+ * Used by kinds whose geometry depends on cross-sibling data that
842
+ * would be O(N²) to recompute per node:
843
+ * - wall: `calculateLevelMiters(walls)` — every wall's mesh
844
+ * reads its junctions from the level-wide miter graph.
845
+ * - zone (planned): shared TSL gradient uniforms.
846
+ *
847
+ * `siblings` is every node of this kind in the same level (including
848
+ * the dirty ones). The dispatcher de-duplicates per level so this
849
+ * runs once even when many walls are dirty in the same frame.
850
+ */
851
+ computeLevelData?: (siblings: ReadonlyArray<z.infer<S>>) => unknown;
852
+ /**
853
+ * Floor-plan level-batch precompute hook. The floor-plan layer calls this
854
+ * once per level per render pass, de-duplicated by kind, before the
855
+ * per-node `def.floorplan` calls. The result lands in `ctx.levelData` for
856
+ * every node of this kind in the level.
857
+ *
858
+ * Used to hoist cross-sibling floor-plan work that would otherwise be
859
+ * O(N²) when rebuilding every node in a kind — e.g. wall mitering. `nodes`
860
+ * is the live-merged scene snapshot; `siblings` is every node of this kind
861
+ * in the level, also live-merged.
862
+ */
863
+ computeFloorplanLevelData?: (args: {
864
+ siblings: ReadonlyArray<z.infer<S>>;
865
+ nodes: Record<string, AnyNode>;
866
+ }) => unknown;
867
+ /**
868
+ * Pure 2D builder for floor-plan rendering. Mirrors `geometry` but emits
869
+ * plain `FloorplanGeometry` data (SVG-renderable) rather than three.js
870
+ * Object3D. Coordinates are level-local meters — the floor-plan panel
871
+ * applies the world→SVG transform.
872
+ *
873
+ * Returns `null` when the kind shouldn't appear in floor plan (e.g. an
874
+ * invisible utility node, or a kind that's 3D-only). Kinds that need
875
+ * floor-plan rendering but no 3D mesh set `floorplan` without `geometry`.
876
+ *
877
+ * See `wiki/architecture/node-definitions.md` ("floor-plan rendering"
878
+ * section) and Phase 5 of the registry plan for the migration plan off
879
+ * the legacy `floorplan-panel.tsx` monolith.
880
+ */
881
+ floorplan?: (node: z.infer<S>, ctx: GeometryContext) => FloorplanGeometry | null;
882
+ /** Extra node IDs whose committed changes invalidate this node's floor-plan cache. */
883
+ floorplanDependencies?: (node: z.infer<S>) => readonly AnyNodeId[];
884
+ /** Stable semantic geometry that associative measurement anchors may reference. */
885
+ measurement?: MeasurementContribution<z.infer<S>>;
886
+ /**
887
+ * Which scope the floor-plan layer walks to find instances of this
888
+ * kind. Default `'level'` — the layer's DFS from the active level id
889
+ * picks the node up via its parent chain. `'building'` — the kind
890
+ * lives as a sibling of levels (elevator is the canonical example:
891
+ * elevators are parented to the *building*, not a level, but the
892
+ * floor-plan should still surface them for every level inside that
893
+ * building). For `'building'`-scoped kinds the layer iterates every
894
+ * instance whose parent matches the active level's building, and
895
+ * synthesises a `GeometryContext` whose `parent` is the active level.
896
+ */
897
+ floorplanScope?: 'level' | 'building';
898
+ /**
899
+ * 2D drag affordances keyed by the string identifier emitted on
900
+ * `endpoint-handle` (and similar interactive floor-plan primitives) via
901
+ * the `affordance` field. The floor-plan registry layer calls
902
+ * `def.floorplanAffordances?.[affordance].start({...})` on pointer-down,
903
+ * receives a session, calls `apply(...)` on pointer-move and
904
+ * `commit()` / `cancel()` on pointer-up / pointer-cancel. The session
905
+ * previews through live override/transform stores during `apply`. Legacy
906
+ * sessions that still write preview state into `useScene` are handled by
907
+ * the dispatcher's snapshot + single-undo compatibility path.
908
+ *
909
+ * Mirrors the existing 3D `affordanceTools` map but for 2D SVG events,
910
+ * and operates on plain JS data instead of mounting React. Kinds with
911
+ * both 3D and 2D affordances expose both fields — they're independent.
912
+ */
913
+ floorplanAffordances?: Record<string, FloorplanAffordance<z.infer<S>>>;
914
+ /**
915
+ * Kind-specific 2D move handler for `useEditor.movingNode`-driven
916
+ * placement in the floor plan. When set, `FloorplanRegistryMove
917
+ * Overlay` invokes this once when `movingNode` becomes a node of
918
+ * this kind, and drives the session through pointer events until
919
+ * pointer-up / Esc. Falls back to the generic free-floating
920
+ * translate when unset.
921
+ *
922
+ * Use this for kinds whose move semantics are anchor-aware:
923
+ * doors / windows need wall hits + reparenting; items with
924
+ * `attachTo` need parent-surface hits. Kinds with simple
925
+ * translate-on-XZ semantics (shelf, spawn, fence) leave this
926
+ * unset and rely on the generic overlay path.
927
+ */
928
+ floorplanMoveTarget?: FloorplanMoveTarget<z.infer<S>>;
929
+ /**
930
+ * Extra floating-menu actions contributed by this kind. The editor renders
931
+ * the returned descriptors generically; kind-specific mutation stays here
932
+ * and runs through `SceneApi`.
933
+ */
934
+ quickActions?: NodeQuickActionProvider<z.infer<S>>;
935
+ /**
936
+ * Sidebar-tree presentation hooks. Lets a kind reshape how the generic
937
+ * scene tree walks its subtree — hiding derived/managed nodes and
938
+ * flattening intermediate containers — without the tree hardcoding any
939
+ * kind. The tree consults these for every node whose kind declares them;
940
+ * kinds whose scene-graph shape matches their desired tree shape omit
941
+ * this entirely.
942
+ */
943
+ tree?: {
944
+ /**
945
+ * Hide this node's row in the sidebar tree (e.g. derived/managed nodes
946
+ * whose contents surface elsewhere via `childIds`).
947
+ */
948
+ hidden?: (node: AnyNode, nodes: Readonly<Partial<Record<AnyNodeId, AnyNode>>>) => boolean;
949
+ /**
950
+ * Optional tree-row label override. When unset the host falls back to
951
+ * `node.name` / `def.presentation.label`.
952
+ */
953
+ label?: (node: AnyNode, nodes: Readonly<Partial<Record<AnyNodeId, AnyNode>>>) => string;
954
+ /**
955
+ * Override the child ids the sidebar tree renders under this node.
956
+ * When unset the tree falls back to the node's own `children`.
957
+ */
958
+ childIds?: (node: AnyNode, nodes: Readonly<Partial<Record<AnyNodeId, AnyNode>>>) => AnyNodeId[];
959
+ };
960
+ /**
961
+ * Selection-proxy behavior overrides. A node opts into proxying by writing
962
+ * `metadata.nodeSelectionProxyId` (see `lib/selection-proxy.ts` for the
963
+ * metadata contract); grouped affordances (move / rotate) then key off the
964
+ * proxy target. `bypassDirectPick` lets a kind keep the proxy for those
965
+ * grouped affordances while still routing a direct canvas pick to the
966
+ * clicked node itself — e.g. corner-generated cabinet modules stay
967
+ * individually selectable even though they proxy to their run.
968
+ */
969
+ selectionProxy?: {
970
+ /** Return true when a direct pick of `node` should select it instead of
971
+ * its resolved proxy target. */
972
+ bypassDirectPick?: (node: AnyNode, proxyTarget: AnyNode) => boolean;
973
+ };
974
+ /**
975
+ * Geometry reads sibling/parent/child nodes (e.g. wall miters, opening
976
+ * dimensions); the floor-plan layer must rebuild it whenever a
977
+ * sibling-affecting node is being dragged live.
978
+ */
979
+ floorplanDependsOnSiblings?: boolean;
980
+ /**
981
+ * Optional hook for kinds whose floor-plan cache invalidation reaches beyond
982
+ * the default framework relationships (wall junction neighbours, host wall
983
+ * opening cuts, gutter siblings under one roof). Called when a node of this
984
+ * kind has a live drag/override in flight; returns the extra entry ids that
985
+ * must rebuild this frame.
986
+ */
987
+ floorplanAffectedIds?: (args: {
988
+ nodeId: AnyNodeId;
989
+ node: AnyNode;
990
+ nodes: Record<AnyNodeId, AnyNode>;
991
+ liveTransforms: Map<string, LiveTransformLike>;
992
+ liveOverrides: Map<string, Record<string, unknown>>;
993
+ }) => readonly AnyNodeId[];
994
+ /**
995
+ * Optional hook letting a kind project the `useLiveNodeOverrides` map
996
+ * into a fresh `nodes` snapshot before its `def.floorplan` builder
997
+ * runs. The floor-plan layer calls this when present and passes the
998
+ * returned map both as the builder's `ctx` source AND as the
999
+ * effective node (so the kind's own override lands in `effectiveNode`).
1000
+ *
1001
+ * Used by wall, whose miter joins read sibling walls via
1002
+ * `ctx.siblings`: during a 2D drag the moved wall + its linked
1003
+ * neighbours publish per-frame `{ start, end, curveOffset }`
1004
+ * overrides, and the floor-plan must merge those into every wall
1005
+ * the builder can see — otherwise miter math snaps back to the
1006
+ * committed positions while the cursor moves. Kinds whose previews
1007
+ * are self-contained leave this unset and the layer hands the raw
1008
+ * `nodes` through.
1009
+ *
1010
+ * Return the input `nodes` unchanged when no override is relevant
1011
+ * so the caller can short-circuit.
1012
+ */
1013
+ floorplanSiblingOverrides?: (args: {
1014
+ nodeId: AnyNodeId;
1015
+ nodes: Record<AnyNodeId, AnyNode>;
1016
+ liveTransforms: Map<string, LiveTransformLike>;
1017
+ liveOverrides: Map<string, Record<string, unknown>>;
1018
+ }) => Record<AnyNodeId, AnyNode>;
1019
+ /**
1020
+ * Typed connection points this kind exposes (duct/pipe open ends,
1021
+ * fitting collars, equipment plenums). Pure function of the node —
1022
+ * returns LEVEL-LOCAL positions/directions (the kind applies its own
1023
+ * transform). Consumed by placement tools for port-snapping and, in a
1024
+ * later slice, by the system graph for connectivity. Kinds with no
1025
+ * connectable geometry omit this.
1026
+ */
1027
+ ports?: (node: z.infer<S>) => NodePort[];
1028
+ system?: SystemContribution;
1029
+ tool?: LazyComponent;
1030
+ /**
1031
+ * Stage-D drag-affordance components — one per kind-owned editor mode
1032
+ * triggered by `useEditor` state. Component receives `{ node }` as its
1033
+ * sole prop. Lazy-loaded by ToolManager when the corresponding editor
1034
+ * state activates (e.g. `curvingFence` → `affordanceTools.curve`).
1035
+ *
1036
+ * Each component is the thin React wrapper around a pure DragAction
1037
+ * primitive that lives in the kind's `actions/` folder. The split keeps
1038
+ * the action data unit-testable while letting the wrapper consume
1039
+ * `useDragAction` + cursor visuals.
1040
+ *
1041
+ * Generic record so per-kind state names don't need to land in the
1042
+ * core type system. ToolManager looks up by string key.
1043
+ */
1044
+ affordanceTools?: Record<string, () => Promise<{
1045
+ default: ComponentType<any>;
1046
+ }>>;
1047
+ affordances?: Affordance<z.infer<S>>[];
1048
+ /**
1049
+ * Contextual shortcut hints shown by `HelperManager` when this kind's
1050
+ * tool is active. Pure data — `HelperManager` renders these via a
1051
+ * generic <RegisteredToolHelper>. Drops the need for a hand-written
1052
+ * `<XxxHelper>` component per kind.
1053
+ *
1054
+ * Static array for now (covers ~all current uses). If a kind needs
1055
+ * state-dependent hints (e.g. different keys during a drag), it keeps
1056
+ * its bespoke helper component instead.
1057
+ */
1058
+ toolHints?: ToolHint[];
1059
+ /**
1060
+ * Which snapping profile this kind uses, so the editor's contextual snapping
1061
+ * HUD + snap math + force-place affordance are node-declared rather than
1062
+ * switched on the kind name (`'item'` free object vs `'structural'` wall/slab/
1063
+ * surface — see `SnapProfile`). The angle lock is derived from the *action*
1064
+ * (setting direction), not declared here. Also gates the "force place" hint:
1065
+ * structural kinds don't collision-reject, so they don't show it.
1066
+ * Omit it for kinds whose placement/move tools haven't moved onto the unified
1067
+ * snapping model yet — they get no snapping chip (no Shift-cycle) until they do.
1068
+ */
1069
+ snapProfile?: SnapProfile;
1070
+ /**
1071
+ * For `structural` kinds: does drafting this kind set a DIRECTION (so the
1072
+ * angle-lock snapping mode is meaningful)? Wall/fence/slab/ceiling drafting
1073
+ * draws directed edges → `true` (the default). Roof/stair/elevator are placed
1074
+ * as axis-aligned footprints, not directional draws → `false`, so their
1075
+ * drafting uses the no-angle `polygon` snap context (grid / lines / off)
1076
+ * instead of the angle-bearing `wall` context. Ignored for `item` kinds
1077
+ * (their context never carries an angle lock).
1078
+ */
1079
+ snapDraftDirectional?: boolean;
1080
+ /**
1081
+ * Optional translucent preview of the node — used by the move tool to
1082
+ * show where the node will land, and by the placement tool's cursor.
1083
+ * Receives the partially-resolved node (or a default-shaped stub during
1084
+ * placement before any commit has happened). Phase 4 may merge this with
1085
+ * the renderer behind an `opacity` prop.
1086
+ */
1087
+ preview?: () => Promise<{
1088
+ default: ComponentType<{
1089
+ node: z.infer<S>;
1090
+ }>;
1091
+ }>;
1092
+ presentation?: Presentation;
1093
+ mcp?: McpOverrides;
1094
+ /**
1095
+ * Optional keyboard shortcut handlers contributed by the kind. The
1096
+ * editor's keyboard hook looks these up by event name (`r` for R /
1097
+ * Shift+R, `t` for T / Shift+T) and runs the matching handler when
1098
+ * the user presses that key with a single node of this kind
1099
+ * selected. The fallback rotation behaviour kicks in only when the
1100
+ * action's `appliesTo` returns false.
1101
+ *
1102
+ * Replaces editor-side per-kind switches in `use-keyboard.ts` — a
1103
+ * kind that wants to override R / T just sets this field instead of
1104
+ * extending a hand-written `if/else` chain. Door / window are
1105
+ * legacy direct calls today (follow-up: migrate them under this
1106
+ * capability too).
1107
+ */
1108
+ keyboardActions?: KeyboardActions;
1109
+ /**
1110
+ * In-world resize / move arrows shown when this kind is selected.
1111
+ *
1112
+ * Pure descriptors — no React, no Three.js. The editor's generic
1113
+ * `<NodeArrowHandles>` reads this list and mounts the matching arrow
1114
+ * components with shared drag plumbing, replacing per-kind
1115
+ * `<XxxSideHandles>` files for the common cases.
1116
+ *
1117
+ * Static array, or a function for shape-dependent affordances
1118
+ * (column `crossSection` / `supportStyle`, stair-segment `segmentType`,
1119
+ * curved-vs-straight stairs). See `./handles.ts` for the variant union.
1120
+ *
1121
+ * Bespoke chrome that doesn't fit the descriptor model (wall corner
1122
+ * leader dashes, fence curving, items with `attachTo`) stays as a
1123
+ * custom React component mounted alongside.
1124
+ */
1125
+ handles?: HandleList<z.infer<S>>;
1126
+ };
1127
+ export type NodeCategory = 'site' | 'structure' | 'furnish' | 'analysis' | 'utility';
1128
+ export type KeyboardActions = {
1129
+ /** R / Shift+R primary action. */
1130
+ r?: KeyboardAction;
1131
+ /** T / Shift+T secondary action. */
1132
+ t?: KeyboardAction;
1133
+ /** E interaction action — operate the node (doors, drawers, appliances). */
1134
+ e?: KeyboardAction;
1135
+ /**
1136
+ * Set for kinds whose R/T rotation turns around a user-cyclable world
1137
+ * axis (Alt cycles Y → X → Z) — duct / pipe fittings with full 3D
1138
+ * orientation. The floating action menu reads this to surface the
1139
+ * active-axis pill above the selected node; kinds with plain Y-only
1140
+ * rotation omit it.
1141
+ */
1142
+ axisCycling?: boolean;
1143
+ };
1144
+ export type KeyboardAction = {
1145
+ /**
1146
+ * Predicate that gates the action. Return `false` when the
1147
+ * keystroke should fall through to the editor's default behaviour
1148
+ * for this kind (typically rotation). Skylight uses this to short-
1149
+ * circuit the action for non-operable type variants.
1150
+ */
1151
+ appliesTo: (node: AnyNode) => boolean;
1152
+ /**
1153
+ * Run the action. The editor handles `preventDefault` and the
1154
+ * shared sfx — the handler should only touch scene / interactive
1155
+ * state.
1156
+ */
1157
+ run: (node: AnyNode) => void;
1158
+ };
1159
+ /**
1160
+ * UI metadata for surfacing a node kind in the tool palette and elsewhere.
1161
+ * Phase 4 ships the consumer (auto-derived palette buttons); definitions can
1162
+ * declare this from Phase 2 onward so the spike's `column` and `shelf` show up
1163
+ * correctly the moment the palette consumes the registry.
1164
+ */
1165
+ export type Presentation = {
1166
+ /** Sentence-case label shown in palette buttons, breadcrumbs, etc. */
1167
+ label: string;
1168
+ /** Optional longer tooltip / help text. */
1169
+ description?: string;
1170
+ /** Icon for palette buttons and tree views. */
1171
+ icon: IconRef;
1172
+ /** Tool palette section. Defaults to `category` when omitted. */
1173
+ paletteSection?: 'site' | 'structure' | 'furnish';
1174
+ /** Sort key within a palette section; lower numbers come first. */
1175
+ paletteOrder?: number;
1176
+ /** Set true for kinds that exist but should NOT appear in the palette
1177
+ * (containers like `site`/`building`/`level`, internal nodes). */
1178
+ hidden?: boolean;
1179
+ /** Set false when selection is edited directly through in-scene affordances
1180
+ * and the generic floating action menu would duplicate or conflict with them. */
1181
+ actionMenu?: boolean;
1182
+ };
1183
+ export type IconRef =
1184
+ /** Iconify identifier, e.g. `lucide:square`. Matches the @iconify-react
1185
+ * setup the editor app already uses for tool icons. */
1186
+ {
1187
+ kind: 'iconify';
1188
+ name: string;
1189
+ }
1190
+ /** URL path to a raster or vector asset (PNG/SVG/...). Matches the
1191
+ * palette's PNG/SVG assets — use this to share the same artwork
1192
+ * between the bottom toolbar and the inspector title. */
1193
+ | {
1194
+ kind: 'url';
1195
+ src: string;
1196
+ }
1197
+ /** Inline SVG path data. Use for asset packs or plugins that want a custom
1198
+ * mark without contributing a React component. */
1199
+ | {
1200
+ kind: 'svg';
1201
+ viewBox: string;
1202
+ path: string;
1203
+ }
1204
+ /** Custom React component, lazy-loaded. Use sparingly — adds a Suspense
1205
+ * boundary per icon. */
1206
+ | {
1207
+ kind: 'component';
1208
+ module: () => Promise<{
1209
+ default: ComponentType;
1210
+ }>;
1211
+ };
1212
+ export type LazyComponent = () => Promise<{
1213
+ default: ComponentType;
1214
+ }>;
1215
+ export type RendererSource<N> = {
1216
+ kind: 'parametric';
1217
+ module: () => Promise<{
1218
+ default: ComponentType<{
1219
+ node: N;
1220
+ }>;
1221
+ }>;
1222
+ } | {
1223
+ kind: 'glb';
1224
+ getAsset: (n: N) => AssetRef;
1225
+ } | {
1226
+ kind: 'instanced-glb';
1227
+ getAsset: (n: N) => AssetRef;
1228
+ };
1229
+ /**
1230
+ * A collective renderer for the baked `/viewer` (see `NodeDefinition.bakeReplaceRenderer`):
1231
+ * a lazy module whose default export takes all of one level's `replace` nodes and
1232
+ * is portaled into that baked level. Three-free indirection, same as `system`.
1233
+ */
1234
+ export type BakeReplaceRenderer<N> = {
1235
+ module: () => Promise<{
1236
+ default: ComponentType<{
1237
+ nodes: N[];
1238
+ }>;
1239
+ }>;
1240
+ };
1241
+ export type AssetRef = {
1242
+ id: string;
1243
+ src: string;
1244
+ };
1245
+ export type SystemContribution = {
1246
+ module: () => Promise<{
1247
+ default: ComponentType<{
1248
+ sceneApi: SceneApi;
1249
+ }>;
1250
+ }>;
1251
+ priority?: number;
1252
+ };
1253
+ export type McpOverrides = {
1254
+ description?: string;
1255
+ semantic?: boolean;
1256
+ };
1257
+ export type DuplicateSubtreeCloneArgs = {
1258
+ root: AnyNode;
1259
+ descendants: AnyNode[];
1260
+ rootId: AnyNodeId;
1261
+ rootPatch: Partial<AnyNode>;
1262
+ nodes: Readonly<Record<AnyNodeId, AnyNode>>;
1263
+ };
1264
+ export type DuplicateSubtreeCloneResult = {
1265
+ root?: AnyNode;
1266
+ descendants?: AnyNode[];
1267
+ parentId?: AnyNodeId | null;
1268
+ };
1269
+ export type DuplicableConfig = {
1270
+ subtree?: boolean;
1271
+ prepareSubtreeClone?: (args: DuplicateSubtreeCloneArgs) => DuplicateSubtreeCloneResult;
1272
+ };
1273
+ export type Capabilities = {
1274
+ movable?: MovableConfig;
1275
+ rotatable?: RotatableConfig;
1276
+ scalable?: ScalableConfig;
1277
+ hostable?: HostableConfig;
1278
+ cuttable?: CuttableConfig;
1279
+ snappable?: SnappableConfig;
1280
+ surfaces?: SurfacesConfig;
1281
+ duplicable?: boolean | DuplicableConfig;
1282
+ deletable?: boolean;
1283
+ groupable?: boolean;
1284
+ selectable?: SelectableConfig;
1285
+ interactive?: boolean;
1286
+ floorPlaced?: FloorPlacedConfig;
1287
+ /**
1288
+ * Plan footprint this kind exposes to the alignment-anchor pool when it
1289
+ * isn't `floorPlaced` and isn't a structural primitive the bridge handles
1290
+ * directly (wall, slab). Lets a kind self-describe where it sits in plan
1291
+ * instead of the core anchor bridge hardcoding it per type. See
1292
+ * `AlignmentFootprintConfig`.
1293
+ */
1294
+ alignmentFootprint?: AlignmentFootprintConfig;
1295
+ /**
1296
+ * Bounds drawn by the 3D drag bounding box during a move. Opt-in: when
1297
+ * omitted, the box auto-measures the rendered mesh, which is correct for
1298
+ * most kinds. Set this when the rendered mesh tree contains extras the
1299
+ * user wouldn't think of as "the thing being dragged" — e.g. an elevator
1300
+ * whose mesh includes per-level landing assemblies, and the user expects
1301
+ * the box to wrap just the shaft they're moving.
1302
+ *
1303
+ * `size`: `[width, height, depth]` in the node's local frame.
1304
+ * `center`: optional full local center. Use this when the footprint is
1305
+ * offset from the node origin, such as a composite cabinet run after modules
1306
+ * have been deleted or shifted.
1307
+ * `centerY`: optional Y center; defaults to `size[1] / 2` (box sits on
1308
+ * the ground plane). Override when the local origin isn't at the base.
1309
+ */
1310
+ dragBounds?: (node: AnyNode, nodes?: Readonly<Record<string, AnyNode>>) => {
1311
+ size: [number, number, number];
1312
+ center?: [number, number, number];
1313
+ centerY?: number;
1314
+ };
1315
+ roofAccessory?: RoofAccessoryConfig;
1316
+ /**
1317
+ * Kind cuts a hole in the ceiling surface it is attached to (e.g. recessed
1318
+ * downlights). The viewer's `CeilingSystem` calls this for each child of a
1319
+ * ceiling to collect extra holes before triangulating. See `CeilingCutCapability`.
1320
+ */
1321
+ ceilingCut?: CeilingCutCapability;
1322
+ paint?: PaintCapability;
1323
+ /**
1324
+ * In-scene click action dispatch (e.g. a cooktop knob toggling its burner).
1325
+ * The editor's selection-manager walks the pointer hit's object chain
1326
+ * through `resolveTarget`; when it returns non-null, `activate` runs and a
1327
+ * `true` return consumes the click (no selection change). Keeps interactive
1328
+ * sub-meshes registry-driven instead of `if (node.type === '<kind>')` arms
1329
+ * in the editor.
1330
+ */
1331
+ sceneAction?: SceneActionCapability;
1332
+ /**
1333
+ * Declares the kind's paintable slots — the `{ slotId, label, default }`
1334
+ * contract shared by items (scanned from the GLB) and procedural kinds
1335
+ * (declared here). Procedural generators tag their emitted geometry with
1336
+ * `userData.slotId` and resolve each slot's material from
1337
+ * `node.slots[slotId]` → this declaration's `default` → role colour. The
1338
+ * declaration is a function of the node because a kind's slot set can depend
1339
+ * on its parameters (a shelf has a `back` slot only when it has a back).
1340
+ */
1341
+ slots?: (node: AnyNode) => SlotDeclaration[];
1342
+ /**
1343
+ * Kind is placed by clicking on a wall (door, window). When set, the
1344
+ * floor-plan layer lets wall background clicks pass through during
1345
+ * placement / move-on-wall — the placement tool's `wall:click` event
1346
+ * needs the SVG's `findClosestWallPoint` handler to run; without
1347
+ * this the wall's registry entry would swallow the click via
1348
+ * `handleSelect`. Read by `FloorplanRegistryLayer` when `movingNode`
1349
+ * is set, so the active move can suspend wall selection.
1350
+ */
1351
+ wallOpeningPlacement?: boolean;
1352
+ /**
1353
+ * Instances of this kind contain levels. When such a node is being
1354
+ * moved, the floor-plan layer falls back to the moving node's id as
1355
+ * the ambient building context — so the floor under the cursor keeps
1356
+ * rendering dimmed throughout the gesture even though the explicit
1357
+ * selection may have been cleared as part of the move handoff. Set
1358
+ * on building; future container kinds (e.g. annexes) opt in by
1359
+ * declaring the same flag.
1360
+ */
1361
+ floorplanLevelContainer?: boolean;
1362
+ /**
1363
+ * Names of schema fields on this kind that are *host references* —
1364
+ * values derived from where the node is placed (rather than declared
1365
+ * by the user as part of the kind's parametric configuration). Read
1366
+ * by host apps at preset-save time to strip these from the stored
1367
+ * payload so a placed instance gets fresh host links at the new
1368
+ * placement site (e.g. a door snapshot loses `wallId`/`wallT`; at
1369
+ * placement the auto-attach UX re-derives them from the wall under
1370
+ * the cursor).
1371
+ *
1372
+ * Kinds with no host refs omit this field (default `[]`).
1373
+ *
1374
+ * Examples:
1375
+ * - door: `['wallId', 'wallT']` (door hosted on a wall)
1376
+ * - window: `['wallId', 'wallT']`
1377
+ * - item with `attachTo`: depends on the asset; the kind's
1378
+ * `defaults()` or the dragging logic populates it dynamically.
1379
+ */
1380
+ hostRefFields?: string[];
1381
+ /**
1382
+ * Whether instances of this kind can be saved as a reusable preset
1383
+ * (unified `items` catalog, `kind='preset'`). The editor itself does
1384
+ * not act on this flag — host apps read it to gate "save as preset"
1385
+ * UI on the selected node. Default resolution (callers should use the
1386
+ * `isPresettable(def)` helper rather than reading this directly):
1387
+ *
1388
+ * - explicit `true` → presettable
1389
+ * - explicit `false` → not presettable
1390
+ * - undefined → presettable when `def.parametrics` exists
1391
+ *
1392
+ * Structural / utility kinds (level, building, site, zone, spawn,
1393
+ * guide, scan, item) opt out explicitly because saving them as a
1394
+ * standalone preset has no meaning — items already have their own
1395
+ * catalog, scans/guides carry user-uploaded imagery, and the rest
1396
+ * are non-leaf scene containers.
1397
+ */
1398
+ presettable?: boolean;
1399
+ /**
1400
+ * Instances of this kind are created by operating a build tool and
1401
+ * drawing on the grid (clicking points), rather than dropping a
1402
+ * finished instance. The tool id equals the node `type`. Host apps may
1403
+ * seed the tool's starting parameters via
1404
+ * `useEditor.setToolDefaults(type, params)` before activating it — the
1405
+ * tool's create path merges those defaults when minting the node and
1406
+ * clears its own entry on deactivation. Used so placing a saved preset
1407
+ * of a drawn kind contributes its build parameters (a fence's
1408
+ * height / style / post spacing) while the user draws the fresh span,
1409
+ * and so a future "small / medium / large" picker can prime the same
1410
+ * tool. Read via the `isDrawnViaTool(def)` helper. Default `false`.
1411
+ */
1412
+ drawTool?: boolean;
1413
+ };
1414
+ /**
1415
+ * Per-kind paint behaviour. Lets the editor's selection-manager
1416
+ * route paint hover / click / preview through a generic dispatcher
1417
+ * instead of adding an `if (node.type === '<kind>')` arm for every
1418
+ * paintable kind.
1419
+ *
1420
+ * The capability owns the four kind-specific decisions:
1421
+ * 1. Which logical surface (`role`) the click landed on.
1422
+ * 2. The patch to commit on click.
1423
+ * 3. How to apply a preview material to the registered mesh
1424
+ * subtree for that role (which mesh, which slot).
1425
+ * 4. How to read the currently-effective material for a role —
1426
+ * drives the color picker's "current value" indicator.
1427
+ *
1428
+ * The editor still owns the visual chrome — hover/cursor styling,
1429
+ * the `selectedMaterialTarget` round-trip, the paint-mode toolbar.
1430
+ * Kinds with no paint behaviour omit `paint`.
1431
+ */
1432
+ /**
1433
+ * One paintable slot a kind exposes. `slotId` is the stable key written into
1434
+ * `node.slots`; `label` is the human name (sentence case). `default` is the
1435
+ * slot's fallback appearance when no override is set — either a `MaterialRef`
1436
+ * (`library:<id>` / `scene:<id>`) or a `#rrggbb` colour. Mirrors the shape
1437
+ * items derive from their GLB material names.
1438
+ */
1439
+ export type SlotDeclaration = {
1440
+ slotId: string;
1441
+ label: string;
1442
+ default?: string;
1443
+ };
1444
+ export type PaintCapability = {
1445
+ /**
1446
+ * Material-picker target represented by this paint capability. Omit when
1447
+ * the kind should not show up as a toolbar target from plain selection.
1448
+ */
1449
+ materialTarget?: MaterialTarget;
1450
+ /**
1451
+ * Opt this kind into the painter's `room` application scope: a paint click
1452
+ * spreads to every same-kind node bounding the clicked node's room (walls and
1453
+ * slabs). The room geometry is resolved by the editor from `Space.polygon`;
1454
+ * this flag only declares that the kind participates.
1455
+ */
1456
+ roomScope?: boolean;
1457
+ /**
1458
+ * Resolve which logical surface the user clicked. Returns `null`
1459
+ * when the face shouldn't be painted (e.g. interior slot exposed
1460
+ * by accident, normal too oblique for an unambiguous side).
1461
+ */
1462
+ resolveRole: (args: PaintResolveArgs) => string | null;
1463
+ /**
1464
+ * Build the node-update patch that applies the new material at
1465
+ * `role`. Returned partial is merged into the node by the editor.
1466
+ */
1467
+ buildPatch: (args: PaintPatchArgs) => Partial<AnyNode>;
1468
+ /**
1469
+ * Optional: fully own the click-commit instead of the default
1470
+ * `updateNode(node.id, buildPatch(...))`. Kinds whose commit has a side
1471
+ * effect (items create a scene material for one-off colours, then store a
1472
+ * `scene:<id>` ref) implement this; kinds that just patch the node omit it.
1473
+ * Must perform its mutations as a single undo step.
1474
+ */
1475
+ commit?: (args: PaintPatchArgs) => void;
1476
+ /**
1477
+ * Apply a preview to the kind's registered mesh subtree at
1478
+ * `role`. The kind builds whatever preview material(s) it needs
1479
+ * (single material, full material array, multi-slot patch — all
1480
+ * up to the kind) and swaps them in. Returns a cleanup callback
1481
+ * that restores the original assignments; the editor calls it
1482
+ * when the preview ends (hover changes, paint commits, paint
1483
+ * cancels).
1484
+ *
1485
+ * Returning `null` means the kind couldn't preview at this role
1486
+ * (typically because the registered mesh isn't mounted yet); the
1487
+ * editor falls back to the "not-allowed" cursor.
1488
+ */
1489
+ applyPreview: (args: PaintPreviewArgs) => (() => void) | null;
1490
+ /**
1491
+ * Read the currently-effective material for `role` on `node`,
1492
+ * after walking any parent-fallback chain (segment → parent roof,
1493
+ * etc.). Powers `resolveActivePaintMaterialFromSelection` — when
1494
+ * the user has a paint target selected, the editor uses this to
1495
+ * show the role's current value in the picker.
1496
+ *
1497
+ * Returns `null` when the role doesn't apply to this kind.
1498
+ */
1499
+ getEffectiveMaterial?: (args: PaintEffectiveMaterialArgs) => {
1500
+ material: MaterialSchema | undefined;
1501
+ materialPreset: string | undefined;
1502
+ } | null;
1503
+ };
1504
+ /**
1505
+ * Per-kind in-scene click actions. A kind that builds interactive sub-meshes
1506
+ * (a gas-hob knob, a switch) tags them via `userData` in its geometry builder,
1507
+ * resolves the tag back out of the pointer hit in `resolveTarget`, and runs
1508
+ * the state change in `activate`. The editor owns only the generic dispatch:
1509
+ * walk the hit object's parent chain, and when `resolveTarget` returns
1510
+ * non-null, call `activate`; a `true` return consumes the click.
1511
+ *
1512
+ * `activate` receives a `SceneApi` so the kind never imports `useScene`
1513
+ * directly; transient animation frames may write through
1514
+ * `useLiveNodeOverrides` + `markDirty` and commit once at the end.
1515
+ */
1516
+ export type SceneActionCapability<T = unknown> = {
1517
+ /** Extract this kind's action target from one object in the hit chain. */
1518
+ resolveTarget: (object: {
1519
+ userData: Record<string, unknown>;
1520
+ }) => T | null;
1521
+ /** Run the action. Return `true` to consume the click (skip selection). */
1522
+ activate: (node: AnyNode, target: T, sceneApi: SceneApi) => boolean;
1523
+ };
1524
+ export type NodeQuickActionIcon = 'add-left' | 'add-right' | 'add' | 'convert';
1525
+ export type NodeQuickActionResult = {
1526
+ selectedIds?: AnyNodeId[];
1527
+ };
1528
+ export type NodeQuickAction = {
1529
+ id: string;
1530
+ label: string;
1531
+ title?: string;
1532
+ /**
1533
+ * Builtin glyph token (side-add arrows, convert) or an {@link IconRef}
1534
+ * for kind-owned marks — quick actions with bespoke glyphs ship them
1535
+ * from the kind's package instead of the menus hardcoding per-action
1536
+ * SVG.
1537
+ */
1538
+ icon?: NodeQuickActionIcon | IconRef;
1539
+ disabled?: boolean;
1540
+ history?: 'single';
1541
+ run: (args: {
1542
+ node: AnyNode;
1543
+ sceneApi: SceneApi;
1544
+ }) => NodeQuickActionResult | undefined;
1545
+ };
1546
+ export type NodeQuickActionProvider<N> = (args: {
1547
+ node: N;
1548
+ nodes: Readonly<Partial<Record<AnyNodeId, AnyNode>>>;
1549
+ }) => NodeQuickAction[];
1550
+ export type PaintResolveArgs = {
1551
+ node: AnyNode;
1552
+ /**
1553
+ * The geometry's material-slot index resolved from the pointer
1554
+ * hit (via three.js groups). `null` when no group covers the
1555
+ * face.
1556
+ */
1557
+ materialIndex: number | null;
1558
+ /** Optional: hit surface normal. Wall uses this for its interior/exterior split. */
1559
+ normal?: readonly [number, number, number];
1560
+ /** Optional: hit local position. Wall uses this to confirm the side. */
1561
+ localPosition?: readonly [number, number, number];
1562
+ /** Optional: name of the three.js object that received the hit. Stair uses this. */
1563
+ hitObjectName?: string;
1564
+ /** Optional: the three.js object that received the pointer hit. Items read userData.slotId off it. */
1565
+ hitObject?: Object3D;
1566
+ /**
1567
+ * Optional: the pointer's world ray, so a kind can re-raycast its OWN subtree
1568
+ * to pick the precise sub-mesh under the cursor — independent of what the
1569
+ * shared scene raycast hit first. Door/window use this: their opening proxy
1570
+ * (a proud invisible cutout) wins the scene raycast over the wall in front of
1571
+ * the recessed door body, then they re-raycast their parts to find the slot.
1572
+ */
1573
+ ray?: Ray;
1574
+ };
1575
+ export type PaintPatchArgs = {
1576
+ node: AnyNode;
1577
+ role: string;
1578
+ material: MaterialSchema | undefined;
1579
+ materialPreset: string | undefined;
1580
+ };
1581
+ export type PaintPreviewArgs = {
1582
+ node: AnyNode;
1583
+ role: string;
1584
+ material: MaterialSchema | undefined;
1585
+ materialPreset: string | undefined;
1586
+ root: Object3D;
1587
+ };
1588
+ export type PaintEffectiveMaterialArgs = {
1589
+ node: AnyNode;
1590
+ role: string;
1591
+ /** Snapshot of the scene `nodes` map — kinds whose effective material walks the parent chain (roof-segment → roof) read parents through it. */
1592
+ nodes: Record<AnyNodeId, AnyNode>;
1593
+ };
1594
+ /**
1595
+ * Kinds mounted on a roof segment via `roofSegmentId`. Presence of this
1596
+ * capability tells the viewer's roof-merge loop two things:
1597
+ *
1598
+ * 1. **Dirty cascade.** When the accessory is dirtied (move / resize /
1599
+ * reparent), the host segment's parent roof needs a re-merge —
1600
+ * otherwise the merged shell shows the previous cut shape. The
1601
+ * generic loop clears the accessory's dirty bit and queues the
1602
+ * parent roof.
1603
+ * 2. **Optional CSG cut.** When `buildCut` is set, the merge loop
1604
+ * subtracts the returned geometry from the host segment's shin /
1605
+ * deck / wall brushes so the accessory has a clean hole to poke
1606
+ * through. Returned geometry is SEGMENT-LOCAL; the viewer welds
1607
+ * vertices, attaches a single material group, and wraps it in a
1608
+ * `three-bvh-csg` Brush — core stays free of three-bvh-csg deps
1609
+ * and kinds don't need to import it.
1610
+ *
1611
+ * Use `buildCut` when the kind pokes THROUGH the roof (skylight,
1612
+ * dormer). Kinds that sit ON TOP (vents, solar panels) declare the
1613
+ * capability without `buildCut` — the cascade still fires but no CSG
1614
+ * cut runs.
1615
+ */
1616
+ export type RoofAccessoryConfig = {
1617
+ buildCut?: (node: AnyNode, hostSegment: AnyNode) => BufferGeometry | null;
1618
+ /**
1619
+ * Which segment brushes `buildCut` subtracts from. Wall-face openings
1620
+ * (door / window) cut only the wall brush — subtracting the same box
1621
+ * from the shin / deck slabs is pointless work and creates tangential
1622
+ * / coplanar CSG cases near the gable and shed slopes. Defaults to
1623
+ * all three (skylight / dormer genuinely poke through the deck).
1624
+ */
1625
+ cutScope?: 'all' | 'wall';
1626
+ /**
1627
+ * The kind's own dirty-driven geometry system consumes its dirty
1628
+ * marks (door / window via DoorSystem / WindowSystem, which already
1629
+ * cascade to the host segment through `parentId`). The roof-merge
1630
+ * loop must then leave those marks alone — consuming them would
1631
+ * starve that system whenever it defers a rebuild (mesh not mounted
1632
+ * yet, per-frame rebuild budget exhausted).
1633
+ */
1634
+ dirtyHandledByOwnSystem?: boolean;
1635
+ };
1636
+ /**
1637
+ * Capability for kinds that cut a hole in their host ceiling when the node is
1638
+ * attached to a ceiling surface (e.g. recessed downlights). The viewer's
1639
+ * `CeilingSystem` queries children of a ceiling for this capability and merges
1640
+ * the returned polygons as extra holes before triangulating, keeping the viewer
1641
+ * free of per-kind branching.
1642
+ *
1643
+ * Returns a rotated-rectangle footprint in ceiling-local [x, z] plan space —
1644
+ * the same coordinate space as `CeilingNode.polygon` and `.holes`. Return
1645
+ * `null` when this particular instance should not cut a hole (e.g. a
1646
+ * non-recessed variant of the same kind).
1647
+ */
1648
+ export type CeilingCutCapability = {
1649
+ buildCeilingHole: (node: AnyNode) => Array<[number, number]> | null;
1650
+ };
1651
+ export type CapabilityCtx = {
1652
+ node: AnyNode;
1653
+ };
1654
+ export type MovableConfig = {
1655
+ axes: ReadonlyArray<'x' | 'y' | 'z'>;
1656
+ gridSnap?: boolean;
1657
+ /**
1658
+ * Pin the dragged node to the cursor (absolute placement) instead of the
1659
+ * default offset-preserving drag, where the node moves by the cursor's
1660
+ * delta from where the drag started. Offset preservation suits large
1661
+ * furniture you grab by an edge; small connector-like kinds (duct
1662
+ * fittings) read as "lagging behind the mouse" — they want the cursor.
1663
+ */
1664
+ cursorAttached?: boolean;
1665
+ /**
1666
+ * Magnetically snap one of this kind's own ports onto a nearby scene
1667
+ * port while dragging — e.g. a register's collar onto a duct run end.
1668
+ * The dragged node shifts in XZ so its closest matching port lands on
1669
+ * the target port. Alt bypasses the snap. Kinds without `def.ports`
1670
+ * can't use this. Snap takes precedence over grid / alignment snap.
1671
+ */
1672
+ portSnap?: {
1673
+ /**
1674
+ * Distribution loops a target port must belong to (e.g.
1675
+ * `['supply', 'return']`). A target port with no `system` always
1676
+ * matches. Omit to match every port.
1677
+ */
1678
+ systems?: readonly string[];
1679
+ /** Snap radius in meters (XZ). Defaults to 0.5. */
1680
+ radius?: number;
1681
+ };
1682
+ /**
1683
+ * The node's `position` lives in a parent node's local frame (a cabinet
1684
+ * module inside its run) rather than the level frame. The generic move
1685
+ * tool converts the plan-frame cursor through these hooks, previews the
1686
+ * child via `useLiveNodeOverrides` (dirtying the parent so its composite
1687
+ * geometry re-flows), and skips the world-frame floor-collision box.
1688
+ */
1689
+ parentFrame?: MovableParentFrame;
1690
+ /**
1691
+ * Optional group-move snap for the generic multi-selection translate gizmo.
1692
+ * Returns an adjusted candidate position for this node when the moving group
1693
+ * should magnetically settle onto a nearby feature (for example, a cabinet
1694
+ * run snapping flush to a wall while the whole selected kitchen moves as one).
1695
+ */
1696
+ groupMoveSnap?: (args: GroupMoveSnapArgs) => [number, number, number] | null;
1697
+ override?: (ctx: CapabilityCtx) => MovableConfig | null;
1698
+ };
1699
+ export type MovableParentFrame = {
1700
+ /** The parent node owning the local frame; `null` → move in plan frame. */
1701
+ resolveParent: (node: AnyNode, nodes: Readonly<Record<string, AnyNode>>) => AnyNode | null;
1702
+ /** Parent's Y rotation, composed onto the child's preview rotation. */
1703
+ parentRotationY: (parent: AnyNode, nodes?: Readonly<Record<string, AnyNode>>) => number;
1704
+ localToPlan: (parent: AnyNode, local: readonly [number, number, number], nodes?: Readonly<Record<string, AnyNode>>) => [number, number, number];
1705
+ planToLocal: (parent: AnyNode, planX: number, localY: number, planZ: number, nodes?: Readonly<Record<string, AnyNode>>) => [number, number, number];
1706
+ /**
1707
+ * Optional 2D live-transform projection. Used by the floor-plan layer for
1708
+ * nodes whose live position is already in the parent-local frame and must not
1709
+ * be treated as a level-frame / floor-placed position.
1710
+ */
1711
+ floorplanLiveTransform?: (args: {
1712
+ node: AnyNode;
1713
+ live: LiveTransformLike;
1714
+ }) => AnyNode;
1715
+ /**
1716
+ * Optional magnetic snap in the parent's local frame (e.g. a module edge
1717
+ * mating flush with a sibling module). Runs when magnetic snapping is
1718
+ * active; returns the (possibly unchanged) local position.
1719
+ */
1720
+ magneticSnap?: (node: AnyNode, parent: AnyNode, local: readonly [number, number, number], nodes: Readonly<Record<string, AnyNode>>) => [number, number, number];
1721
+ /** Optional snap-line matches for the parent-frame magnetic snap result. */
1722
+ magneticSnapMatches?: (node: AnyNode, parent: AnyNode, local: readonly [number, number, number], snappedLocal: readonly [number, number, number], nodes: Readonly<Record<string, AnyNode>>) => ParentFrameSnapMatch[];
1723
+ /**
1724
+ * Called after a move of the child commits, with the LIVE (post-commit)
1725
+ * child and parent. Lets the kind run derived-state maintenance the
1726
+ * generic tool can't know about (a cabinet run re-flowing its layout and
1727
+ * re-anchoring linked corner runs to the moved module's new edge).
1728
+ */
1729
+ onCommit?: (node: AnyNode, parent: AnyNode, sceneApi: SceneApi) => void;
1730
+ };
1731
+ export type ParentFrameSnapMatch = {
1732
+ axis: 'x' | 'z';
1733
+ candidateNodeId: AnyNodeId;
1734
+ from: {
1735
+ x: number;
1736
+ z: number;
1737
+ };
1738
+ to: {
1739
+ x: number;
1740
+ z: number;
1741
+ };
1742
+ };
1743
+ export type GroupMoveSnapArgs = {
1744
+ node: AnyNode;
1745
+ candidatePosition: [number, number, number];
1746
+ movingIds: readonly AnyNodeId[];
1747
+ nodes: Readonly<Record<string, AnyNode>>;
1748
+ levelId: AnyNodeId | null;
1749
+ };
1750
+ export type LiveTransformLike = {
1751
+ position: [number, number, number];
1752
+ rotation: number;
1753
+ };
1754
+ export type RotatableConfig = {
1755
+ axes: ReadonlyArray<'x' | 'y' | 'z'>;
1756
+ snapAngles?: readonly number[];
1757
+ override?: (ctx: CapabilityCtx) => RotatableConfig | null;
1758
+ };
1759
+ export type ScalableConfig = {
1760
+ axes: ReadonlyArray<'x' | 'y' | 'z'>;
1761
+ min?: number;
1762
+ max?: number;
1763
+ override?: (ctx: CapabilityCtx) => ScalableConfig | null;
1764
+ };
1765
+ export type HostableConfig = {
1766
+ parents: readonly string[];
1767
+ align?: 'top' | 'bottom' | 'center' | 'face';
1768
+ fromAsset?: 'attachTo';
1769
+ modes?: Record<string, Partial<HostableConfig>>;
1770
+ override?: (ctx: CapabilityCtx) => HostableConfig | null;
1771
+ };
1772
+ export type CuttableConfig = {
1773
+ hostKinds: readonly string[];
1774
+ override?: (ctx: CapabilityCtx) => CuttableConfig | null;
1775
+ };
1776
+ export type SnappableConfig = {
1777
+ points?: readonly SnapPointKind[];
1778
+ override?: (ctx: CapabilityCtx) => SnappableConfig | null;
1779
+ };
1780
+ export type SnapPointKind = 'start' | 'end' | 'midpoint' | 'center' | 'corners';
1781
+ export type SurfacesConfig = {
1782
+ top?: {
1783
+ height: number | ((n: AnyNode) => number);
1784
+ };
1785
+ sides?: {
1786
+ faces: 'all' | ReadonlyArray<readonly [number, number, number]>;
1787
+ };
1788
+ custom?: SurfaceQuery;
1789
+ };
1790
+ export type SurfaceQuery = (n: AnyNode) => SurfacePoint[];
1791
+ export type SurfacePoint = {
1792
+ position: readonly [number, number, number];
1793
+ normal: readonly [number, number, number];
1794
+ };
1795
+ export type SelectableConfig = {
1796
+ hitVolume?: 'bbox' | 'mesh' | 'none';
1797
+ override?: (ctx: CapabilityCtx) => SelectableConfig | null;
1798
+ };
1799
+ export type FloorPlacedFootprint = {
1800
+ dimensions: [number, number, number];
1801
+ rotation: [number, number, number];
1802
+ position?: [number, number, number];
1803
+ };
1804
+ export type FloorPlacedFootprintContext = {
1805
+ nodes: Readonly<Record<AnyNodeId, AnyNode>>;
1806
+ };
1807
+ export type FloorPlacedFootprintResolver = (node: AnyNode, ctx?: FloorPlacedFootprintContext) => FloorPlacedFootprint;
1808
+ export type FloorPlacedFootprintsResolver = (node: AnyNode, ctx?: FloorPlacedFootprintContext) => readonly FloorPlacedFootprint[];
1809
+ /**
1810
+ * Floor-placed kinds rest directly on a level and need their Y lifted by
1811
+ * any slab the footprint overlaps. The generic `<FloorElevationSystem>`
1812
+ * computes `slabElevation + node.position[1]` and writes it onto the
1813
+ * registered mesh on every dirty mark. `footprint` returns the default
1814
+ * world-space footprint the spatial-grid manager uses to find overlapping
1815
+ * slabs; `footprints` lets composite kinds expose multiple footprint
1816
+ * segments, with the canonical resolver taking the max slab elevation;
1817
+ * `applies` is an optional predicate to skip nodes that share a kind but
1818
+ * are mounted off-floor (items attached to a wall / ceiling).
1819
+ */
1820
+ export type FloorPlacedConfig = {
1821
+ footprint?: FloorPlacedFootprintResolver;
1822
+ footprints?: FloorPlacedFootprintsResolver;
1823
+ applies?: (node: AnyNode) => boolean;
1824
+ /**
1825
+ * Opt this kind into floor-placement collision: its footprint blocks other
1826
+ * placements (it's an obstacle in `canPlaceOnFloor`) AND its own
1827
+ * placement/move refuses to overlap another colliding footprint (red ghost,
1828
+ * Alt to force). Solid furniture-like kinds (item / shelf / column) set this;
1829
+ * markers and port-mated kinds (spawn / MEP / stair) leave it off so they
1830
+ * neither block nor get blocked. Default off.
1831
+ */
1832
+ collides?: boolean;
1833
+ };
1834
+ /**
1835
+ * Plan footprint a kind contributes to the alignment-anchor pool when it is
1836
+ * neither `floorPlaced` (columns / items, whose footprint the bridge already
1837
+ * reads) nor a primitive the bridge knows structurally (walls → segments,
1838
+ * slabs → polygons). Two shapes:
1839
+ *
1840
+ * - `box` — a rotatable rectangle centred on the node's `position`. Use
1841
+ * when the kind also moves by its footprint edges (elevator): the anchor
1842
+ * bridge relocates the box to the proposed drag point, so one descriptor
1843
+ * serves both the static candidate and the moving node.
1844
+ * - `aabb` — an already-resolved XZ bounding box, for kinds whose plan
1845
+ * shape isn't a centred rectangle (stair: a segment chain or annular
1846
+ * sector). The moving-anchor bridge can relocate these by patching the
1847
+ * proposed plan position and resolving the AABB again.
1848
+ *
1849
+ * `nodes` is supplied only when a kind needs siblings / children to resolve
1850
+ * its footprint (a straight stair walks its `stair-segment` children); box
1851
+ * kinds derive everything from `node` alone.
1852
+ */
1853
+ export type AlignmentFootprint = {
1854
+ shape: 'box';
1855
+ dimensions: [number, number, number];
1856
+ rotation: [number, number, number];
1857
+ } | {
1858
+ shape: 'aabb';
1859
+ minX: number;
1860
+ minZ: number;
1861
+ maxX: number;
1862
+ maxZ: number;
1863
+ };
1864
+ export type AlignmentFootprintConfig = (node: AnyNode, nodes?: Readonly<Record<string, AnyNode>>) => AlignmentFootprint | null;
1865
+ export type Relations = {
1866
+ linkedBy?: 'endpoint-match' | 'polygon-share' | {
1867
+ custom: (n: AnyNode) => AnyNodeId[];
1868
+ };
1869
+ hosts?: readonly string[];
1870
+ affectsSpatial?: readonly string[];
1871
+ cascadeDelete?: 'descendants' | 'children' | 'none';
1872
+ };
1873
+ export type ParametricDescriptor<N> = {
1874
+ groups: ParamGroup<N>[];
1875
+ invariants?: ReadonlyArray<(n: N) => Issue[]>;
1876
+ /**
1877
+ * Co-update hook for fields that must stay consistent when edited
1878
+ * from the inspector. Called with the node AFTER `patch` is merged
1879
+ * plus the patch itself (so the hook can tell which field the user
1880
+ * touched); whatever it returns is folded into the same update.
1881
+ * Direct store/MCP writes bypass it — keep real invariants in
1882
+ * `invariants`.
1883
+ */
1884
+ derive?: (next: N, patch: Partial<N>) => Partial<N>;
1885
+ /**
1886
+ * Cross-node companion to `derive`: after an inspector edit lands on
1887
+ * this node, return patches for OTHER nodes that must follow to keep
1888
+ * the scene consistent — e.g. duct runs re-trimmed onto a resized
1889
+ * fitting's collars. `prev` is the node before the edit, `next` after
1890
+ * (with `derive` already folded in). Applied in the same gesture via
1891
+ * `updateNodes`.
1892
+ */
1893
+ reconcile?: (prev: N, next: N) => Array<{
1894
+ id: AnyNodeId;
1895
+ data: Partial<AnyNode>;
1896
+ }>;
1897
+ /**
1898
+ * Deletion companion to `reconcile`: when a node of this kind is about
1899
+ * to be removed, return patches for OTHER nodes that must follow to
1900
+ * undo whatever the node imposed on its neighbours — e.g. an
1901
+ * auto-inserted elbow re-extends the duct runs it trimmed back onto the
1902
+ * corner it replaced. Called with the node and the live scene `nodes`
1903
+ * map BEFORE the deletion lands; patches targeting nodes also being
1904
+ * deleted are ignored. Applied in the same `set` as the delete so it's
1905
+ * one undo step. Fires only on `deleteNodes` (user-intent deletes) —
1906
+ * NOT on `applyNodeChanges`, whose deletes are internal re-routes that
1907
+ * rewrite neighbours explicitly in the same batch and would fight a
1908
+ * restore.
1909
+ */
1910
+ onDelete?: (node: N, nodes: Record<AnyNodeId, AnyNode>) => Array<{
1911
+ id: AnyNodeId;
1912
+ data: Partial<AnyNode>;
1913
+ }>;
1914
+ /**
1915
+ * Companion deletes that should be folded into the same user-intent delete
1916
+ * gesture — e.g. deleting the last module of a cabinet run should remove
1917
+ * the now-empty run node too. Called against the live scene BEFORE
1918
+ * deletion; returned ids are recursively expanded through the normal
1919
+ * descendant cascade. `pendingDeleteIds` holds every id already part of
1920
+ * the gesture so "would my parent become empty?" checks see sibling
1921
+ * deletes from the same multi-select.
1922
+ */
1923
+ onDeleteCascade?: (node: N, nodes: Record<AnyNodeId, AnyNode>, pendingDeleteIds: ReadonlySet<AnyNodeId>) => AnyNodeId[];
1924
+ customPanel?: () => Promise<{
1925
+ default: ComponentType<{
1926
+ node: N;
1927
+ }>;
1928
+ }>;
1929
+ /**
1930
+ * Extra buttons rendered in the inspector's Actions section
1931
+ * (below Move/Delete). Lets a kind declare "do this thing to the
1932
+ * current node" affordances without escaping to a full custom
1933
+ * panel. Buttons whose `enabledIf` returns false stay disabled.
1934
+ */
1935
+ actions?: ParamAction<N>[];
1936
+ /**
1937
+ * Lazy-loaded React subsection rendered AFTER the auto-derived
1938
+ * groups and BEFORE the Actions section. Used by kinds that want
1939
+ * to list their child nodes inline — e.g. the gutter's downspout
1940
+ * list with an "Add Downspout" button at the bottom, same shape as
1941
+ * the roof panel's gutter / vent lists. Kind owns the layout; the
1942
+ * inspector just slots it in.
1943
+ */
1944
+ trailingSection?: () => Promise<{
1945
+ default: ComponentType<{
1946
+ node: N;
1947
+ }>;
1948
+ }>;
1949
+ };
1950
+ export type ParamAction<N> = {
1951
+ label: string;
1952
+ /** Optional asset URL for a leading icon — same shape as palette icons. */
1953
+ iconSrc?: string;
1954
+ enabledIf?: (n: N) => boolean;
1955
+ /** Click handler. Receives the current node value at click time. */
1956
+ onClick: (n: N) => void;
1957
+ };
1958
+ export type ParamGroup<N> = {
1959
+ label: string;
1960
+ fields: ParamField<N>[];
1961
+ };
1962
+ export type ParamField<N> = {
1963
+ key: keyof N;
1964
+ kind: 'number';
1965
+ unit?: string;
1966
+ min?: number;
1967
+ max?: number;
1968
+ step?: number;
1969
+ visibleIf?: (n: N) => boolean;
1970
+ customEditor?: ComponentType;
1971
+ } | {
1972
+ key: keyof N;
1973
+ kind: 'boolean';
1974
+ visibleIf?: (n: N) => boolean;
1975
+ } | {
1976
+ key: keyof N;
1977
+ kind: 'enum';
1978
+ options: readonly string[];
1979
+ /** Defaults to 'select' (dropdown). 'segmented' renders the inline
1980
+ * tabbed switcher — better for short option lists (2-4 items). */
1981
+ display?: 'select' | 'segmented';
1982
+ visibleIf?: (n: N) => boolean;
1983
+ } | {
1984
+ key: keyof N;
1985
+ kind: 'vec3';
1986
+ visibleIf?: (n: N) => boolean;
1987
+ } | {
1988
+ key: keyof N;
1989
+ kind: 'color';
1990
+ visibleIf?: (n: N) => boolean;
1991
+ } | {
1992
+ key: keyof N;
1993
+ kind: 'material';
1994
+ visibleIf?: (n: N) => boolean;
1995
+ } | {
1996
+ key: keyof N;
1997
+ kind: 'ref';
1998
+ refKind: string;
1999
+ visibleIf?: (n: N) => boolean;
2000
+ }
2001
+ /** Escape hatch for fields that don't map to a single node key —
2002
+ * derived values (`length` from `start`/`end`), sliders with
2003
+ * dynamic min/max (curve sagitta bounded by chord length),
2004
+ * composed editors, etc. The kind owns the rendering and the
2005
+ * update logic. `key` here is just a stable React key/label. */
2006
+ | {
2007
+ key: string;
2008
+ kind: 'custom';
2009
+ component: ComponentType<{
2010
+ node: N;
2011
+ onUpdate: (patch: Partial<N>) => void;
2012
+ }>;
2013
+ visibleIf?: (n: N) => boolean;
2014
+ };
2015
+ export type Issue = {
2016
+ field?: string;
2017
+ msg: string;
2018
+ severity?: 'error' | 'warning';
2019
+ };
2020
+ export type Affordance<N> = {
2021
+ id: string;
2022
+ mount: 'on-selection' | 'on-hover' | 'always';
2023
+ enabled?: (n: N, ctx: EditorCtx) => boolean;
2024
+ component: () => Promise<{
2025
+ default: ComponentType<{
2026
+ node: N;
2027
+ }>;
2028
+ }>;
2029
+ };
2030
+ export type EditorCtx = {
2031
+ modifiers: Modifiers;
2032
+ };
2033
+ export type Vec2 = readonly [number, number];
2034
+ export type Modifiers = {
2035
+ shift: boolean;
2036
+ alt: boolean;
2037
+ ctrl: boolean;
2038
+ meta: boolean;
2039
+ };
2040
+ export type DragAction<Ctx, Draft> = {
2041
+ begin: (input: {
2042
+ node?: AnyNode;
2043
+ point: Vec2;
2044
+ handleId?: string;
2045
+ modifiers?: Modifiers;
2046
+ }) => Ctx;
2047
+ preview: (ctx: Ctx, point: Vec2, modifiers: Modifiers) => Draft;
2048
+ snap?: (draft: Draft, ctx: Ctx, services: SnapServicesLike) => Draft;
2049
+ apply: (draft: Draft, ctx: Ctx, scene: SceneApi) => Iterable<AnyNodeId>;
2050
+ commit?: (draft: Draft, ctx: Ctx, scene: SceneApi) => boolean;
2051
+ cancel: (ctx: Ctx, scene: SceneApi) => void;
2052
+ };
2053
+ export type SnapServicesLike = unknown;
2054
+ export type SceneApi = {
2055
+ get: <N extends AnyNode = AnyNode>(id: AnyNodeId) => N | undefined;
2056
+ /**
2057
+ * Snapshot of the full nodes record. For descriptors / placement
2058
+ * callbacks that need to walk many siblings or resolve cross-node
2059
+ * structure (elevator level entries, building level chains, etc.)
2060
+ * without N round-trips through `get`. Returns the live reference —
2061
+ * do not mutate.
2062
+ */
2063
+ nodes: () => Readonly<Record<AnyNodeId, AnyNode>>;
2064
+ update: (id: AnyNodeId, patch: Partial<AnyNode>) => void;
2065
+ upsert: (node: AnyNode, parentId?: AnyNodeId) => AnyNodeId;
2066
+ delete: (id: AnyNodeId) => void;
2067
+ restore: (id: AnyNodeId) => void;
2068
+ restoreAll: () => void;
2069
+ markDirty: (id: AnyNodeId) => void;
2070
+ pauseHistory: () => void;
2071
+ resumeHistory: () => void;
2072
+ /**
2073
+ * Collect the subtree of live nodes rooted at `rootId` — `root` plus
2074
+ * every descendant reachable via `children[]` in BFS order. Returns
2075
+ * live node references (no clones); the caller decides whether to
2076
+ * persist by value or pass them straight into {@link cloneNodesInto}.
2077
+ * Returns `null` if `rootId` is missing.
2078
+ */
2079
+ getSubtree: (rootId: AnyNodeId) => Subtree | null;
2080
+ /**
2081
+ * Clone a flat array of nodes into the live scene with fresh IDs and
2082
+ * rewired parent / children references. Intentionally generic — see
2083
+ * {@link cloneNodesInto} for the transformations applied. Does NOT
2084
+ * strip or re-derive host references (e.g. `wallId` on a door); the
2085
+ * caller is responsible for that policy (read {@link Capabilities.hostRefFields}
2086
+ * on the relevant definition).
2087
+ *
2088
+ * Returns the new root id, or `null` if insertion failed.
2089
+ */
2090
+ cloneNodesInto: (nodes: ReadonlyArray<AnyNode>, opts: CloneNodesIntoOptions) => AnyNodeId | null;
2091
+ };
2092
+ export interface NodeRegistry {
2093
+ has: (kind: string) => boolean;
2094
+ get: (kind: string) => AnyNodeDefinition | undefined;
2095
+ entries: () => IterableIterator<[string, AnyNodeDefinition]>;
2096
+ schemas: () => ZodObject<any>[];
2097
+ readonly size: number;
2098
+ }
2099
+ //# sourceMappingURL=types.d.ts.map