@pascal-app/core 0.8.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/dist/events/bus.d.ts +67 -13
  2. package/dist/events/bus.d.ts.map +1 -1
  3. package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
  4. package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
  5. package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
  6. package/dist/hooks/scene-registry/scene-registry.d.ts +14 -23
  7. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  8. package/dist/hooks/scene-registry/scene-registry.js +59 -26
  9. package/dist/hooks/scene-registry/scene-registry.test.d.ts +2 -0
  10. package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +1 -0
  11. package/dist/hooks/scene-registry/scene-registry.test.js +26 -0
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +14 -0
  13. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -0
  14. package/dist/hooks/spatial-grid/floor-placed-elevation.js +51 -0
  15. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +2 -0
  16. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +1 -0
  17. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +366 -0
  18. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +79 -11
  19. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  20. package/dist/hooks/spatial-grid/spatial-grid-manager.js +508 -141
  21. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +28 -1
  22. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  23. package/dist/hooks/spatial-grid/spatial-grid-sync.js +109 -18
  24. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +2 -0
  25. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +1 -0
  26. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +363 -0
  27. package/dist/index.d.ts +36 -8
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +33 -6
  30. package/dist/lib/level-name.d.ts +4 -0
  31. package/dist/lib/level-name.d.ts.map +1 -0
  32. package/dist/lib/level-name.js +10 -0
  33. package/dist/lib/measurement-geometry.d.ts +19 -0
  34. package/dist/lib/measurement-geometry.d.ts.map +1 -0
  35. package/dist/lib/measurement-geometry.js +232 -0
  36. package/dist/lib/measurement-geometry.test.d.ts +2 -0
  37. package/dist/lib/measurement-geometry.test.d.ts.map +1 -0
  38. package/dist/lib/measurement-geometry.test.js +102 -0
  39. package/dist/lib/polygon-geometry.d.ts +0 -1
  40. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  41. package/dist/lib/polygon-geometry.js +0 -18
  42. package/dist/lib/polygon-relations.d.ts +10 -0
  43. package/dist/lib/polygon-relations.d.ts.map +1 -0
  44. package/dist/lib/polygon-relations.js +73 -0
  45. package/dist/lib/selection-proxy.d.ts +4 -0
  46. package/dist/lib/selection-proxy.d.ts.map +1 -0
  47. package/dist/lib/selection-proxy.js +12 -0
  48. package/dist/lib/slab-polygon.d.ts +31 -2
  49. package/dist/lib/slab-polygon.d.ts.map +1 -1
  50. package/dist/lib/slab-polygon.js +525 -31
  51. package/dist/lib/slab-polygon.test.d.ts +2 -0
  52. package/dist/lib/slab-polygon.test.d.ts.map +1 -0
  53. package/dist/lib/slab-polygon.test.js +635 -0
  54. package/dist/lib/slots.d.ts +14 -0
  55. package/dist/lib/slots.d.ts.map +1 -0
  56. package/dist/lib/slots.js +26 -0
  57. package/dist/lib/space-detection-pause.test.d.ts +2 -0
  58. package/dist/lib/space-detection-pause.test.d.ts.map +1 -0
  59. package/dist/lib/space-detection-pause.test.js +42 -0
  60. package/dist/lib/space-detection.d.ts +54 -2
  61. package/dist/lib/space-detection.d.ts.map +1 -1
  62. package/dist/lib/space-detection.js +599 -124
  63. package/dist/lib/space-detection.test.d.ts +2 -0
  64. package/dist/lib/space-detection.test.d.ts.map +1 -0
  65. package/dist/lib/space-detection.test.js +376 -0
  66. package/dist/lib/wall-distance.d.ts +60 -0
  67. package/dist/lib/wall-distance.d.ts.map +1 -0
  68. package/dist/lib/wall-distance.js +85 -0
  69. package/dist/lib/zone-quantities.d.ts +21 -0
  70. package/dist/lib/zone-quantities.d.ts.map +1 -0
  71. package/dist/lib/zone-quantities.js +183 -0
  72. package/dist/lib/zone-quantities.test.d.ts +2 -0
  73. package/dist/lib/zone-quantities.test.d.ts.map +1 -0
  74. package/dist/lib/zone-quantities.test.js +82 -0
  75. package/dist/material-library.d.ts +20 -1
  76. package/dist/material-library.d.ts.map +1 -1
  77. package/dist/material-library.js +3737 -200
  78. package/dist/registry/__bench__/relations-resolver.bench.d.ts +15 -0
  79. package/dist/registry/__bench__/relations-resolver.bench.d.ts.map +1 -0
  80. package/dist/registry/__bench__/relations-resolver.bench.js +170 -0
  81. package/dist/registry/handles.d.ts +394 -0
  82. package/dist/registry/handles.d.ts.map +1 -0
  83. package/dist/registry/handles.js +24 -0
  84. package/dist/registry/index.d.ts +7 -0
  85. package/dist/registry/index.d.ts.map +1 -0
  86. package/dist/registry/index.js +4 -0
  87. package/dist/registry/registry.d.ts +136 -0
  88. package/dist/registry/registry.d.ts.map +1 -0
  89. package/dist/registry/registry.js +282 -0
  90. package/dist/registry/registry.test.d.ts +2 -0
  91. package/dist/registry/registry.test.d.ts.map +1 -0
  92. package/dist/registry/registry.test.js +196 -0
  93. package/dist/registry/relations-resolver.d.ts +42 -0
  94. package/dist/registry/relations-resolver.d.ts.map +1 -0
  95. package/dist/registry/relations-resolver.js +77 -0
  96. package/dist/registry/relations-resolver.test.d.ts +2 -0
  97. package/dist/registry/relations-resolver.test.d.ts.map +1 -0
  98. package/dist/registry/relations-resolver.test.js +183 -0
  99. package/dist/registry/scene-api.d.ts +44 -0
  100. package/dist/registry/scene-api.d.ts.map +1 -0
  101. package/dist/registry/scene-api.js +111 -0
  102. package/dist/registry/scene-api.test.d.ts +2 -0
  103. package/dist/registry/scene-api.test.d.ts.map +1 -0
  104. package/dist/registry/scene-api.test.js +183 -0
  105. package/dist/registry/subtree.d.ts +67 -0
  106. package/dist/registry/subtree.d.ts.map +1 -0
  107. package/dist/registry/subtree.js +123 -0
  108. package/dist/registry/subtree.test.d.ts +2 -0
  109. package/dist/registry/subtree.test.d.ts.map +1 -0
  110. package/dist/registry/subtree.test.js +163 -0
  111. package/dist/registry/types.d.ts +2099 -0
  112. package/dist/registry/types.d.ts.map +1 -0
  113. package/dist/registry/types.js +1 -0
  114. package/dist/schema/index.d.ts +35 -4
  115. package/dist/schema/index.d.ts.map +1 -1
  116. package/dist/schema/index.js +32 -4
  117. package/dist/schema/material.d.ts +13 -2
  118. package/dist/schema/material.d.ts.map +1 -1
  119. package/dist/schema/material.js +17 -5
  120. package/dist/schema/material.test.d.ts +2 -0
  121. package/dist/schema/material.test.d.ts.map +1 -0
  122. package/dist/schema/material.test.js +43 -0
  123. package/dist/schema/nodes/box-vent.d.ts +74 -0
  124. package/dist/schema/nodes/box-vent.d.ts.map +1 -0
  125. package/dist/schema/nodes/box-vent.js +63 -0
  126. package/dist/schema/nodes/building.d.ts +1 -1
  127. package/dist/schema/nodes/building.d.ts.map +1 -1
  128. package/dist/schema/nodes/building.js +3 -2
  129. package/dist/schema/nodes/cabinet.d.ts +534 -0
  130. package/dist/schema/nodes/cabinet.d.ts.map +1 -0
  131. package/dist/schema/nodes/cabinet.js +137 -0
  132. package/dist/schema/nodes/ceiling.d.ts +5 -2
  133. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  134. package/dist/schema/nodes/ceiling.js +5 -1
  135. package/dist/schema/nodes/chimney.d.ts +158 -0
  136. package/dist/schema/nodes/chimney.d.ts.map +1 -0
  137. package/dist/schema/nodes/chimney.js +67 -0
  138. package/dist/schema/nodes/column.d.ts +600 -7
  139. package/dist/schema/nodes/column.d.ts.map +1 -1
  140. package/dist/schema/nodes/column.js +589 -6
  141. package/dist/schema/nodes/cupola.d.ts +66 -0
  142. package/dist/schema/nodes/cupola.d.ts.map +1 -0
  143. package/dist/schema/nodes/cupola.js +28 -0
  144. package/dist/schema/nodes/door.d.ts +13 -5
  145. package/dist/schema/nodes/door.d.ts.map +1 -1
  146. package/dist/schema/nodes/door.js +12 -0
  147. package/dist/schema/nodes/dormer.d.ts +228 -0
  148. package/dist/schema/nodes/dormer.d.ts.map +1 -0
  149. package/dist/schema/nodes/dormer.js +119 -0
  150. package/dist/schema/nodes/downspout.d.ts +75 -0
  151. package/dist/schema/nodes/downspout.d.ts.map +1 -0
  152. package/dist/schema/nodes/downspout.js +63 -0
  153. package/dist/schema/nodes/duct-fitting.d.ts +91 -0
  154. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
  155. package/dist/schema/nodes/duct-fitting.js +92 -0
  156. package/dist/schema/nodes/duct-segment.d.ts +61 -0
  157. package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
  158. package/dist/schema/nodes/duct-segment.js +73 -0
  159. package/dist/schema/nodes/duct-terminal.d.ts +57 -0
  160. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
  161. package/dist/schema/nodes/duct-terminal.js +51 -0
  162. package/dist/schema/nodes/elevator.d.ts +106 -0
  163. package/dist/schema/nodes/elevator.d.ts.map +1 -0
  164. package/dist/schema/nodes/elevator.js +57 -0
  165. package/dist/schema/nodes/eyebrow-vent.d.ts +68 -0
  166. package/dist/schema/nodes/eyebrow-vent.d.ts.map +1 -0
  167. package/dist/schema/nodes/eyebrow-vent.js +40 -0
  168. package/dist/schema/nodes/fence.d.ts +19 -2
  169. package/dist/schema/nodes/fence.d.ts.map +1 -1
  170. package/dist/schema/nodes/fence.js +27 -2
  171. package/dist/schema/nodes/gutter.d.ts +84 -0
  172. package/dist/schema/nodes/gutter.d.ts.map +1 -0
  173. package/dist/schema/nodes/gutter.js +76 -0
  174. package/dist/schema/nodes/hvac-equipment.d.ts +63 -0
  175. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
  176. package/dist/schema/nodes/hvac-equipment.js +55 -0
  177. package/dist/schema/nodes/item.d.ts +12 -0
  178. package/dist/schema/nodes/item.d.ts.map +1 -1
  179. package/dist/schema/nodes/item.js +20 -0
  180. package/dist/schema/nodes/level.d.ts +1 -1
  181. package/dist/schema/nodes/level.d.ts.map +1 -1
  182. package/dist/schema/nodes/level.js +4 -0
  183. package/dist/schema/nodes/lineset.d.ts +42 -0
  184. package/dist/schema/nodes/lineset.d.ts.map +1 -0
  185. package/dist/schema/nodes/lineset.js +38 -0
  186. package/dist/schema/nodes/liquid-line.d.ts +34 -0
  187. package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
  188. package/dist/schema/nodes/liquid-line.js +24 -0
  189. package/dist/schema/nodes/measurement.d.ts +306 -0
  190. package/dist/schema/nodes/measurement.d.ts.map +1 -0
  191. package/dist/schema/nodes/measurement.js +87 -0
  192. package/dist/schema/nodes/measurement.test.d.ts +2 -0
  193. package/dist/schema/nodes/measurement.test.d.ts.map +1 -0
  194. package/dist/schema/nodes/measurement.test.js +139 -0
  195. package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
  196. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
  197. package/dist/schema/nodes/pipe-fitting.js +45 -0
  198. package/dist/schema/nodes/pipe-segment.d.ts +48 -0
  199. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
  200. package/dist/schema/nodes/pipe-segment.js +36 -0
  201. package/dist/schema/nodes/pipe-trap.d.ts +43 -0
  202. package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
  203. package/dist/schema/nodes/pipe-trap.js +36 -0
  204. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +2 -0
  205. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +1 -0
  206. package/dist/schema/nodes/ridge-vent-defaults.test.js +205 -0
  207. package/dist/schema/nodes/ridge-vent.d.ts +82 -0
  208. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -0
  209. package/dist/schema/nodes/ridge-vent.js +248 -0
  210. package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
  211. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
  212. package/dist/schema/nodes/roof-segment-shape.js +318 -0
  213. package/dist/schema/nodes/roof-segment-shape.test.d.ts +2 -0
  214. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +1 -0
  215. package/dist/schema/nodes/roof-segment-shape.test.js +175 -0
  216. package/dist/schema/nodes/roof-segment-surface.test.d.ts +2 -0
  217. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +1 -0
  218. package/dist/schema/nodes/roof-segment-surface.test.js +48 -0
  219. package/dist/schema/nodes/roof-segment-trim.test.d.ts +2 -0
  220. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +1 -0
  221. package/dist/schema/nodes/roof-segment-trim.test.js +107 -0
  222. package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
  223. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
  224. package/dist/schema/nodes/roof-segment-walls.js +330 -0
  225. package/dist/schema/nodes/roof-segment-walls.test.d.ts +2 -0
  226. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +1 -0
  227. package/dist/schema/nodes/roof-segment-walls.test.js +92 -0
  228. package/dist/schema/nodes/roof-segment.d.ts +273 -3
  229. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  230. package/dist/schema/nodes/roof-segment.js +539 -3
  231. package/dist/schema/nodes/roof.d.ts +8 -8
  232. package/dist/schema/nodes/roof.d.ts.map +1 -1
  233. package/dist/schema/nodes/roof.js +4 -16
  234. package/dist/schema/nodes/shelf.d.ts +106 -0
  235. package/dist/schema/nodes/shelf.d.ts.map +1 -0
  236. package/dist/schema/nodes/shelf.js +90 -0
  237. package/dist/schema/nodes/site.d.ts +1 -122
  238. package/dist/schema/nodes/site.d.ts.map +1 -1
  239. package/dist/schema/nodes/site.js +2 -6
  240. package/dist/schema/nodes/skylight.d.ts +279 -0
  241. package/dist/schema/nodes/skylight.d.ts.map +1 -0
  242. package/dist/schema/nodes/skylight.js +152 -0
  243. package/dist/schema/nodes/slab.d.ts +5 -2
  244. package/dist/schema/nodes/slab.d.ts.map +1 -1
  245. package/dist/schema/nodes/slab.js +5 -1
  246. package/dist/schema/nodes/solar-panel.d.ts +117 -0
  247. package/dist/schema/nodes/solar-panel.d.ts.map +1 -0
  248. package/dist/schema/nodes/solar-panel.js +47 -0
  249. package/dist/schema/nodes/stair-segment.d.ts +4 -4
  250. package/dist/schema/nodes/stair.d.ts +13 -12
  251. package/dist/schema/nodes/stair.d.ts.map +1 -1
  252. package/dist/schema/nodes/stair.js +3 -0
  253. package/dist/schema/nodes/surface-hole-metadata.d.ts +2 -0
  254. package/dist/schema/nodes/surface-hole-metadata.d.ts.map +1 -1
  255. package/dist/schema/nodes/surface-hole-metadata.js +3 -1
  256. package/dist/schema/nodes/turbine-vent.d.ts +68 -0
  257. package/dist/schema/nodes/turbine-vent.d.ts.map +1 -0
  258. package/dist/schema/nodes/turbine-vent.js +41 -0
  259. package/dist/schema/nodes/wall.d.ts +245 -6
  260. package/dist/schema/nodes/wall.d.ts.map +1 -1
  261. package/dist/schema/nodes/wall.js +257 -0
  262. package/dist/schema/nodes/wall.test.d.ts +2 -0
  263. package/dist/schema/nodes/wall.test.d.ts.map +1 -0
  264. package/dist/schema/nodes/wall.test.js +208 -0
  265. package/dist/schema/nodes/window.d.ts +11 -3
  266. package/dist/schema/nodes/window.d.ts.map +1 -1
  267. package/dist/schema/nodes/window.js +12 -0
  268. package/dist/schema/nodes/zone.d.ts +2 -0
  269. package/dist/schema/nodes/zone.d.ts.map +1 -1
  270. package/dist/schema/nodes/zone.js +6 -0
  271. package/dist/schema/scene-material.d.ts +41 -0
  272. package/dist/schema/scene-material.d.ts.map +1 -0
  273. package/dist/schema/scene-material.js +9 -0
  274. package/dist/schema/types.d.ts +2850 -655
  275. package/dist/schema/types.d.ts.map +1 -1
  276. package/dist/schema/types.js +49 -0
  277. package/dist/services/alignment-anchors.d.ts +100 -0
  278. package/dist/services/alignment-anchors.d.ts.map +1 -0
  279. package/dist/services/alignment-anchors.js +312 -0
  280. package/dist/services/alignment-anchors.test.d.ts +2 -0
  281. package/dist/services/alignment-anchors.test.d.ts.map +1 -0
  282. package/dist/services/alignment-anchors.test.js +383 -0
  283. package/dist/services/alignment.d.ts +131 -0
  284. package/dist/services/alignment.d.ts.map +1 -0
  285. package/dist/services/alignment.js +171 -0
  286. package/dist/services/alignment.test.d.ts +2 -0
  287. package/dist/services/alignment.test.d.ts.map +1 -0
  288. package/dist/services/alignment.test.js +92 -0
  289. package/dist/services/drag-session.d.ts +47 -0
  290. package/dist/services/drag-session.d.ts.map +1 -0
  291. package/dist/services/drag-session.js +100 -0
  292. package/dist/services/drag-session.test.d.ts +2 -0
  293. package/dist/services/drag-session.test.d.ts.map +1 -0
  294. package/dist/services/drag-session.test.js +219 -0
  295. package/dist/services/hosting.d.ts +88 -0
  296. package/dist/services/hosting.d.ts.map +1 -0
  297. package/dist/services/hosting.js +131 -0
  298. package/dist/services/hosting.test.d.ts +2 -0
  299. package/dist/services/hosting.test.d.ts.map +1 -0
  300. package/dist/services/hosting.test.js +230 -0
  301. package/dist/services/index.d.ts +13 -0
  302. package/dist/services/index.d.ts.map +1 -0
  303. package/dist/services/index.js +12 -0
  304. package/dist/services/level-height.d.ts +28 -0
  305. package/dist/services/level-height.d.ts.map +1 -0
  306. package/dist/services/level-height.js +62 -0
  307. package/dist/services/movement.d.ts +48 -0
  308. package/dist/services/movement.d.ts.map +1 -0
  309. package/dist/services/movement.js +65 -0
  310. package/dist/services/movement.test.d.ts +2 -0
  311. package/dist/services/movement.test.d.ts.map +1 -0
  312. package/dist/services/movement.test.js +133 -0
  313. package/dist/services/opening-guides.d.ts +141 -0
  314. package/dist/services/opening-guides.d.ts.map +1 -0
  315. package/dist/services/opening-guides.js +267 -0
  316. package/dist/services/opening-guides.test.d.ts +2 -0
  317. package/dist/services/opening-guides.test.d.ts.map +1 -0
  318. package/dist/services/opening-guides.test.js +208 -0
  319. package/dist/services/port-connectivity.d.ts +115 -0
  320. package/dist/services/port-connectivity.d.ts.map +1 -0
  321. package/dist/services/port-connectivity.js +300 -0
  322. package/dist/services/port-connectivity.test.d.ts +2 -0
  323. package/dist/services/port-connectivity.test.d.ts.map +1 -0
  324. package/dist/services/port-connectivity.test.js +297 -0
  325. package/dist/services/riser-diagram.d.ts +40 -0
  326. package/dist/services/riser-diagram.d.ts.map +1 -0
  327. package/dist/services/riser-diagram.js +110 -0
  328. package/dist/services/riser-diagram.test.d.ts +2 -0
  329. package/dist/services/riser-diagram.test.d.ts.map +1 -0
  330. package/dist/services/riser-diagram.test.js +66 -0
  331. package/dist/services/single-undo-dance.test.d.ts +2 -0
  332. package/dist/services/single-undo-dance.test.d.ts.map +1 -0
  333. package/dist/services/single-undo-dance.test.js +210 -0
  334. package/dist/services/snap.d.ts +80 -0
  335. package/dist/services/snap.d.ts.map +1 -0
  336. package/dist/services/snap.js +140 -0
  337. package/dist/services/snap.test.d.ts +2 -0
  338. package/dist/services/snap.test.d.ts.map +1 -0
  339. package/dist/services/snap.test.js +137 -0
  340. package/dist/services/system-graph.d.ts +30 -0
  341. package/dist/services/system-graph.d.ts.map +1 -0
  342. package/dist/services/system-graph.js +164 -0
  343. package/dist/services/system-graph.test.d.ts +2 -0
  344. package/dist/services/system-graph.test.d.ts.map +1 -0
  345. package/dist/services/system-graph.test.js +138 -0
  346. package/dist/services/validate-dwv.d.ts +32 -0
  347. package/dist/services/validate-dwv.d.ts.map +1 -0
  348. package/dist/services/validate-dwv.js +118 -0
  349. package/dist/services/validate-dwv.test.d.ts +2 -0
  350. package/dist/services/validate-dwv.test.d.ts.map +1 -0
  351. package/dist/services/validate-dwv.test.js +109 -0
  352. package/dist/solar-panel-presets.d.ts +17 -0
  353. package/dist/solar-panel-presets.d.ts.map +1 -0
  354. package/dist/solar-panel-presets.js +43 -0
  355. package/dist/store/actions/node-actions.d.ts +14 -2
  356. package/dist/store/actions/node-actions.d.ts.map +1 -1
  357. package/dist/store/actions/node-actions.js +585 -15
  358. package/dist/store/actions/node-mutation-sanitize.test.d.ts +2 -0
  359. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +1 -0
  360. package/dist/store/actions/node-mutation-sanitize.test.js +147 -0
  361. package/dist/store/actions/reparent.test.d.ts +2 -0
  362. package/dist/store/actions/reparent.test.d.ts.map +1 -0
  363. package/dist/store/actions/reparent.test.js +212 -0
  364. package/dist/store/actions/ridge-vent-update.test.d.ts +2 -0
  365. package/dist/store/actions/ridge-vent-update.test.d.ts.map +1 -0
  366. package/dist/store/actions/ridge-vent-update.test.js +264 -0
  367. package/dist/store/history-control.d.ts +11 -0
  368. package/dist/store/history-control.d.ts.map +1 -1
  369. package/dist/store/history-control.js +33 -0
  370. package/dist/store/use-interactive.d.ts +39 -0
  371. package/dist/store/use-interactive.d.ts.map +1 -1
  372. package/dist/store/use-interactive.js +75 -0
  373. package/dist/store/use-live-node-overrides.d.ts +22 -0
  374. package/dist/store/use-live-node-overrides.d.ts.map +1 -0
  375. package/dist/store/use-live-node-overrides.js +59 -0
  376. package/dist/store/use-scene-dirty-tracking.test.d.ts +2 -0
  377. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +1 -0
  378. package/dist/store/use-scene-dirty-tracking.test.js +68 -0
  379. package/dist/store/use-scene-elevator-migration.test.d.ts +2 -0
  380. package/dist/store/use-scene-elevator-migration.test.d.ts.map +1 -0
  381. package/dist/store/use-scene-elevator-migration.test.js +109 -0
  382. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +2 -0
  383. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +1 -0
  384. package/dist/store/use-scene-measurement-roundtrip.test.js +97 -0
  385. package/dist/store/use-scene-plugins.test.d.ts +2 -0
  386. package/dist/store/use-scene-plugins.test.d.ts.map +1 -0
  387. package/dist/store/use-scene-plugins.test.js +51 -0
  388. package/dist/store/use-scene-wall-slot-migration.test.d.ts +2 -0
  389. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +1 -0
  390. package/dist/store/use-scene-wall-slot-migration.test.js +210 -0
  391. package/dist/store/use-scene-window-migration.test.d.ts +2 -0
  392. package/dist/store/use-scene-window-migration.test.d.ts.map +1 -0
  393. package/dist/store/use-scene-window-migration.test.js +85 -0
  394. package/dist/store/use-scene.d.ts +28 -2
  395. package/dist/store/use-scene.d.ts.map +1 -1
  396. package/dist/store/use-scene.js +546 -49
  397. package/dist/systems/elevator/elevator-dispatch.d.ts +12 -0
  398. package/dist/systems/elevator/elevator-dispatch.d.ts.map +1 -0
  399. package/dist/systems/elevator/elevator-dispatch.js +57 -0
  400. package/dist/systems/elevator/elevator-geometry.d.ts +15 -0
  401. package/dist/systems/elevator/elevator-geometry.d.ts.map +1 -0
  402. package/dist/systems/elevator/elevator-geometry.js +50 -0
  403. package/dist/systems/elevator/elevator-opening-sync.d.ts +6 -0
  404. package/dist/systems/elevator/elevator-opening-sync.d.ts.map +1 -0
  405. package/dist/systems/elevator/elevator-opening-sync.js +218 -0
  406. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +2 -0
  407. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +1 -0
  408. package/dist/systems/elevator/elevator-opening-sync.test.js +146 -0
  409. package/dist/systems/elevator/elevator-opening-system.d.ts +2 -0
  410. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -0
  411. package/dist/systems/elevator/elevator-opening-system.js +48 -0
  412. package/dist/systems/elevator/elevator-runtime-system.d.ts +2 -0
  413. package/dist/systems/elevator/elevator-runtime-system.d.ts.map +1 -0
  414. package/dist/systems/elevator/elevator-runtime-system.js +8 -0
  415. package/dist/systems/elevator/elevator-runtime.d.ts +18 -0
  416. package/dist/systems/elevator/elevator-runtime.d.ts.map +1 -0
  417. package/dist/systems/elevator/elevator-runtime.js +225 -0
  418. package/dist/systems/elevator/elevator-runtime.test.d.ts +2 -0
  419. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +1 -0
  420. package/dist/systems/elevator/elevator-runtime.test.js +64 -0
  421. package/dist/systems/elevator/elevator-service.d.ts +19 -0
  422. package/dist/systems/elevator/elevator-service.d.ts.map +1 -0
  423. package/dist/systems/elevator/elevator-service.js +111 -0
  424. package/dist/systems/fence/fence-centerline.d.ts +12 -0
  425. package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
  426. package/dist/systems/fence/fence-centerline.js +37 -0
  427. package/dist/systems/fence/fence-spline.d.ts +39 -0
  428. package/dist/systems/fence/fence-spline.d.ts.map +1 -0
  429. package/dist/systems/fence/fence-spline.js +196 -0
  430. package/dist/systems/fence/fence-spline.test.d.ts +2 -0
  431. package/dist/systems/fence/fence-spline.test.d.ts.map +1 -0
  432. package/dist/systems/fence/fence-spline.test.js +136 -0
  433. package/dist/systems/stair/stair-footprint.d.ts +44 -0
  434. package/dist/systems/stair/stair-footprint.d.ts.map +1 -0
  435. package/dist/systems/stair/stair-footprint.js +178 -0
  436. package/dist/systems/stair/stair-opening-preview.d.ts +2447 -0
  437. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -0
  438. package/dist/systems/stair/stair-opening-preview.js +93 -0
  439. package/dist/systems/stair/stair-opening-preview.test.d.ts +2 -0
  440. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +1 -0
  441. package/dist/systems/stair/stair-opening-preview.test.js +80 -0
  442. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  443. package/dist/systems/stair/stair-opening-sync.js +155 -147
  444. package/dist/systems/stair/stair-opening-sync.test.js +424 -2
  445. package/dist/systems/stair/stair-opening-system.d.ts +2 -0
  446. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -0
  447. package/dist/systems/stair/stair-opening-system.js +104 -0
  448. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  449. package/dist/systems/wall/wall-mitering.js +38 -2
  450. package/dist/systems/wall/wall-mitering.test.d.ts +2 -0
  451. package/dist/systems/wall/wall-mitering.test.d.ts.map +1 -0
  452. package/dist/systems/wall/wall-mitering.test.js +63 -0
  453. package/dist/systems/wall/wall-move.d.ts +48 -0
  454. package/dist/systems/wall/wall-move.d.ts.map +1 -0
  455. package/dist/systems/wall/wall-move.js +193 -0
  456. package/dist/utils/clone-scene-graph.d.ts +7 -4
  457. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  458. package/dist/utils/clone-scene-graph.js +29 -7
  459. package/dist/utils/clone-scene-graph.test.d.ts +2 -0
  460. package/dist/utils/clone-scene-graph.test.d.ts.map +1 -0
  461. package/dist/utils/clone-scene-graph.test.js +62 -0
  462. package/dist/utils/heal-scene-graph.d.ts +18 -0
  463. package/dist/utils/heal-scene-graph.d.ts.map +1 -0
  464. package/dist/utils/heal-scene-graph.js +90 -0
  465. package/dist/utils/heal-scene-graph.test.d.ts +2 -0
  466. package/dist/utils/heal-scene-graph.test.d.ts.map +1 -0
  467. package/dist/utils/heal-scene-graph.test.js +88 -0
  468. package/dist/validation/validate-build-json.d.ts +44 -0
  469. package/dist/validation/validate-build-json.d.ts.map +1 -0
  470. package/dist/validation/validate-build-json.js +256 -0
  471. package/package.json +11 -4
@@ -1,5 +1,10 @@
1
+ import { getRenderableSlabPolygon } from '../../lib/slab-polygon';
2
+ import { nodeRegistry } from '../../registry';
1
3
  import { getScaledDimensions, isLowProfileItemSurface } from '../../schema';
2
4
  import useScene from '../../store/use-scene';
5
+ import { getWallCurveFrameAt, isCurvedWall } from '../../systems/wall/wall-curve';
6
+ import { DEFAULT_WALL_THICKNESS } from '../../systems/wall/wall-footprint';
7
+ import { getFloorPlacedFootprints } from './floor-placed-elevation';
3
8
  import { SpatialGrid } from './spatial-grid';
4
9
  import { WallSpatialGrid } from './wall-spatial-grid';
5
10
  // ============================================================================
@@ -38,6 +43,24 @@ function getItemFootprint(position, dimensions, rotation, inset = 0) {
38
43
  [x + (-halfW * cos - halfD * sin), z + (-halfW * sin + halfD * cos)],
39
44
  ];
40
45
  }
46
+ /**
47
+ * Axis-aligned XZ extent of a footprint at `position`, rotated by `yRot`. The
48
+ * rotated width/depth is the same conservative bound the floor-placement draft
49
+ * uses, so a draft and an existing node are compared with identical math.
50
+ */
51
+ function footprintBoundsXZ(position, dimensions, yRot) {
52
+ const [width, , depth] = dimensions;
53
+ const cos = Math.abs(Math.cos(yRot));
54
+ const sin = Math.abs(Math.sin(yRot));
55
+ const rotatedW = width * cos + depth * sin;
56
+ const rotatedD = width * sin + depth * cos;
57
+ return {
58
+ minX: position[0] - rotatedW / 2,
59
+ maxX: position[0] + rotatedW / 2,
60
+ minZ: position[2] - rotatedD / 2,
61
+ maxZ: position[2] + rotatedD / 2,
62
+ };
63
+ }
41
64
  function getItemLocalBounds(item) {
42
65
  const [width, height, depth] = getScaledDimensions(item);
43
66
  const minZ = item.asset.attachTo === 'wall-side' ? -depth : -depth / 2;
@@ -97,6 +120,24 @@ function resolveNodeLevelId(node, nodes) {
97
120
  }
98
121
  return 'default';
99
122
  }
123
+ function expandIgnoredNodeIds(ignoreIds, nodes) {
124
+ const ignored = new Set(ignoreIds ?? []);
125
+ const queue = [...ignored];
126
+ while (queue.length > 0) {
127
+ const id = queue.pop();
128
+ const node = nodes[id];
129
+ const children = node?.children;
130
+ if (!Array.isArray(children))
131
+ continue;
132
+ for (const childId of children) {
133
+ if (typeof childId !== 'string' || ignored.has(childId))
134
+ continue;
135
+ ignored.add(childId);
136
+ queue.push(childId);
137
+ }
138
+ }
139
+ return ignored;
140
+ }
100
141
  /**
101
142
  * Test if two line segments (a1->a2) and (b1->b2) intersect.
102
143
  */
@@ -162,91 +203,399 @@ export function itemOverlapsPolygon(position, dimensions, rotation, polygon, ins
162
203
  }
163
204
  return false;
164
205
  }
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
+ }
165
280
  /**
166
- * Check if wall segment (a) is substantially on polygon edge segment (b).
167
- * Returns true only if BOTH endpoints of the wall are on or very close to the edge.
168
- * This prevents walls that just touch one point from being detected.
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.
169
286
  */
170
- function segmentsCollinearAndOverlap(ax1, az1, ax2, az2, bx1, bz1, bx2, bz2) {
171
- const EPSILON = 1e-6;
172
- // Cross product to check collinearity
173
- const cross1 = (ax2 - ax1) * (bz1 - az1) - (az2 - az1) * (bx1 - ax1);
174
- const cross2 = (ax2 - ax1) * (bz2 - az1) - (az2 - az1) * (bx2 - ax1);
175
- if (Math.abs(cross1) > EPSILON || Math.abs(cross2) > EPSILON) {
176
- return false; // Not collinear
177
- }
178
- // Check if a point is on segment b
179
- const onSegment = (px, pz, qx, qz, rx, rz) => Math.min(px, qx) - EPSILON <= rx &&
180
- rx <= Math.max(px, qx) + EPSILON &&
181
- Math.min(pz, qz) - EPSILON <= rz &&
182
- rz <= Math.max(pz, qz) + EPSILON;
183
- // BOTH endpoints of wall (a) must be on edge (b) for substantial overlap
184
- const a1OnB = onSegment(bx1, bz1, bx2, bz2, ax1, az1);
185
- const a2OnB = onSegment(bx1, bz1, bx2, bz2, ax2, az2);
186
- return a1OnB && a2OnB;
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;
187
332
  }
188
333
  /**
189
- * Test if a wall segment overlaps with a polygon.
190
- * A wall is considered to overlap if:
191
- * - Its midpoint is inside the polygon (wall crosses through)
192
- * - At least one endpoint is inside (wall partially or fully in slab)
193
- * - It's collinear with and overlaps a polygon edge (wall on slab boundary)
194
- *
195
- * Note: A wall with just one endpoint touching the edge but the rest outside
196
- * is NOT considered overlapping (adjacent only).
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.
197
340
  */
198
- export function wallOverlapsPolygon(start, end, polygon) {
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
+ ];
199
396
  const dx = end[0] - start[0];
200
397
  const dz = end[1] - start[1];
201
- const len = Math.sqrt(dx * dx + dz * dz);
202
- // Nudge endpoint test points a tiny step inward along the wall direction before
203
- // testing containment. pointInPolygon (ray casting) produces false positives for
204
- // points exactly on polygon vertices or edges — specifically the minimum-z corner
205
- // of an axis-aligned polygon returns "inside" because the ray hits the opposite
206
- // vertical edge exactly at its base. Nudging by 1e-6 m avoids this: a wall that
207
- // merely starts at a slab corner and extends outward will have its nudged point
208
- // clearly outside, while a wall that genuinely starts inside stays inside.
209
- if (len > 1e-10) {
210
- const step = Math.min(1e-6, len * 0.01);
211
- const nx = (dx / len) * step;
212
- const nz = (dz / len) * step;
213
- if (pointInPolygon(start[0] + nx, start[1] + nz, polygon))
214
- return true;
215
- if (pointInPolygon(end[0] - nx, end[1] - nz, polygon))
216
- return true;
217
- // Also nudge perpendicular to the wall (into the slab interior) for walls that
218
- // lie exactly on the slab boundary. The along-wall nudge keeps points on the
219
- // boundary where pointInPolygon is unreliable; a perpendicular inward nudge
220
- // moves the point clearly inside (or outside) the polygon.
221
- // Sample the wall at 1/4, 1/2, 3/4 positions with a perpendicular nudge.
222
- const PERP_STEP = 1e-4;
223
- const pnx = (-nz / step) * PERP_STEP; // perpendicular left
224
- const pnz = (nx / step) * PERP_STEP;
225
- for (const t of [0.25, 0.5, 0.75]) {
226
- const bx = start[0] + dx * t;
227
- const bz = start[1] + dz * t;
228
- if (pointInPolygon(bx + pnx, bz + pnz, polygon))
229
- return true;
230
- if (pointInPolygon(bx - pnx, bz - pnz, polygon))
231
- return true;
232
- }
233
- }
234
- // Check if midpoint is inside (catches walls crossing through)
235
- const midX = (start[0] + end[0]) / 2;
236
- const midZ = (start[1] + end[1]) / 2;
237
- if (pointInPolygon(midX, midZ, polygon))
238
- return true;
239
- // Check if the wall is collinear with and overlaps any polygon edge
240
- const n = polygon.length;
241
- for (let i = 0; i < n; i++) {
242
- const j = (i + 1) % n;
243
- const [p1x, p1z] = polygon[i];
244
- const [p2x, p2z] = polygon[j];
245
- if (segmentsCollinearAndOverlap(start[0], start[1], end[0], end[1], p1x, p1z, p2x, p2z)) {
246
- return true;
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]);
247
512
  }
248
513
  }
249
- return false;
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;
250
599
  }
251
600
  export class SpatialGridManager {
252
601
  floorGrids = new Map(); // levelId -> grid
@@ -433,37 +782,51 @@ export class SpatialGridManager {
433
782
  }
434
783
  // Query methods
435
784
  canPlaceOnFloor(levelId, position, dimensions, rotation, ignoreIds) {
785
+ return this.canPlaceOnFloorFootprints(levelId, [{ position, dimensions, rotation }], ignoreIds);
786
+ }
787
+ canPlaceOnFloorFootprints(levelId, footprints, ignoreIds) {
436
788
  const nodes = useScene.getState().nodes;
437
- const ignoreSet = new Set(ignoreIds ?? []);
438
- const [width, , depth] = dimensions;
439
- const yRot = rotation[1];
440
- const cos = Math.abs(Math.cos(yRot));
441
- const sin = Math.abs(Math.sin(yRot));
442
- const rotatedW = width * cos + depth * sin;
443
- const rotatedD = width * sin + depth * cos;
444
- const draftBounds = {
445
- minX: position[0] - rotatedW / 2,
446
- maxX: position[0] + rotatedW / 2,
447
- minZ: position[2] - rotatedD / 2,
448
- maxZ: position[2] + rotatedD / 2,
449
- };
789
+ const ignoreSet = expandIgnoredNodeIds(ignoreIds, nodes);
790
+ const draftBounds = footprints.map((footprint) => footprintBoundsXZ(footprint.position, footprint.dimensions, footprint.rotation[1] ?? 0));
791
+ for (let i = 0; i < draftBounds.length; i += 1) {
792
+ const a = draftBounds[i];
793
+ for (let j = i + 1; j < draftBounds.length; j += 1) {
794
+ const b = draftBounds[j];
795
+ if (intervalsOverlap(a.minX, a.maxX, b.minX, b.maxX) &&
796
+ intervalsOverlap(a.minZ, a.maxZ, b.minZ, b.maxZ)) {
797
+ return { valid: false, conflictIds: [] };
798
+ }
799
+ }
800
+ }
801
+ // A floor placement conflicts with any other COLLIDING floor-resting node,
802
+ // not just items — every kind whose `floorPlaced.collides` is set (item /
803
+ // shelf / column / cabinet / stair) contributes its footprint(s) as an
804
+ // obstacle. Each candidate's XZ extent is read from the same declarative
805
+ // footprint the elevation + sync paths use, so adding a colliding kind
806
+ // needs no change here.
450
807
  const conflicts = [];
451
808
  for (const node of Object.values(nodes)) {
452
- if (node.type !== 'item')
809
+ if (ignoreSet.has(node.id))
453
810
  continue;
454
- const item = node;
455
- if (item.asset.attachTo)
811
+ const floorPlaced = nodeRegistry.get(node.type)?.capabilities?.floorPlaced;
812
+ if (!floorPlaced?.collides)
456
813
  continue;
457
- if (isLowProfileItemSurface(item))
814
+ if (floorPlaced.applies && !floorPlaced.applies(node))
458
815
  continue;
459
- if (ignoreSet.has(item.id))
816
+ // Low-profile item surfaces (rugs, mats) are stack-on targets, not
817
+ // obstacles — keep the long-standing item-only exemption.
818
+ if (node.type === 'item' && isLowProfileItemSurface(node))
460
819
  continue;
461
- if (resolveNodeLevelId(item, nodes) !== levelId)
820
+ if (resolveNodeLevelId(node, nodes) !== levelId)
462
821
  continue;
463
- const bounds = getItemParentAabb(item);
464
- if (intervalsOverlap(draftBounds.minX, draftBounds.maxX, bounds.minX, bounds.maxX) &&
465
- intervalsOverlap(draftBounds.minZ, draftBounds.maxZ, bounds.minZ, bounds.maxZ)) {
466
- conflicts.push(item.id);
822
+ for (const footprint of getFloorPlacedFootprints(floorPlaced, node, { nodes })) {
823
+ const fpRotation = Array.isArray(footprint.rotation) ? (footprint.rotation[1] ?? 0) : 0;
824
+ const bounds = footprintBoundsXZ(footprint.position ?? node.position, footprint.dimensions, fpRotation);
825
+ if (draftBounds.some((draft) => intervalsOverlap(draft.minX, draft.maxX, bounds.minX, bounds.maxX) &&
826
+ intervalsOverlap(draft.minZ, draft.maxZ, bounds.minZ, bounds.maxZ))) {
827
+ conflicts.push(node.id);
828
+ break;
829
+ }
467
830
  }
468
831
  }
469
832
  return { valid: conflicts.length === 0, conflictIds: conflicts };
@@ -595,55 +958,59 @@ export class SpatialGridManager {
595
958
  }
596
959
  /**
597
960
  * Get the slab elevation for a wall by checking if it overlaps with any slab polygon (excluding holes).
598
- * Uses wallOverlapsPolygon which handles edge cases (points on boundary, collinear segments).
599
961
  * Returns the highest slab elevation found, or 0 if none.
962
+ *
963
+ * Accepts an optional `curveOffset` so curved walls evaluate overlap
964
+ * against their actual centerline samples, not just the chord.
600
965
  */
601
- getSlabElevationForWall(levelId, start, end) {
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) {
602
970
  const slabMap = this.slabsByLevel.get(levelId);
603
- if (!slabMap)
604
- return 0;
605
- let maxElevation = Number.NEGATIVE_INFINITY;
606
- for (const slab of slabMap.values()) {
607
- if (slab.polygon.length < 3)
608
- continue;
609
- if (!wallOverlapsPolygon(start, end, slab.polygon))
610
- continue;
611
- const holes = slab.holes || [];
612
- if (holes.length === 0) {
613
- // No holes: wall is on this slab
614
- const elevation = slab.elevation ?? 0.05;
615
- if (elevation > maxElevation)
616
- maxElevation = elevation;
971
+ if (!slabMap) {
972
+ return {
973
+ elevation: 0,
974
+ baseElevation: 0,
975
+ baseSegments: [{ start: 0, end: 1, elevation: 0 }],
976
+ };
977
+ }
978
+ return computeWallSlabSupport({ start, end, curveOffset, thickness }, [...slabMap.values()], this.getLevelWallNodes(levelId));
979
+ }
980
+ /**
981
+ * Walls on a level, resolved fresh from the scene store (the manager's
982
+ * own wall map is only maintained on create/delete, not on updates).
983
+ * Cached per scene `nodes` record so per-pointer-tick callers
984
+ * (door/window move) don't rescan the node map.
985
+ */
986
+ levelWallsCache = new WeakMap();
987
+ getLevelWallNodes(levelId) {
988
+ const nodes = useScene.getState().nodes;
989
+ let byLevel = this.levelWallsCache.get(nodes);
990
+ if (!byLevel) {
991
+ byLevel = new Map();
992
+ this.levelWallsCache.set(nodes, byLevel);
993
+ }
994
+ const cached = byLevel.get(levelId);
995
+ if (cached)
996
+ return cached;
997
+ const walls = [];
998
+ for (const node of Object.values(nodes)) {
999
+ if (node.type !== 'wall')
617
1000
  continue;
1001
+ // Walk the parent chain to the owning level (guarded against cycles).
1002
+ let current = node;
1003
+ let guard = 0;
1004
+ while (current && current.type !== 'level' && guard < 16) {
1005
+ current = current.parentId ? nodes[current.parentId] : undefined;
1006
+ guard += 1;
618
1007
  }
619
- // Sample multiple points along the wall to check whether any portion lies on
620
- // solid slab (not inside any hole). Checking only the midpoint fails when the
621
- // midpoint falls in a staircase hole but the wall's endpoints are on solid slab.
622
- const dx = end[0] - start[0];
623
- const dz = end[1] - start[1];
624
- let hasValidPoint = false;
625
- for (const t of [0, 0.25, 0.5, 0.75, 1]) {
626
- const px = start[0] + dx * t;
627
- const pz = start[1] + dz * t;
628
- let inHole = false;
629
- for (const hole of holes) {
630
- if (hole.length >= 3 && pointInPolygon(px, pz, hole)) {
631
- inHole = true;
632
- break;
633
- }
634
- }
635
- if (!inHole) {
636
- hasValidPoint = true;
637
- break;
638
- }
639
- }
640
- if (hasValidPoint) {
641
- const elevation = slab.elevation ?? 0.05;
642
- if (elevation > maxElevation)
643
- maxElevation = elevation;
1008
+ if (current?.type === 'level' && current.id === levelId) {
1009
+ walls.push(node);
644
1010
  }
645
1011
  }
646
- return maxElevation === Number.NEGATIVE_INFINITY ? 0 : maxElevation;
1012
+ byLevel.set(levelId, walls);
1013
+ return walls;
647
1014
  }
648
1015
  /**
649
1016
  * Check if an item can be placed on a ceiling.