@pascal-app/core 0.9.1 → 1.0.0-beta.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 (378) hide show
  1. package/README.md +17 -10
  2. package/dist/events/bus.d.ts +49 -2
  3. package/dist/events/bus.d.ts.map +1 -1
  4. package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
  5. package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
  6. package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
  7. package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
  8. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  9. package/dist/hooks/scene-registry/scene-registry.js +27 -1
  10. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
  11. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +135 -28
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  15. package/dist/hooks/spatial-grid/spatial-grid-manager.js +427 -271
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -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 +208 -9
  19. package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
  20. package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
  21. package/dist/hooks/spatial-grid/support-host-id.js +2 -0
  22. package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
  23. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
  24. package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
  25. package/dist/index.d.ts +33 -11
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +32 -10
  28. package/dist/lib/level-name.d.ts +4 -0
  29. package/dist/lib/level-name.d.ts.map +1 -0
  30. package/dist/lib/level-name.js +10 -0
  31. package/dist/lib/measurement-geometry.d.ts +23 -0
  32. package/dist/lib/measurement-geometry.d.ts.map +1 -0
  33. package/dist/lib/measurement-geometry.js +249 -0
  34. package/dist/lib/polygon-geometry.d.ts +0 -1
  35. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  36. package/dist/lib/polygon-geometry.js +0 -18
  37. package/dist/lib/selection-proxy.d.ts +4 -0
  38. package/dist/lib/selection-proxy.d.ts.map +1 -0
  39. package/dist/lib/selection-proxy.js +12 -0
  40. package/dist/lib/slab-polygon.d.ts +31 -2
  41. package/dist/lib/slab-polygon.d.ts.map +1 -1
  42. package/dist/lib/slab-polygon.js +557 -31
  43. package/dist/lib/slots.d.ts +14 -0
  44. package/dist/lib/slots.d.ts.map +1 -0
  45. package/dist/lib/slots.js +26 -0
  46. package/dist/lib/space-detection.d.ts +40 -6
  47. package/dist/lib/space-detection.d.ts.map +1 -1
  48. package/dist/lib/space-detection.js +574 -192
  49. package/dist/lib/terrain-brush.d.ts +163 -0
  50. package/dist/lib/terrain-brush.d.ts.map +1 -0
  51. package/dist/lib/terrain-brush.js +359 -0
  52. package/dist/lib/terrain-codec.d.ts +51 -0
  53. package/dist/lib/terrain-codec.d.ts.map +1 -0
  54. package/dist/lib/terrain-codec.js +167 -0
  55. package/dist/lib/terrain-field.d.ts +150 -0
  56. package/dist/lib/terrain-field.d.ts.map +1 -0
  57. package/dist/lib/terrain-field.js +248 -0
  58. package/dist/lib/terrain-raycast.d.ts +44 -0
  59. package/dist/lib/terrain-raycast.d.ts.map +1 -0
  60. package/dist/lib/terrain-raycast.js +188 -0
  61. package/dist/lib/terrain-source.d.ts +60 -0
  62. package/dist/lib/terrain-source.d.ts.map +1 -0
  63. package/dist/lib/terrain-source.js +90 -0
  64. package/dist/lib/terrain-support.d.ts +60 -0
  65. package/dist/lib/terrain-support.d.ts.map +1 -0
  66. package/dist/lib/terrain-support.js +150 -0
  67. package/dist/lib/wall-distance.d.ts +60 -0
  68. package/dist/lib/wall-distance.d.ts.map +1 -0
  69. package/dist/lib/wall-distance.js +85 -0
  70. package/dist/lib/zone-quantities.d.ts +21 -0
  71. package/dist/lib/zone-quantities.d.ts.map +1 -0
  72. package/dist/lib/zone-quantities.js +428 -0
  73. package/dist/material-library.d.ts +28 -1
  74. package/dist/material-library.d.ts.map +1 -1
  75. package/dist/material-library.js +2233 -441
  76. package/dist/registry/handles.d.ts +91 -3
  77. package/dist/registry/handles.d.ts.map +1 -1
  78. package/dist/registry/index.d.ts +3 -3
  79. package/dist/registry/index.d.ts.map +1 -1
  80. package/dist/registry/index.js +1 -1
  81. package/dist/registry/registry.d.ts +46 -3
  82. package/dist/registry/registry.d.ts.map +1 -1
  83. package/dist/registry/registry.js +82 -1
  84. package/dist/registry/subtree.d.ts.map +1 -1
  85. package/dist/registry/subtree.js +8 -1
  86. package/dist/registry/types.d.ts +781 -39
  87. package/dist/registry/types.d.ts.map +1 -1
  88. package/dist/schema/index.d.ts +27 -8
  89. package/dist/schema/index.d.ts.map +1 -1
  90. package/dist/schema/index.js +23 -6
  91. package/dist/schema/material.d.ts +3 -2
  92. package/dist/schema/material.d.ts.map +1 -1
  93. package/dist/schema/material.js +7 -5
  94. package/dist/schema/nodes/box-vent.d.ts +2 -2
  95. package/dist/schema/nodes/cabinet.d.ts +536 -0
  96. package/dist/schema/nodes/cabinet.d.ts.map +1 -0
  97. package/dist/schema/nodes/cabinet.js +139 -0
  98. package/dist/schema/nodes/ceiling.d.ts +4 -3
  99. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  100. package/dist/schema/nodes/ceiling.js +11 -1
  101. package/dist/schema/nodes/chimney.d.ts +4 -4
  102. package/dist/schema/nodes/column.d.ts +4 -2
  103. package/dist/schema/nodes/column.d.ts.map +1 -1
  104. package/dist/schema/nodes/column.js +5 -0
  105. package/dist/schema/nodes/construction-dimension.d.ts +195 -0
  106. package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
  107. package/dist/schema/nodes/construction-dimension.js +151 -0
  108. package/dist/schema/nodes/cupola.d.ts +2 -2
  109. package/dist/schema/nodes/door.d.ts +42 -5
  110. package/dist/schema/nodes/door.d.ts.map +1 -1
  111. package/dist/schema/nodes/door.js +32 -0
  112. package/dist/schema/nodes/dormer.d.ts +8 -8
  113. package/dist/schema/nodes/dormer.js +1 -1
  114. package/dist/schema/nodes/downspout.d.ts +2 -2
  115. package/dist/schema/nodes/duct-fitting.d.ts +91 -0
  116. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
  117. package/dist/schema/nodes/duct-fitting.js +92 -0
  118. package/dist/schema/nodes/duct-segment.d.ts +61 -0
  119. package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
  120. package/dist/schema/nodes/duct-segment.js +73 -0
  121. package/dist/schema/nodes/duct-terminal.d.ts +58 -0
  122. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
  123. package/dist/schema/nodes/duct-terminal.js +53 -0
  124. package/dist/schema/nodes/elevator.d.ts +3 -2
  125. package/dist/schema/nodes/elevator.d.ts.map +1 -1
  126. package/dist/schema/nodes/elevator.js +3 -0
  127. package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
  128. package/dist/schema/nodes/fence.d.ts +20 -2
  129. package/dist/schema/nodes/fence.d.ts.map +1 -1
  130. package/dist/schema/nodes/fence.js +33 -2
  131. package/dist/schema/nodes/gutter.d.ts +2 -2
  132. package/dist/schema/nodes/hvac-equipment.d.ts +64 -0
  133. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
  134. package/dist/schema/nodes/hvac-equipment.js +57 -0
  135. package/dist/schema/nodes/item.d.ts +9 -0
  136. package/dist/schema/nodes/item.d.ts.map +1 -1
  137. package/dist/schema/nodes/item.js +24 -0
  138. package/dist/schema/nodes/level.d.ts +2 -1
  139. package/dist/schema/nodes/level.d.ts.map +1 -1
  140. package/dist/schema/nodes/level.js +12 -31
  141. package/dist/schema/nodes/lineset.d.ts +42 -0
  142. package/dist/schema/nodes/lineset.d.ts.map +1 -0
  143. package/dist/schema/nodes/lineset.js +38 -0
  144. package/dist/schema/nodes/liquid-line.d.ts +34 -0
  145. package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
  146. package/dist/schema/nodes/liquid-line.js +24 -0
  147. package/dist/schema/nodes/measurement.d.ts +306 -0
  148. package/dist/schema/nodes/measurement.d.ts.map +1 -0
  149. package/dist/schema/nodes/measurement.js +87 -0
  150. package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
  151. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
  152. package/dist/schema/nodes/pipe-fitting.js +45 -0
  153. package/dist/schema/nodes/pipe-segment.d.ts +48 -0
  154. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
  155. package/dist/schema/nodes/pipe-segment.js +36 -0
  156. package/dist/schema/nodes/pipe-trap.d.ts +43 -0
  157. package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
  158. package/dist/schema/nodes/pipe-trap.js +36 -0
  159. package/dist/schema/nodes/ridge-vent.d.ts +18 -3
  160. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  161. package/dist/schema/nodes/ridge-vent.js +221 -3
  162. package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
  163. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
  164. package/dist/schema/nodes/roof-segment-shape.js +318 -0
  165. package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
  166. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
  167. package/dist/schema/nodes/roof-segment-walls.js +330 -0
  168. package/dist/schema/nodes/roof-segment.d.ts +84 -8
  169. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  170. package/dist/schema/nodes/roof-segment.js +290 -5
  171. package/dist/schema/nodes/roof.d.ts +8 -8
  172. package/dist/schema/nodes/shelf.d.ts +4 -2
  173. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  174. package/dist/schema/nodes/shelf.js +8 -0
  175. package/dist/schema/nodes/site.d.ts +9 -0
  176. package/dist/schema/nodes/site.d.ts.map +1 -1
  177. package/dist/schema/nodes/site.js +8 -6
  178. package/dist/schema/nodes/skylight.d.ts +10 -10
  179. package/dist/schema/nodes/slab.d.ts +8 -2
  180. package/dist/schema/nodes/slab.d.ts.map +1 -1
  181. package/dist/schema/nodes/slab.js +18 -2
  182. package/dist/schema/nodes/solar-panel.d.ts +4 -4
  183. package/dist/schema/nodes/spawn.d.ts +1 -0
  184. package/dist/schema/nodes/spawn.d.ts.map +1 -1
  185. package/dist/schema/nodes/spawn.js +2 -0
  186. package/dist/schema/nodes/stair-segment.d.ts +4 -4
  187. package/dist/schema/nodes/stair.d.ts +16 -13
  188. package/dist/schema/nodes/stair.d.ts.map +1 -1
  189. package/dist/schema/nodes/stair.js +11 -1
  190. package/dist/schema/nodes/structural-grid.d.ts +27 -0
  191. package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
  192. package/dist/schema/nodes/structural-grid.js +17 -0
  193. package/dist/schema/nodes/turbine-vent.d.ts +2 -2
  194. package/dist/schema/nodes/wall.d.ts +248 -6
  195. package/dist/schema/nodes/wall.d.ts.map +1 -1
  196. package/dist/schema/nodes/wall.js +266 -0
  197. package/dist/schema/nodes/window.d.ts +40 -3
  198. package/dist/schema/nodes/window.d.ts.map +1 -1
  199. package/dist/schema/nodes/window.js +30 -0
  200. package/dist/schema/nodes/zone.d.ts +22 -0
  201. package/dist/schema/nodes/zone.d.ts.map +1 -1
  202. package/dist/schema/nodes/zone.js +21 -0
  203. package/dist/schema/scene-material.d.ts +41 -0
  204. package/dist/schema/scene-material.d.ts.map +1 -0
  205. package/dist/schema/scene-material.js +9 -0
  206. package/dist/schema/terrain.d.ts +28 -0
  207. package/dist/schema/terrain.d.ts.map +1 -0
  208. package/dist/schema/terrain.js +30 -0
  209. package/dist/schema/types.d.ts +1300 -97
  210. package/dist/schema/types.d.ts.map +1 -1
  211. package/dist/schema/types.js +27 -0
  212. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
  213. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
  214. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
  215. package/dist/services/alignment-anchors.d.ts.map +1 -1
  216. package/dist/services/alignment-anchors.js +30 -1
  217. package/dist/services/alignment.d.ts +5 -0
  218. package/dist/services/alignment.d.ts.map +1 -1
  219. package/dist/services/alignment.js +2 -0
  220. package/dist/services/hosting.d.ts +8 -0
  221. package/dist/services/hosting.d.ts.map +1 -1
  222. package/dist/services/hosting.js +13 -4
  223. package/dist/services/index.d.ts +9 -2
  224. package/dist/services/index.d.ts.map +1 -1
  225. package/dist/services/index.js +9 -2
  226. package/dist/services/level-height.d.ts +27 -0
  227. package/dist/services/level-height.d.ts.map +1 -0
  228. package/dist/services/level-height.js +94 -0
  229. package/dist/services/opening-guides.d.ts +141 -0
  230. package/dist/services/opening-guides.d.ts.map +1 -0
  231. package/dist/services/opening-guides.js +267 -0
  232. package/dist/services/port-connectivity.d.ts +115 -0
  233. package/dist/services/port-connectivity.d.ts.map +1 -0
  234. package/dist/services/port-connectivity.js +300 -0
  235. package/dist/services/riser-diagram.d.ts +40 -0
  236. package/dist/services/riser-diagram.d.ts.map +1 -0
  237. package/dist/services/riser-diagram.js +110 -0
  238. package/dist/services/snap.d.ts +9 -1
  239. package/dist/services/snap.d.ts.map +1 -1
  240. package/dist/services/snap.js +21 -1
  241. package/dist/services/storey.d.ts +103 -0
  242. package/dist/services/storey.d.ts.map +1 -0
  243. package/dist/services/storey.js +296 -0
  244. package/dist/services/system-graph.d.ts +30 -0
  245. package/dist/services/system-graph.d.ts.map +1 -0
  246. package/dist/services/system-graph.js +164 -0
  247. package/dist/services/validate-dwv.d.ts +32 -0
  248. package/dist/services/validate-dwv.d.ts.map +1 -0
  249. package/dist/services/validate-dwv.js +118 -0
  250. package/dist/store/actions/node-actions.d.ts.map +1 -1
  251. package/dist/store/actions/node-actions.js +141 -4
  252. package/dist/store/history-control.d.ts +31 -0
  253. package/dist/store/history-control.d.ts.map +1 -1
  254. package/dist/store/history-control.js +130 -0
  255. package/dist/store/use-live-terrain.d.ts +65 -0
  256. package/dist/store/use-live-terrain.d.ts.map +1 -0
  257. package/dist/store/use-live-terrain.js +31 -0
  258. package/dist/store/use-live-transforms.d.ts +8 -0
  259. package/dist/store/use-live-transforms.d.ts.map +1 -1
  260. package/dist/store/use-scene.d.ts +47 -3
  261. package/dist/store/use-scene.d.ts.map +1 -1
  262. package/dist/store/use-scene.js +992 -55
  263. package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
  264. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
  265. package/dist/systems/elevator/elevator-opening-system.js +27 -21
  266. package/dist/systems/elevator/elevator-service.d.ts +0 -2
  267. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  268. package/dist/systems/elevator/elevator-service.js +2 -24
  269. package/dist/systems/fence/fence-centerline.d.ts +12 -0
  270. package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
  271. package/dist/systems/fence/fence-centerline.js +37 -0
  272. package/dist/systems/fence/fence-spline.d.ts +39 -0
  273. package/dist/systems/fence/fence-spline.d.ts.map +1 -0
  274. package/dist/systems/fence/fence-spline.js +196 -0
  275. package/dist/systems/slab/slab-placement.d.ts +11 -0
  276. package/dist/systems/slab/slab-placement.d.ts.map +1 -0
  277. package/dist/systems/slab/slab-placement.js +14 -0
  278. package/dist/systems/slab/slab-support.d.ts +137 -0
  279. package/dist/systems/slab/slab-support.d.ts.map +1 -0
  280. package/dist/systems/slab/slab-support.js +557 -0
  281. package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
  282. package/dist/systems/stair/stair-footprint.js +21 -11
  283. package/dist/systems/stair/stair-opening-preview.d.ts +1300 -505
  284. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  285. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  286. package/dist/systems/stair/stair-opening-sync.js +19 -33
  287. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  288. package/dist/systems/stair/stair-opening-system.js +32 -4
  289. package/dist/systems/stair/stair-rise.d.ts +22 -0
  290. package/dist/systems/stair/stair-rise.d.ts.map +1 -0
  291. package/dist/systems/stair/stair-rise.js +79 -0
  292. package/dist/systems/wall/wall-curve.d.ts +10 -0
  293. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  294. package/dist/systems/wall/wall-curve.js +1 -1
  295. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  296. package/dist/systems/wall/wall-mitering.js +44 -8
  297. package/dist/systems/wall/wall-top.d.ts +40 -0
  298. package/dist/systems/wall/wall-top.d.ts.map +1 -0
  299. package/dist/systems/wall/wall-top.js +46 -0
  300. package/dist/utils/clone-scene-graph.d.ts +1 -0
  301. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  302. package/dist/utils/clone-scene-graph.js +56 -5
  303. package/dist/utils/heal-scene-graph.d.ts +18 -0
  304. package/dist/utils/heal-scene-graph.d.ts.map +1 -0
  305. package/dist/utils/heal-scene-graph.js +106 -0
  306. package/dist/validation/validate-build-json.d.ts +3 -0
  307. package/dist/validation/validate-build-json.d.ts.map +1 -1
  308. package/dist/validation/validate-build-json.js +90 -5
  309. package/package.json +4 -4
  310. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
  311. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
  312. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -273
  313. package/dist/lib/space-detection-pause.test.d.ts +0 -2
  314. package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
  315. package/dist/lib/space-detection-pause.test.js +0 -42
  316. package/dist/lib/space-detection.test.d.ts +0 -2
  317. package/dist/lib/space-detection.test.d.ts.map +0 -1
  318. package/dist/lib/space-detection.test.js +0 -77
  319. package/dist/registry/registry.test.d.ts +0 -2
  320. package/dist/registry/registry.test.d.ts.map +0 -1
  321. package/dist/registry/registry.test.js +0 -183
  322. package/dist/registry/relations-resolver.test.d.ts +0 -2
  323. package/dist/registry/relations-resolver.test.d.ts.map +0 -1
  324. package/dist/registry/relations-resolver.test.js +0 -183
  325. package/dist/registry/scene-api.test.d.ts +0 -2
  326. package/dist/registry/scene-api.test.d.ts.map +0 -1
  327. package/dist/registry/scene-api.test.js +0 -183
  328. package/dist/registry/subtree.test.d.ts +0 -2
  329. package/dist/registry/subtree.test.d.ts.map +0 -1
  330. package/dist/registry/subtree.test.js +0 -133
  331. package/dist/schema/asset-url.test.d.ts +0 -2
  332. package/dist/schema/asset-url.test.d.ts.map +0 -1
  333. package/dist/schema/asset-url.test.js +0 -134
  334. package/dist/services/alignment-anchors.test.d.ts +0 -2
  335. package/dist/services/alignment-anchors.test.d.ts.map +0 -1
  336. package/dist/services/alignment-anchors.test.js +0 -383
  337. package/dist/services/alignment.test.d.ts +0 -2
  338. package/dist/services/alignment.test.d.ts.map +0 -1
  339. package/dist/services/alignment.test.js +0 -92
  340. package/dist/services/drag-session.test.d.ts +0 -2
  341. package/dist/services/drag-session.test.d.ts.map +0 -1
  342. package/dist/services/drag-session.test.js +0 -219
  343. package/dist/services/hosting.test.d.ts +0 -2
  344. package/dist/services/hosting.test.d.ts.map +0 -1
  345. package/dist/services/hosting.test.js +0 -199
  346. package/dist/services/movement.test.d.ts +0 -2
  347. package/dist/services/movement.test.d.ts.map +0 -1
  348. package/dist/services/movement.test.js +0 -133
  349. package/dist/services/single-undo-dance.test.d.ts +0 -2
  350. package/dist/services/single-undo-dance.test.d.ts.map +0 -1
  351. package/dist/services/single-undo-dance.test.js +0 -210
  352. package/dist/services/snap.test.d.ts +0 -2
  353. package/dist/services/snap.test.d.ts.map +0 -1
  354. package/dist/services/snap.test.js +0 -95
  355. package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
  356. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
  357. package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
  358. package/dist/store/actions/reparent.test.d.ts +0 -2
  359. package/dist/store/actions/reparent.test.d.ts.map +0 -1
  360. package/dist/store/actions/reparent.test.js +0 -212
  361. package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
  362. package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
  363. package/dist/store/use-scene-elevator-migration.test.js +0 -109
  364. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
  365. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
  366. package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
  367. package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
  368. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
  369. package/dist/systems/elevator/elevator-runtime.test.js +0 -64
  370. package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
  371. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
  372. package/dist/systems/stair/stair-opening-preview.test.js +0 -80
  373. package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
  374. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
  375. package/dist/systems/stair/stair-opening-sync.test.js +0 -446
  376. package/dist/utils/clone-scene-graph.test.d.ts +0 -2
  377. package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
  378. package/dist/utils/clone-scene-graph.test.js +0 -59
@@ -1,27 +1,22 @@
1
+ import { getRenderableSlabPolygon } from '../../lib/slab-polygon';
2
+ import { terrainSupportLift } from '../../lib/terrain-support';
3
+ import { nodeRegistry } from '../../registry';
1
4
  import { getScaledDimensions, isLowProfileItemSurface } from '../../schema';
5
+ import { getWallPlaneTop } from '../../services/storey';
6
+ import useLiveNodeOverrides, { getEffectiveNode } from '../../store/use-live-node-overrides';
7
+ import useLiveTransforms from '../../store/use-live-transforms';
2
8
  import useScene from '../../store/use-scene';
3
- import { isCurvedWall, sampleWallCenterline } from '../../systems/wall/wall-curve';
9
+ import { computeWallSlabSupport, pointInPolygon, SUPPORT_ELEVATION_EPSILON, } from '../../systems/slab/slab-support';
4
10
  import { DEFAULT_WALL_THICKNESS } from '../../systems/wall/wall-footprint';
11
+ import { resolveWallEffectiveHeight } from '../../systems/wall/wall-top';
12
+ import { getFloorPlacedFootprints } from './floor-placed-elevation';
5
13
  import { SpatialGrid } from './spatial-grid';
14
+ import { GROUND_SUPPORT_ID } from './support-host-id';
6
15
  import { WallSpatialGrid } from './wall-spatial-grid';
16
+ export { computeWallSlabElevation, computeWallSlabSupport, pointInPolygon, SUPPORT_ELEVATION_EPSILON, wallOverlapsPolygon, } from '../../systems/slab/slab-support';
7
17
  // ============================================================================
8
18
  // GEOMETRY HELPERS
9
19
  // ============================================================================
10
- /**
11
- * Point-in-polygon test using ray casting algorithm.
12
- */
13
- export function pointInPolygon(px, pz, polygon) {
14
- let inside = false;
15
- const n = polygon.length;
16
- for (let i = 0, j = n - 1; i < n; j = i++) {
17
- const xi = polygon[i][0], zi = polygon[i][1];
18
- const xj = polygon[j][0], zj = polygon[j][1];
19
- if (zi > pz !== zj > pz && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
20
- inside = !inside;
21
- }
22
- }
23
- return inside;
24
- }
25
20
  /**
26
21
  * Compute the 4 XZ footprint corners of an item given its position, dimensions, and Y rotation.
27
22
  */
@@ -40,6 +35,24 @@ function getItemFootprint(position, dimensions, rotation, inset = 0) {
40
35
  [x + (-halfW * cos - halfD * sin), z + (-halfW * sin + halfD * cos)],
41
36
  ];
42
37
  }
38
+ /**
39
+ * Axis-aligned XZ extent of a footprint at `position`, rotated by `yRot`. The
40
+ * rotated width/depth is the same conservative bound the floor-placement draft
41
+ * uses, so a draft and an existing node are compared with identical math.
42
+ */
43
+ function footprintBoundsXZ(position, dimensions, yRot) {
44
+ const [width, , depth] = dimensions;
45
+ const cos = Math.abs(Math.cos(yRot));
46
+ const sin = Math.abs(Math.sin(yRot));
47
+ const rotatedW = width * cos + depth * sin;
48
+ const rotatedD = width * sin + depth * cos;
49
+ return {
50
+ minX: position[0] - rotatedW / 2,
51
+ maxX: position[0] + rotatedW / 2,
52
+ minZ: position[2] - rotatedD / 2,
53
+ maxZ: position[2] + rotatedD / 2,
54
+ };
55
+ }
43
56
  function getItemLocalBounds(item) {
44
57
  const [width, height, depth] = getScaledDimensions(item);
45
58
  const minZ = item.asset.attachTo === 'wall-side' ? -depth : -depth / 2;
@@ -99,6 +112,24 @@ function resolveNodeLevelId(node, nodes) {
99
112
  }
100
113
  return 'default';
101
114
  }
115
+ function expandIgnoredNodeIds(ignoreIds, nodes) {
116
+ const ignored = new Set(ignoreIds ?? []);
117
+ const queue = [...ignored];
118
+ while (queue.length > 0) {
119
+ const id = queue.pop();
120
+ const node = nodes[id];
121
+ const children = node?.children;
122
+ if (!Array.isArray(children))
123
+ continue;
124
+ for (const childId of children) {
125
+ if (typeof childId !== 'string' || ignored.has(childId))
126
+ continue;
127
+ ignored.add(childId);
128
+ queue.push(childId);
129
+ }
130
+ }
131
+ return ignored;
132
+ }
102
133
  /**
103
134
  * Test if two line segments (a1->a2) and (b1->b2) intersect.
104
135
  */
@@ -164,169 +195,6 @@ export function itemOverlapsPolygon(position, dimensions, rotation, polygon, ins
164
195
  }
165
196
  return false;
166
197
  }
167
- /**
168
- * Check if wall segment (a) is substantially on polygon edge segment (b).
169
- * Returns true only if BOTH endpoints of the wall are on or very close to the edge.
170
- * This prevents walls that just touch one point from being detected.
171
- */
172
- function segmentsCollinearAndOverlap(ax1, az1, ax2, az2, bx1, bz1, bx2, bz2) {
173
- const EPSILON = 1e-6;
174
- // Cross product to check collinearity
175
- const cross1 = (ax2 - ax1) * (bz1 - az1) - (az2 - az1) * (bx1 - ax1);
176
- const cross2 = (ax2 - ax1) * (bz2 - az1) - (az2 - az1) * (bx2 - ax1);
177
- if (Math.abs(cross1) > EPSILON || Math.abs(cross2) > EPSILON) {
178
- return false; // Not collinear
179
- }
180
- // Check if a point is on segment b
181
- const onSegment = (px, pz, qx, qz, rx, rz) => Math.min(px, qx) - EPSILON <= rx &&
182
- rx <= Math.max(px, qx) + EPSILON &&
183
- Math.min(pz, qz) - EPSILON <= rz &&
184
- rz <= Math.max(pz, qz) + EPSILON;
185
- // BOTH endpoints of wall (a) must be on edge (b) for substantial overlap
186
- const a1OnB = onSegment(bx1, bz1, bx2, bz2, ax1, az1);
187
- const a2OnB = onSegment(bx1, bz1, bx2, bz2, ax2, az2);
188
- return a1OnB && a2OnB;
189
- }
190
- /**
191
- * Test if a wall segment overlaps with a polygon.
192
- * A wall is considered to overlap if:
193
- * - Its midpoint is inside the polygon (wall crosses through)
194
- * - At least one endpoint is inside (wall partially or fully in slab)
195
- * - It's collinear with and overlaps a polygon edge (wall on slab boundary)
196
- * - (curved walls) any sample along the centerline is inside
197
- *
198
- * Note: A wall with just one endpoint touching the edge but the rest outside
199
- * is NOT considered overlapping (adjacent only).
200
- */
201
- export function wallOverlapsPolygon(startOrWall, endOrPolygon, polygonArg) {
202
- // Two call shapes:
203
- // wallOverlapsPolygon(wallLike, polygon) — preferred; curve-aware
204
- // wallOverlapsPolygon(start, end, polygon) — legacy chord-only
205
- let start;
206
- let end;
207
- let polygon;
208
- let curveOffset = 0;
209
- let thickness = DEFAULT_WALL_THICKNESS;
210
- if (Array.isArray(startOrWall)) {
211
- start = startOrWall;
212
- end = endOrPolygon;
213
- polygon = polygonArg;
214
- }
215
- else {
216
- start = startOrWall.start;
217
- end = startOrWall.end;
218
- curveOffset = startOrWall.curveOffset ?? 0;
219
- thickness = startOrWall.thickness ?? DEFAULT_WALL_THICKNESS;
220
- polygon = endOrPolygon;
221
- }
222
- const halfThickness = Math.max(thickness / 2, 0);
223
- // Curved walls: sample the centerline. The chord-based checks below miss
224
- // walls that bow into/out of the slab — e.g. endpoints on the slab
225
- // boundary with the curve arcing inward through the slab interior. Without
226
- // this, `getSlabElevationForWall` returns 0 (wall drops to floor) and
227
- // `markNodesOverlappingSlab` never re-dirties the wall when the slab Y
228
- // moves.
229
- if (curveOffset !== 0) {
230
- const wallLike = { start, end, curveOffset };
231
- if (isCurvedWall(wallLike)) {
232
- const samples = sampleWallCenterline(wallLike, 16);
233
- for (let i = 0; i < samples.length; i++) {
234
- const point = samples[i];
235
- if (pointInPolygon(point.x, point.y, polygon))
236
- return true;
237
- // Also test ±halfThickness perpendicular at each sample so a curve
238
- // skirting the slab edge with its centerline just outside still
239
- // registers — its body sits inside the slab.
240
- if (halfThickness > 0 && i > 0) {
241
- const prev = samples[i - 1];
242
- const sx = point.x - prev.x;
243
- const sz = point.y - prev.y;
244
- const sl = Math.sqrt(sx * sx + sz * sz);
245
- if (sl > 1e-10) {
246
- const tnx = (-sz / sl) * halfThickness;
247
- const tnz = (sx / sl) * halfThickness;
248
- if (pointInPolygon(point.x + tnx, point.y + tnz, polygon))
249
- return true;
250
- if (pointInPolygon(point.x - tnx, point.y - tnz, polygon))
251
- return true;
252
- }
253
- }
254
- }
255
- }
256
- }
257
- const dx = end[0] - start[0];
258
- const dz = end[1] - start[1];
259
- const len = Math.sqrt(dx * dx + dz * dz);
260
- // Nudge endpoint test points a tiny step inward along the wall direction before
261
- // testing containment. pointInPolygon (ray casting) produces false positives for
262
- // points exactly on polygon vertices or edges — specifically the minimum-z corner
263
- // of an axis-aligned polygon returns "inside" because the ray hits the opposite
264
- // vertical edge exactly at its base. Nudging by 1e-6 m avoids this: a wall that
265
- // merely starts at a slab corner and extends outward will have its nudged point
266
- // clearly outside, while a wall that genuinely starts inside stays inside.
267
- if (len > 1e-10) {
268
- const step = Math.min(1e-6, len * 0.01);
269
- const nx = (dx / len) * step;
270
- const nz = (dz / len) * step;
271
- if (pointInPolygon(start[0] + nx, start[1] + nz, polygon))
272
- return true;
273
- if (pointInPolygon(end[0] - nx, end[1] - nz, polygon))
274
- return true;
275
- // Also nudge perpendicular to the wall (into the slab interior) for walls that
276
- // lie exactly on the slab boundary. The along-wall nudge keeps points on the
277
- // boundary where pointInPolygon is unreliable; a perpendicular inward nudge
278
- // moves the point clearly inside (or outside) the polygon.
279
- // Sample the wall at 1/4, 1/2, 3/4 positions with a perpendicular nudge.
280
- const PERP_STEP = 1e-4;
281
- const pnx = (-nz / step) * PERP_STEP; // perpendicular left
282
- const pnz = (nx / step) * PERP_STEP;
283
- for (const t of [0.25, 0.5, 0.75]) {
284
- const bx = start[0] + dx * t;
285
- const bz = start[1] + dz * t;
286
- if (pointInPolygon(bx + pnx, bz + pnz, polygon))
287
- return true;
288
- if (pointInPolygon(bx - pnx, bz - pnz, polygon))
289
- return true;
290
- }
291
- // Wall-thickness perpendicular test. Walls aren't infinitely thin lines;
292
- // a wall whose centerline sits just outside the slab boundary still has
293
- // half its body inside the slab and should follow the slab elevation.
294
- // Without this, the perimeter walls of a room often miss the slab-overlap
295
- // detection (the slab polygon is the room's interior, the wall centerline
296
- // sits on or just outside its edge) and stay at Y=0 while the slab moves
297
- // up.
298
- if (halfThickness > 0) {
299
- const ux = dx / len;
300
- const uz = dz / len;
301
- const tnx = -uz * halfThickness;
302
- const tnz = ux * halfThickness;
303
- for (const t of [0, 0.25, 0.5, 0.75, 1]) {
304
- const bx = start[0] + dx * t;
305
- const bz = start[1] + dz * t;
306
- if (pointInPolygon(bx + tnx, bz + tnz, polygon))
307
- return true;
308
- if (pointInPolygon(bx - tnx, bz - tnz, polygon))
309
- return true;
310
- }
311
- }
312
- }
313
- // Check if midpoint is inside (catches walls crossing through)
314
- const midX = (start[0] + end[0]) / 2;
315
- const midZ = (start[1] + end[1]) / 2;
316
- if (pointInPolygon(midX, midZ, polygon))
317
- return true;
318
- // Check if the wall is collinear with and overlaps any polygon edge
319
- const n = polygon.length;
320
- for (let i = 0; i < n; i++) {
321
- const j = (i + 1) % n;
322
- const [p1x, p1z] = polygon[i];
323
- const [p2x, p2z] = polygon[j];
324
- if (segmentsCollinearAndOverlap(start[0], start[1], end[0], end[1], p1x, p1z, p2x, p2z)) {
325
- return true;
326
- }
327
- }
328
- return false;
329
- }
330
198
  export class SpatialGridManager {
331
199
  floorGrids = new Map(); // levelId -> grid
332
200
  wallGrids = new Map(); // levelId -> wall grid
@@ -361,7 +229,14 @@ export class SpatialGridManager {
361
229
  }
362
230
  getWallHeight(wallId) {
363
231
  const wall = this.walls.get(wallId);
364
- return wall?.height ?? 2.5; // Default wall height
232
+ if (!wall)
233
+ return 0;
234
+ if (wall.height != null)
235
+ return wall.height;
236
+ const nodes = useScene.getState().nodes;
237
+ const levelId = resolveNodeLevelId(wall, nodes);
238
+ const support = this.getSlabSupportForWall(levelId, wall.start, wall.end, wall.curveOffset ?? 0, wall.thickness, wall.supportSlabId ?? null, undefined, wall.supportOffset);
239
+ return resolveWallEffectiveHeight(wall, getWallPlaneTop(wall, levelId, nodes), support.elevation);
365
240
  }
366
241
  getCeilingGrid(ceilingId) {
367
242
  if (!this.ceilingGrids.has(ceilingId)) {
@@ -375,10 +250,122 @@ export class SpatialGridManager {
375
250
  }
376
251
  return this.slabsByLevel.get(levelId);
377
252
  }
253
+ /**
254
+ * Per-slab RENDERED polygon cache (`getRenderableSlabPolygon`). Item
255
+ * support queries run per frame and the projection scans the level's
256
+ * walls + sibling slabs, so the result is cached per slab id and
257
+ * dropped for the whole level whenever a slab or wall on that level
258
+ * flows through the manager's create/update/delete handlers.
259
+ */
260
+ renderedSlabPolygons = new Map();
261
+ invalidateRenderedSlabPolygons(levelId) {
262
+ const slabMap = this.slabsByLevel.get(levelId);
263
+ if (!slabMap)
264
+ return;
265
+ for (const slabId of slabMap.keys())
266
+ this.renderedSlabPolygons.delete(slabId);
267
+ }
268
+ /**
269
+ * True while a slab or wall on `levelId` has a live preview: group drags
270
+ * publish translated slab polygons and wall endpoints to
271
+ * `useLiveNodeOverrides`, and the slab move tool / room-preset stamp
272
+ * publish a translation DELTA to `useLiveTransforms` — either way the
273
+ * scene store commits only on release, so the committed cache and index
274
+ * would elect support against pre-drag footprints (items and walls
275
+ * visibly drop to ground mid-preview). Support queries then read
276
+ * live-effective records and skip the rendered-polygon cache.
277
+ */
278
+ levelHasLivePreview(levelId) {
279
+ const nodes = useScene.getState().nodes;
280
+ const structuralOnLevel = (id) => {
281
+ const node = nodes[id];
282
+ if (!node || (node.type !== 'slab' && node.type !== 'wall'))
283
+ return false;
284
+ return resolveNodeLevelId(node, nodes) === levelId;
285
+ };
286
+ const overrides = useLiveNodeOverrides.getState().overrides;
287
+ for (const id of overrides.keys()) {
288
+ if (structuralOnLevel(id))
289
+ return true;
290
+ }
291
+ const transforms = useLiveTransforms.getState().transforms;
292
+ for (const id of transforms.keys()) {
293
+ if (structuralOnLevel(id))
294
+ return true;
295
+ }
296
+ return false;
297
+ }
298
+ /**
299
+ * The live-effective slab record: field overrides merged, then the
300
+ * `useLiveTransforms` DELTA (slab publishers — move tool, room-preset
301
+ * stamp — store a translation, not an absolute position) applied to the
302
+ * polygon, holes, and elevation. Mapping happens exactly ONCE at each
303
+ * public query's loop entry: `slabSupportsFootprint` /
304
+ * `getRenderedSlabPolygon` take the already-effective record and must
305
+ * never re-map, or the delta would apply twice.
306
+ */
307
+ effectiveSlabRecord(slab) {
308
+ let effective = getEffectiveNode(slab);
309
+ const live = useLiveTransforms.getState().get(slab.id);
310
+ if (live) {
311
+ const [dx, dy, dz] = live.position;
312
+ if (dx !== 0 || dy !== 0 || dz !== 0) {
313
+ effective = {
314
+ ...effective,
315
+ polygon: effective.polygon.map(([x, z]) => [x + dx, z + dz]),
316
+ holes: (effective.holes || []).map((hole) => hole.map(([x, z]) => [x + dx, z + dz])),
317
+ elevation: (effective.elevation ?? 0.05) + dy,
318
+ };
319
+ }
320
+ }
321
+ return effective;
322
+ }
323
+ getRenderedSlabPolygon(levelId, slab) {
324
+ const live = this.levelHasLivePreview(levelId);
325
+ if (!live) {
326
+ const cached = this.renderedSlabPolygons.get(slab.id);
327
+ if (cached)
328
+ return cached;
329
+ }
330
+ const siblingSlabs = [];
331
+ for (const other of this.getSlabMap(levelId).values()) {
332
+ if (other.id !== slab.id)
333
+ siblingSlabs.push(live ? this.effectiveSlabRecord(other) : other);
334
+ }
335
+ const walls = this.getLevelWallNodes(levelId);
336
+ const polygon = getRenderableSlabPolygon(slab, {
337
+ walls: live ? walls.map((wall) => getEffectiveNode(wall)) : walls,
338
+ siblingSlabs,
339
+ });
340
+ if (!live)
341
+ this.renderedSlabPolygons.set(slab.id, polygon);
342
+ return polygon;
343
+ }
344
+ /**
345
+ * Support test shared by election, candidate listing, and persisted-host
346
+ * validation: the footprint overlaps the slab's RENDERED polygon (what
347
+ * users see — matching the wall election in `computeWallSlabSupport`),
348
+ * with the center-point hole veto kept against the stored holes (holes
349
+ * are data, never render-offset).
350
+ */
351
+ slabSupportsFootprint(levelId, slab, position, dimensions, rotation) {
352
+ if (slab.polygon.length < 3)
353
+ return false;
354
+ const rendered = this.getRenderedSlabPolygon(levelId, slab);
355
+ if (!itemOverlapsPolygon(position, dimensions, rotation, rendered, 0.01))
356
+ return false;
357
+ const [cx, , cz] = position;
358
+ for (const hole of slab.holes || []) {
359
+ if (hole.length >= 3 && pointInPolygon(cx, cz, hole))
360
+ return false;
361
+ }
362
+ return true;
363
+ }
378
364
  // Called when nodes change
379
365
  handleNodeCreated(node, levelId) {
380
366
  if (node.type === 'slab') {
381
367
  this.getSlabMap(levelId).set(node.id, node);
368
+ this.invalidateRenderedSlabPolygons(levelId);
382
369
  }
383
370
  else if (node.type === 'ceiling') {
384
371
  this.ceilings.set(node.id, node);
@@ -386,6 +373,8 @@ export class SpatialGridManager {
386
373
  else if (node.type === 'wall') {
387
374
  const wall = node;
388
375
  this.walls.set(wall.id, wall);
376
+ // Rendered slab polygons adopt wall bands — a new wall can extend them.
377
+ this.invalidateRenderedSlabPolygons(levelId);
389
378
  }
390
379
  else if (node.type === 'item') {
391
380
  const item = node;
@@ -430,6 +419,7 @@ export class SpatialGridManager {
430
419
  handleNodeUpdated(node, levelId) {
431
420
  if (node.type === 'slab') {
432
421
  this.getSlabMap(levelId).set(node.id, node);
422
+ this.invalidateRenderedSlabPolygons(levelId);
433
423
  }
434
424
  else if (node.type === 'ceiling') {
435
425
  this.ceilings.set(node.id, node);
@@ -437,6 +427,7 @@ export class SpatialGridManager {
437
427
  else if (node.type === 'wall') {
438
428
  const wall = node;
439
429
  this.walls.set(wall.id, wall);
430
+ this.invalidateRenderedSlabPolygons(levelId);
440
431
  }
441
432
  else if (node.type === 'item') {
442
433
  const item = node;
@@ -486,6 +477,9 @@ export class SpatialGridManager {
486
477
  }
487
478
  handleNodeDeleted(nodeId, nodeType, levelId) {
488
479
  if (nodeType === 'slab') {
480
+ // Invalidate before removal so the deleted slab's own cache entry
481
+ // (still keyed in the level map here) is dropped with its siblings'.
482
+ this.invalidateRenderedSlabPolygons(levelId);
489
483
  this.getSlabMap(levelId).delete(nodeId);
490
484
  }
491
485
  else if (nodeType === 'ceiling') {
@@ -494,6 +488,7 @@ export class SpatialGridManager {
494
488
  }
495
489
  else if (nodeType === 'wall') {
496
490
  this.walls.delete(nodeId);
491
+ this.invalidateRenderedSlabPolygons(levelId);
497
492
  // Remove all items attached to this wall from the spatial grid
498
493
  const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId);
499
494
  return removedItemIds; // Caller can use this to delete the items from scene
@@ -512,37 +507,51 @@ export class SpatialGridManager {
512
507
  }
513
508
  // Query methods
514
509
  canPlaceOnFloor(levelId, position, dimensions, rotation, ignoreIds) {
510
+ return this.canPlaceOnFloorFootprints(levelId, [{ position, dimensions, rotation }], ignoreIds);
511
+ }
512
+ canPlaceOnFloorFootprints(levelId, footprints, ignoreIds) {
515
513
  const nodes = useScene.getState().nodes;
516
- const ignoreSet = new Set(ignoreIds ?? []);
517
- const [width, , depth] = dimensions;
518
- const yRot = rotation[1];
519
- const cos = Math.abs(Math.cos(yRot));
520
- const sin = Math.abs(Math.sin(yRot));
521
- const rotatedW = width * cos + depth * sin;
522
- const rotatedD = width * sin + depth * cos;
523
- const draftBounds = {
524
- minX: position[0] - rotatedW / 2,
525
- maxX: position[0] + rotatedW / 2,
526
- minZ: position[2] - rotatedD / 2,
527
- maxZ: position[2] + rotatedD / 2,
528
- };
514
+ const ignoreSet = expandIgnoredNodeIds(ignoreIds, nodes);
515
+ const draftBounds = footprints.map((footprint) => footprintBoundsXZ(footprint.position, footprint.dimensions, footprint.rotation[1] ?? 0));
516
+ for (let i = 0; i < draftBounds.length; i += 1) {
517
+ const a = draftBounds[i];
518
+ for (let j = i + 1; j < draftBounds.length; j += 1) {
519
+ const b = draftBounds[j];
520
+ if (intervalsOverlap(a.minX, a.maxX, b.minX, b.maxX) &&
521
+ intervalsOverlap(a.minZ, a.maxZ, b.minZ, b.maxZ)) {
522
+ return { valid: false, conflictIds: [] };
523
+ }
524
+ }
525
+ }
526
+ // A floor placement conflicts with any other COLLIDING floor-resting node,
527
+ // not just items — every kind whose `floorPlaced.collides` is set (item /
528
+ // shelf / column / cabinet / stair) contributes its footprint(s) as an
529
+ // obstacle. Each candidate's XZ extent is read from the same declarative
530
+ // footprint the elevation + sync paths use, so adding a colliding kind
531
+ // needs no change here.
529
532
  const conflicts = [];
530
533
  for (const node of Object.values(nodes)) {
531
- if (node.type !== 'item')
534
+ if (ignoreSet.has(node.id))
532
535
  continue;
533
- const item = node;
534
- if (item.asset.attachTo)
536
+ const floorPlaced = nodeRegistry.get(node.type)?.capabilities?.floorPlaced;
537
+ if (!floorPlaced?.collides)
535
538
  continue;
536
- if (isLowProfileItemSurface(item))
539
+ if (floorPlaced.applies && !floorPlaced.applies(node))
537
540
  continue;
538
- if (ignoreSet.has(item.id))
541
+ // Low-profile item surfaces (rugs, mats) are stack-on targets, not
542
+ // obstacles — keep the long-standing item-only exemption.
543
+ if (node.type === 'item' && isLowProfileItemSurface(node))
539
544
  continue;
540
- if (resolveNodeLevelId(item, nodes) !== levelId)
545
+ if (resolveNodeLevelId(node, nodes) !== levelId)
541
546
  continue;
542
- const bounds = getItemParentAabb(item);
543
- if (intervalsOverlap(draftBounds.minX, draftBounds.maxX, bounds.minX, bounds.maxX) &&
544
- intervalsOverlap(draftBounds.minZ, draftBounds.maxZ, bounds.minZ, bounds.maxZ)) {
545
- conflicts.push(item.id);
547
+ for (const footprint of getFloorPlacedFootprints(floorPlaced, node, { nodes })) {
548
+ const fpRotation = Array.isArray(footprint.rotation) ? (footprint.rotation[1] ?? 0) : 0;
549
+ const bounds = footprintBoundsXZ(footprint.position ?? node.position, footprint.dimensions, fpRotation);
550
+ if (draftBounds.some((draft) => intervalsOverlap(draft.minX, draft.maxX, bounds.minX, bounds.maxX) &&
551
+ intervalsOverlap(draft.minZ, draft.maxZ, bounds.minZ, bounds.maxZ))) {
552
+ conflicts.push(node.id);
553
+ break;
554
+ }
546
555
  }
547
556
  }
548
557
  return { valid: conflicts.length === 0, conflictIds: conflicts };
@@ -617,7 +626,8 @@ export class SpatialGridManager {
617
626
  if (!slabMap)
618
627
  return 0;
619
628
  let maxElevation = 0;
620
- for (const slab of slabMap.values()) {
629
+ for (const stored of slabMap.values()) {
630
+ const slab = this.effectiveSlabRecord(stored);
621
631
  if (slab.polygon.length >= 3 && pointInPolygon(x, z, slab.polygon)) {
622
632
  // Check if point is in any hole
623
633
  let inHole = false;
@@ -640,96 +650,222 @@ export class SpatialGridManager {
640
650
  }
641
651
  /**
642
652
  * Get the slab elevation for an item using its full footprint (bounding box).
643
- * Checks if any part of the item's rotated footprint overlaps with any slab polygon (excluding holes).
644
- * Returns the highest overlapping slab elevation, or 0 if none.
653
+ * Thin wrapper over {@link getSlabSupportForItem} for callers (and tests)
654
+ * that only need the number.
655
+ */
656
+ getSlabElevationForItem(levelId, position, dimensions, rotation, maxElevation) {
657
+ return this.getSlabSupportForItem(levelId, position, dimensions, rotation, maxElevation)
658
+ .elevation;
659
+ }
660
+ /**
661
+ * Elect the supporting slab for a footprint: the highest-elevation slab
662
+ * whose RENDERED polygon the footprint overlaps (center-point hole veto
663
+ * applies). Returns `{ elevation: 0, slabId: null }` when nothing
664
+ * overlaps.
665
+ *
666
+ * `maxElevation` is the pointer-decided cap: when set, only slabs whose
667
+ * walking surface sits at or below `maxElevation +
668
+ * SUPPORT_ELEVATION_EPSILON` may win — a deck hanging above the surface
669
+ * the cursor ray actually hit never captures the election.
645
670
  */
646
- getSlabElevationForItem(levelId, position, dimensions, rotation) {
671
+ getSlabSupportForItem(levelId, position, dimensions, rotation, maxElevation) {
647
672
  const slabMap = this.slabsByLevel.get(levelId);
648
673
  if (!slabMap)
649
- return 0;
650
- let maxElevation = Number.NEGATIVE_INFINITY;
651
- for (const slab of slabMap.values()) {
652
- if (slab.polygon.length >= 3 &&
653
- itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)) {
654
- // Check if item is entirely within a hole (if so, ignore this slab)
655
- // We consider it entirely in a hole if the item center is in the hole
674
+ return { elevation: 0, slabId: null };
675
+ let winningElevation = Number.NEGATIVE_INFINITY;
676
+ let winnerId = null;
677
+ for (const stored of slabMap.values()) {
678
+ const slab = this.effectiveSlabRecord(stored);
679
+ const elevation = slab.elevation ?? 0.05;
680
+ if (maxElevation != null && elevation > maxElevation + SUPPORT_ELEVATION_EPSILON)
681
+ continue;
682
+ if (!this.slabSupportsFootprint(levelId, slab, position, dimensions, rotation))
683
+ continue;
684
+ if (elevation > winningElevation) {
685
+ winningElevation = elevation;
686
+ winnerId = slab.id;
687
+ }
688
+ }
689
+ return winnerId === null
690
+ ? { elevation: 0, slabId: null }
691
+ : { elevation: winningElevation, slabId: winnerId };
692
+ }
693
+ /**
694
+ * The walking surface the pointer actually points at: the nearest slab
695
+ * plane the ray crosses INSIDE that slab's rendered polygon (hole veto
696
+ * applies), or the level base (`elevation: 0, slabId: null`) when it
697
+ * crosses none. Ray origin/direction are level-local. Deliberately a
698
+ * point test, not a footprint test — it answers "which surface is under
699
+ * the cursor", which then caps the footprint election so a deck hanging
700
+ * above the aimed-at floor never lifts the placement. `point` is the
701
+ * ray's crossing of that surface's plane — the stable plan point
702
+ * callers should elect/preview at (see {@link PointedSupportSurface}).
703
+ */
704
+ getPointedSupportSurface(levelId, rayOrigin, rayDirection) {
705
+ const slabMap = this.slabsByLevel.get(levelId);
706
+ const [ox, oy, oz] = rayOrigin;
707
+ const [dx, dy, dz] = rayDirection;
708
+ if (Math.abs(dy) < 1e-9)
709
+ return { elevation: 0, slabId: null, point: null };
710
+ let best = null;
711
+ if (slabMap) {
712
+ for (const stored of slabMap.values()) {
713
+ const slab = this.effectiveSlabRecord(stored);
714
+ if (slab.polygon.length < 3)
715
+ continue;
716
+ const elevation = slab.elevation ?? 0.05;
717
+ const t = (elevation - oy) / dy;
718
+ if (t <= 0)
719
+ continue;
720
+ if (best && t >= best.t)
721
+ continue;
722
+ const x = ox + dx * t;
723
+ const z = oz + dz * t;
724
+ const rendered = this.getRenderedSlabPolygon(levelId, slab);
725
+ if (rendered.length < 3 || !pointInPolygon(x, z, rendered))
726
+ continue;
656
727
  let inHole = false;
657
- const [cx, , cz] = position;
658
- const holes = slab.holes || [];
659
- for (const hole of holes) {
660
- if (hole.length >= 3 && pointInPolygon(cx, cz, hole)) {
728
+ for (const hole of slab.holes || []) {
729
+ if (hole.length >= 3 && pointInPolygon(x, z, hole)) {
661
730
  inHole = true;
662
731
  break;
663
732
  }
664
733
  }
665
- if (!inHole) {
666
- const elevation = slab.elevation ?? 0.05;
667
- if (elevation > maxElevation) {
668
- maxElevation = elevation;
669
- }
670
- }
734
+ if (inHole)
735
+ continue;
736
+ best = { t, elevation, slabId: slab.id };
671
737
  }
672
738
  }
673
- return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation;
739
+ if (best) {
740
+ return {
741
+ elevation: best.elevation,
742
+ slabId: best.slabId,
743
+ point: [ox + dx * best.t, oz + dz * best.t],
744
+ };
745
+ }
746
+ const tBase = -oy / dy;
747
+ return {
748
+ elevation: 0,
749
+ slabId: null,
750
+ point: tBase > 0 ? [ox + dx * tBase, oz + dz * tBase] : null,
751
+ };
752
+ }
753
+ /**
754
+ * All slabs supporting a footprint, one entry per overlapping slab
755
+ * (highest elevation first; slab id breaks ties deterministically).
756
+ * Commit-side ambiguity check: persist a `supportSlabId` only when the
757
+ * candidates carry ≥ 2 distinct elevations.
758
+ */
759
+ getSupportCandidatesForFootprint(levelId, position, dimensions, rotation) {
760
+ const slabMap = this.slabsByLevel.get(levelId);
761
+ if (!slabMap)
762
+ return [];
763
+ const candidates = [];
764
+ for (const stored of slabMap.values()) {
765
+ const slab = this.effectiveSlabRecord(stored);
766
+ if (!this.slabSupportsFootprint(levelId, slab, position, dimensions, rotation))
767
+ continue;
768
+ candidates.push({ slabId: slab.id, elevation: slab.elevation ?? 0.05 });
769
+ }
770
+ candidates.sort((a, b) => b.elevation - a.elevation || (a.slabId < b.slabId ? -1 : a.slabId > b.slabId ? 1 : 0));
771
+ return candidates;
772
+ }
773
+ /**
774
+ * Elevation of a persisted support host for a footprint, or null when
775
+ * the slab no longer exists on the level or no longer overlaps the
776
+ * footprint (same overlap test as election). Deliberately read-only: a
777
+ * host reshaped away is NOT cleared — callers fall back to election and
778
+ * the stale reference resumes hosting if the slab's polygon returns.
779
+ * Slab deletion is the only writer (`deleteNodesAction` strips it).
780
+ */
781
+ getHostSlabElevationForFootprint(levelId, slabId, position, dimensions, rotation) {
782
+ const stored = this.slabsByLevel.get(levelId)?.get(slabId);
783
+ if (!stored)
784
+ return null;
785
+ const slab = this.effectiveSlabRecord(stored);
786
+ if (!this.slabSupportsFootprint(levelId, slab, position, dimensions, rotation))
787
+ return null;
788
+ return slab.elevation ?? 0.05;
674
789
  }
675
790
  /**
676
791
  * Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
677
- * Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
678
792
  * Returns the highest slab elevation found, or 0 if none.
679
793
  *
680
794
  * Accepts an optional `curveOffset` so curved walls evaluate overlap
681
795
  * against their actual centerline samples, not just the chord.
682
796
  */
683
- getSlabElevationForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS) {
797
+ getSlabElevationForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS, preferredSlabId) {
798
+ return this.getSlabSupportForWall(levelId, start, end, curveOffset, thickness, preferredSlabId)
799
+ .elevation;
800
+ }
801
+ getSlabSupportForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS, preferredSlabId, maxElevation, supportOffset = 0) {
802
+ if (preferredSlabId === GROUND_SUPPORT_ID) {
803
+ const nodes = useScene.getState().nodes;
804
+ const elevation = (terrainSupportLift(nodes, levelId, start[0], start[1]) ?? 0) + supportOffset;
805
+ return {
806
+ elevation,
807
+ electedSlabId: null,
808
+ baseElevation: elevation,
809
+ baseSegments: [{ start: 0, end: 1, elevation }],
810
+ };
811
+ }
684
812
  const slabMap = this.slabsByLevel.get(levelId);
685
- if (!slabMap)
686
- return 0;
687
- const wallLike = { start, end, curveOffset, thickness };
688
- const isCurved = curveOffset !== 0 && isCurvedWall(wallLike);
689
- const holeSamplePoints = isCurved
690
- ? sampleWallCenterline(wallLike, 8)
691
- : [0, 0.25, 0.5, 0.75, 1].map((t) => ({
692
- x: start[0] + (end[0] - start[0]) * t,
693
- y: start[1] + (end[1] - start[1]) * t,
694
- }));
695
- let maxElevation = Number.NEGATIVE_INFINITY;
696
- for (const slab of slabMap.values()) {
697
- if (slab.polygon.length < 3)
698
- continue;
699
- if (!wallOverlapsPolygon(wallLike, slab.polygon))
700
- continue;
701
- const holes = slab.holes || [];
702
- if (holes.length === 0) {
703
- // No holes: wall is on this slab
704
- const elevation = slab.elevation ?? 0.05;
705
- if (elevation > maxElevation)
706
- maxElevation = elevation;
813
+ if (!slabMap) {
814
+ const elevation = supportOffset;
815
+ return {
816
+ elevation,
817
+ electedSlabId: null,
818
+ baseElevation: elevation,
819
+ baseSegments: [{ start: 0, end: 1, elevation }],
820
+ };
821
+ }
822
+ const support = computeWallSlabSupport({ start, end, curveOffset, thickness }, [...slabMap.values()].map((slab) => this.effectiveSlabRecord(slab)), this.getLevelWallNodes(levelId).map((wall) => getEffectiveNode(wall)), preferredSlabId, maxElevation);
823
+ if (supportOffset === 0)
824
+ return support;
825
+ return {
826
+ ...support,
827
+ elevation: support.elevation + supportOffset,
828
+ baseElevation: support.baseElevation + supportOffset,
829
+ baseSegments: support.baseSegments.map((segment) => ({
830
+ ...segment,
831
+ elevation: segment.elevation + supportOffset,
832
+ })),
833
+ };
834
+ }
835
+ /**
836
+ * Walls on a level, resolved fresh from the scene store (the manager's
837
+ * own wall map is only maintained on create/delete, not on updates).
838
+ * Cached per scene `nodes` record so per-pointer-tick callers
839
+ * (door/window move) don't rescan the node map.
840
+ */
841
+ levelWallsCache = new WeakMap();
842
+ getLevelWallNodes(levelId) {
843
+ const nodes = useScene.getState().nodes;
844
+ let byLevel = this.levelWallsCache.get(nodes);
845
+ if (!byLevel) {
846
+ byLevel = new Map();
847
+ this.levelWallsCache.set(nodes, byLevel);
848
+ }
849
+ const cached = byLevel.get(levelId);
850
+ if (cached)
851
+ return cached;
852
+ const walls = [];
853
+ for (const node of Object.values(nodes)) {
854
+ if (node.type !== 'wall')
707
855
  continue;
856
+ // Walk the parent chain to the owning level (guarded against cycles).
857
+ let current = node;
858
+ let guard = 0;
859
+ while (current && current.type !== 'level' && guard < 16) {
860
+ current = current.parentId ? nodes[current.parentId] : undefined;
861
+ guard += 1;
708
862
  }
709
- // Sample multiple points along the wall to check whether any portion lies on
710
- // solid slab (not inside any hole). Checking only the midpoint fails when the
711
- // midpoint falls in a staircase hole but the wall's endpoints are on solid slab.
712
- let hasValidPoint = false;
713
- for (const sample of holeSamplePoints) {
714
- let inHole = false;
715
- for (const hole of holes) {
716
- if (hole.length >= 3 && pointInPolygon(sample.x, sample.y, hole)) {
717
- inHole = true;
718
- break;
719
- }
720
- }
721
- if (!inHole) {
722
- hasValidPoint = true;
723
- break;
724
- }
725
- }
726
- if (hasValidPoint) {
727
- const elevation = slab.elevation ?? 0.05;
728
- if (elevation > maxElevation)
729
- maxElevation = elevation;
863
+ if (current?.type === 'level' && current.id === levelId) {
864
+ walls.push(node);
730
865
  }
731
866
  }
732
- return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation;
867
+ byLevel.set(levelId, walls);
868
+ return walls;
733
869
  }
734
870
  /**
735
871
  * Check if an item can be placed on a ceiling.
@@ -789,6 +925,7 @@ export class SpatialGridManager {
789
925
  return { valid: conflicts.length === 0, conflictIds: conflicts };
790
926
  }
791
927
  clearLevel(levelId) {
928
+ this.invalidateRenderedSlabPolygons(levelId);
792
929
  this.floorGrids.delete(levelId);
793
930
  this.wallGrids.delete(levelId);
794
931
  this.slabsByLevel.delete(levelId);
@@ -801,7 +938,26 @@ export class SpatialGridManager {
801
938
  this.ceilingGrids.clear();
802
939
  this.ceilings.clear();
803
940
  this.itemCeilingMap.clear();
941
+ this.renderedSlabPolygons.clear();
804
942
  }
805
943
  }
806
944
  // Singleton instance
807
945
  export const spatialGridManager = new SpatialGridManager();
946
+ /** Level-local Y where the rendered wall mesh begins. */
947
+ export function getWallBaseElevationForNodes(wall, nodes) {
948
+ const levelId = resolveNodeLevelId(wall, nodes);
949
+ return spatialGridManager.getSlabSupportForWall(levelId, wall.start, wall.end, wall.curveOffset ?? 0, wall.thickness, wall.supportSlabId ?? null, undefined, wall.supportOffset).elevation;
950
+ }
951
+ /**
952
+ * Effective (extruded) height of a wall resolved from a nodes record:
953
+ * {@link resolveWallEffectiveHeight} over the covering-clamped plane top
954
+ * (`getWallPlaneTop`) and the singleton manager's slab election — so the
955
+ * value always agrees with the rendered wall. One shared resolver for the
956
+ * editor overlays (measurement label, action menu, side handles) that used
957
+ * to copy this derivation locally.
958
+ */
959
+ export function getWallEffectiveHeightForNodes(wall, nodes) {
960
+ const levelId = resolveNodeLevelId(wall, nodes);
961
+ const baseElevation = getWallBaseElevationForNodes(wall, nodes);
962
+ return resolveWallEffectiveHeight(wall, getWallPlaneTop(wall, levelId, nodes), baseElevation);
963
+ }