@pascal-app/core 0.7.0 → 0.9.1

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 (318) hide show
  1. package/dist/events/bus.d.ts +47 -18
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/events/bus.js +1 -1
  4. package/dist/hooks/scene-registry/scene-registry.d.ts +6 -22
  5. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  6. package/dist/hooks/scene-registry/scene-registry.js +33 -26
  7. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
  8. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
  9. package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
  10. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
  11. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +273 -0
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +13 -2
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  15. package/dist/hooks/spatial-grid/spatial-grid-manager.js +95 -9
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  18. package/dist/hooks/spatial-grid/spatial-grid-sync.js +91 -18
  19. package/dist/index.d.ts +22 -4
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +19 -2
  22. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  23. package/dist/lib/polygon-relations.d.ts +10 -0
  24. package/dist/lib/polygon-relations.d.ts.map +1 -0
  25. package/dist/lib/polygon-relations.js +73 -0
  26. package/dist/lib/space-detection-pause.test.d.ts +2 -0
  27. package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
  28. package/dist/lib/space-detection-pause.test.js +42 -0
  29. package/dist/lib/space-detection.d.ts +30 -1
  30. package/dist/lib/space-detection.d.ts.map +1 -1
  31. package/dist/lib/space-detection.js +222 -41
  32. package/dist/lib/space-detection.test.d.ts +2 -0
  33. package/dist/lib/space-detection.test.d.ts.map +1 -0
  34. package/dist/lib/space-detection.test.js +77 -0
  35. package/dist/material-library.d.ts +1 -1
  36. package/dist/material-library.d.ts.map +1 -1
  37. package/dist/material-library.js +1957 -141
  38. package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
  39. package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
  40. package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
  41. package/dist/registry/handles.d.ts +321 -0
  42. package/dist/registry/handles.d.ts.map +1 -0
  43. package/dist/registry/handles.js +24 -0
  44. package/dist/registry/index.d.ts +7 -0
  45. package/dist/registry/index.d.ts.map +1 -0
  46. package/dist/registry/index.js +4 -0
  47. package/dist/registry/registry.d.ts +93 -0
  48. package/dist/registry/registry.d.ts.map +1 -0
  49. package/dist/registry/registry.js +201 -0
  50. package/dist/registry/registry.test.d.ts +2 -0
  51. package/dist/registry/registry.test.d.ts.map +1 -0
  52. package/dist/registry/registry.test.js +183 -0
  53. package/dist/registry/relations-resolver.d.ts +42 -0
  54. package/dist/registry/relations-resolver.d.ts.map +1 -0
  55. package/dist/registry/relations-resolver.js +77 -0
  56. package/dist/registry/relations-resolver.test.d.ts +2 -0
  57. package/dist/registry/relations-resolver.test.d.ts.map +1 -0
  58. package/dist/registry/relations-resolver.test.js +183 -0
  59. package/dist/registry/scene-api.d.ts +44 -0
  60. package/dist/registry/scene-api.d.ts.map +1 -0
  61. package/dist/registry/scene-api.js +111 -0
  62. package/dist/registry/scene-api.test.d.ts +2 -0
  63. package/dist/registry/scene-api.test.d.ts.map +1 -0
  64. package/dist/registry/scene-api.test.js +183 -0
  65. package/dist/registry/subtree.d.ts +67 -0
  66. package/dist/registry/subtree.d.ts.map +1 -0
  67. package/dist/registry/subtree.js +119 -0
  68. package/dist/registry/subtree.test.d.ts +2 -0
  69. package/dist/registry/subtree.test.d.ts.map +1 -0
  70. package/dist/registry/subtree.test.js +133 -0
  71. package/dist/registry/types.d.ts +1420 -0
  72. package/dist/registry/types.d.ts.map +1 -0
  73. package/dist/registry/types.js +1 -0
  74. package/dist/schema/asset-url.test.js +0 -4
  75. package/dist/schema/index.d.ts +17 -2
  76. package/dist/schema/index.d.ts.map +1 -1
  77. package/dist/schema/index.js +17 -3
  78. package/dist/schema/material.d.ts +12 -2
  79. package/dist/schema/material.d.ts.map +1 -1
  80. package/dist/schema/material.js +10 -0
  81. package/dist/schema/nodes/box-vent.d.ts +74 -0
  82. package/dist/schema/nodes/box-vent.d.ts.map +1 -0
  83. package/dist/schema/nodes/box-vent.js +63 -0
  84. package/dist/schema/nodes/building.d.ts +1 -1
  85. package/dist/schema/nodes/building.d.ts.map +1 -1
  86. package/dist/schema/nodes/building.js +3 -2
  87. package/dist/schema/nodes/ceiling.d.ts +3 -1
  88. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  89. package/dist/schema/nodes/ceiling.js +1 -1
  90. package/dist/schema/nodes/chimney.d.ts +158 -0
  91. package/dist/schema/nodes/chimney.d.ts.map +1 -0
  92. package/dist/schema/nodes/chimney.js +67 -0
  93. package/dist/schema/nodes/column.d.ts +598 -6
  94. package/dist/schema/nodes/column.d.ts.map +1 -1
  95. package/dist/schema/nodes/column.js +586 -6
  96. package/dist/schema/nodes/cupola.d.ts +66 -0
  97. package/dist/schema/nodes/cupola.d.ts.map +1 -0
  98. package/dist/schema/nodes/cupola.js +28 -0
  99. package/dist/schema/nodes/door.d.ts +1 -1
  100. package/dist/schema/nodes/dormer.d.ts +228 -0
  101. package/dist/schema/nodes/dormer.d.ts.map +1 -0
  102. package/dist/schema/nodes/dormer.js +119 -0
  103. package/dist/schema/nodes/downspout.d.ts +75 -0
  104. package/dist/schema/nodes/downspout.d.ts.map +1 -0
  105. package/dist/schema/nodes/downspout.js +63 -0
  106. package/dist/schema/nodes/elevator.d.ts +105 -0
  107. package/dist/schema/nodes/elevator.d.ts.map +1 -0
  108. package/dist/schema/nodes/elevator.js +54 -0
  109. package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
  110. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
  111. package/dist/schema/nodes/eyebrow-vent.js +40 -0
  112. package/dist/schema/nodes/fence.d.ts +3 -2
  113. package/dist/schema/nodes/fence.d.ts.map +1 -1
  114. package/dist/schema/nodes/fence.js +4 -2
  115. package/dist/schema/nodes/gutter.d.ts +84 -0
  116. package/dist/schema/nodes/gutter.d.ts.map +1 -0
  117. package/dist/schema/nodes/gutter.js +76 -0
  118. package/dist/schema/nodes/item.d.ts +16 -0
  119. package/dist/schema/nodes/item.d.ts.map +1 -1
  120. package/dist/schema/nodes/item.js +21 -0
  121. package/dist/schema/nodes/level.d.ts +1 -1
  122. package/dist/schema/nodes/level.d.ts.map +1 -1
  123. package/dist/schema/nodes/level.js +2 -0
  124. package/dist/schema/nodes/ridge-vent.d.ts +67 -0
  125. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
  126. package/dist/schema/nodes/ridge-vent.js +30 -0
  127. package/dist/schema/nodes/roof-segment.d.ts +198 -4
  128. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  129. package/dist/schema/nodes/roof-segment.js +254 -3
  130. package/dist/schema/nodes/roof.d.ts +6 -6
  131. package/dist/schema/nodes/roof.d.ts.map +1 -1
  132. package/dist/schema/nodes/roof.js +9 -21
  133. package/dist/schema/nodes/shelf.d.ts +105 -0
  134. package/dist/schema/nodes/shelf.d.ts.map +1 -0
  135. package/dist/schema/nodes/shelf.js +84 -0
  136. package/dist/schema/nodes/site.d.ts +1 -116
  137. package/dist/schema/nodes/site.d.ts.map +1 -1
  138. package/dist/schema/nodes/site.js +2 -6
  139. package/dist/schema/nodes/skylight.d.ts +279 -0
  140. package/dist/schema/nodes/skylight.d.ts.map +1 -0
  141. package/dist/schema/nodes/skylight.js +152 -0
  142. package/dist/schema/nodes/slab.d.ts +3 -1
  143. package/dist/schema/nodes/slab.d.ts.map +1 -1
  144. package/dist/schema/nodes/slab.js +1 -1
  145. package/dist/schema/nodes/solar-panel.d.ts +117 -0
  146. package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
  147. package/dist/schema/nodes/solar-panel.js +47 -0
  148. package/dist/schema/nodes/stair-segment.d.ts +1 -1
  149. package/dist/schema/nodes/stair.d.ts +6 -6
  150. package/dist/schema/nodes/stair.d.ts.map +1 -1
  151. package/dist/schema/nodes/stair.js +9 -7
  152. package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
  153. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
  154. package/dist/schema/nodes/surface-hole-metadata.js +3 -1
  155. package/dist/schema/nodes/turbine-vent.d.ts +68 -0
  156. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
  157. package/dist/schema/nodes/turbine-vent.js +41 -0
  158. package/dist/schema/nodes/wall.d.ts +3 -3
  159. package/dist/schema/nodes/window.d.ts +1 -1
  160. package/dist/schema/types.d.ts +1448 -236
  161. package/dist/schema/types.d.ts.map +1 -1
  162. package/dist/schema/types.js +26 -0
  163. package/dist/services/alignment-anchors.d.ts +100 -0
  164. package/dist/services/alignment-anchors.d.ts.map +1 -0
  165. package/dist/services/alignment-anchors.js +283 -0
  166. package/dist/services/alignment-anchors.test.d.ts +2 -0
  167. package/dist/services/alignment-anchors.test.d.ts.map +1 -0
  168. package/dist/services/alignment-anchors.test.js +383 -0
  169. package/dist/services/alignment.d.ts +126 -0
  170. package/dist/services/alignment.d.ts.map +1 -0
  171. package/dist/services/alignment.js +169 -0
  172. package/dist/services/alignment.test.d.ts +2 -0
  173. package/dist/services/alignment.test.d.ts.map +1 -0
  174. package/dist/services/alignment.test.js +92 -0
  175. package/dist/services/drag-session.d.ts +47 -0
  176. package/dist/services/drag-session.d.ts.map +1 -0
  177. package/dist/services/drag-session.js +100 -0
  178. package/dist/services/drag-session.test.d.ts +2 -0
  179. package/dist/services/drag-session.test.d.ts.map +1 -0
  180. package/dist/services/drag-session.test.js +219 -0
  181. package/dist/services/hosting.d.ts +80 -0
  182. package/dist/services/hosting.d.ts.map +1 -0
  183. package/dist/services/hosting.js +122 -0
  184. package/dist/services/hosting.test.d.ts +2 -0
  185. package/dist/services/hosting.test.d.ts.map +1 -0
  186. package/dist/services/hosting.test.js +199 -0
  187. package/dist/services/index.d.ts +7 -0
  188. package/dist/services/index.d.ts.map +1 -0
  189. package/dist/services/index.js +6 -0
  190. package/dist/services/movement.d.ts +48 -0
  191. package/dist/services/movement.d.ts.map +1 -0
  192. package/dist/services/movement.js +65 -0
  193. package/dist/services/movement.test.d.ts +2 -0
  194. package/dist/services/movement.test.d.ts.map +1 -0
  195. package/dist/services/movement.test.js +133 -0
  196. package/dist/services/single-undo-dance.test.d.ts +2 -0
  197. package/dist/services/single-undo-dance.test.d.ts.map +1 -0
  198. package/dist/services/single-undo-dance.test.js +210 -0
  199. package/dist/services/snap.d.ts +72 -0
  200. package/dist/services/snap.d.ts.map +1 -0
  201. package/dist/services/snap.js +120 -0
  202. package/dist/services/snap.test.d.ts +2 -0
  203. package/dist/services/snap.test.d.ts.map +1 -0
  204. package/dist/services/snap.test.js +95 -0
  205. package/dist/solar-panel-presets.d.ts +17 -0
  206. package/dist/solar-panel-presets.d.ts.map +1 -0
  207. package/dist/solar-panel-presets.js +43 -0
  208. package/dist/store/actions/node-actions.d.ts +14 -2
  209. package/dist/store/actions/node-actions.d.ts.map +1 -1
  210. package/dist/store/actions/node-actions.js +499 -20
  211. package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
  212. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
  213. package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
  214. package/dist/store/actions/reparent.test.d.ts +2 -0
  215. package/dist/store/actions/reparent.test.d.ts.map +1 -0
  216. package/dist/store/actions/reparent.test.js +212 -0
  217. package/dist/store/use-interactive.d.ts +39 -0
  218. package/dist/store/use-interactive.d.ts.map +1 -1
  219. package/dist/store/use-interactive.js +75 -0
  220. package/dist/store/use-live-node-overrides.d.ts +22 -0
  221. package/dist/store/use-live-node-overrides.d.ts.map +1 -0
  222. package/dist/store/use-live-node-overrides.js +59 -0
  223. package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
  224. package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
  225. package/dist/store/use-scene-elevator-migration.test.js +109 -0
  226. package/dist/store/use-scene.d.ts +11 -0
  227. package/dist/store/use-scene.d.ts.map +1 -1
  228. package/dist/store/use-scene.js +207 -11
  229. package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
  230. package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
  231. package/dist/systems/elevator/elevator-dispatch.js +57 -0
  232. package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
  233. package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
  234. package/dist/systems/elevator/elevator-geometry.js +50 -0
  235. package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
  236. package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
  237. package/dist/systems/elevator/elevator-opening-sync.js +218 -0
  238. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
  239. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
  240. package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
  241. package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
  242. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
  243. package/dist/systems/elevator/elevator-opening-system.js +48 -0
  244. package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
  245. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
  246. package/dist/systems/elevator/elevator-runtime-system.js +8 -0
  247. package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
  248. package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
  249. package/dist/systems/elevator/elevator-runtime.js +225 -0
  250. package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
  251. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
  252. package/dist/systems/elevator/elevator-runtime.test.js +64 -0
  253. package/dist/systems/elevator/elevator-service.d.ts +19 -0
  254. package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
  255. package/dist/systems/elevator/elevator-service.js +111 -0
  256. package/dist/systems/stair/stair-footprint.d.ts +44 -0
  257. package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
  258. package/dist/systems/stair/stair-footprint.js +168 -0
  259. package/dist/systems/stair/stair-opening-preview.d.ts +1777 -0
  260. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
  261. package/dist/systems/stair/stair-opening-preview.js +93 -0
  262. package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
  263. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
  264. package/dist/systems/stair/stair-opening-preview.test.js +80 -0
  265. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  266. package/dist/systems/stair/stair-opening-sync.js +183 -185
  267. package/dist/systems/stair/stair-opening-sync.test.js +385 -4
  268. package/dist/systems/stair/stair-opening-system.d.ts +2 -0
  269. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
  270. package/dist/systems/stair/stair-opening-system.js +104 -0
  271. package/dist/systems/wall/wall-curve.d.ts +1 -1
  272. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  273. package/dist/systems/wall/wall-curve.js +1 -1
  274. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  275. package/dist/systems/wall/wall-mitering.js +2 -6
  276. package/dist/systems/wall/wall-move.d.ts +48 -0
  277. package/dist/systems/wall/wall-move.d.ts.map +1 -0
  278. package/dist/systems/wall/wall-move.js +193 -0
  279. package/dist/utils/clone-scene-graph.d.ts +6 -4
  280. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  281. package/dist/utils/clone-scene-graph.js +6 -4
  282. package/dist/utils/clone-scene-graph.test.d.ts +2 -0
  283. package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
  284. package/dist/utils/clone-scene-graph.test.js +59 -0
  285. package/dist/validation/validate-build-json.d.ts +43 -0
  286. package/dist/validation/validate-build-json.d.ts.map +1 -0
  287. package/dist/validation/validate-build-json.js +227 -0
  288. package/package.json +11 -3
  289. package/dist/materials.d.ts +0 -10
  290. package/dist/materials.d.ts.map +0 -1
  291. package/dist/materials.js +0 -22
  292. package/dist/systems/ceiling/ceiling-system.d.ts +0 -8
  293. package/dist/systems/ceiling/ceiling-system.d.ts.map +0 -1
  294. package/dist/systems/ceiling/ceiling-system.js +0 -92
  295. package/dist/systems/door/door-system.d.ts +0 -2
  296. package/dist/systems/door/door-system.d.ts.map +0 -1
  297. package/dist/systems/door/door-system.js +0 -195
  298. package/dist/systems/fence/fence-system.d.ts +0 -2
  299. package/dist/systems/fence/fence-system.d.ts.map +0 -1
  300. package/dist/systems/fence/fence-system.js +0 -187
  301. package/dist/systems/item/item-system.d.ts +0 -2
  302. package/dist/systems/item/item-system.d.ts.map +0 -1
  303. package/dist/systems/item/item-system.js +0 -48
  304. package/dist/systems/roof/roof-system.d.ts +0 -16
  305. package/dist/systems/roof/roof-system.d.ts.map +0 -1
  306. package/dist/systems/roof/roof-system.js +0 -797
  307. package/dist/systems/slab/slab-system.d.ts +0 -8
  308. package/dist/systems/slab/slab-system.d.ts.map +0 -1
  309. package/dist/systems/slab/slab-system.js +0 -214
  310. package/dist/systems/stair/stair-system.d.ts +0 -2
  311. package/dist/systems/stair/stair-system.d.ts.map +0 -1
  312. package/dist/systems/stair/stair-system.js +0 -776
  313. package/dist/systems/wall/wall-system.d.ts +0 -12
  314. package/dist/systems/wall/wall-system.d.ts.map +0 -1
  315. package/dist/systems/wall/wall-system.js +0 -455
  316. package/dist/systems/window/window-system.d.ts +0 -2
  317. package/dist/systems/window/window-system.d.ts.map +0 -1
  318. package/dist/systems/window/window-system.js +0 -131
@@ -0,0 +1,1420 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { BufferGeometry, Object3D } from 'three';
3
+ import type { ZodObject, z } from 'zod';
4
+ import type { MaterialSchema } from '../schema/material';
5
+ import type { AnyNode, AnyNodeId } from '../schema/types';
6
+ import type { HandleList } from './handles';
7
+ import type { CloneNodesIntoOptions, Subtree } from './subtree';
8
+ export type GeometryContext = {
9
+ /** Look up any node by ID. Returns undefined if the node doesn't exist. */
10
+ resolve: <N = AnyNode>(id: AnyNodeId) => N | undefined;
11
+ /** Resolved children of this node (filters out unresolvable IDs). */
12
+ children: AnyNode[];
13
+ /** Same kind, same parent — drives wall mitering / endpoint-match. */
14
+ siblings: AnyNode[];
15
+ /** Resolved parent (null for root-level nodes). */
16
+ parent: AnyNode | null;
17
+ /**
18
+ * Pre-computed level-batch data, populated by the dispatcher when the
19
+ * kind declares `def.computeLevelData`. Shared across every
20
+ * `def.geometry(node, ctx)` call in the same level batch within a
21
+ * single frame, so kinds whose geometry depends on cross-sibling
22
+ * data (wall mitering, gradient sky uniforms across a zone, etc.)
23
+ * don't pay an O(N²) recomputation cost.
24
+ *
25
+ * Typed as `unknown` at the framework boundary — kinds cast to their
26
+ * own `LevelData` shape inside `def.geometry` (the same kind owns
27
+ * both the `computeLevelData` return shape and the `geometry`
28
+ * consumer, so the cast is internal). Only populated for `def.
29
+ * geometry` calls today; not used by `def.floorplan` (which already
30
+ * has cheap access to siblings through `ctx.siblings`).
31
+ */
32
+ levelData?: unknown;
33
+ /**
34
+ * Optional view state — only populated for `def.floorplan` builders. The
35
+ * 2D floor-plan layer surfaces selection / hover here so kinds can vary
36
+ * their output (themed stroke when selected, endpoint dots when
37
+ * selected, hatch overlay, hover-side highlight). For `def.geometry`
38
+ * (3D) this is always undefined — the 3D selection outline is handled
39
+ * by the merged-outline post-process pass instead.
40
+ */
41
+ viewState?: {
42
+ selected: boolean;
43
+ /** Marquee or programmatic highlight — shows selected chrome without keyboard focus. */
44
+ highlighted: boolean;
45
+ /** Pointer-hovered. */
46
+ hovered: boolean;
47
+ /**
48
+ * True while this node is the target of an active 2D move (i.e.
49
+ * `useEditor.movingNode === node`). Used by kinds whose move
50
+ * preview includes extra chrome — e.g. door / window emit
51
+ * dimension lines showing the distance to adjacent openings or
52
+ * wall ends only during the move.
53
+ */
54
+ moving: boolean;
55
+ /**
56
+ * The kind's theme palette. Theme-aware colors (selection stroke,
57
+ * endpoint handle fill, hatch color) live here so kinds don't need
58
+ * to import `useViewer.theme` themselves.
59
+ */
60
+ palette: FloorplanPalette;
61
+ };
62
+ };
63
+ export type FloorplanPalette = {
64
+ selectedStroke: string;
65
+ selectedFill: string;
66
+ /** Hatch / cross-stroke color used for selected fills with patterns. */
67
+ selectedHatch: string;
68
+ /**
69
+ * Stroke colour applied to a wall (and fence by analogy) when the
70
+ * pointer hovers it. Light blue in the legacy palette — distinct from
71
+ * the orange endpoint-handle hover so the body and its handles can
72
+ * both glow independently. Pass through `viewState.palette.wall
73
+ * HoverStroke` in `def.floorplan` when `viewState.hovered === true`
74
+ * and the node isn't selected.
75
+ */
76
+ wallHoverStroke: string;
77
+ endpointHandleFill: string;
78
+ endpointHandleStroke: string;
79
+ endpointHandleHoverStroke: string;
80
+ endpointHandleActiveFill: string;
81
+ endpointHandleActiveStroke: string;
82
+ /**
83
+ * Curve sagitta handle slot — distinct teal colour-set the legacy
84
+ * `FloorplanWallCurveLayer` uses so users can tell endpoint dots
85
+ * (orange) and curve dots (teal) apart at a glance.
86
+ */
87
+ curveHandleFill: string;
88
+ curveHandleStroke: string;
89
+ curveHandleHoverStroke: string;
90
+ measurementStroke: string;
91
+ measurementLabelBackground: string;
92
+ measurementLabelText: string;
93
+ };
94
+ export type FloorplanPoint = readonly [x: number, y: number];
95
+ export type FloorplanStyle = {
96
+ stroke?: string;
97
+ fill?: string;
98
+ strokeWidth?: number;
99
+ strokeDasharray?: string;
100
+ opacity?: number;
101
+ /**
102
+ * When `'non-scaling-stroke'`, the SVG renderer interprets `strokeWidth`
103
+ * as a constant screen-pixel width regardless of viewport zoom. Maps
104
+ * straight to the SVG `vector-effect` attribute. Default (undefined)
105
+ * treats `strokeWidth` as plan-unit metres.
106
+ *
107
+ * Kinds that emit hand-drawn-looking strokes (fence body, wall hairlines,
108
+ * post markers) want non-scaling so the visual weight stays stable as
109
+ * the user zooms. Kinds whose stroke represents a real-world thickness
110
+ * (wall body in floor plan, slab outline) leave it undefined.
111
+ */
112
+ vectorEffect?: 'non-scaling-stroke';
113
+ strokeLinecap?: 'butt' | 'round' | 'square';
114
+ strokeLinejoin?: 'miter' | 'round' | 'bevel';
115
+ strokeOpacity?: number;
116
+ fillOpacity?: number;
117
+ /**
118
+ * SVG `pointer-events`. Default (undefined) lets the renderer pick its
119
+ * normal behaviour — `visiblePainted` for filled shapes, `stroke` for
120
+ * line / hit-line. Set `'none'` to make a primitive completely
121
+ * passthrough — useful for chrome that should be visible but never
122
+ * trigger selection or drag (e.g. a wall's body once it's already
123
+ * selected, where only the side-arrows / corner handles should grab
124
+ * the pointer).
125
+ */
126
+ pointerEvents?: 'none' | 'auto' | 'all' | 'stroke' | 'fill' | 'visible' | 'visiblePainted';
127
+ /**
128
+ * CSS `cursor` for the rendered primitive. Defaults to inheriting the
129
+ * registry entry wrapper's `cursor: 'pointer'`. Override to neutralise
130
+ * a hover affordance — e.g. a selected wall body that catches the
131
+ * pointer (to block fall-through to the slab below) but should not
132
+ * advertise itself as a drag target.
133
+ */
134
+ cursor?: string;
135
+ };
136
+ export type ToolHint = {
137
+ /** Key combo or input label, e.g. 'Left click', 'Shift', 'Esc'. */
138
+ key: string;
139
+ /** Description of what the input does. Sentence case. */
140
+ label: string;
141
+ };
142
+ export type FloorplanGeometry = ({
143
+ kind: 'path';
144
+ d: string;
145
+ } & FloorplanStyle) | ({
146
+ kind: 'polygon';
147
+ points: readonly FloorplanPoint[];
148
+ } & FloorplanStyle) | ({
149
+ kind: 'polyline';
150
+ points: readonly FloorplanPoint[];
151
+ } & FloorplanStyle) | ({
152
+ kind: 'rect';
153
+ x: number;
154
+ y: number;
155
+ width: number;
156
+ height: number;
157
+ rx?: number;
158
+ ry?: number;
159
+ } & FloorplanStyle) | ({
160
+ kind: 'circle';
161
+ cx: number;
162
+ cy: number;
163
+ r: number;
164
+ } & FloorplanStyle) | ({
165
+ kind: 'line';
166
+ x1: number;
167
+ y1: number;
168
+ x2: number;
169
+ y2: number;
170
+ } & FloorplanStyle)
171
+ /**
172
+ * Plain SVG text in plan space. Used for short labels that need to
173
+ * sit at a specific plan coordinate — e.g. the elevator served-level
174
+ * chips' floor numbers. Rotates with the floor plan's transform
175
+ * (same as polygon coordinates) so it shares the building's
176
+ * orientation. For text that needs to stay screen-upright regardless
177
+ * of plan rotation, use `dimension-label` instead (it auto-flips
178
+ * upside-down labels).
179
+ *
180
+ * `fontSize` is in plan metres — typical values are 0.1–0.2m. The
181
+ * registry layer doesn't apply any text-rendering chrome (no plate,
182
+ * no rotation auto-flip) — it's just a styled `<text>` element.
183
+ */
184
+ | {
185
+ kind: 'text';
186
+ x: number;
187
+ y: number;
188
+ text: string;
189
+ fontSize: number;
190
+ fill?: string;
191
+ fontWeight?: number | string;
192
+ fontFamily?: string;
193
+ textAnchor?: 'start' | 'middle' | 'end';
194
+ dominantBaseline?: 'auto' | 'middle' | 'central' | 'hanging' | 'alphabetic';
195
+ opacity?: number;
196
+ /**
197
+ * Outlined-text styling — when `stroke` is set the renderer applies
198
+ * `stroke` / `strokeWidth` plus `paintOrder='stroke'` so the stroke
199
+ * is drawn under the fill. Used by zone name labels for the
200
+ * "white text inside a colored outline" look that stays legible
201
+ * against any fill color.
202
+ */
203
+ stroke?: string;
204
+ strokeWidth?: number;
205
+ paintOrder?: 'stroke' | 'fill' | 'normal';
206
+ /**
207
+ * When true, the registry layer counter-rotates the label by
208
+ * `sceneRotationDeg` so it reads horizontally on screen regardless
209
+ * of the floor-plan's scene rotation (default 90°).
210
+ */
211
+ upright?: boolean;
212
+ }
213
+ /**
214
+ * Bitmap overlay — captured top-down asset thumbnail, AI-generated
215
+ * floor-plan symbol, scan slice, etc. `url` is passed through the
216
+ * editor's `loadAssetUrl` resolver (handles CDN / Supabase storage),
217
+ * so kinds emit the raw `asset.floorPlanUrl` and don't worry about
218
+ * fetching.
219
+ *
220
+ * `rotation` is in radians around `center`. The image is drawn at
221
+ * `center` with size `width × height` in plan-local metres;
222
+ * `preserveAspectRatio` controls letterboxing (default
223
+ * `'xMidYMid meet'`).
224
+ */
225
+ | {
226
+ kind: 'image';
227
+ url: string;
228
+ center: FloorplanPoint;
229
+ width: number;
230
+ height: number;
231
+ rotation?: number;
232
+ preserveAspectRatio?: string;
233
+ opacity?: number;
234
+ } | {
235
+ kind: 'group';
236
+ children: FloorplanGeometry[];
237
+ /** Optional transform applied to all children. Rotation in radians. */
238
+ transform?: {
239
+ translate?: FloorplanPoint;
240
+ rotate?: number;
241
+ };
242
+ }
243
+ /**
244
+ * Hatched fill overlay — same polygon shape as the kind's main fill but
245
+ * stroked with diagonal lines on top. Used for the selected-wall hatch
246
+ * effect from the legacy floor-plan panel. The 2D layer mounts a
247
+ * shared `<pattern>` in `<defs>` and references it via `fill=url(...)`.
248
+ */
249
+ | {
250
+ kind: 'hatch';
251
+ points: readonly FloorplanPoint[];
252
+ color: string;
253
+ opacity?: number;
254
+ }
255
+ /**
256
+ * Transparent click-detection segment. Sits on top of the kind's main
257
+ * geometry with a wide stroke so the user doesn't need to pixel-hunt
258
+ * the polygon. `select` is the only affordance for now — clicking
259
+ * triggers selection of the owning node.
260
+ */
261
+ | {
262
+ kind: 'hit-line';
263
+ x1: number;
264
+ y1: number;
265
+ x2: number;
266
+ y2: number;
267
+ /** Stroke width in screen pixels — converted to plan units by the dispatcher. */
268
+ strokeWidthPx: number;
269
+ cursor?: string;
270
+ /**
271
+ * Override the default `pointer-events="stroke"`. Use `'none'` when
272
+ * a kind wants to keep the line painted (for hit-debugging or layout
273
+ * stability) but route grabs through other affordances instead.
274
+ */
275
+ pointerEvents?: 'none' | 'stroke' | 'auto';
276
+ }
277
+ /**
278
+ * Endpoint manipulation handle — the 5-circle stack from the legacy
279
+ * floor-plan: outer hover glow ring + hover ring + filled outer +
280
+ * inner dot + transparent hit. Rendered with theme-aware colors from
281
+ * `viewState.palette`. `affordance` keys into a kind-owned drag flow
282
+ * the dispatcher invokes; `payload` is opaque kind data the
283
+ * affordance handler unpacks.
284
+ */
285
+ | {
286
+ kind: 'endpoint-handle';
287
+ point: FloorplanPoint;
288
+ /** `active` = currently being dragged; `idle` = visible but inert. */
289
+ state: 'idle' | 'active';
290
+ /**
291
+ * Visual colour-set. `'endpoint'` (default) → orange — wall /
292
+ * fence endpoints, polygon vertices. `'curve'` → teal — the
293
+ * sagitta midpoint handle. Other values are reserved for future
294
+ * affordances (rotation, scale) without expanding the union.
295
+ */
296
+ variant?: 'endpoint' | 'curve';
297
+ affordance: string;
298
+ payload: unknown;
299
+ }
300
+ /**
301
+ * Smaller "insert here" handle drawn between two polygon vertices.
302
+ * Visually a small white dot with a `+` icon; hover-expanded. Triggers
303
+ * an affordance that typically inserts a new vertex at the midpoint
304
+ * and then drags it (matches the legacy slab / ceiling boundary
305
+ * editor's edge-midpoint behaviour).
306
+ */
307
+ | {
308
+ kind: 'midpoint-handle';
309
+ point: FloorplanPoint;
310
+ affordance: string;
311
+ payload: unknown;
312
+ }
313
+ /**
314
+ * Hit-target along an entire polygon edge. Renders as a transparent
315
+ * wide stroke for click detection; the dispatcher overlays a glow +
316
+ * solid stroke when hovered or actively being dragged. Used by the
317
+ * slab / ceiling boundary editor's "drag whole edge perpendicular"
318
+ * affordance — both endpoints translate together along the edge
319
+ * normal.
320
+ */
321
+ | {
322
+ kind: 'edge-handle';
323
+ x1: number;
324
+ y1: number;
325
+ x2: number;
326
+ y2: number;
327
+ affordance: string;
328
+ payload: unknown;
329
+ }
330
+ /**
331
+ * "Grab to move" handle drawn at a node's centroid — the orange dot
332
+ * users click-and-drag to move a door / window / item in the
333
+ * floorplan without going through the inspector's Move button.
334
+ *
335
+ * Pointer-down on the handle sets `useEditor.movingNode` to the
336
+ * owning node, which `FloorplanRegistryMoveOverlay` picks up and
337
+ * routes through the kind's `def.floorplanMoveTarget`. So both
338
+ * entry points (Move button + dot grab) share the same move
339
+ * pipeline — no parallel kind-side logic.
340
+ */
341
+ | {
342
+ kind: 'move-handle';
343
+ point: FloorplanPoint;
344
+ }
345
+ /**
346
+ * Directional move handle drawn as an arrow pointing AWAY from the
347
+ * owning node, rotated by `angle` (radians; 0 = +x). Used by wall to
348
+ * place two arrows on perpendicular sides at the wall midpoint —
349
+ * mirrors the 3D `WallMoveSideHandles`. Routes through the same
350
+ * `onMoveHandlePointerDown` → `setMovingNode` path as `move-handle`.
351
+ */
352
+ | {
353
+ kind: 'move-arrow';
354
+ point: FloorplanPoint;
355
+ /** Rotation in radians; 0 points along +x in plan coords. */
356
+ angle: number;
357
+ /**
358
+ * Optional affordance routing. When set, pointer-down on the arrow
359
+ * starts a `def.floorplanAffordances?.[affordance]` session with the
360
+ * given `payload` (same dispatch path as `edge-handle`) instead of
361
+ * the default `setMovingNode` flow. Used by doors for the in-plane
362
+ * width-resize handles that visually mirror the move arrow shape but
363
+ * drive a different mutation.
364
+ */
365
+ affordance?: string;
366
+ payload?: unknown;
367
+ }
368
+ /**
369
+ * Curved two-headed rotation arrow — the 2D counterpart of the 3D
370
+ * `arc-resize` handle's `shape: 'rotate'` gizmo. Visually a short arc
371
+ * with arrowheads at each end pointing tangentially in opposite
372
+ * directions, so it reads as "rotate either way" rather than "drag
373
+ * along a line." Always routes through an affordance (rotation has no
374
+ * sensible default Move semantics).
375
+ *
376
+ * `angle` is the radial-outward direction in plan coords — the icon's
377
+ * local +X axis points away from the pivot, with the arc curving
378
+ * around it. Emitters typically compute this as
379
+ * `atan2(handle.y − pivot.y, handle.x − pivot.x)`.
380
+ */
381
+ | {
382
+ kind: 'rotate-arrow';
383
+ point: FloorplanPoint;
384
+ /** Radial-outward direction from the rotation pivot, in radians. */
385
+ angle: number;
386
+ affordance: string;
387
+ payload?: unknown;
388
+ /**
389
+ * Rotation pivot (plan coords) this handle turns the node around.
390
+ * When present, the floor-plan layer draws a live angle wedge + degree
391
+ * readout swept from grab to the current pointer bearing during the
392
+ * drag — the 2D twin of the 3D rotate gizmo's readout. Emitters that
393
+ * already compute the pivot to place the handle should pass it through.
394
+ */
395
+ pivot?: FloorplanPoint;
396
+ }
397
+ /**
398
+ * Centered length / distance label. Renders as a small rounded
399
+ * background plate with text, oriented along `angle` (radians). The
400
+ * 2D layer flips the label upright when it would otherwise be upside
401
+ * down. Use this for simple "what length am I?" badges (fence, item
402
+ * width, draft preview).
403
+ */
404
+ | {
405
+ kind: 'dimension-label';
406
+ cx: number;
407
+ cy: number;
408
+ text: string;
409
+ /** Rotation in radians. The renderer auto-flips to keep text upright. */
410
+ angle: number;
411
+ }
412
+ /**
413
+ * Architect's dimension overlay — extension lines from the edge
414
+ * endpoints out past the dimension line, two dimension line halves
415
+ * with the label sitting in the gap, end ticks perpendicular to the
416
+ * line. Used for the selected wall's full measurement; the rounded
417
+ * plate label is the wrong shape when you want plan-drawing chrome.
418
+ *
419
+ * The renderer computes the segment geometry from these inputs so the
420
+ * kind only needs to know "where is the edge and which way does the
421
+ * dimension line offset." `offsetNormal` is a unit vector
422
+ * perpendicular to the edge; pass the *outward* normal so the line
423
+ * sits on the side facing away from the wall interior.
424
+ */
425
+ | {
426
+ kind: 'dimension';
427
+ start: FloorplanPoint;
428
+ end: FloorplanPoint;
429
+ /** Outward-pointing unit normal — the dimension line offsets along this. */
430
+ offsetNormal: FloorplanPoint;
431
+ /** Distance (plan units) from the edge to the dimension line. */
432
+ offsetDistance: number;
433
+ /** How far past the offset point the extension line continues. */
434
+ extensionOvershoot: number;
435
+ text: string;
436
+ /** Optional override for the line/text colour. Defaults to the palette accent. */
437
+ stroke?: string;
438
+ };
439
+ export type FloorplanAffordancePoint = readonly [x: number, y: number];
440
+ export type FloorplanAffordanceModifiers = {
441
+ shiftKey: boolean;
442
+ altKey: boolean;
443
+ ctrlKey: boolean;
444
+ metaKey: boolean;
445
+ };
446
+ export type FloorplanAffordanceSession = {
447
+ /** Node IDs the drag may mutate. Used by the dispatcher for the snapshot. */
448
+ affectedIds: AnyNodeId[];
449
+ /**
450
+ * Run a single drag tick. Two patterns are supported:
451
+ * - **Scene-write preview**: implementation calls `scene.updateNodes`
452
+ * each tick; the dispatcher captures a pre-drag snapshot and runs
453
+ * a single-undo dance on commit (revert → resume → re-apply diff).
454
+ * Suitable for affordances whose commit is a pure diff of the
455
+ * affected fields.
456
+ * - **Live-override preview**: implementation publishes per-frame
457
+ * overrides to `useLiveNodeOverrides` (or another preview store);
458
+ * `useScene` stays untouched during the drag. The session must
459
+ * also expose `commit()` below, since there's no scene diff for
460
+ * the dispatcher to write back.
461
+ *
462
+ * Snap logic, linked-node cascade, and angle locking live here.
463
+ */
464
+ apply(args: {
465
+ planPoint: FloorplanAffordancePoint;
466
+ modifiers: FloorplanAffordanceModifiers;
467
+ }): void;
468
+ /**
469
+ * Called on pointer-up. Return `true` if the drag should commit;
470
+ * `false` reverts to the snapshot (e.g. wall too short, vertex
471
+ * collapsed onto neighbour).
472
+ */
473
+ canCommit(): boolean;
474
+ /**
475
+ * Optional atomic-commit hook — mirror of the same field on
476
+ * `FloorplanMoveTargetSession`. When present, the dispatcher
477
+ * reverts to the pre-drag baseline (no-op if `apply()` never wrote
478
+ * to scene), resumes history, then calls `commit()` instead of
479
+ * re-applying a diff. The session owns the full final write
480
+ * (typically `applyNodeChanges` or `updateNodes`) plus clearing any
481
+ * live overrides it published in `apply()`.
482
+ */
483
+ commit?(): void;
484
+ };
485
+ export type FloorplanAffordance<N> = {
486
+ start(args: {
487
+ node: N;
488
+ /** Opaque kind-specific payload from the handle primitive. */
489
+ payload: unknown;
490
+ /** Current scene snapshot at drag start. */
491
+ nodes: Record<AnyNodeId, AnyNode>;
492
+ /** Initial pointer position in plan coordinates. */
493
+ initialPlanPoint: FloorplanAffordancePoint;
494
+ /** Active editor grid step in meters. */
495
+ gridSnapStep: number;
496
+ }): FloorplanAffordanceSession;
497
+ };
498
+ export type FloorplanMoveTargetSession = {
499
+ /** Node IDs the move may mutate. Used by the dispatcher for snapshot capture. */
500
+ affectedIds: AnyNodeId[];
501
+ /**
502
+ * Single move-preview tick. Implementations call `scene.updateNodes`
503
+ * directly to drive the live preview (no separate draft state).
504
+ */
505
+ apply(args: {
506
+ planPoint: FloorplanAffordancePoint;
507
+ modifiers: FloorplanAffordanceModifiers;
508
+ }): void;
509
+ /**
510
+ * Called on pointer-up. Return `true` to commit the current scene
511
+ * state; `false` reverts to the snapshot (e.g. dropped in invalid
512
+ * area, overlap detected, ...).
513
+ */
514
+ canCommit(): boolean;
515
+ /**
516
+ * Optional atomic-commit hook. The default overlay path snapshots
517
+ * each affected node before drag and writes a diff back on commit —
518
+ * fine for kinds whose commit is a pure position update, but
519
+ * insufficient when commit needs to also create or delete nodes
520
+ * (e.g. wall move emits bridge wall creates + collapsed wall deletes
521
+ * via `planWallMoveJunctions`).
522
+ *
523
+ * When present, the overlay reverts to the pre-drag baseline,
524
+ * resumes history, and calls `commit()` instead of the default
525
+ * `updateNodes(finalUpdates)`. The session is responsible for the
526
+ * full final write (typically `applyNodeChanges`) plus any
527
+ * post-commit selection / metadata. The overlay still emits the
528
+ * standard place SFX and clears `movingNode` after `commit()`
529
+ * returns.
530
+ */
531
+ commit?(): void;
532
+ };
533
+ export type FloorplanMoveTarget<N> = (args: {
534
+ node: N;
535
+ nodes: Record<AnyNodeId, AnyNode>;
536
+ }) => FloorplanMoveTargetSession;
537
+ export type Plugin = {
538
+ id: string;
539
+ apiVersion: 1;
540
+ nodes?: AnyNodeDefinition[];
541
+ };
542
+ export type AnyNodeDefinition = NodeDefinition<ZodObject<any>>;
543
+ export type SurfaceRole = 'wall' | 'floor' | 'ceiling' | 'roof' | 'joinery' | 'glazing' | 'furnishing';
544
+ export type NodeDefinition<S extends ZodObject<any>> = {
545
+ kind: string;
546
+ schemaVersion: number;
547
+ schema: S;
548
+ category: NodeCategory;
549
+ surfaceRole?: SurfaceRole;
550
+ defaults: () => Omit<z.infer<S>, 'id' | 'type'>;
551
+ migrate?: Record<number, (old: unknown) => unknown>;
552
+ capabilities: Capabilities;
553
+ relations?: Relations;
554
+ parametrics?: ParametricDescriptor<z.infer<S>>;
555
+ /**
556
+ * Renderer for this kind. Optional under the three-checkbox composition
557
+ * model (see `wiki/architecture/node-definitions.md`): when omitted, the
558
+ * framework mounts a generic empty-group renderer that the per-kind
559
+ * geometry/system fills. Required today only because the generic
560
+ * renderer is not yet implemented — Phase 4 lands it, then this field
561
+ * becomes truly optional at runtime too. Making the type optional now so
562
+ * milestone-A skeletons (like wall) can compile before their runtime
563
+ * port; downstream consumers (`<NodeRenderer>`, `RegisteredSystems`)
564
+ * already null-guard on `def.renderer` so omitting it is safe.
565
+ */
566
+ renderer?: RendererSource<z.infer<S>>;
567
+ /**
568
+ * Pure geometry builder. When set, the framework's generic
569
+ * `<GeometrySystem>` calls this on every dirty mark — `nodes` keyed by
570
+ * `def.geometry`'s presence are picked up; the returned `Object3D`'s
571
+ * children replace the registered group's children. Together with
572
+ * `<ParametricNodeRenderer>` this lets a kind ship without per-kind
573
+ * `renderer.tsx` or `system.tsx` files (see
574
+ * `wiki/architecture/node-definitions.md`). Combine with `renderer` if
575
+ * you want JSX-side composition (drei, `<Html>`, GLB) AND parametric
576
+ * rebuilds; combine with `system` if you also need per-frame imperative
577
+ * work (animations, named-mesh material poking).
578
+ */
579
+ geometry?: (node: z.infer<S>, ctx: GeometryContext) => Object3D;
580
+ /**
581
+ * Optional cache key over the geometry-relevant inputs of `node`. When
582
+ * set, `<GeometrySystem>` skips the rebuild (dispose + re-create the
583
+ * group's children) if the key is unchanged since the last build for
584
+ * this node — even though the node was marked dirty. Use for kinds whose
585
+ * geometry depends *only* on their own fields (not on `children`,
586
+ * `position`, neighbours, or `ctx`): a hosted child reparenting onto a
587
+ * shelf, say, dirties the shelf but doesn't change its boards, so without
588
+ * this the boards needlessly remount and any pointer hover churns
589
+ * (enter/leave) as the meshes are swapped. Must NOT be set for kinds with
590
+ * neighbour-dependent geometry (e.g. wall/fence miters via `ctx`), whose
591
+ * inputs aren't captured by the node alone.
592
+ */
593
+ geometryKey?: (node: z.infer<S>) => string;
594
+ /**
595
+ * Level-batch precompute hook. Called by `<GeometrySystem>` once per
596
+ * level per frame, **before** the per-node `def.geometry` calls in
597
+ * that batch. The result lands in `ctx.levelData` for every node in
598
+ * the same level.
599
+ *
600
+ * Used by kinds whose geometry depends on cross-sibling data that
601
+ * would be O(N²) to recompute per node:
602
+ * - wall: `calculateLevelMiters(walls)` — every wall's mesh
603
+ * reads its junctions from the level-wide miter graph.
604
+ * - zone (planned): shared TSL gradient uniforms.
605
+ *
606
+ * `siblings` is every node of this kind in the same level (including
607
+ * the dirty ones). The dispatcher de-duplicates per level so this
608
+ * runs once even when many walls are dirty in the same frame.
609
+ */
610
+ computeLevelData?: (siblings: ReadonlyArray<z.infer<S>>) => unknown;
611
+ /**
612
+ * Pure 2D builder for floor-plan rendering. Mirrors `geometry` but emits
613
+ * plain `FloorplanGeometry` data (SVG-renderable) rather than three.js
614
+ * Object3D. Coordinates are level-local meters — the floor-plan panel
615
+ * applies the world→SVG transform.
616
+ *
617
+ * Returns `null` when the kind shouldn't appear in floor plan (e.g. an
618
+ * invisible utility node, or a kind that's 3D-only). Kinds that need
619
+ * floor-plan rendering but no 3D mesh set `floorplan` without `geometry`.
620
+ *
621
+ * See `wiki/architecture/node-definitions.md` ("floor-plan rendering"
622
+ * section) and Phase 5 of the registry plan for the migration plan off
623
+ * the legacy `floorplan-panel.tsx` monolith.
624
+ */
625
+ floorplan?: (node: z.infer<S>, ctx: GeometryContext) => FloorplanGeometry | null;
626
+ /**
627
+ * Which scope the floor-plan layer walks to find instances of this
628
+ * kind. Default `'level'` — the layer's DFS from the active level id
629
+ * picks the node up via its parent chain. `'building'` — the kind
630
+ * lives as a sibling of levels (elevator is the canonical example:
631
+ * elevators are parented to the *building*, not a level, but the
632
+ * floor-plan should still surface them for every level inside that
633
+ * building). For `'building'`-scoped kinds the layer iterates every
634
+ * instance whose parent matches the active level's building, and
635
+ * synthesises a `GeometryContext` whose `parent` is the active level.
636
+ */
637
+ floorplanScope?: 'level' | 'building';
638
+ /**
639
+ * 2D drag affordances keyed by the string identifier emitted on
640
+ * `endpoint-handle` (and similar interactive floor-plan primitives) via
641
+ * the `affordance` field. The floor-plan registry layer calls
642
+ * `def.floorplanAffordances?.[affordance].start({...})` on pointer-down,
643
+ * receives a session, calls `apply(...)` on pointer-move and
644
+ * `commit()` / `cancel()` on pointer-up / pointer-cancel. The session
645
+ * mutates scene state directly during `apply`; the dispatcher handles
646
+ * the snapshot + single-undo dance around it.
647
+ *
648
+ * Mirrors the existing 3D `affordanceTools` map but for 2D SVG events,
649
+ * and operates on plain JS data instead of mounting React. Kinds with
650
+ * both 3D and 2D affordances expose both fields — they're independent.
651
+ */
652
+ floorplanAffordances?: Record<string, FloorplanAffordance<z.infer<S>>>;
653
+ /**
654
+ * Kind-specific 2D move handler for `useEditor.movingNode`-driven
655
+ * placement in the floor plan. When set, `FloorplanRegistryMove
656
+ * Overlay` invokes this once when `movingNode` becomes a node of
657
+ * this kind, and drives the session through pointer events until
658
+ * pointer-up / Esc. Falls back to the generic free-floating
659
+ * translate when unset.
660
+ *
661
+ * Use this for kinds whose move semantics are anchor-aware:
662
+ * doors / windows need wall hits + reparenting; items with
663
+ * `attachTo` need parent-surface hits. Kinds with simple
664
+ * translate-on-XZ semantics (shelf, spawn, fence) leave this
665
+ * unset and rely on the generic overlay path.
666
+ */
667
+ floorplanMoveTarget?: FloorplanMoveTarget<z.infer<S>>;
668
+ /**
669
+ * Optional hook letting a kind project the `useLiveNodeOverrides` map
670
+ * into a fresh `nodes` snapshot before its `def.floorplan` builder
671
+ * runs. The floor-plan layer calls this when present and passes the
672
+ * returned map both as the builder's `ctx` source AND as the
673
+ * effective node (so the kind's own override lands in `effectiveNode`).
674
+ *
675
+ * Used by wall, whose miter joins read sibling walls via
676
+ * `ctx.siblings`: during a 2D drag the moved wall + its linked
677
+ * neighbours publish per-frame `{ start, end, curveOffset }`
678
+ * overrides, and the floor-plan must merge those into every wall
679
+ * the builder can see — otherwise miter math snaps back to the
680
+ * committed positions while the cursor moves. Kinds whose previews
681
+ * are self-contained leave this unset and the layer hands the raw
682
+ * `nodes` through.
683
+ *
684
+ * Return the input `nodes` unchanged when no override is relevant
685
+ * so the caller can short-circuit.
686
+ */
687
+ floorplanSiblingOverrides?: (args: {
688
+ nodeId: AnyNodeId;
689
+ nodes: Record<AnyNodeId, AnyNode>;
690
+ liveOverrides: Map<string, Record<string, unknown>>;
691
+ }) => Record<AnyNodeId, AnyNode>;
692
+ system?: SystemContribution;
693
+ tool?: LazyComponent;
694
+ /**
695
+ * Stage-D drag-affordance components — one per kind-owned editor mode
696
+ * triggered by `useEditor` state. Component receives `{ node }` as its
697
+ * sole prop. Lazy-loaded by ToolManager when the corresponding editor
698
+ * state activates (e.g. `curvingFence` → `affordanceTools.curve`).
699
+ *
700
+ * Each component is the thin React wrapper around a pure DragAction
701
+ * primitive that lives in the kind's `actions/` folder. The split keeps
702
+ * the action data unit-testable while letting the wrapper consume
703
+ * `useDragAction` + cursor visuals.
704
+ *
705
+ * Generic record so per-kind state names don't need to land in the
706
+ * core type system. ToolManager looks up by string key.
707
+ */
708
+ affordanceTools?: Record<string, () => Promise<{
709
+ default: ComponentType<any>;
710
+ }>>;
711
+ affordances?: Affordance<z.infer<S>>[];
712
+ /**
713
+ * Contextual shortcut hints shown by `HelperManager` when this kind's
714
+ * tool is active. Pure data — `HelperManager` renders these via a
715
+ * generic <RegisteredToolHelper>. Drops the need for a hand-written
716
+ * `<XxxHelper>` component per kind.
717
+ *
718
+ * Static array for now (covers ~all current uses). If a kind needs
719
+ * state-dependent hints (e.g. different keys during a drag), it keeps
720
+ * its bespoke helper component instead.
721
+ */
722
+ toolHints?: ToolHint[];
723
+ /**
724
+ * Optional translucent preview of the node — used by the move tool to
725
+ * show where the node will land, and by the placement tool's cursor.
726
+ * Receives the partially-resolved node (or a default-shaped stub during
727
+ * placement before any commit has happened). Phase 4 may merge this with
728
+ * the renderer behind an `opacity` prop.
729
+ */
730
+ preview?: () => Promise<{
731
+ default: ComponentType<{
732
+ node: z.infer<S>;
733
+ }>;
734
+ }>;
735
+ presentation?: Presentation;
736
+ mcp?: McpOverrides;
737
+ /**
738
+ * Optional keyboard shortcut handlers contributed by the kind. The
739
+ * editor's keyboard hook looks these up by event name (`r` for R /
740
+ * Shift+R, `t` for T / Shift+T) and runs the matching handler when
741
+ * the user presses that key with a single node of this kind
742
+ * selected. The fallback rotation behaviour kicks in only when the
743
+ * action's `appliesTo` returns false.
744
+ *
745
+ * Replaces editor-side per-kind switches in `use-keyboard.ts` — a
746
+ * kind that wants to override R / T just sets this field instead of
747
+ * extending a hand-written `if/else` chain. Door / window are
748
+ * legacy direct calls today (follow-up: migrate them under this
749
+ * capability too).
750
+ */
751
+ keyboardActions?: KeyboardActions;
752
+ /**
753
+ * In-world resize / move arrows shown when this kind is selected.
754
+ *
755
+ * Pure descriptors — no React, no Three.js. The editor's generic
756
+ * `<NodeArrowHandles>` reads this list and mounts the matching arrow
757
+ * components with shared drag plumbing, replacing per-kind
758
+ * `<XxxSideHandles>` files for the common cases.
759
+ *
760
+ * Static array, or a function for shape-dependent affordances
761
+ * (column `crossSection` / `supportStyle`, stair-segment `segmentType`,
762
+ * curved-vs-straight stairs). See `./handles.ts` for the variant union.
763
+ *
764
+ * Bespoke chrome that doesn't fit the descriptor model (wall corner
765
+ * leader dashes, fence curving, items with `attachTo`) stays as a
766
+ * custom React component mounted alongside.
767
+ */
768
+ handles?: HandleList<z.infer<S>>;
769
+ };
770
+ export type NodeCategory = 'site' | 'structure' | 'furnish' | 'analysis' | 'utility';
771
+ export type KeyboardActions = {
772
+ /** R / Shift+R primary action. */
773
+ r?: KeyboardAction;
774
+ /** T / Shift+T secondary action. */
775
+ t?: KeyboardAction;
776
+ };
777
+ export type KeyboardAction = {
778
+ /**
779
+ * Predicate that gates the action. Return `false` when the
780
+ * keystroke should fall through to the editor's default behaviour
781
+ * for this kind (typically rotation). Skylight uses this to short-
782
+ * circuit the action for non-operable type variants.
783
+ */
784
+ appliesTo: (node: AnyNode) => boolean;
785
+ /**
786
+ * Run the action. The editor handles `preventDefault` and the
787
+ * shared sfx — the handler should only touch scene / interactive
788
+ * state.
789
+ */
790
+ run: (node: AnyNode) => void;
791
+ };
792
+ /**
793
+ * UI metadata for surfacing a node kind in the tool palette and elsewhere.
794
+ * Phase 4 ships the consumer (auto-derived palette buttons); definitions can
795
+ * declare this from Phase 2 onward so the spike's `column` and `shelf` show up
796
+ * correctly the moment the palette consumes the registry.
797
+ */
798
+ export type Presentation = {
799
+ /** Sentence-case label shown in palette buttons, breadcrumbs, etc. */
800
+ label: string;
801
+ /** Optional longer tooltip / help text. */
802
+ description?: string;
803
+ /** Icon for palette buttons and tree views. */
804
+ icon: IconRef;
805
+ /** Tool palette section. Defaults to `category` when omitted. */
806
+ paletteSection?: 'site' | 'structure' | 'furnish';
807
+ /** Sort key within a palette section; lower numbers come first. */
808
+ paletteOrder?: number;
809
+ /** Set true for kinds that exist but should NOT appear in the palette
810
+ * (containers like `site`/`building`/`level`, internal nodes). */
811
+ hidden?: boolean;
812
+ };
813
+ export type IconRef =
814
+ /** Iconify identifier, e.g. `lucide:square`. Matches the @iconify-react
815
+ * setup the editor app already uses for tool icons. */
816
+ {
817
+ kind: 'iconify';
818
+ name: string;
819
+ }
820
+ /** URL path to a raster or vector asset (PNG/SVG/...). Matches the
821
+ * palette's PNG/SVG assets — use this to share the same artwork
822
+ * between the bottom toolbar and the inspector title. */
823
+ | {
824
+ kind: 'url';
825
+ src: string;
826
+ }
827
+ /** Inline SVG path data. Use for asset packs or plugins that want a custom
828
+ * mark without contributing a React component. */
829
+ | {
830
+ kind: 'svg';
831
+ viewBox: string;
832
+ path: string;
833
+ }
834
+ /** Custom React component, lazy-loaded. Use sparingly — adds a Suspense
835
+ * boundary per icon. */
836
+ | {
837
+ kind: 'component';
838
+ module: () => Promise<{
839
+ default: ComponentType;
840
+ }>;
841
+ };
842
+ export type LazyComponent = () => Promise<{
843
+ default: ComponentType;
844
+ }>;
845
+ export type RendererSource<N> = {
846
+ kind: 'parametric';
847
+ module: () => Promise<{
848
+ default: ComponentType<{
849
+ node: N;
850
+ }>;
851
+ }>;
852
+ } | {
853
+ kind: 'glb';
854
+ getAsset: (n: N) => AssetRef;
855
+ } | {
856
+ kind: 'instanced-glb';
857
+ getAsset: (n: N) => AssetRef;
858
+ };
859
+ export type AssetRef = {
860
+ id: string;
861
+ src: string;
862
+ };
863
+ export type SystemContribution = {
864
+ module: () => Promise<{
865
+ default: ComponentType;
866
+ }>;
867
+ priority?: number;
868
+ };
869
+ export type McpOverrides = {
870
+ description?: string;
871
+ semantic?: boolean;
872
+ };
873
+ export type Capabilities = {
874
+ movable?: MovableConfig;
875
+ rotatable?: RotatableConfig;
876
+ scalable?: ScalableConfig;
877
+ hostable?: HostableConfig;
878
+ cuttable?: CuttableConfig;
879
+ snappable?: SnappableConfig;
880
+ surfaces?: SurfacesConfig;
881
+ duplicable?: boolean;
882
+ deletable?: boolean;
883
+ groupable?: boolean;
884
+ selectable?: SelectableConfig;
885
+ interactive?: boolean;
886
+ floorPlaced?: FloorPlacedConfig;
887
+ /**
888
+ * Plan footprint this kind exposes to the alignment-anchor pool when it
889
+ * isn't `floorPlaced` and isn't a structural primitive the bridge handles
890
+ * directly (wall, slab). Lets a kind self-describe where it sits in plan
891
+ * instead of the core anchor bridge hardcoding it per type. See
892
+ * `AlignmentFootprintConfig`.
893
+ */
894
+ alignmentFootprint?: AlignmentFootprintConfig;
895
+ /**
896
+ * Bounds drawn by the 3D drag bounding box during a move. Opt-in: when
897
+ * omitted, the box auto-measures the rendered mesh, which is correct for
898
+ * most kinds. Set this when the rendered mesh tree contains extras the
899
+ * user wouldn't think of as "the thing being dragged" — e.g. an elevator
900
+ * whose mesh includes per-level landing assemblies, and the user expects
901
+ * the box to wrap just the shaft they're moving.
902
+ *
903
+ * `size`: `[width, height, depth]` in the node's local frame.
904
+ * `centerY`: optional Y center; defaults to `size[1] / 2` (box sits on
905
+ * the ground plane). Override when the local origin isn't at the base.
906
+ */
907
+ dragBounds?: (node: AnyNode, nodes?: Readonly<Record<string, AnyNode>>) => {
908
+ size: [number, number, number];
909
+ centerY?: number;
910
+ };
911
+ roofAccessory?: RoofAccessoryConfig;
912
+ /**
913
+ * Kind cuts a hole in the ceiling surface it is attached to (e.g. recessed
914
+ * downlights). The viewer's `CeilingSystem` calls this for each child of a
915
+ * ceiling to collect extra holes before triangulating. See `CeilingCutCapability`.
916
+ */
917
+ ceilingCut?: CeilingCutCapability;
918
+ paint?: PaintCapability;
919
+ /**
920
+ * Kind is placed by clicking on a wall (door, window). When set, the
921
+ * floor-plan layer lets wall background clicks pass through during
922
+ * placement / move-on-wall — the placement tool's `wall:click` event
923
+ * needs the SVG's `findClosestWallPoint` handler to run; without
924
+ * this the wall's registry entry would swallow the click via
925
+ * `handleSelect`. Read by `FloorplanRegistryLayer` when `movingNode`
926
+ * is set, so the active move can suspend wall selection.
927
+ */
928
+ wallOpeningPlacement?: boolean;
929
+ /**
930
+ * Instances of this kind contain levels. When such a node is being
931
+ * moved, the floor-plan layer falls back to the moving node's id as
932
+ * the ambient building context — so the floor under the cursor keeps
933
+ * rendering dimmed throughout the gesture even though the explicit
934
+ * selection may have been cleared as part of the move handoff. Set
935
+ * on building; future container kinds (e.g. annexes) opt in by
936
+ * declaring the same flag.
937
+ */
938
+ floorplanLevelContainer?: boolean;
939
+ /**
940
+ * Names of schema fields on this kind that are *host references* —
941
+ * values derived from where the node is placed (rather than declared
942
+ * by the user as part of the kind's parametric configuration). Read
943
+ * by host apps at preset-save time to strip these from the stored
944
+ * payload so a placed instance gets fresh host links at the new
945
+ * placement site (e.g. a door snapshot loses `wallId`/`wallT`; at
946
+ * placement the auto-attach UX re-derives them from the wall under
947
+ * the cursor).
948
+ *
949
+ * Kinds with no host refs omit this field (default `[]`).
950
+ *
951
+ * Examples:
952
+ * - door: `['wallId', 'wallT']` (door hosted on a wall)
953
+ * - window: `['wallId', 'wallT']`
954
+ * - item with `attachTo`: depends on the asset; the kind's
955
+ * `defaults()` or the dragging logic populates it dynamically.
956
+ */
957
+ hostRefFields?: string[];
958
+ /**
959
+ * Whether instances of this kind can be saved as a reusable preset
960
+ * (unified `items` catalog, `kind='preset'`). The editor itself does
961
+ * not act on this flag — host apps read it to gate "save as preset"
962
+ * UI on the selected node. Default resolution (callers should use the
963
+ * `isPresettable(def)` helper rather than reading this directly):
964
+ *
965
+ * - explicit `true` → presettable
966
+ * - explicit `false` → not presettable
967
+ * - undefined → presettable when `def.parametrics` exists
968
+ *
969
+ * Structural / utility kinds (level, building, site, zone, spawn,
970
+ * guide, scan, item) opt out explicitly because saving them as a
971
+ * standalone preset has no meaning — items already have their own
972
+ * catalog, scans/guides carry user-uploaded imagery, and the rest
973
+ * are non-leaf scene containers.
974
+ */
975
+ presettable?: boolean;
976
+ /**
977
+ * Instances of this kind are created by operating a build tool and
978
+ * drawing on the grid (clicking points), rather than dropping a
979
+ * finished instance. The tool id equals the node `type`. Host apps may
980
+ * seed the tool's starting parameters via
981
+ * `useEditor.setToolDefaults(type, params)` before activating it — the
982
+ * tool's create path merges those defaults when minting the node and
983
+ * clears its own entry on deactivation. Used so placing a saved preset
984
+ * of a drawn kind contributes its build parameters (a fence's
985
+ * height / style / post spacing) while the user draws the fresh span,
986
+ * and so a future "small / medium / large" picker can prime the same
987
+ * tool. Read via the `isDrawnViaTool(def)` helper. Default `false`.
988
+ */
989
+ drawTool?: boolean;
990
+ };
991
+ /**
992
+ * Per-kind paint behaviour. Lets the editor's selection-manager
993
+ * route paint hover / click / preview through a generic dispatcher
994
+ * instead of adding an `if (node.type === '<kind>')` arm for every
995
+ * paintable kind.
996
+ *
997
+ * The capability owns the four kind-specific decisions:
998
+ * 1. Which logical surface (`role`) the click landed on.
999
+ * 2. The patch to commit on click.
1000
+ * 3. How to apply a preview material to the registered mesh
1001
+ * subtree for that role (which mesh, which slot).
1002
+ * 4. How to read the currently-effective material for a role —
1003
+ * drives the color picker's "current value" indicator.
1004
+ *
1005
+ * The editor still owns the visual chrome — hover/cursor styling,
1006
+ * the `selectedMaterialTarget` round-trip, the paint-mode toolbar.
1007
+ * Kinds with no paint behaviour omit `paint`.
1008
+ */
1009
+ export type PaintCapability = {
1010
+ /**
1011
+ * Resolve which logical surface the user clicked. Returns `null`
1012
+ * when the face shouldn't be painted (e.g. interior slot exposed
1013
+ * by accident, normal too oblique for an unambiguous side).
1014
+ */
1015
+ resolveRole: (args: PaintResolveArgs) => string | null;
1016
+ /**
1017
+ * Build the node-update patch that applies the new material at
1018
+ * `role`. Returned partial is merged into the node by the editor.
1019
+ */
1020
+ buildPatch: (args: PaintPatchArgs) => Partial<AnyNode>;
1021
+ /**
1022
+ * Apply a preview to the kind's registered mesh subtree at
1023
+ * `role`. The kind builds whatever preview material(s) it needs
1024
+ * (single material, full material array, multi-slot patch — all
1025
+ * up to the kind) and swaps them in. Returns a cleanup callback
1026
+ * that restores the original assignments; the editor calls it
1027
+ * when the preview ends (hover changes, paint commits, paint
1028
+ * cancels).
1029
+ *
1030
+ * Returning `null` means the kind couldn't preview at this role
1031
+ * (typically because the registered mesh isn't mounted yet); the
1032
+ * editor falls back to the "not-allowed" cursor.
1033
+ */
1034
+ applyPreview: (args: PaintPreviewArgs) => (() => void) | null;
1035
+ /**
1036
+ * Read the currently-effective material for `role` on `node`,
1037
+ * after walking any parent-fallback chain (segment → parent roof,
1038
+ * etc.). Powers `resolveActivePaintMaterialFromSelection` — when
1039
+ * the user has a paint target selected, the editor uses this to
1040
+ * show the role's current value in the picker.
1041
+ *
1042
+ * Returns `null` when the role doesn't apply to this kind.
1043
+ */
1044
+ getEffectiveMaterial?: (args: PaintEffectiveMaterialArgs) => {
1045
+ material: MaterialSchema | undefined;
1046
+ materialPreset: string | undefined;
1047
+ } | null;
1048
+ };
1049
+ export type PaintResolveArgs = {
1050
+ node: AnyNode;
1051
+ /**
1052
+ * The geometry's material-slot index resolved from the pointer
1053
+ * hit (via three.js groups). `null` when no group covers the
1054
+ * face.
1055
+ */
1056
+ materialIndex: number | null;
1057
+ /** Optional: hit surface normal. Wall uses this for its interior/exterior split. */
1058
+ normal?: readonly [number, number, number];
1059
+ /** Optional: hit local position. Wall uses this to confirm the side. */
1060
+ localPosition?: readonly [number, number, number];
1061
+ /** Optional: name of the three.js object that received the hit. Stair uses this. */
1062
+ hitObjectName?: string;
1063
+ };
1064
+ export type PaintPatchArgs = {
1065
+ node: AnyNode;
1066
+ role: string;
1067
+ material: MaterialSchema | undefined;
1068
+ materialPreset: string | undefined;
1069
+ };
1070
+ export type PaintPreviewArgs = {
1071
+ node: AnyNode;
1072
+ role: string;
1073
+ material: MaterialSchema | undefined;
1074
+ materialPreset: string | undefined;
1075
+ root: Object3D;
1076
+ };
1077
+ export type PaintEffectiveMaterialArgs = {
1078
+ node: AnyNode;
1079
+ role: string;
1080
+ /** Snapshot of the scene `nodes` map — kinds whose effective material walks the parent chain (roof-segment → roof) read parents through it. */
1081
+ nodes: Record<AnyNodeId, AnyNode>;
1082
+ };
1083
+ /**
1084
+ * Kinds mounted on a roof segment via `roofSegmentId`. Presence of this
1085
+ * capability tells the viewer's roof-merge loop two things:
1086
+ *
1087
+ * 1. **Dirty cascade.** When the accessory is dirtied (move / resize /
1088
+ * reparent), the host segment's parent roof needs a re-merge —
1089
+ * otherwise the merged shell shows the previous cut shape. The
1090
+ * generic loop clears the accessory's dirty bit and queues the
1091
+ * parent roof.
1092
+ * 2. **Optional CSG cut.** When `buildCut` is set, the merge loop
1093
+ * subtracts the returned geometry from the host segment's shin /
1094
+ * deck / wall brushes so the accessory has a clean hole to poke
1095
+ * through. Returned geometry is SEGMENT-LOCAL; the viewer welds
1096
+ * vertices, attaches a single material group, and wraps it in a
1097
+ * `three-bvh-csg` Brush — core stays free of three-bvh-csg deps
1098
+ * and kinds don't need to import it.
1099
+ *
1100
+ * Use `buildCut` when the kind pokes THROUGH the roof (skylight,
1101
+ * dormer). Kinds that sit ON TOP (vents, solar panels) declare the
1102
+ * capability without `buildCut` — the cascade still fires but no CSG
1103
+ * cut runs.
1104
+ */
1105
+ export type RoofAccessoryConfig = {
1106
+ buildCut?: (node: AnyNode, hostSegment: AnyNode) => BufferGeometry | null;
1107
+ };
1108
+ /**
1109
+ * Capability for kinds that cut a hole in their host ceiling when the node is
1110
+ * attached to a ceiling surface (e.g. recessed downlights). The viewer's
1111
+ * `CeilingSystem` queries children of a ceiling for this capability and merges
1112
+ * the returned polygons as extra holes before triangulating, keeping the viewer
1113
+ * free of per-kind branching.
1114
+ *
1115
+ * Returns a rotated-rectangle footprint in ceiling-local [x, z] plan space —
1116
+ * the same coordinate space as `CeilingNode.polygon` and `.holes`. Return
1117
+ * `null` when this particular instance should not cut a hole (e.g. a
1118
+ * non-recessed variant of the same kind).
1119
+ */
1120
+ export type CeilingCutCapability = {
1121
+ buildCeilingHole: (node: AnyNode) => Array<[number, number]> | null;
1122
+ };
1123
+ export type CapabilityCtx = {
1124
+ node: AnyNode;
1125
+ };
1126
+ export type MovableConfig = {
1127
+ axes: ReadonlyArray<'x' | 'y' | 'z'>;
1128
+ gridSnap?: boolean;
1129
+ override?: (ctx: CapabilityCtx) => MovableConfig | null;
1130
+ };
1131
+ export type RotatableConfig = {
1132
+ axes: ReadonlyArray<'x' | 'y' | 'z'>;
1133
+ snapAngles?: readonly number[];
1134
+ override?: (ctx: CapabilityCtx) => RotatableConfig | null;
1135
+ };
1136
+ export type ScalableConfig = {
1137
+ axes: ReadonlyArray<'x' | 'y' | 'z'>;
1138
+ min?: number;
1139
+ max?: number;
1140
+ override?: (ctx: CapabilityCtx) => ScalableConfig | null;
1141
+ };
1142
+ export type HostableConfig = {
1143
+ parents: readonly string[];
1144
+ align?: 'top' | 'bottom' | 'center' | 'face';
1145
+ fromAsset?: 'attachTo';
1146
+ modes?: Record<string, Partial<HostableConfig>>;
1147
+ override?: (ctx: CapabilityCtx) => HostableConfig | null;
1148
+ };
1149
+ export type CuttableConfig = {
1150
+ hostKinds: readonly string[];
1151
+ override?: (ctx: CapabilityCtx) => CuttableConfig | null;
1152
+ };
1153
+ export type SnappableConfig = {
1154
+ points?: readonly SnapPointKind[];
1155
+ override?: (ctx: CapabilityCtx) => SnappableConfig | null;
1156
+ };
1157
+ export type SnapPointKind = 'start' | 'end' | 'midpoint' | 'center' | 'corners';
1158
+ export type SurfacesConfig = {
1159
+ top?: {
1160
+ height: number | ((n: AnyNode) => number);
1161
+ };
1162
+ sides?: {
1163
+ faces: 'all' | ReadonlyArray<readonly [number, number, number]>;
1164
+ };
1165
+ custom?: SurfaceQuery;
1166
+ };
1167
+ export type SurfaceQuery = (n: AnyNode) => SurfacePoint[];
1168
+ export type SurfacePoint = {
1169
+ position: readonly [number, number, number];
1170
+ normal: readonly [number, number, number];
1171
+ };
1172
+ export type SelectableConfig = {
1173
+ hitVolume?: 'bbox' | 'mesh' | 'none';
1174
+ override?: (ctx: CapabilityCtx) => SelectableConfig | null;
1175
+ };
1176
+ export type FloorPlacedFootprint = {
1177
+ dimensions: [number, number, number];
1178
+ rotation: [number, number, number];
1179
+ position?: [number, number, number];
1180
+ };
1181
+ export type FloorPlacedFootprintContext = {
1182
+ nodes: Readonly<Record<AnyNodeId, AnyNode>>;
1183
+ };
1184
+ export type FloorPlacedFootprintResolver = (node: AnyNode, ctx?: FloorPlacedFootprintContext) => FloorPlacedFootprint;
1185
+ export type FloorPlacedFootprintsResolver = (node: AnyNode, ctx?: FloorPlacedFootprintContext) => readonly FloorPlacedFootprint[];
1186
+ /**
1187
+ * Floor-placed kinds rest directly on a level and need their Y lifted by
1188
+ * any slab the footprint overlaps. The generic `<FloorElevationSystem>`
1189
+ * computes `slabElevation + node.position[1]` and writes it onto the
1190
+ * registered mesh on every dirty mark. `footprint` returns the default
1191
+ * world-space footprint the spatial-grid manager uses to find overlapping
1192
+ * slabs; `footprints` lets composite kinds expose multiple footprint
1193
+ * segments, with the canonical resolver taking the max slab elevation;
1194
+ * `applies` is an optional predicate to skip nodes that share a kind but
1195
+ * are mounted off-floor (items attached to a wall / ceiling).
1196
+ */
1197
+ export type FloorPlacedConfig = {
1198
+ footprint?: FloorPlacedFootprintResolver;
1199
+ footprints?: FloorPlacedFootprintsResolver;
1200
+ applies?: (node: AnyNode) => boolean;
1201
+ };
1202
+ /**
1203
+ * Plan footprint a kind contributes to the alignment-anchor pool when it is
1204
+ * neither `floorPlaced` (columns / items, whose footprint the bridge already
1205
+ * reads) nor a primitive the bridge knows structurally (walls → segments,
1206
+ * slabs → polygons). Two shapes:
1207
+ *
1208
+ * - `box` — a rotatable rectangle centred on the node's `position`. Use
1209
+ * when the kind also moves by its footprint edges (elevator): the anchor
1210
+ * bridge relocates the box to the proposed drag point, so one descriptor
1211
+ * serves both the static candidate and the moving node.
1212
+ * - `aabb` — an already-resolved XZ bounding box, for kinds whose plan
1213
+ * shape isn't a centred rectangle (stair: a segment chain or annular
1214
+ * sector). The moving-anchor bridge can relocate these by patching the
1215
+ * proposed plan position and resolving the AABB again.
1216
+ *
1217
+ * `nodes` is supplied only when a kind needs siblings / children to resolve
1218
+ * its footprint (a straight stair walks its `stair-segment` children); box
1219
+ * kinds derive everything from `node` alone.
1220
+ */
1221
+ export type AlignmentFootprint = {
1222
+ shape: 'box';
1223
+ dimensions: [number, number, number];
1224
+ rotation: [number, number, number];
1225
+ } | {
1226
+ shape: 'aabb';
1227
+ minX: number;
1228
+ minZ: number;
1229
+ maxX: number;
1230
+ maxZ: number;
1231
+ };
1232
+ export type AlignmentFootprintConfig = (node: AnyNode, nodes?: Readonly<Record<string, AnyNode>>) => AlignmentFootprint | null;
1233
+ export type Relations = {
1234
+ linkedBy?: 'endpoint-match' | 'polygon-share' | {
1235
+ custom: (n: AnyNode) => AnyNodeId[];
1236
+ };
1237
+ hosts?: readonly string[];
1238
+ affectsSpatial?: readonly string[];
1239
+ cascadeDelete?: 'descendants' | 'children' | 'none';
1240
+ };
1241
+ export type ParametricDescriptor<N> = {
1242
+ groups: ParamGroup<N>[];
1243
+ invariants?: ReadonlyArray<(n: N) => Issue[]>;
1244
+ derive?: (n: N) => Partial<N>;
1245
+ customPanel?: () => Promise<{
1246
+ default: ComponentType<{
1247
+ node: N;
1248
+ }>;
1249
+ }>;
1250
+ /**
1251
+ * Extra buttons rendered in the inspector's Actions section
1252
+ * (below Move/Delete). Lets a kind declare "do this thing to the
1253
+ * current node" affordances without escaping to a full custom
1254
+ * panel. Buttons whose `enabledIf` returns false stay disabled.
1255
+ */
1256
+ actions?: ParamAction<N>[];
1257
+ /**
1258
+ * Lazy-loaded React subsection rendered AFTER the auto-derived
1259
+ * groups and BEFORE the Actions section. Used by kinds that want
1260
+ * to list their child nodes inline — e.g. the gutter's downspout
1261
+ * list with an "Add Downspout" button at the bottom, same shape as
1262
+ * the roof panel's gutter / vent lists. Kind owns the layout; the
1263
+ * inspector just slots it in.
1264
+ */
1265
+ trailingSection?: () => Promise<{
1266
+ default: ComponentType<{
1267
+ node: N;
1268
+ }>;
1269
+ }>;
1270
+ };
1271
+ export type ParamAction<N> = {
1272
+ label: string;
1273
+ /** Optional asset URL for a leading icon — same shape as palette icons. */
1274
+ iconSrc?: string;
1275
+ enabledIf?: (n: N) => boolean;
1276
+ /** Click handler. Receives the current node value at click time. */
1277
+ onClick: (n: N) => void;
1278
+ };
1279
+ export type ParamGroup<N> = {
1280
+ label: string;
1281
+ fields: ParamField<N>[];
1282
+ };
1283
+ export type ParamField<N> = {
1284
+ key: keyof N;
1285
+ kind: 'number';
1286
+ unit?: string;
1287
+ min?: number;
1288
+ max?: number;
1289
+ step?: number;
1290
+ visibleIf?: (n: N) => boolean;
1291
+ customEditor?: ComponentType;
1292
+ } | {
1293
+ key: keyof N;
1294
+ kind: 'boolean';
1295
+ visibleIf?: (n: N) => boolean;
1296
+ } | {
1297
+ key: keyof N;
1298
+ kind: 'enum';
1299
+ options: readonly string[];
1300
+ /** Defaults to 'select' (dropdown). 'segmented' renders the inline
1301
+ * tabbed switcher — better for short option lists (2-4 items). */
1302
+ display?: 'select' | 'segmented';
1303
+ visibleIf?: (n: N) => boolean;
1304
+ } | {
1305
+ key: keyof N;
1306
+ kind: 'vec3';
1307
+ visibleIf?: (n: N) => boolean;
1308
+ } | {
1309
+ key: keyof N;
1310
+ kind: 'color';
1311
+ visibleIf?: (n: N) => boolean;
1312
+ } | {
1313
+ key: keyof N;
1314
+ kind: 'material';
1315
+ visibleIf?: (n: N) => boolean;
1316
+ } | {
1317
+ key: keyof N;
1318
+ kind: 'ref';
1319
+ refKind: string;
1320
+ visibleIf?: (n: N) => boolean;
1321
+ }
1322
+ /** Escape hatch for fields that don't map to a single node key —
1323
+ * derived values (`length` from `start`/`end`), sliders with
1324
+ * dynamic min/max (curve sagitta bounded by chord length),
1325
+ * composed editors, etc. The kind owns the rendering and the
1326
+ * update logic. `key` here is just a stable React key/label. */
1327
+ | {
1328
+ key: string;
1329
+ kind: 'custom';
1330
+ component: ComponentType<{
1331
+ node: N;
1332
+ onUpdate: (patch: Partial<N>) => void;
1333
+ }>;
1334
+ visibleIf?: (n: N) => boolean;
1335
+ };
1336
+ export type Issue = {
1337
+ field?: string;
1338
+ msg: string;
1339
+ severity?: 'error' | 'warning';
1340
+ };
1341
+ export type Affordance<N> = {
1342
+ id: string;
1343
+ mount: 'on-selection' | 'on-hover' | 'always';
1344
+ enabled?: (n: N, ctx: EditorCtx) => boolean;
1345
+ component: () => Promise<{
1346
+ default: ComponentType<{
1347
+ node: N;
1348
+ }>;
1349
+ }>;
1350
+ };
1351
+ export type EditorCtx = {
1352
+ modifiers: Modifiers;
1353
+ };
1354
+ export type Vec2 = readonly [number, number];
1355
+ export type Modifiers = {
1356
+ shift: boolean;
1357
+ alt: boolean;
1358
+ ctrl: boolean;
1359
+ meta: boolean;
1360
+ };
1361
+ export type DragAction<Ctx, Draft> = {
1362
+ begin: (input: {
1363
+ node?: AnyNode;
1364
+ point: Vec2;
1365
+ handleId?: string;
1366
+ modifiers?: Modifiers;
1367
+ }) => Ctx;
1368
+ preview: (ctx: Ctx, point: Vec2, modifiers: Modifiers) => Draft;
1369
+ snap?: (draft: Draft, ctx: Ctx, services: SnapServicesLike) => Draft;
1370
+ apply: (draft: Draft, ctx: Ctx, scene: SceneApi) => Iterable<AnyNodeId>;
1371
+ commit?: (draft: Draft, ctx: Ctx, scene: SceneApi) => boolean;
1372
+ cancel: (ctx: Ctx, scene: SceneApi) => void;
1373
+ };
1374
+ export type SnapServicesLike = unknown;
1375
+ export type SceneApi = {
1376
+ get: <N extends AnyNode = AnyNode>(id: AnyNodeId) => N | undefined;
1377
+ /**
1378
+ * Snapshot of the full nodes record. For descriptors / placement
1379
+ * callbacks that need to walk many siblings or resolve cross-node
1380
+ * structure (elevator level entries, building level chains, etc.)
1381
+ * without N round-trips through `get`. Returns the live reference —
1382
+ * do not mutate.
1383
+ */
1384
+ nodes: () => Readonly<Record<AnyNodeId, AnyNode>>;
1385
+ update: (id: AnyNodeId, patch: Partial<AnyNode>) => void;
1386
+ upsert: (node: AnyNode, parentId?: AnyNodeId) => AnyNodeId;
1387
+ delete: (id: AnyNodeId) => void;
1388
+ restore: (id: AnyNodeId) => void;
1389
+ restoreAll: () => void;
1390
+ markDirty: (id: AnyNodeId) => void;
1391
+ pauseHistory: () => void;
1392
+ resumeHistory: () => void;
1393
+ /**
1394
+ * Collect the subtree of live nodes rooted at `rootId` — `root` plus
1395
+ * every descendant reachable via `children[]` in BFS order. Returns
1396
+ * live node references (no clones); the caller decides whether to
1397
+ * persist by value or pass them straight into {@link cloneNodesInto}.
1398
+ * Returns `null` if `rootId` is missing.
1399
+ */
1400
+ getSubtree: (rootId: AnyNodeId) => Subtree | null;
1401
+ /**
1402
+ * Clone a flat array of nodes into the live scene with fresh IDs and
1403
+ * rewired parent / children references. Intentionally generic — see
1404
+ * {@link cloneNodesInto} for the transformations applied. Does NOT
1405
+ * strip or re-derive host references (e.g. `wallId` on a door); the
1406
+ * caller is responsible for that policy (read {@link Capabilities.hostRefFields}
1407
+ * on the relevant definition).
1408
+ *
1409
+ * Returns the new root id, or `null` if insertion failed.
1410
+ */
1411
+ cloneNodesInto: (nodes: ReadonlyArray<AnyNode>, opts: CloneNodesIntoOptions) => AnyNodeId | null;
1412
+ };
1413
+ export interface NodeRegistry {
1414
+ has: (kind: string) => boolean;
1415
+ get: (kind: string) => AnyNodeDefinition | undefined;
1416
+ entries: () => IterableIterator<[string, AnyNodeDefinition]>;
1417
+ schemas: () => ZodObject<any>[];
1418
+ readonly size: number;
1419
+ }
1420
+ //# sourceMappingURL=types.d.ts.map