@pascal-app/core 0.9.2 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/README.md +17 -10
  2. package/dist/events/bus.d.ts +21 -2
  3. package/dist/events/bus.d.ts.map +1 -1
  4. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
  5. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  6. package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +116 -66
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  9. package/dist/hooks/spatial-grid/spatial-grid-manager.js +319 -441
  10. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
  11. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  12. package/dist/hooks/spatial-grid/spatial-grid-sync.js +204 -12
  13. package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
  14. package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
  15. package/dist/hooks/spatial-grid/support-host-id.js +2 -0
  16. package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
  17. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
  18. package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
  19. package/dist/index.d.ts +23 -11
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +22 -10
  22. package/dist/lib/measurement-geometry.d.ts +4 -0
  23. package/dist/lib/measurement-geometry.d.ts.map +1 -1
  24. package/dist/lib/measurement-geometry.js +27 -10
  25. package/dist/lib/slab-polygon.d.ts.map +1 -1
  26. package/dist/lib/slab-polygon.js +33 -1
  27. package/dist/lib/space-detection.d.ts +15 -4
  28. package/dist/lib/space-detection.d.ts.map +1 -1
  29. package/dist/lib/space-detection.js +232 -91
  30. package/dist/lib/terrain-brush.d.ts +163 -0
  31. package/dist/lib/terrain-brush.d.ts.map +1 -0
  32. package/dist/lib/terrain-brush.js +359 -0
  33. package/dist/lib/terrain-codec.d.ts +51 -0
  34. package/dist/lib/terrain-codec.d.ts.map +1 -0
  35. package/dist/lib/terrain-codec.js +167 -0
  36. package/dist/lib/terrain-field.d.ts +150 -0
  37. package/dist/lib/terrain-field.d.ts.map +1 -0
  38. package/dist/lib/terrain-field.js +248 -0
  39. package/dist/lib/terrain-raycast.d.ts +44 -0
  40. package/dist/lib/terrain-raycast.d.ts.map +1 -0
  41. package/dist/lib/terrain-raycast.js +188 -0
  42. package/dist/lib/terrain-source.d.ts +60 -0
  43. package/dist/lib/terrain-source.d.ts.map +1 -0
  44. package/dist/lib/terrain-source.js +90 -0
  45. package/dist/lib/terrain-support.d.ts +85 -0
  46. package/dist/lib/terrain-support.d.ts.map +1 -0
  47. package/dist/lib/terrain-support.js +218 -0
  48. package/dist/lib/zone-quantities.d.ts +1 -1
  49. package/dist/lib/zone-quantities.d.ts.map +1 -1
  50. package/dist/lib/zone-quantities.js +311 -66
  51. package/dist/material-library.d.ts +9 -1
  52. package/dist/material-library.d.ts.map +1 -1
  53. package/dist/material-library.js +56 -4
  54. package/dist/registry/handles.d.ts +16 -1
  55. package/dist/registry/handles.d.ts.map +1 -1
  56. package/dist/registry/index.d.ts +1 -1
  57. package/dist/registry/index.d.ts.map +1 -1
  58. package/dist/registry/subtree.d.ts.map +1 -1
  59. package/dist/registry/subtree.js +5 -2
  60. package/dist/registry/types.d.ts +83 -1
  61. package/dist/registry/types.d.ts.map +1 -1
  62. package/dist/schema/index.d.ts +6 -3
  63. package/dist/schema/index.d.ts.map +1 -1
  64. package/dist/schema/index.js +6 -3
  65. package/dist/schema/nodes/cabinet.d.ts +2 -0
  66. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  67. package/dist/schema/nodes/cabinet.js +4 -2
  68. package/dist/schema/nodes/ceiling.d.ts +1 -1
  69. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  70. package/dist/schema/nodes/ceiling.js +7 -1
  71. package/dist/schema/nodes/column.d.ts +1 -0
  72. package/dist/schema/nodes/column.d.ts.map +1 -1
  73. package/dist/schema/nodes/column.js +2 -0
  74. package/dist/schema/nodes/construction-dimension.d.ts +195 -0
  75. package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
  76. package/dist/schema/nodes/construction-dimension.js +151 -0
  77. package/dist/schema/nodes/door.d.ts +29 -0
  78. package/dist/schema/nodes/door.d.ts.map +1 -1
  79. package/dist/schema/nodes/door.js +20 -0
  80. package/dist/schema/nodes/duct-terminal.d.ts +1 -0
  81. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -1
  82. package/dist/schema/nodes/duct-terminal.js +2 -0
  83. package/dist/schema/nodes/fence.d.ts +2 -0
  84. package/dist/schema/nodes/fence.d.ts.map +1 -1
  85. package/dist/schema/nodes/fence.js +8 -0
  86. package/dist/schema/nodes/hvac-equipment.d.ts +1 -0
  87. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -1
  88. package/dist/schema/nodes/hvac-equipment.js +2 -0
  89. package/dist/schema/nodes/item.d.ts +1 -0
  90. package/dist/schema/nodes/item.d.ts.map +1 -1
  91. package/dist/schema/nodes/item.js +13 -0
  92. package/dist/schema/nodes/level.d.ts +2 -1
  93. package/dist/schema/nodes/level.d.ts.map +1 -1
  94. package/dist/schema/nodes/level.js +12 -33
  95. package/dist/schema/nodes/shelf.d.ts +1 -0
  96. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  97. package/dist/schema/nodes/shelf.js +2 -0
  98. package/dist/schema/nodes/site.d.ts +9 -0
  99. package/dist/schema/nodes/site.d.ts.map +1 -1
  100. package/dist/schema/nodes/site.js +8 -6
  101. package/dist/schema/nodes/slab.d.ts +5 -0
  102. package/dist/schema/nodes/slab.d.ts.map +1 -1
  103. package/dist/schema/nodes/slab.js +14 -2
  104. package/dist/schema/nodes/spawn.d.ts +1 -0
  105. package/dist/schema/nodes/spawn.d.ts.map +1 -1
  106. package/dist/schema/nodes/spawn.js +2 -0
  107. package/dist/schema/nodes/stair.d.ts +3 -1
  108. package/dist/schema/nodes/stair.d.ts.map +1 -1
  109. package/dist/schema/nodes/stair.js +8 -1
  110. package/dist/schema/nodes/structural-grid.d.ts +27 -0
  111. package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
  112. package/dist/schema/nodes/structural-grid.js +17 -0
  113. package/dist/schema/nodes/wall.d.ts +5 -2
  114. package/dist/schema/nodes/wall.d.ts.map +1 -1
  115. package/dist/schema/nodes/wall.js +13 -4
  116. package/dist/schema/nodes/window.d.ts +29 -0
  117. package/dist/schema/nodes/window.d.ts.map +1 -1
  118. package/dist/schema/nodes/window.js +18 -0
  119. package/dist/schema/nodes/zone.d.ts +20 -0
  120. package/dist/schema/nodes/zone.d.ts.map +1 -1
  121. package/dist/schema/nodes/zone.js +15 -0
  122. package/dist/schema/terrain.d.ts +28 -0
  123. package/dist/schema/terrain.d.ts.map +1 -0
  124. package/dist/schema/terrain.js +30 -0
  125. package/dist/schema/types.d.ts +211 -3
  126. package/dist/schema/types.d.ts.map +1 -1
  127. package/dist/schema/types.js +4 -0
  128. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
  129. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
  130. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
  131. package/dist/services/index.d.ts +2 -1
  132. package/dist/services/index.d.ts.map +1 -1
  133. package/dist/services/index.js +2 -1
  134. package/dist/services/level-height.d.ts +8 -9
  135. package/dist/services/level-height.d.ts.map +1 -1
  136. package/dist/services/level-height.js +48 -16
  137. package/dist/services/storey.d.ts +103 -0
  138. package/dist/services/storey.d.ts.map +1 -0
  139. package/dist/services/storey.js +296 -0
  140. package/dist/store/actions/node-actions.d.ts.map +1 -1
  141. package/dist/store/actions/node-actions.js +47 -3
  142. package/dist/store/history-control.d.ts +20 -0
  143. package/dist/store/history-control.d.ts.map +1 -1
  144. package/dist/store/history-control.js +110 -13
  145. package/dist/store/use-live-terrain.d.ts +65 -0
  146. package/dist/store/use-live-terrain.d.ts.map +1 -0
  147. package/dist/store/use-live-terrain.js +31 -0
  148. package/dist/store/use-live-transforms.d.ts +8 -0
  149. package/dist/store/use-live-transforms.d.ts.map +1 -1
  150. package/dist/store/use-scene.d.ts +31 -2
  151. package/dist/store/use-scene.d.ts.map +1 -1
  152. package/dist/store/use-scene.js +638 -8
  153. package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
  154. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
  155. package/dist/systems/elevator/elevator-opening-system.js +27 -21
  156. package/dist/systems/elevator/elevator-service.d.ts +0 -2
  157. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  158. package/dist/systems/elevator/elevator-service.js +2 -24
  159. package/dist/systems/slab/slab-placement.d.ts +11 -0
  160. package/dist/systems/slab/slab-placement.d.ts.map +1 -0
  161. package/dist/systems/slab/slab-placement.js +14 -0
  162. package/dist/systems/slab/slab-support.d.ts +150 -0
  163. package/dist/systems/slab/slab-support.d.ts.map +1 -0
  164. package/dist/systems/slab/slab-support.js +575 -0
  165. package/dist/systems/stair/stair-opening-preview.d.ts +1060 -935
  166. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  167. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  168. package/dist/systems/stair/stair-opening-sync.js +18 -15
  169. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  170. package/dist/systems/stair/stair-opening-system.js +32 -4
  171. package/dist/systems/stair/stair-rise.d.ts +22 -0
  172. package/dist/systems/stair/stair-rise.d.ts.map +1 -0
  173. package/dist/systems/stair/stair-rise.js +79 -0
  174. package/dist/systems/wall/wall-curve.d.ts +10 -0
  175. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  176. package/dist/systems/wall/wall-curve.js +1 -1
  177. package/dist/systems/wall/wall-mitering.js +6 -6
  178. package/dist/systems/wall/wall-top.d.ts +40 -0
  179. package/dist/systems/wall/wall-top.d.ts.map +1 -0
  180. package/dist/systems/wall/wall-top.js +46 -0
  181. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  182. package/dist/utils/clone-scene-graph.js +34 -3
  183. package/dist/utils/heal-scene-graph.d.ts +1 -1
  184. package/dist/utils/heal-scene-graph.d.ts.map +1 -1
  185. package/dist/utils/heal-scene-graph.js +25 -9
  186. package/dist/validation/validate-build-json.d.ts +2 -0
  187. package/dist/validation/validate-build-json.d.ts.map +1 -1
  188. package/dist/validation/validate-build-json.js +59 -3
  189. package/package.json +2 -2
  190. package/dist/hooks/scene-registry/scene-registry.test.d.ts +0 -2
  191. package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +0 -1
  192. package/dist/hooks/scene-registry/scene-registry.test.js +0 -26
  193. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
  194. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
  195. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -366
  196. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +0 -2
  197. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +0 -1
  198. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +0 -363
  199. package/dist/lib/measurement-geometry.test.d.ts +0 -2
  200. package/dist/lib/measurement-geometry.test.d.ts.map +0 -1
  201. package/dist/lib/measurement-geometry.test.js +0 -102
  202. package/dist/lib/slab-polygon.test.d.ts +0 -2
  203. package/dist/lib/slab-polygon.test.d.ts.map +0 -1
  204. package/dist/lib/slab-polygon.test.js +0 -635
  205. package/dist/lib/space-detection-pause.test.d.ts +0 -2
  206. package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
  207. package/dist/lib/space-detection-pause.test.js +0 -42
  208. package/dist/lib/space-detection.test.d.ts +0 -2
  209. package/dist/lib/space-detection.test.d.ts.map +0 -1
  210. package/dist/lib/space-detection.test.js +0 -376
  211. package/dist/lib/zone-quantities.test.d.ts +0 -2
  212. package/dist/lib/zone-quantities.test.d.ts.map +0 -1
  213. package/dist/lib/zone-quantities.test.js +0 -82
  214. package/dist/registry/registry.test.d.ts +0 -2
  215. package/dist/registry/registry.test.d.ts.map +0 -1
  216. package/dist/registry/registry.test.js +0 -196
  217. package/dist/registry/relations-resolver.test.d.ts +0 -2
  218. package/dist/registry/relations-resolver.test.d.ts.map +0 -1
  219. package/dist/registry/relations-resolver.test.js +0 -183
  220. package/dist/registry/scene-api.test.d.ts +0 -2
  221. package/dist/registry/scene-api.test.d.ts.map +0 -1
  222. package/dist/registry/scene-api.test.js +0 -183
  223. package/dist/registry/subtree.test.d.ts +0 -2
  224. package/dist/registry/subtree.test.d.ts.map +0 -1
  225. package/dist/registry/subtree.test.js +0 -163
  226. package/dist/schema/asset-url.test.d.ts +0 -2
  227. package/dist/schema/asset-url.test.d.ts.map +0 -1
  228. package/dist/schema/asset-url.test.js +0 -134
  229. package/dist/schema/material.test.d.ts +0 -2
  230. package/dist/schema/material.test.d.ts.map +0 -1
  231. package/dist/schema/material.test.js +0 -43
  232. package/dist/schema/nodes/measurement.test.d.ts +0 -2
  233. package/dist/schema/nodes/measurement.test.d.ts.map +0 -1
  234. package/dist/schema/nodes/measurement.test.js +0 -139
  235. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +0 -2
  236. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +0 -1
  237. package/dist/schema/nodes/ridge-vent-defaults.test.js +0 -205
  238. package/dist/schema/nodes/roof-segment-shape.test.d.ts +0 -2
  239. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +0 -1
  240. package/dist/schema/nodes/roof-segment-shape.test.js +0 -175
  241. package/dist/schema/nodes/roof-segment-surface.test.d.ts +0 -2
  242. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +0 -1
  243. package/dist/schema/nodes/roof-segment-surface.test.js +0 -48
  244. package/dist/schema/nodes/roof-segment-trim.test.d.ts +0 -2
  245. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +0 -1
  246. package/dist/schema/nodes/roof-segment-trim.test.js +0 -107
  247. package/dist/schema/nodes/roof-segment-walls.test.d.ts +0 -2
  248. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +0 -1
  249. package/dist/schema/nodes/roof-segment-walls.test.js +0 -92
  250. package/dist/schema/nodes/wall.test.d.ts +0 -2
  251. package/dist/schema/nodes/wall.test.d.ts.map +0 -1
  252. package/dist/schema/nodes/wall.test.js +0 -208
  253. package/dist/services/alignment-anchors.test.d.ts +0 -2
  254. package/dist/services/alignment-anchors.test.d.ts.map +0 -1
  255. package/dist/services/alignment-anchors.test.js +0 -383
  256. package/dist/services/alignment.test.d.ts +0 -2
  257. package/dist/services/alignment.test.d.ts.map +0 -1
  258. package/dist/services/alignment.test.js +0 -92
  259. package/dist/services/drag-session.test.d.ts +0 -2
  260. package/dist/services/drag-session.test.d.ts.map +0 -1
  261. package/dist/services/drag-session.test.js +0 -219
  262. package/dist/services/hosting.test.d.ts +0 -2
  263. package/dist/services/hosting.test.d.ts.map +0 -1
  264. package/dist/services/hosting.test.js +0 -230
  265. package/dist/services/movement.test.d.ts +0 -2
  266. package/dist/services/movement.test.d.ts.map +0 -1
  267. package/dist/services/movement.test.js +0 -133
  268. package/dist/services/opening-guides.test.d.ts +0 -2
  269. package/dist/services/opening-guides.test.d.ts.map +0 -1
  270. package/dist/services/opening-guides.test.js +0 -208
  271. package/dist/services/port-connectivity.test.d.ts +0 -2
  272. package/dist/services/port-connectivity.test.d.ts.map +0 -1
  273. package/dist/services/port-connectivity.test.js +0 -297
  274. package/dist/services/riser-diagram.test.d.ts +0 -2
  275. package/dist/services/riser-diagram.test.d.ts.map +0 -1
  276. package/dist/services/riser-diagram.test.js +0 -66
  277. package/dist/services/single-undo-dance.test.d.ts +0 -2
  278. package/dist/services/single-undo-dance.test.d.ts.map +0 -1
  279. package/dist/services/single-undo-dance.test.js +0 -210
  280. package/dist/services/snap.test.d.ts +0 -2
  281. package/dist/services/snap.test.d.ts.map +0 -1
  282. package/dist/services/snap.test.js +0 -137
  283. package/dist/services/system-graph.test.d.ts +0 -2
  284. package/dist/services/system-graph.test.d.ts.map +0 -1
  285. package/dist/services/system-graph.test.js +0 -138
  286. package/dist/services/validate-dwv.test.d.ts +0 -2
  287. package/dist/services/validate-dwv.test.d.ts.map +0 -1
  288. package/dist/services/validate-dwv.test.js +0 -109
  289. package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
  290. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
  291. package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
  292. package/dist/store/actions/reparent.test.d.ts +0 -2
  293. package/dist/store/actions/reparent.test.d.ts.map +0 -1
  294. package/dist/store/actions/reparent.test.js +0 -212
  295. package/dist/store/actions/ridge-vent-update.test.d.ts +0 -2
  296. package/dist/store/actions/ridge-vent-update.test.d.ts.map +0 -1
  297. package/dist/store/actions/ridge-vent-update.test.js +0 -264
  298. package/dist/store/use-scene-dirty-tracking.test.d.ts +0 -2
  299. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +0 -1
  300. package/dist/store/use-scene-dirty-tracking.test.js +0 -68
  301. package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
  302. package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
  303. package/dist/store/use-scene-elevator-migration.test.js +0 -109
  304. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +0 -2
  305. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +0 -1
  306. package/dist/store/use-scene-measurement-roundtrip.test.js +0 -97
  307. package/dist/store/use-scene-plugins.test.d.ts +0 -2
  308. package/dist/store/use-scene-plugins.test.d.ts.map +0 -1
  309. package/dist/store/use-scene-plugins.test.js +0 -51
  310. package/dist/store/use-scene-wall-slot-migration.test.d.ts +0 -2
  311. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +0 -1
  312. package/dist/store/use-scene-wall-slot-migration.test.js +0 -210
  313. package/dist/store/use-scene-window-migration.test.d.ts +0 -2
  314. package/dist/store/use-scene-window-migration.test.d.ts.map +0 -1
  315. package/dist/store/use-scene-window-migration.test.js +0 -85
  316. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
  317. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
  318. package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
  319. package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
  320. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
  321. package/dist/systems/elevator/elevator-runtime.test.js +0 -64
  322. package/dist/systems/fence/fence-spline.test.d.ts +0 -2
  323. package/dist/systems/fence/fence-spline.test.d.ts.map +0 -1
  324. package/dist/systems/fence/fence-spline.test.js +0 -136
  325. package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
  326. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
  327. package/dist/systems/stair/stair-opening-preview.test.js +0 -80
  328. package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
  329. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
  330. package/dist/systems/stair/stair-opening-sync.test.js +0 -485
  331. package/dist/systems/wall/wall-mitering.test.d.ts +0 -2
  332. package/dist/systems/wall/wall-mitering.test.d.ts.map +0 -1
  333. package/dist/systems/wall/wall-mitering.test.js +0 -63
  334. package/dist/utils/clone-scene-graph.test.d.ts +0 -2
  335. package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
  336. package/dist/utils/clone-scene-graph.test.js +0 -62
  337. package/dist/utils/heal-scene-graph.test.d.ts +0 -2
  338. package/dist/utils/heal-scene-graph.test.d.ts.map +0 -1
  339. package/dist/utils/heal-scene-graph.test.js +0 -88
@@ -1,30 +1,22 @@
1
1
  import { getRenderableSlabPolygon } from '../../lib/slab-polygon';
2
+ import { levelBaseElevationAt } from '../../lib/terrain-support';
2
3
  import { nodeRegistry } from '../../registry';
3
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';
4
8
  import useScene from '../../store/use-scene';
5
- import { getWallCurveFrameAt, isCurvedWall } from '../../systems/wall/wall-curve';
9
+ import { computeWallSlabSupport, pointInPolygon, SUPPORT_ELEVATION_EPSILON, } from '../../systems/slab/slab-support';
6
10
  import { DEFAULT_WALL_THICKNESS } from '../../systems/wall/wall-footprint';
11
+ import { resolveWallEffectiveHeight } from '../../systems/wall/wall-top';
7
12
  import { getFloorPlacedFootprints } from './floor-placed-elevation';
8
13
  import { SpatialGrid } from './spatial-grid';
14
+ import { GROUND_SUPPORT_ID } from './support-host-id';
9
15
  import { WallSpatialGrid } from './wall-spatial-grid';
16
+ export { computeWallSlabElevation, computeWallSlabSupport, pointInPolygon, SUPPORT_ELEVATION_EPSILON, wallOverlapsPolygon, } from '../../systems/slab/slab-support';
10
17
  // ============================================================================
11
18
  // GEOMETRY HELPERS
12
19
  // ============================================================================
13
- /**
14
- * Point-in-polygon test using ray casting algorithm.
15
- */
16
- export function pointInPolygon(px, pz, polygon) {
17
- let inside = false;
18
- const n = polygon.length;
19
- for (let i = 0, j = n - 1; i < n; j = i++) {
20
- const xi = polygon[i][0], zi = polygon[i][1];
21
- const xj = polygon[j][0], zj = polygon[j][1];
22
- if (zi > pz !== zj > pz && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
23
- inside = !inside;
24
- }
25
- }
26
- return inside;
27
- }
28
20
  /**
29
21
  * Compute the 4 XZ footprint corners of an item given its position, dimensions, and Y rotation.
30
22
  */
@@ -203,400 +195,6 @@ export function itemOverlapsPolygon(position, dimensions, rotation, polygon, ins
203
195
  }
204
196
  return false;
205
197
  }
206
- function pointSegmentDistance(px, pz, ax, az, bx, bz) {
207
- const dx = bx - ax;
208
- const dz = bz - az;
209
- const lengthSquared = dx * dx + dz * dz;
210
- if (lengthSquared < 1e-18)
211
- return Math.hypot(px - ax, pz - az);
212
- const t = Math.max(0, Math.min(1, ((px - ax) * dx + (pz - az) * dz) / lengthSquared));
213
- return Math.hypot(px - (ax + dx * t), pz - (az + dz * t));
214
- }
215
- // Ray-cast pointInPolygon is unreliable for points exactly on the polygon
216
- // boundary: the answer flips depending on which side of the polygon the edge
217
- // is on. Interval classification below therefore treats "within this distance
218
- // of the boundary" as inside explicitly, so walls sitting exactly on a slab
219
- // edge (the common case — auto-slab polygons derive from wall centerlines)
220
- // classify identically on every side of the slab.
221
- const ON_BOUNDARY_EPSILON = 1e-4;
222
- function pointOnPolygonBoundary(px, pz, polygon) {
223
- const n = polygon.length;
224
- for (let i = 0; i < n; i++) {
225
- const [ax, az] = polygon[i];
226
- const [bx, bz] = polygon[(i + 1) % n];
227
- if (pointSegmentDistance(px, pz, ax, az, bx, bz) <= ON_BOUNDARY_EPSILON)
228
- return true;
229
- }
230
- return false;
231
- }
232
- function mergeIntervals(intervals) {
233
- if (intervals.length <= 1)
234
- return intervals;
235
- const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
236
- const merged = [[sorted[0][0], sorted[0][1]]];
237
- for (let i = 1; i < sorted.length; i++) {
238
- const [intervalStart, intervalEnd] = sorted[i];
239
- const last = merged[merged.length - 1];
240
- if (intervalStart <= last[1] + 1e-9) {
241
- last[1] = Math.max(last[1], intervalEnd);
242
- }
243
- else {
244
- merged.push([intervalStart, intervalEnd]);
245
- }
246
- }
247
- return merged;
248
- }
249
- /** Total length of a merged (sorted, disjoint) interval list. */
250
- function intervalsLength(intervals) {
251
- let total = 0;
252
- for (const [intervalStart, intervalEnd] of intervals)
253
- total += intervalEnd - intervalStart;
254
- return total;
255
- }
256
- /** `base` minus `cut`. Both inputs may be unsorted; the result is merged. */
257
- function subtractIntervals(base, cut) {
258
- if (base.length === 0 || cut.length === 0)
259
- return mergeIntervals(base);
260
- const cuts = mergeIntervals(cut);
261
- const result = [];
262
- for (const [baseStart, baseEnd] of mergeIntervals(base)) {
263
- let cursor = baseStart;
264
- for (const [cutStart, cutEnd] of cuts) {
265
- if (cutEnd <= cursor)
266
- continue;
267
- if (cutStart >= baseEnd)
268
- break;
269
- if (cutStart > cursor)
270
- result.push([cursor, cutStart]);
271
- cursor = cutEnd;
272
- if (cursor >= baseEnd)
273
- break;
274
- }
275
- if (cursor < baseEnd)
276
- result.push([cursor, baseEnd]);
277
- }
278
- return result;
279
- }
280
- /**
281
- * Sub-intervals of segment (ax,az)→(bx,bz) that lie inside the polygon (and,
282
- * when `includeBoundary`, on its boundary), as [t0, t1] fractions of the
283
- * segment. The segment is split at every crossing with a polygon edge and
284
- * each sub-interval is classified by its midpoint, so no test point ever
285
- * sits on a crossing.
286
- */
287
- function segmentInsideIntervals(ax, az, bx, bz, polygon, includeBoundary) {
288
- const dx = bx - ax;
289
- const dz = bz - az;
290
- const length = Math.hypot(dx, dz);
291
- if (length < 1e-9)
292
- return [];
293
- const ts = [0, 1];
294
- const n = polygon.length;
295
- for (let i = 0; i < n; i++) {
296
- const [px, pz] = polygon[i];
297
- const [qx, qz] = polygon[(i + 1) % n];
298
- const ex = qx - px;
299
- const ez = qz - pz;
300
- const denom = dx * ez - dz * ex;
301
- if (Math.abs(denom) < 1e-12)
302
- continue; // parallel/collinear — nothing to split at
303
- const t = ((px - ax) * ez - (pz - az) * ex) / denom;
304
- const s = ((px - ax) * dz - (pz - az) * dx) / denom;
305
- if (t > 0 && t < 1 && s >= -1e-9 && s <= 1 + 1e-9)
306
- ts.push(t);
307
- }
308
- ts.sort((a, b) => a - b);
309
- const inside = [];
310
- for (let i = 1; i < ts.length; i++) {
311
- const t0 = ts[i - 1];
312
- const t1 = ts[i];
313
- if (t1 - t0 < 1e-9)
314
- continue;
315
- const tm = (t0 + t1) / 2;
316
- const mx = ax + dx * tm;
317
- const mz = az + dz * tm;
318
- const midpointInside = pointOnPolygonBoundary(mx, mz, polygon)
319
- ? includeBoundary
320
- : pointInPolygon(mx, mz, polygon);
321
- if (midpointInside)
322
- inside.push([t0, t1]);
323
- }
324
- return inside;
325
- }
326
- function polylineLength(points) {
327
- let total = 0;
328
- for (let i = 1; i < points.length; i++) {
329
- total += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
330
- }
331
- return total;
332
- }
333
- /**
334
- * Inside sub-intervals of a polyline against a polygon, in cumulative
335
- * arc-length units from the polyline start (merged, disjoint). Boundary
336
- * contact counts as inside for slab support (walls sit exactly on slab
337
- * edges — see ON_BOUNDARY_EPSILON above); hole callers pass
338
- * `includeBoundary: false` so a wall running along a stairwell hole's
339
- * rim keeps the rim's support.
340
- */
341
- function polylineInsideIntervals(points, polygon, includeBoundary = true) {
342
- const intervals = [];
343
- let offset = 0;
344
- for (let i = 1; i < points.length; i++) {
345
- const a = points[i - 1];
346
- const b = points[i];
347
- const segmentLength = Math.hypot(b.x - a.x, b.y - a.y);
348
- if (segmentLength < 1e-9)
349
- continue;
350
- for (const [t0, t1] of segmentInsideIntervals(a.x, a.y, b.x, b.y, polygon, includeBoundary)) {
351
- intervals.push([offset + t0 * segmentLength, offset + t1 * segmentLength]);
352
- }
353
- offset += segmentLength;
354
- }
355
- return mergeIntervals(intervals);
356
- }
357
- function polylineInsideLength(points, polygon) {
358
- return intervalsLength(polylineInsideIntervals(points, polygon));
359
- }
360
- // Minimum length of wall that must lie on/inside a slab polygon before the
361
- // wall counts as overlapping it. Point contact (a perpendicular wall butting
362
- // into a room's edge) clips to ~zero length and never reaches this, so such
363
- // walls don't follow the slab's elevation.
364
- const WALL_SLAB_MIN_OVERLAP = 0.05;
365
- /**
366
- * Centerline of the wall plus its two face lines (centerline offset by
367
- * ±halfThickness). The face lines catch walls whose centerline sits on or
368
- * just outside the slab boundary but whose body reaches onto the slab —
369
- * e.g. slab polygons drawn to the room's interior faces.
370
- */
371
- function wallTestPolylines(start, end, curveOffset, halfThickness) {
372
- const wallLike = { start, end, curveOffset };
373
- if (curveOffset !== 0 && isCurvedWall(wallLike)) {
374
- const count = 16;
375
- const center = [];
376
- const left = [];
377
- const right = [];
378
- for (let i = 0; i <= count; i++) {
379
- const frame = getWallCurveFrameAt(wallLike, i / count);
380
- center.push(frame.point);
381
- left.push({
382
- x: frame.point.x + frame.normal.x * halfThickness,
383
- y: frame.point.y + frame.normal.y * halfThickness,
384
- });
385
- right.push({
386
- x: frame.point.x - frame.normal.x * halfThickness,
387
- y: frame.point.y - frame.normal.y * halfThickness,
388
- });
389
- }
390
- return halfThickness > 0 ? [center, left, right] : [center];
391
- }
392
- const center = [
393
- { x: start[0], y: start[1] },
394
- { x: end[0], y: end[1] },
395
- ];
396
- const dx = end[0] - start[0];
397
- const dz = end[1] - start[1];
398
- const len = Math.hypot(dx, dz);
399
- if (len < 1e-10 || halfThickness <= 0)
400
- return [center];
401
- const nx = (-dz / len) * halfThickness;
402
- const nz = (dx / len) * halfThickness;
403
- return [
404
- center,
405
- [
406
- { x: start[0] + nx, y: start[1] + nz },
407
- { x: end[0] + nx, y: end[1] + nz },
408
- ],
409
- [
410
- { x: start[0] - nx, y: start[1] - nz },
411
- { x: end[0] - nx, y: end[1] - nz },
412
- ],
413
- ];
414
- }
415
- /**
416
- * Test whether a wall overlaps a slab polygon along a segment of its length.
417
- *
418
- * The wall's centerline and both face lines are clipped against the polygon;
419
- * the wall overlaps when the longest clipped inside-or-on-boundary length
420
- * exceeds a threshold (5cm, halved for very short walls). Because interval
421
- * midpoints classify "on the boundary" as inside explicitly (never by
422
- * ray-cast tie-breaking), a wall sitting exactly on a slab edge resolves
423
- * identically on every side of the slab.
424
- *
425
- * A wall that only touches the polygon at a point — a perpendicular wall
426
- * butting into a room's edge, or a corner-to-corner touch — clips to ~zero
427
- * length and does NOT overlap.
428
- */
429
- export function wallOverlapsPolygon(startOrWall, endOrPolygon, polygonArg) {
430
- // Two call shapes:
431
- // wallOverlapsPolygon(wallLike, polygon) — preferred; curve-aware
432
- // wallOverlapsPolygon(start, end, polygon) — legacy chord-only
433
- let start;
434
- let end;
435
- let polygon;
436
- let curveOffset = 0;
437
- let thickness = DEFAULT_WALL_THICKNESS;
438
- if (Array.isArray(startOrWall)) {
439
- start = startOrWall;
440
- end = endOrPolygon;
441
- polygon = polygonArg;
442
- }
443
- else {
444
- start = startOrWall.start;
445
- end = startOrWall.end;
446
- curveOffset = startOrWall.curveOffset ?? 0;
447
- thickness = startOrWall.thickness ?? DEFAULT_WALL_THICKNESS;
448
- polygon = endOrPolygon;
449
- }
450
- const halfThickness = Math.max(thickness / 2, 0);
451
- const polylines = wallTestPolylines(start, end, curveOffset, halfThickness);
452
- const centerLength = polylineLength(polylines[0]);
453
- if (centerLength < 1e-9)
454
- return false;
455
- let overlap = 0;
456
- for (const line of polylines) {
457
- overlap = Math.max(overlap, polylineInsideLength(line, polygon));
458
- }
459
- const threshold = Math.max(1e-3, Math.min(WALL_SLAB_MIN_OVERLAP, centerLength * 0.5));
460
- return overlap >= threshold;
461
- }
462
- // A slab elevation must support at least this fraction of the wall's
463
- // length before it can dictate the wall's base. Below majority, a raised
464
- // slab reaching one endpoint would hoist the whole wall off the floor
465
- // that actually carries it.
466
- const WALL_SLAB_SUPPORT_MAJORITY = 0.5;
467
- // Slabs whose elevations differ by less than this pool their support:
468
- // a wall shared between two rooms' slabs is covered roughly half by
469
- // each, and must still follow their common elevation.
470
- const WALL_SLAB_ELEVATION_POOL_EPSILON = 1e-4;
471
- export function computeWallSlabSupport(wallLike, slabs, levelWalls) {
472
- const { start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS } = wallLike;
473
- const halfThickness = Math.max(thickness / 2, 0);
474
- const polylines = wallTestPolylines(start, end, curveOffset, halfThickness);
475
- const polylineLengths = polylines.map(polylineLength);
476
- const wallLength = polylineLengths[0];
477
- if (wallLength < 1e-9) {
478
- return { elevation: 0, baseElevation: 0, baseSegments: [] };
479
- }
480
- const minSupport = Math.max(1e-3, Math.min(WALL_SLAB_MIN_OVERLAP, wallLength * 0.5));
481
- const groups = [];
482
- for (const slab of slabs) {
483
- if (slab.polygon.length < 3)
484
- continue;
485
- const renderedPolygon = getRenderableSlabPolygon(slab, {
486
- walls: levelWalls,
487
- siblingSlabs: slabs.filter((other) => other.id !== slab.id),
488
- });
489
- let supported = 0;
490
- const perPolyline = polylines.map((line) => {
491
- let intervals = polylineInsideIntervals(line, renderedPolygon);
492
- for (const hole of slab.holes || []) {
493
- if (intervals.length === 0)
494
- break;
495
- if (hole.length < 3)
496
- continue;
497
- intervals = subtractIntervals(intervals, polylineInsideIntervals(line, hole, false));
498
- }
499
- supported = Math.max(supported, intervalsLength(intervals));
500
- return intervals;
501
- });
502
- if (supported < minSupport)
503
- continue;
504
- const elevation = slab.elevation ?? 0.05;
505
- let group = groups.find((candidate) => Math.abs(candidate.elevation - elevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON);
506
- if (!group) {
507
- group = { elevation, perPolyline: polylines.map(() => []) };
508
- groups.push(group);
509
- }
510
- for (let i = 0; i < perPolyline.length; i++) {
511
- group.perPolyline[i].push(...perPolyline[i]);
512
- }
513
- }
514
- const evaluatedGroups = groups.map((group) => {
515
- let coverage = 0;
516
- const mergedPerPolyline = group.perPolyline.map(mergeIntervals);
517
- for (let i = 0; i < group.perPolyline.length; i++) {
518
- const lineLength = polylineLengths[i];
519
- if (lineLength < 1e-9)
520
- continue;
521
- coverage = Math.max(coverage, intervalsLength(mergedPerPolyline[i]) / lineLength);
522
- }
523
- return { ...group, coverage, mergedPerPolyline };
524
- });
525
- let majorityElevation = Number.NEGATIVE_INFINITY;
526
- let bestElevation = Number.NEGATIVE_INFINITY;
527
- let bestCoverage = -1;
528
- for (const group of evaluatedGroups) {
529
- if (group.coverage >= WALL_SLAB_SUPPORT_MAJORITY - 1e-6) {
530
- majorityElevation = Math.max(majorityElevation, group.elevation);
531
- }
532
- if (group.coverage > bestCoverage + 1e-6 ||
533
- (Math.abs(group.coverage - bestCoverage) <= 1e-6 && group.elevation > bestElevation)) {
534
- bestCoverage = group.coverage;
535
- bestElevation = group.elevation;
536
- }
537
- }
538
- const elevation = majorityElevation !== Number.NEGATIVE_INFINITY
539
- ? majorityElevation
540
- : bestElevation === Number.NEGATIVE_INFINITY
541
- ? 0
542
- : bestElevation;
543
- const normalizedIntervals = (group, polylineIndex) => {
544
- const lineLength = polylineLengths[polylineIndex];
545
- if (lineLength < 1e-9)
546
- return [];
547
- return group.mergedPerPolyline[polylineIndex].map(([intervalStart, intervalEnd]) => [intervalStart / lineLength, intervalEnd / lineLength]);
548
- };
549
- const normalizedByGroup = evaluatedGroups.map((group) => ({
550
- elevation: group.elevation,
551
- perPolyline: group.mergedPerPolyline.map((_, index) => normalizedIntervals(group, index)),
552
- }));
553
- const breakpoints = [0, 1];
554
- for (const group of normalizedByGroup) {
555
- for (const intervals of group.perPolyline) {
556
- for (const [intervalStart, intervalEnd] of intervals) {
557
- breakpoints.push(intervalStart, intervalEnd);
558
- }
559
- }
560
- }
561
- breakpoints.sort((left, right) => left - right);
562
- const uniqueBreakpoints = breakpoints.filter((value, index) => index === 0 || value - breakpoints[index - 1] > 1e-7);
563
- const highestAt = (polylineIndex, t) => {
564
- let highest = Number.NEGATIVE_INFINITY;
565
- for (const group of normalizedByGroup) {
566
- if (group.perPolyline[polylineIndex]?.some(([intervalStart, intervalEnd]) => t >= intervalStart - 1e-7 && t <= intervalEnd + 1e-7)) {
567
- highest = Math.max(highest, group.elevation);
568
- }
569
- }
570
- return highest;
571
- };
572
- const baseSegments = [];
573
- for (let index = 1; index < uniqueBreakpoints.length; index++) {
574
- const start = uniqueBreakpoints[index - 1];
575
- const end = uniqueBreakpoints[index];
576
- if (end - start < 1e-7)
577
- continue;
578
- const midpoint = (start + end) / 2;
579
- const leftElevation = polylines.length >= 3 ? highestAt(1, midpoint) : Number.NEGATIVE_INFINITY;
580
- const rightElevation = polylines.length >= 3 ? highestAt(2, midpoint) : Number.NEGATIVE_INFINITY;
581
- const faceElevations = [leftElevation, rightElevation].filter(Number.isFinite);
582
- const segmentElevation = faceElevations.length > 0 ? Math.min(...faceElevations) : Math.max(highestAt(0, midpoint), 0);
583
- const previous = baseSegments[baseSegments.length - 1];
584
- if (previous &&
585
- Math.abs(previous.elevation - segmentElevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON) {
586
- previous.end = end;
587
- }
588
- else {
589
- baseSegments.push({ start, end, elevation: segmentElevation });
590
- }
591
- }
592
- if (baseSegments.length === 0)
593
- baseSegments.push({ start: 0, end: 1, elevation });
594
- const baseElevation = Math.min(...baseSegments.map((segment) => segment.elevation));
595
- return { elevation, baseElevation, baseSegments };
596
- }
597
- export function computeWallSlabElevation(wallLike, slabs, levelWalls) {
598
- return computeWallSlabSupport(wallLike, slabs, levelWalls).elevation;
599
- }
600
198
  export class SpatialGridManager {
601
199
  floorGrids = new Map(); // levelId -> grid
602
200
  wallGrids = new Map(); // levelId -> wall grid
@@ -631,7 +229,14 @@ export class SpatialGridManager {
631
229
  }
632
230
  getWallHeight(wallId) {
633
231
  const wall = this.walls.get(wallId);
634
- 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);
635
240
  }
636
241
  getCeilingGrid(ceilingId) {
637
242
  if (!this.ceilingGrids.has(ceilingId)) {
@@ -645,10 +250,122 @@ export class SpatialGridManager {
645
250
  }
646
251
  return this.slabsByLevel.get(levelId);
647
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
+ }
648
364
  // Called when nodes change
649
365
  handleNodeCreated(node, levelId) {
650
366
  if (node.type === 'slab') {
651
367
  this.getSlabMap(levelId).set(node.id, node);
368
+ this.invalidateRenderedSlabPolygons(levelId);
652
369
  }
653
370
  else if (node.type === 'ceiling') {
654
371
  this.ceilings.set(node.id, node);
@@ -656,6 +373,8 @@ export class SpatialGridManager {
656
373
  else if (node.type === 'wall') {
657
374
  const wall = node;
658
375
  this.walls.set(wall.id, wall);
376
+ // Rendered slab polygons adopt wall bands — a new wall can extend them.
377
+ this.invalidateRenderedSlabPolygons(levelId);
659
378
  }
660
379
  else if (node.type === 'item') {
661
380
  const item = node;
@@ -700,6 +419,7 @@ export class SpatialGridManager {
700
419
  handleNodeUpdated(node, levelId) {
701
420
  if (node.type === 'slab') {
702
421
  this.getSlabMap(levelId).set(node.id, node);
422
+ this.invalidateRenderedSlabPolygons(levelId);
703
423
  }
704
424
  else if (node.type === 'ceiling') {
705
425
  this.ceilings.set(node.id, node);
@@ -707,6 +427,7 @@ export class SpatialGridManager {
707
427
  else if (node.type === 'wall') {
708
428
  const wall = node;
709
429
  this.walls.set(wall.id, wall);
430
+ this.invalidateRenderedSlabPolygons(levelId);
710
431
  }
711
432
  else if (node.type === 'item') {
712
433
  const item = node;
@@ -756,6 +477,9 @@ export class SpatialGridManager {
756
477
  }
757
478
  handleNodeDeleted(nodeId, nodeType, levelId) {
758
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);
759
483
  this.getSlabMap(levelId).delete(nodeId);
760
484
  }
761
485
  else if (nodeType === 'ceiling') {
@@ -764,6 +488,7 @@ export class SpatialGridManager {
764
488
  }
765
489
  else if (nodeType === 'wall') {
766
490
  this.walls.delete(nodeId);
491
+ this.invalidateRenderedSlabPolygons(levelId);
767
492
  // Remove all items attached to this wall from the spatial grid
768
493
  const removedItemIds = this.getWallGrid(levelId).removeWall(nodeId);
769
494
  return removedItemIds; // Caller can use this to delete the items from scene
@@ -901,7 +626,8 @@ export class SpatialGridManager {
901
626
  if (!slabMap)
902
627
  return 0;
903
628
  let maxElevation = 0;
904
- for (const slab of slabMap.values()) {
629
+ for (const stored of slabMap.values()) {
630
+ const slab = this.effectiveSlabRecord(stored);
905
631
  if (slab.polygon.length >= 3 && pointInPolygon(x, z, slab.polygon)) {
906
632
  // Check if point is in any hole
907
633
  let inHole = false;
@@ -924,37 +650,142 @@ export class SpatialGridManager {
924
650
  }
925
651
  /**
926
652
  * Get the slab elevation for an item using its full footprint (bounding box).
927
- * Checks if any part of the item's rotated footprint overlaps with any slab polygon (excluding holes).
928
- * 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.
929
655
  */
930
- getSlabElevationForItem(levelId, position, dimensions, rotation) {
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.
670
+ */
671
+ getSlabSupportForItem(levelId, position, dimensions, rotation, maxElevation) {
931
672
  const slabMap = this.slabsByLevel.get(levelId);
932
673
  if (!slabMap)
933
- return 0;
934
- let maxElevation = Number.NEGATIVE_INFINITY;
935
- for (const slab of slabMap.values()) {
936
- if (slab.polygon.length >= 3 &&
937
- itemOverlapsPolygon(position, dimensions, rotation, slab.polygon, 0.01)) {
938
- // Check if item is entirely within a hole (if so, ignore this slab)
939
- // 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;
940
727
  let inHole = false;
941
- const [cx, , cz] = position;
942
- const holes = slab.holes || [];
943
- for (const hole of holes) {
944
- if (hole.length >= 3 && pointInPolygon(cx, cz, hole)) {
728
+ for (const hole of slab.holes || []) {
729
+ if (hole.length >= 3 && pointInPolygon(x, z, hole)) {
945
730
  inHole = true;
946
731
  break;
947
732
  }
948
733
  }
949
- if (!inHole) {
950
- const elevation = slab.elevation ?? 0.05;
951
- if (elevation > maxElevation) {
952
- maxElevation = elevation;
953
- }
954
- }
734
+ if (inHole)
735
+ continue;
736
+ best = { t, elevation, slabId: slab.id };
955
737
  }
956
738
  }
957
- 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;
958
789
  }
959
790
  /**
960
791
  * Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
@@ -963,19 +794,46 @@ export class SpatialGridManager {
963
794
  * Accepts an optional `curveOffset` so curved walls evaluate overlap
964
795
  * against their actual centerline samples, not just the chord.
965
796
  */
966
- getSlabElevationForWall(levelId, start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS) {
967
- return this.getSlabSupportForWall(levelId, start, end, curveOffset, thickness).elevation;
968
- }
969
- getSlabSupportForWall(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
+ // Sampled at the wall's own start point — the same anchor the mesh is
803
+ // positioned at, so the resolver and the renderer cannot disagree about
804
+ // where the ground is under this wall.
805
+ const levelBase = levelBaseElevationAt(useScene.getState().nodes, levelId, start[0], start[1]);
806
+ if (preferredSlabId === GROUND_SUPPORT_ID) {
807
+ const elevation = levelBase + supportOffset;
808
+ return {
809
+ elevation,
810
+ electedSlabId: null,
811
+ baseElevation: elevation,
812
+ baseSegments: [{ start: 0, end: 1, elevation }],
813
+ };
814
+ }
970
815
  const slabMap = this.slabsByLevel.get(levelId);
971
816
  if (!slabMap) {
817
+ const elevation = levelBase + supportOffset;
972
818
  return {
973
- elevation: 0,
974
- baseElevation: 0,
975
- baseSegments: [{ start: 0, end: 1, elevation: 0 }],
819
+ elevation,
820
+ electedSlabId: null,
821
+ baseElevation: elevation,
822
+ baseSegments: [{ start: 0, end: 1, elevation }],
976
823
  };
977
824
  }
978
- return computeWallSlabSupport({ start, end, curveOffset, thickness }, [...slabMap.values()], this.getLevelWallNodes(levelId));
825
+ const support = computeWallSlabSupport({ start, end, curveOffset, thickness }, [...slabMap.values()].map((slab) => this.effectiveSlabRecord(slab)), this.getLevelWallNodes(levelId).map((wall) => getEffectiveNode(wall)), preferredSlabId, maxElevation, levelBase);
826
+ if (supportOffset === 0)
827
+ return support;
828
+ return {
829
+ ...support,
830
+ elevation: support.elevation + supportOffset,
831
+ baseElevation: support.baseElevation + supportOffset,
832
+ baseSegments: support.baseSegments.map((segment) => ({
833
+ ...segment,
834
+ elevation: segment.elevation + supportOffset,
835
+ })),
836
+ };
979
837
  }
980
838
  /**
981
839
  * Walls on a level, resolved fresh from the scene store (the manager's
@@ -1070,6 +928,7 @@ export class SpatialGridManager {
1070
928
  return { valid: conflicts.length === 0, conflictIds: conflicts };
1071
929
  }
1072
930
  clearLevel(levelId) {
931
+ this.invalidateRenderedSlabPolygons(levelId);
1073
932
  this.floorGrids.delete(levelId);
1074
933
  this.wallGrids.delete(levelId);
1075
934
  this.slabsByLevel.delete(levelId);
@@ -1082,7 +941,26 @@ export class SpatialGridManager {
1082
941
  this.ceilingGrids.clear();
1083
942
  this.ceilings.clear();
1084
943
  this.itemCeilingMap.clear();
944
+ this.renderedSlabPolygons.clear();
1085
945
  }
1086
946
  }
1087
947
  // Singleton instance
1088
948
  export const spatialGridManager = new SpatialGridManager();
949
+ /** Level-local Y where the rendered wall mesh begins. */
950
+ export function getWallBaseElevationForNodes(wall, nodes) {
951
+ const levelId = resolveNodeLevelId(wall, nodes);
952
+ return spatialGridManager.getSlabSupportForWall(levelId, wall.start, wall.end, wall.curveOffset ?? 0, wall.thickness, wall.supportSlabId ?? null, undefined, wall.supportOffset).elevation;
953
+ }
954
+ /**
955
+ * Effective (extruded) height of a wall resolved from a nodes record:
956
+ * {@link resolveWallEffectiveHeight} over the covering-clamped plane top
957
+ * (`getWallPlaneTop`) and the singleton manager's slab election — so the
958
+ * value always agrees with the rendered wall. One shared resolver for the
959
+ * editor overlays (measurement label, action menu, side handles) that used
960
+ * to copy this derivation locally.
961
+ */
962
+ export function getWallEffectiveHeightForNodes(wall, nodes) {
963
+ const levelId = resolveNodeLevelId(wall, nodes);
964
+ const baseElevation = getWallBaseElevationForNodes(wall, nodes);
965
+ return resolveWallEffectiveHeight(wall, getWallPlaneTop(wall, levelId, nodes), baseElevation);
966
+ }