@pascal-app/core 0.9.1 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/README.md +17 -10
  2. package/dist/events/bus.d.ts +49 -2
  3. package/dist/events/bus.d.ts.map +1 -1
  4. package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
  5. package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
  6. package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
  7. package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
  8. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  9. package/dist/hooks/scene-registry/scene-registry.js +27 -1
  10. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
  11. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +135 -28
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  15. package/dist/hooks/spatial-grid/spatial-grid-manager.js +427 -271
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  18. package/dist/hooks/spatial-grid/spatial-grid-sync.js +208 -9
  19. package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
  20. package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
  21. package/dist/hooks/spatial-grid/support-host-id.js +2 -0
  22. package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
  23. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
  24. package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
  25. package/dist/index.d.ts +33 -11
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +32 -10
  28. package/dist/lib/level-name.d.ts +4 -0
  29. package/dist/lib/level-name.d.ts.map +1 -0
  30. package/dist/lib/level-name.js +10 -0
  31. package/dist/lib/measurement-geometry.d.ts +23 -0
  32. package/dist/lib/measurement-geometry.d.ts.map +1 -0
  33. package/dist/lib/measurement-geometry.js +249 -0
  34. package/dist/lib/polygon-geometry.d.ts +0 -1
  35. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  36. package/dist/lib/polygon-geometry.js +0 -18
  37. package/dist/lib/selection-proxy.d.ts +4 -0
  38. package/dist/lib/selection-proxy.d.ts.map +1 -0
  39. package/dist/lib/selection-proxy.js +12 -0
  40. package/dist/lib/slab-polygon.d.ts +31 -2
  41. package/dist/lib/slab-polygon.d.ts.map +1 -1
  42. package/dist/lib/slab-polygon.js +557 -31
  43. package/dist/lib/slots.d.ts +14 -0
  44. package/dist/lib/slots.d.ts.map +1 -0
  45. package/dist/lib/slots.js +26 -0
  46. package/dist/lib/space-detection.d.ts +40 -6
  47. package/dist/lib/space-detection.d.ts.map +1 -1
  48. package/dist/lib/space-detection.js +574 -192
  49. package/dist/lib/terrain-brush.d.ts +163 -0
  50. package/dist/lib/terrain-brush.d.ts.map +1 -0
  51. package/dist/lib/terrain-brush.js +359 -0
  52. package/dist/lib/terrain-codec.d.ts +51 -0
  53. package/dist/lib/terrain-codec.d.ts.map +1 -0
  54. package/dist/lib/terrain-codec.js +167 -0
  55. package/dist/lib/terrain-field.d.ts +150 -0
  56. package/dist/lib/terrain-field.d.ts.map +1 -0
  57. package/dist/lib/terrain-field.js +248 -0
  58. package/dist/lib/terrain-raycast.d.ts +44 -0
  59. package/dist/lib/terrain-raycast.d.ts.map +1 -0
  60. package/dist/lib/terrain-raycast.js +188 -0
  61. package/dist/lib/terrain-source.d.ts +60 -0
  62. package/dist/lib/terrain-source.d.ts.map +1 -0
  63. package/dist/lib/terrain-source.js +90 -0
  64. package/dist/lib/terrain-support.d.ts +60 -0
  65. package/dist/lib/terrain-support.d.ts.map +1 -0
  66. package/dist/lib/terrain-support.js +150 -0
  67. package/dist/lib/wall-distance.d.ts +60 -0
  68. package/dist/lib/wall-distance.d.ts.map +1 -0
  69. package/dist/lib/wall-distance.js +85 -0
  70. package/dist/lib/zone-quantities.d.ts +21 -0
  71. package/dist/lib/zone-quantities.d.ts.map +1 -0
  72. package/dist/lib/zone-quantities.js +428 -0
  73. package/dist/material-library.d.ts +28 -1
  74. package/dist/material-library.d.ts.map +1 -1
  75. package/dist/material-library.js +2233 -441
  76. package/dist/registry/handles.d.ts +91 -3
  77. package/dist/registry/handles.d.ts.map +1 -1
  78. package/dist/registry/index.d.ts +3 -3
  79. package/dist/registry/index.d.ts.map +1 -1
  80. package/dist/registry/index.js +1 -1
  81. package/dist/registry/registry.d.ts +46 -3
  82. package/dist/registry/registry.d.ts.map +1 -1
  83. package/dist/registry/registry.js +82 -1
  84. package/dist/registry/subtree.d.ts.map +1 -1
  85. package/dist/registry/subtree.js +8 -1
  86. package/dist/registry/types.d.ts +781 -39
  87. package/dist/registry/types.d.ts.map +1 -1
  88. package/dist/schema/index.d.ts +27 -8
  89. package/dist/schema/index.d.ts.map +1 -1
  90. package/dist/schema/index.js +23 -6
  91. package/dist/schema/material.d.ts +3 -2
  92. package/dist/schema/material.d.ts.map +1 -1
  93. package/dist/schema/material.js +7 -5
  94. package/dist/schema/nodes/box-vent.d.ts +2 -2
  95. package/dist/schema/nodes/cabinet.d.ts +536 -0
  96. package/dist/schema/nodes/cabinet.d.ts.map +1 -0
  97. package/dist/schema/nodes/cabinet.js +139 -0
  98. package/dist/schema/nodes/ceiling.d.ts +4 -3
  99. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  100. package/dist/schema/nodes/ceiling.js +11 -1
  101. package/dist/schema/nodes/chimney.d.ts +4 -4
  102. package/dist/schema/nodes/column.d.ts +4 -2
  103. package/dist/schema/nodes/column.d.ts.map +1 -1
  104. package/dist/schema/nodes/column.js +5 -0
  105. package/dist/schema/nodes/construction-dimension.d.ts +195 -0
  106. package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
  107. package/dist/schema/nodes/construction-dimension.js +151 -0
  108. package/dist/schema/nodes/cupola.d.ts +2 -2
  109. package/dist/schema/nodes/door.d.ts +42 -5
  110. package/dist/schema/nodes/door.d.ts.map +1 -1
  111. package/dist/schema/nodes/door.js +32 -0
  112. package/dist/schema/nodes/dormer.d.ts +8 -8
  113. package/dist/schema/nodes/dormer.js +1 -1
  114. package/dist/schema/nodes/downspout.d.ts +2 -2
  115. package/dist/schema/nodes/duct-fitting.d.ts +91 -0
  116. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
  117. package/dist/schema/nodes/duct-fitting.js +92 -0
  118. package/dist/schema/nodes/duct-segment.d.ts +61 -0
  119. package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
  120. package/dist/schema/nodes/duct-segment.js +73 -0
  121. package/dist/schema/nodes/duct-terminal.d.ts +58 -0
  122. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
  123. package/dist/schema/nodes/duct-terminal.js +53 -0
  124. package/dist/schema/nodes/elevator.d.ts +3 -2
  125. package/dist/schema/nodes/elevator.d.ts.map +1 -1
  126. package/dist/schema/nodes/elevator.js +3 -0
  127. package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
  128. package/dist/schema/nodes/fence.d.ts +20 -2
  129. package/dist/schema/nodes/fence.d.ts.map +1 -1
  130. package/dist/schema/nodes/fence.js +33 -2
  131. package/dist/schema/nodes/gutter.d.ts +2 -2
  132. package/dist/schema/nodes/hvac-equipment.d.ts +64 -0
  133. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
  134. package/dist/schema/nodes/hvac-equipment.js +57 -0
  135. package/dist/schema/nodes/item.d.ts +9 -0
  136. package/dist/schema/nodes/item.d.ts.map +1 -1
  137. package/dist/schema/nodes/item.js +24 -0
  138. package/dist/schema/nodes/level.d.ts +2 -1
  139. package/dist/schema/nodes/level.d.ts.map +1 -1
  140. package/dist/schema/nodes/level.js +12 -31
  141. package/dist/schema/nodes/lineset.d.ts +42 -0
  142. package/dist/schema/nodes/lineset.d.ts.map +1 -0
  143. package/dist/schema/nodes/lineset.js +38 -0
  144. package/dist/schema/nodes/liquid-line.d.ts +34 -0
  145. package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
  146. package/dist/schema/nodes/liquid-line.js +24 -0
  147. package/dist/schema/nodes/measurement.d.ts +306 -0
  148. package/dist/schema/nodes/measurement.d.ts.map +1 -0
  149. package/dist/schema/nodes/measurement.js +87 -0
  150. package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
  151. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
  152. package/dist/schema/nodes/pipe-fitting.js +45 -0
  153. package/dist/schema/nodes/pipe-segment.d.ts +48 -0
  154. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
  155. package/dist/schema/nodes/pipe-segment.js +36 -0
  156. package/dist/schema/nodes/pipe-trap.d.ts +43 -0
  157. package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
  158. package/dist/schema/nodes/pipe-trap.js +36 -0
  159. package/dist/schema/nodes/ridge-vent.d.ts +18 -3
  160. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  161. package/dist/schema/nodes/ridge-vent.js +221 -3
  162. package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
  163. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
  164. package/dist/schema/nodes/roof-segment-shape.js +318 -0
  165. package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
  166. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
  167. package/dist/schema/nodes/roof-segment-walls.js +330 -0
  168. package/dist/schema/nodes/roof-segment.d.ts +84 -8
  169. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  170. package/dist/schema/nodes/roof-segment.js +290 -5
  171. package/dist/schema/nodes/roof.d.ts +8 -8
  172. package/dist/schema/nodes/shelf.d.ts +4 -2
  173. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  174. package/dist/schema/nodes/shelf.js +8 -0
  175. package/dist/schema/nodes/site.d.ts +9 -0
  176. package/dist/schema/nodes/site.d.ts.map +1 -1
  177. package/dist/schema/nodes/site.js +8 -6
  178. package/dist/schema/nodes/skylight.d.ts +10 -10
  179. package/dist/schema/nodes/slab.d.ts +8 -2
  180. package/dist/schema/nodes/slab.d.ts.map +1 -1
  181. package/dist/schema/nodes/slab.js +18 -2
  182. package/dist/schema/nodes/solar-panel.d.ts +4 -4
  183. package/dist/schema/nodes/spawn.d.ts +1 -0
  184. package/dist/schema/nodes/spawn.d.ts.map +1 -1
  185. package/dist/schema/nodes/spawn.js +2 -0
  186. package/dist/schema/nodes/stair-segment.d.ts +4 -4
  187. package/dist/schema/nodes/stair.d.ts +16 -13
  188. package/dist/schema/nodes/stair.d.ts.map +1 -1
  189. package/dist/schema/nodes/stair.js +11 -1
  190. package/dist/schema/nodes/structural-grid.d.ts +27 -0
  191. package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
  192. package/dist/schema/nodes/structural-grid.js +17 -0
  193. package/dist/schema/nodes/turbine-vent.d.ts +2 -2
  194. package/dist/schema/nodes/wall.d.ts +248 -6
  195. package/dist/schema/nodes/wall.d.ts.map +1 -1
  196. package/dist/schema/nodes/wall.js +266 -0
  197. package/dist/schema/nodes/window.d.ts +40 -3
  198. package/dist/schema/nodes/window.d.ts.map +1 -1
  199. package/dist/schema/nodes/window.js +30 -0
  200. package/dist/schema/nodes/zone.d.ts +22 -0
  201. package/dist/schema/nodes/zone.d.ts.map +1 -1
  202. package/dist/schema/nodes/zone.js +21 -0
  203. package/dist/schema/scene-material.d.ts +41 -0
  204. package/dist/schema/scene-material.d.ts.map +1 -0
  205. package/dist/schema/scene-material.js +9 -0
  206. package/dist/schema/terrain.d.ts +28 -0
  207. package/dist/schema/terrain.d.ts.map +1 -0
  208. package/dist/schema/terrain.js +30 -0
  209. package/dist/schema/types.d.ts +1300 -97
  210. package/dist/schema/types.d.ts.map +1 -1
  211. package/dist/schema/types.js +27 -0
  212. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
  213. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
  214. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
  215. package/dist/services/alignment-anchors.d.ts.map +1 -1
  216. package/dist/services/alignment-anchors.js +30 -1
  217. package/dist/services/alignment.d.ts +5 -0
  218. package/dist/services/alignment.d.ts.map +1 -1
  219. package/dist/services/alignment.js +2 -0
  220. package/dist/services/hosting.d.ts +8 -0
  221. package/dist/services/hosting.d.ts.map +1 -1
  222. package/dist/services/hosting.js +13 -4
  223. package/dist/services/index.d.ts +9 -2
  224. package/dist/services/index.d.ts.map +1 -1
  225. package/dist/services/index.js +9 -2
  226. package/dist/services/level-height.d.ts +27 -0
  227. package/dist/services/level-height.d.ts.map +1 -0
  228. package/dist/services/level-height.js +94 -0
  229. package/dist/services/opening-guides.d.ts +141 -0
  230. package/dist/services/opening-guides.d.ts.map +1 -0
  231. package/dist/services/opening-guides.js +267 -0
  232. package/dist/services/port-connectivity.d.ts +115 -0
  233. package/dist/services/port-connectivity.d.ts.map +1 -0
  234. package/dist/services/port-connectivity.js +300 -0
  235. package/dist/services/riser-diagram.d.ts +40 -0
  236. package/dist/services/riser-diagram.d.ts.map +1 -0
  237. package/dist/services/riser-diagram.js +110 -0
  238. package/dist/services/snap.d.ts +9 -1
  239. package/dist/services/snap.d.ts.map +1 -1
  240. package/dist/services/snap.js +21 -1
  241. package/dist/services/storey.d.ts +103 -0
  242. package/dist/services/storey.d.ts.map +1 -0
  243. package/dist/services/storey.js +296 -0
  244. package/dist/services/system-graph.d.ts +30 -0
  245. package/dist/services/system-graph.d.ts.map +1 -0
  246. package/dist/services/system-graph.js +164 -0
  247. package/dist/services/validate-dwv.d.ts +32 -0
  248. package/dist/services/validate-dwv.d.ts.map +1 -0
  249. package/dist/services/validate-dwv.js +118 -0
  250. package/dist/store/actions/node-actions.d.ts.map +1 -1
  251. package/dist/store/actions/node-actions.js +141 -4
  252. package/dist/store/history-control.d.ts +31 -0
  253. package/dist/store/history-control.d.ts.map +1 -1
  254. package/dist/store/history-control.js +130 -0
  255. package/dist/store/use-live-terrain.d.ts +65 -0
  256. package/dist/store/use-live-terrain.d.ts.map +1 -0
  257. package/dist/store/use-live-terrain.js +31 -0
  258. package/dist/store/use-live-transforms.d.ts +8 -0
  259. package/dist/store/use-live-transforms.d.ts.map +1 -1
  260. package/dist/store/use-scene.d.ts +47 -3
  261. package/dist/store/use-scene.d.ts.map +1 -1
  262. package/dist/store/use-scene.js +992 -55
  263. package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
  264. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
  265. package/dist/systems/elevator/elevator-opening-system.js +27 -21
  266. package/dist/systems/elevator/elevator-service.d.ts +0 -2
  267. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  268. package/dist/systems/elevator/elevator-service.js +2 -24
  269. package/dist/systems/fence/fence-centerline.d.ts +12 -0
  270. package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
  271. package/dist/systems/fence/fence-centerline.js +37 -0
  272. package/dist/systems/fence/fence-spline.d.ts +39 -0
  273. package/dist/systems/fence/fence-spline.d.ts.map +1 -0
  274. package/dist/systems/fence/fence-spline.js +196 -0
  275. package/dist/systems/slab/slab-placement.d.ts +11 -0
  276. package/dist/systems/slab/slab-placement.d.ts.map +1 -0
  277. package/dist/systems/slab/slab-placement.js +14 -0
  278. package/dist/systems/slab/slab-support.d.ts +137 -0
  279. package/dist/systems/slab/slab-support.d.ts.map +1 -0
  280. package/dist/systems/slab/slab-support.js +557 -0
  281. package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
  282. package/dist/systems/stair/stair-footprint.js +21 -11
  283. package/dist/systems/stair/stair-opening-preview.d.ts +1300 -505
  284. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  285. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  286. package/dist/systems/stair/stair-opening-sync.js +19 -33
  287. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  288. package/dist/systems/stair/stair-opening-system.js +32 -4
  289. package/dist/systems/stair/stair-rise.d.ts +22 -0
  290. package/dist/systems/stair/stair-rise.d.ts.map +1 -0
  291. package/dist/systems/stair/stair-rise.js +79 -0
  292. package/dist/systems/wall/wall-curve.d.ts +10 -0
  293. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  294. package/dist/systems/wall/wall-curve.js +1 -1
  295. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  296. package/dist/systems/wall/wall-mitering.js +44 -8
  297. package/dist/systems/wall/wall-top.d.ts +40 -0
  298. package/dist/systems/wall/wall-top.d.ts.map +1 -0
  299. package/dist/systems/wall/wall-top.js +46 -0
  300. package/dist/utils/clone-scene-graph.d.ts +1 -0
  301. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  302. package/dist/utils/clone-scene-graph.js +56 -5
  303. package/dist/utils/heal-scene-graph.d.ts +18 -0
  304. package/dist/utils/heal-scene-graph.d.ts.map +1 -0
  305. package/dist/utils/heal-scene-graph.js +106 -0
  306. package/dist/validation/validate-build-json.d.ts +3 -0
  307. package/dist/validation/validate-build-json.d.ts.map +1 -1
  308. package/dist/validation/validate-build-json.js +90 -5
  309. package/package.json +4 -4
  310. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
  311. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
  312. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -273
  313. package/dist/lib/space-detection-pause.test.d.ts +0 -2
  314. package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
  315. package/dist/lib/space-detection-pause.test.js +0 -42
  316. package/dist/lib/space-detection.test.d.ts +0 -2
  317. package/dist/lib/space-detection.test.d.ts.map +0 -1
  318. package/dist/lib/space-detection.test.js +0 -77
  319. package/dist/registry/registry.test.d.ts +0 -2
  320. package/dist/registry/registry.test.d.ts.map +0 -1
  321. package/dist/registry/registry.test.js +0 -183
  322. package/dist/registry/relations-resolver.test.d.ts +0 -2
  323. package/dist/registry/relations-resolver.test.d.ts.map +0 -1
  324. package/dist/registry/relations-resolver.test.js +0 -183
  325. package/dist/registry/scene-api.test.d.ts +0 -2
  326. package/dist/registry/scene-api.test.d.ts.map +0 -1
  327. package/dist/registry/scene-api.test.js +0 -183
  328. package/dist/registry/subtree.test.d.ts +0 -2
  329. package/dist/registry/subtree.test.d.ts.map +0 -1
  330. package/dist/registry/subtree.test.js +0 -133
  331. package/dist/schema/asset-url.test.d.ts +0 -2
  332. package/dist/schema/asset-url.test.d.ts.map +0 -1
  333. package/dist/schema/asset-url.test.js +0 -134
  334. package/dist/services/alignment-anchors.test.d.ts +0 -2
  335. package/dist/services/alignment-anchors.test.d.ts.map +0 -1
  336. package/dist/services/alignment-anchors.test.js +0 -383
  337. package/dist/services/alignment.test.d.ts +0 -2
  338. package/dist/services/alignment.test.d.ts.map +0 -1
  339. package/dist/services/alignment.test.js +0 -92
  340. package/dist/services/drag-session.test.d.ts +0 -2
  341. package/dist/services/drag-session.test.d.ts.map +0 -1
  342. package/dist/services/drag-session.test.js +0 -219
  343. package/dist/services/hosting.test.d.ts +0 -2
  344. package/dist/services/hosting.test.d.ts.map +0 -1
  345. package/dist/services/hosting.test.js +0 -199
  346. package/dist/services/movement.test.d.ts +0 -2
  347. package/dist/services/movement.test.d.ts.map +0 -1
  348. package/dist/services/movement.test.js +0 -133
  349. package/dist/services/single-undo-dance.test.d.ts +0 -2
  350. package/dist/services/single-undo-dance.test.d.ts.map +0 -1
  351. package/dist/services/single-undo-dance.test.js +0 -210
  352. package/dist/services/snap.test.d.ts +0 -2
  353. package/dist/services/snap.test.d.ts.map +0 -1
  354. package/dist/services/snap.test.js +0 -95
  355. package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
  356. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
  357. package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
  358. package/dist/store/actions/reparent.test.d.ts +0 -2
  359. package/dist/store/actions/reparent.test.d.ts.map +0 -1
  360. package/dist/store/actions/reparent.test.js +0 -212
  361. package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
  362. package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
  363. package/dist/store/use-scene-elevator-migration.test.js +0 -109
  364. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
  365. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
  366. package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
  367. package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
  368. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
  369. package/dist/systems/elevator/elevator-runtime.test.js +0 -64
  370. package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
  371. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
  372. package/dist/systems/stair/stair-opening-preview.test.js +0 -80
  373. package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
  374. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
  375. package/dist/systems/stair/stair-opening-sync.test.js +0 -446
  376. package/dist/utils/clone-scene-graph.test.d.ts +0 -2
  377. package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
  378. package/dist/utils/clone-scene-graph.test.js +0 -59
@@ -0,0 +1,557 @@
1
+ import { getRenderableSlabPolygon } from '../../lib/slab-polygon';
2
+ import { getWallCurveFrameAt, isCurvedWall } from '../wall/wall-curve';
3
+ import { DEFAULT_WALL_THICKNESS } from '../wall/wall-footprint';
4
+ import { MIN_WALL_HEIGHT } from '../wall/wall-top';
5
+ /**
6
+ * Clamp-never-ask upper bound for a slab's elevation. A plane-bound wall
7
+ * (no stored `height`) keeps its top at the storey plane, so a slab that
8
+ * rises past `storeyHeight - MIN_WALL_HEIGHT` while electing as that
9
+ * wall's base would squeeze the wall body below its minimum (and at the
10
+ * plane, to nothing). Walls with explicit heights don't constrain — their
11
+ * top rides the elected base, not the plane. Negative proposals (the
12
+ * drag-through-zero path that commits the `recessed` intent) pass
13
+ * through untouched: this is a purely numeric upper bound.
14
+ *
15
+ * The election runs against `levelSlabs` with `proposedElevation`
16
+ * substituted into `slab`, so a slab that would only WIN the election at
17
+ * the proposed elevation still clamps, and a slab out-elected by a
18
+ * sibling doesn't. Pure.
19
+ */
20
+ export function clampSlabElevationForWalls(proposedElevation, slab, levelWalls, levelSlabs, storeyHeight) {
21
+ const bound = storeyHeight - MIN_WALL_HEIGHT;
22
+ if (proposedElevation <= bound)
23
+ return { elevation: proposedElevation, clamped: false };
24
+ if (slab.polygon.length < 3)
25
+ return { elevation: proposedElevation, clamped: false };
26
+ const substituted = levelSlabs.some((candidate) => candidate.id === slab.id)
27
+ ? levelSlabs.map((candidate) => candidate.id === slab.id ? { ...candidate, elevation: proposedElevation } : candidate)
28
+ : [...levelSlabs, { ...slab, elevation: proposedElevation }];
29
+ for (const wall of levelWalls) {
30
+ if (wall.height != null)
31
+ continue;
32
+ const wallLike = {
33
+ start: wall.start,
34
+ end: wall.end,
35
+ curveOffset: wall.curveOffset,
36
+ thickness: wall.thickness,
37
+ };
38
+ // Cheap pre-filter: a wall that never reaches the slab's footprint
39
+ // can't elect it, whatever the election says about sibling slabs.
40
+ if (!wallOverlapsPolygon(wallLike, slab.polygon))
41
+ continue;
42
+ const support = computeWallSlabSupport(wallLike, substituted, levelWalls);
43
+ if (Math.abs(support.elevation - proposedElevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON) {
44
+ return { elevation: bound, clamped: true };
45
+ }
46
+ }
47
+ return { elevation: proposedElevation, clamped: false };
48
+ }
49
+ /**
50
+ * Static upper bound for a slab-elevation drag: probe the election with
51
+ * the slab raised above every sibling and the storey plane. If any
52
+ * plane-bound wall would elect it there, the drag may not pass
53
+ * `storeyHeight - MIN_WALL_HEIGHT`; otherwise it is unbounded above.
54
+ */
55
+ export function getSlabElevationUpperBound(slab, levelWalls, levelSlabs, storeyHeight) {
56
+ const probe = Math.max(storeyHeight, ...levelSlabs.map((candidate) => candidate.elevation ?? 0.05)) + 1;
57
+ return clampSlabElevationForWalls(probe, slab, levelWalls, levelSlabs, storeyHeight).clamped
58
+ ? storeyHeight - MIN_WALL_HEIGHT
59
+ : Number.POSITIVE_INFINITY;
60
+ }
61
+ /**
62
+ * Point-in-polygon test using ray casting algorithm.
63
+ */
64
+ export function pointInPolygon(px, pz, polygon) {
65
+ let inside = false;
66
+ const n = polygon.length;
67
+ for (let i = 0, j = n - 1; i < n; j = i++) {
68
+ const xi = polygon[i][0], zi = polygon[i][1];
69
+ const xj = polygon[j][0], zj = polygon[j][1];
70
+ if (zi > pz !== zj > pz && px < ((xj - xi) * (pz - zi)) / (zj - zi) + xi) {
71
+ inside = !inside;
72
+ }
73
+ }
74
+ return inside;
75
+ }
76
+ function pointSegmentDistance(px, pz, ax, az, bx, bz) {
77
+ const dx = bx - ax;
78
+ const dz = bz - az;
79
+ const lengthSquared = dx * dx + dz * dz;
80
+ if (lengthSquared < 1e-18)
81
+ return Math.hypot(px - ax, pz - az);
82
+ const t = Math.max(0, Math.min(1, ((px - ax) * dx + (pz - az) * dz) / lengthSquared));
83
+ return Math.hypot(px - (ax + dx * t), pz - (az + dz * t));
84
+ }
85
+ // Ray-cast pointInPolygon is unreliable for points exactly on the polygon
86
+ // boundary: the answer flips depending on which side of the polygon the edge
87
+ // is on. Interval classification below therefore treats "within this distance
88
+ // of the boundary" as inside explicitly, so walls sitting exactly on a slab
89
+ // edge (the common case — auto-slab polygons derive from wall centerlines)
90
+ // classify identically on every side of the slab.
91
+ const ON_BOUNDARY_EPSILON = 1e-4;
92
+ export function pointOnPolygonBoundary(px, pz, polygon) {
93
+ const n = polygon.length;
94
+ for (let i = 0; i < n; i++) {
95
+ const [ax, az] = polygon[i];
96
+ const [bx, bz] = polygon[(i + 1) % n];
97
+ if (pointSegmentDistance(px, pz, ax, az, bx, bz) <= ON_BOUNDARY_EPSILON)
98
+ return true;
99
+ }
100
+ return false;
101
+ }
102
+ function mergeIntervals(intervals) {
103
+ if (intervals.length <= 1)
104
+ return intervals;
105
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
106
+ const merged = [[sorted[0][0], sorted[0][1]]];
107
+ for (let i = 1; i < sorted.length; i++) {
108
+ const [intervalStart, intervalEnd] = sorted[i];
109
+ const last = merged[merged.length - 1];
110
+ if (intervalStart <= last[1] + 1e-9) {
111
+ last[1] = Math.max(last[1], intervalEnd);
112
+ }
113
+ else {
114
+ merged.push([intervalStart, intervalEnd]);
115
+ }
116
+ }
117
+ return merged;
118
+ }
119
+ /** Total length of a merged (sorted, disjoint) interval list. */
120
+ function intervalsLength(intervals) {
121
+ let total = 0;
122
+ for (const [intervalStart, intervalEnd] of intervals)
123
+ total += intervalEnd - intervalStart;
124
+ return total;
125
+ }
126
+ /** `base` minus `cut`. Both inputs may be unsorted; the result is merged. */
127
+ function subtractIntervals(base, cut) {
128
+ if (base.length === 0 || cut.length === 0)
129
+ return mergeIntervals(base);
130
+ const cuts = mergeIntervals(cut);
131
+ const result = [];
132
+ for (const [baseStart, baseEnd] of mergeIntervals(base)) {
133
+ let cursor = baseStart;
134
+ for (const [cutStart, cutEnd] of cuts) {
135
+ if (cutEnd <= cursor)
136
+ continue;
137
+ if (cutStart >= baseEnd)
138
+ break;
139
+ if (cutStart > cursor)
140
+ result.push([cursor, cutStart]);
141
+ cursor = cutEnd;
142
+ if (cursor >= baseEnd)
143
+ break;
144
+ }
145
+ if (cursor < baseEnd)
146
+ result.push([cursor, baseEnd]);
147
+ }
148
+ return result;
149
+ }
150
+ /**
151
+ * Sub-intervals of segment (ax,az)→(bx,bz) that lie inside the polygon (and,
152
+ * when `includeBoundary`, on its boundary), as [t0, t1] fractions of the
153
+ * segment. The segment is split at every crossing with a polygon edge and
154
+ * each sub-interval is classified by its midpoint, so no test point ever
155
+ * sits on a crossing.
156
+ */
157
+ function segmentInsideIntervals(ax, az, bx, bz, polygon, includeBoundary) {
158
+ const dx = bx - ax;
159
+ const dz = bz - az;
160
+ const length = Math.hypot(dx, dz);
161
+ if (length < 1e-9)
162
+ return [];
163
+ const ts = [0, 1];
164
+ const n = polygon.length;
165
+ for (let i = 0; i < n; i++) {
166
+ const [px, pz] = polygon[i];
167
+ const [qx, qz] = polygon[(i + 1) % n];
168
+ const ex = qx - px;
169
+ const ez = qz - pz;
170
+ const denom = dx * ez - dz * ex;
171
+ if (Math.abs(denom) < 1e-12)
172
+ continue; // parallel/collinear — nothing to split at
173
+ const t = ((px - ax) * ez - (pz - az) * ex) / denom;
174
+ const s = ((px - ax) * dz - (pz - az) * dx) / denom;
175
+ if (t > 0 && t < 1 && s >= -1e-9 && s <= 1 + 1e-9)
176
+ ts.push(t);
177
+ }
178
+ ts.sort((a, b) => a - b);
179
+ const inside = [];
180
+ for (let i = 1; i < ts.length; i++) {
181
+ const t0 = ts[i - 1];
182
+ const t1 = ts[i];
183
+ if (t1 - t0 < 1e-9)
184
+ continue;
185
+ const tm = (t0 + t1) / 2;
186
+ const mx = ax + dx * tm;
187
+ const mz = az + dz * tm;
188
+ const midpointInside = pointOnPolygonBoundary(mx, mz, polygon)
189
+ ? includeBoundary
190
+ : pointInPolygon(mx, mz, polygon);
191
+ if (midpointInside)
192
+ inside.push([t0, t1]);
193
+ }
194
+ return inside;
195
+ }
196
+ function polylineLength(points) {
197
+ let total = 0;
198
+ for (let i = 1; i < points.length; i++) {
199
+ total += Math.hypot(points[i].x - points[i - 1].x, points[i].y - points[i - 1].y);
200
+ }
201
+ return total;
202
+ }
203
+ /**
204
+ * Inside sub-intervals of a polyline against a polygon, in cumulative
205
+ * arc-length units from the polyline start (merged, disjoint). Boundary
206
+ * contact counts as inside for slab support (walls sit exactly on slab
207
+ * edges — see ON_BOUNDARY_EPSILON above); hole callers pass
208
+ * `includeBoundary: false` so a wall running along a stairwell hole's
209
+ * rim keeps the rim's support.
210
+ */
211
+ function polylineInsideIntervals(points, polygon, includeBoundary = true) {
212
+ const intervals = [];
213
+ let offset = 0;
214
+ for (let i = 1; i < points.length; i++) {
215
+ const a = points[i - 1];
216
+ const b = points[i];
217
+ const segmentLength = Math.hypot(b.x - a.x, b.y - a.y);
218
+ if (segmentLength < 1e-9)
219
+ continue;
220
+ for (const [t0, t1] of segmentInsideIntervals(a.x, a.y, b.x, b.y, polygon, includeBoundary)) {
221
+ intervals.push([offset + t0 * segmentLength, offset + t1 * segmentLength]);
222
+ }
223
+ offset += segmentLength;
224
+ }
225
+ return mergeIntervals(intervals);
226
+ }
227
+ // Minimum length of wall that must lie on/inside a slab polygon before the
228
+ // wall counts as overlapping it. Point contact (a perpendicular wall butting
229
+ // into a room's edge) clips to ~zero length and never reaches this, so such
230
+ // walls don't follow the slab's elevation.
231
+ const WALL_SLAB_MIN_OVERLAP = 0.05;
232
+ /**
233
+ * Centerline of the wall plus its two face lines (centerline offset by
234
+ * ±halfThickness). The face lines catch walls whose centerline sits on or
235
+ * just outside the slab boundary but whose body reaches onto the slab —
236
+ * e.g. slab polygons drawn to the room's interior faces.
237
+ */
238
+ function wallTestPolylines(start, end, curveOffset, halfThickness) {
239
+ const wallLike = { start, end, curveOffset };
240
+ if (curveOffset !== 0 && isCurvedWall(wallLike)) {
241
+ const count = 16;
242
+ const center = [];
243
+ const left = [];
244
+ const right = [];
245
+ for (let i = 0; i <= count; i++) {
246
+ const frame = getWallCurveFrameAt(wallLike, i / count);
247
+ center.push(frame.point);
248
+ left.push({
249
+ x: frame.point.x + frame.normal.x * halfThickness,
250
+ y: frame.point.y + frame.normal.y * halfThickness,
251
+ });
252
+ right.push({
253
+ x: frame.point.x - frame.normal.x * halfThickness,
254
+ y: frame.point.y - frame.normal.y * halfThickness,
255
+ });
256
+ }
257
+ return halfThickness > 0 ? [center, left, right] : [center];
258
+ }
259
+ const center = [
260
+ { x: start[0], y: start[1] },
261
+ { x: end[0], y: end[1] },
262
+ ];
263
+ const dx = end[0] - start[0];
264
+ const dz = end[1] - start[1];
265
+ const len = Math.hypot(dx, dz);
266
+ if (len < 1e-10 || halfThickness <= 0)
267
+ return [center];
268
+ const nx = (-dz / len) * halfThickness;
269
+ const nz = (dx / len) * halfThickness;
270
+ return [
271
+ center,
272
+ [
273
+ { x: start[0] + nx, y: start[1] + nz },
274
+ { x: end[0] + nx, y: end[1] + nz },
275
+ ],
276
+ [
277
+ { x: start[0] - nx, y: start[1] - nz },
278
+ { x: end[0] - nx, y: end[1] - nz },
279
+ ],
280
+ ];
281
+ }
282
+ /**
283
+ * Test whether a wall overlaps a slab polygon along a segment of its length.
284
+ *
285
+ * The wall's centerline and both face lines are clipped against the polygon;
286
+ * the wall overlaps when the longest clipped inside-or-on-boundary length
287
+ * exceeds a threshold (5cm, halved for very short walls). Because interval
288
+ * midpoints classify "on the boundary" as inside explicitly (never by
289
+ * ray-cast tie-breaking), a wall sitting exactly on a slab edge resolves
290
+ * identically on every side of the slab.
291
+ *
292
+ * A wall that only touches the polygon at a point — a perpendicular wall
293
+ * butting into a room's edge, or a corner-to-corner touch — clips to ~zero
294
+ * length and does NOT overlap.
295
+ */
296
+ export function wallOverlapsPolygon(startOrWall, endOrPolygon, polygonArg) {
297
+ // Two call shapes:
298
+ // wallOverlapsPolygon(wallLike, polygon) — preferred; curve-aware
299
+ // wallOverlapsPolygon(start, end, polygon) — legacy chord-only
300
+ let start;
301
+ let end;
302
+ let polygon;
303
+ let curveOffset = 0;
304
+ let thickness = DEFAULT_WALL_THICKNESS;
305
+ if (Array.isArray(startOrWall)) {
306
+ start = startOrWall;
307
+ end = endOrPolygon;
308
+ polygon = polygonArg;
309
+ }
310
+ else {
311
+ start = startOrWall.start;
312
+ end = startOrWall.end;
313
+ curveOffset = startOrWall.curveOffset ?? 0;
314
+ thickness = startOrWall.thickness ?? DEFAULT_WALL_THICKNESS;
315
+ polygon = endOrPolygon;
316
+ }
317
+ return wallOverlapsSlabFootprint({ start, end, curveOffset, thickness }, polygon);
318
+ }
319
+ /**
320
+ * {@link wallOverlapsPolygon} with the slab's stored holes subtracted from
321
+ * the covered length: a wall whose band only reaches the polygon inside a
322
+ * hole does not overlap. Hole boundaries keep coverage (rim convention —
323
+ * see {@link computeWallSlabSupport}). Polygon boundary contact counts as
324
+ * covered, so a wall sitting exactly on a slab edge resolves identically
325
+ * on every side of the slab. Pure.
326
+ */
327
+ export function wallOverlapsSlabFootprint(wallLike, polygon, holes) {
328
+ const { start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS } = wallLike;
329
+ const halfThickness = Math.max(thickness / 2, 0);
330
+ const polylines = wallTestPolylines(start, end, curveOffset, halfThickness);
331
+ const centerLength = polylineLength(polylines[0]);
332
+ if (centerLength < 1e-9)
333
+ return false;
334
+ let overlap = 0;
335
+ for (const line of polylines) {
336
+ let intervals = polylineInsideIntervals(line, polygon);
337
+ for (const hole of holes ?? []) {
338
+ if (intervals.length === 0)
339
+ break;
340
+ if (hole.length < 3)
341
+ continue;
342
+ intervals = subtractIntervals(intervals, polylineInsideIntervals(line, hole, false));
343
+ }
344
+ overlap = Math.max(overlap, intervalsLength(intervals));
345
+ }
346
+ const threshold = Math.max(1e-3, Math.min(WALL_SLAB_MIN_OVERLAP, centerLength * 0.5));
347
+ return overlap >= threshold;
348
+ }
349
+ /**
350
+ * Tolerance for the pointer-decided support cap: a slab still counts as
351
+ * "the surface you're pointing at (or below)" when its walking surface is
352
+ * within this many meters ABOVE the pointed elevation. Absorbs elevation
353
+ * noise between the ray hit and slab tops without letting a deck hanging
354
+ * clearly above the hit point capture the election. Defined here (rather
355
+ * than in the spatial-grid manager, which re-exports it) so the wall
356
+ * election below can honour the same cap without an import cycle.
357
+ */
358
+ export const SUPPORT_ELEVATION_EPSILON = 0.05;
359
+ // A slab elevation must support at least this fraction of the wall's
360
+ // length before it can dictate the wall's base. Below majority, a raised
361
+ // slab reaching one endpoint would hoist the whole wall off the floor
362
+ // that actually carries it.
363
+ const WALL_SLAB_SUPPORT_MAJORITY = 0.5;
364
+ // Slabs whose elevations differ by less than this pool their support:
365
+ // a wall shared between two rooms' slabs is covered roughly half by
366
+ // each, and must still follow their common elevation.
367
+ const WALL_SLAB_ELEVATION_POOL_EPSILON = 1e-4;
368
+ /**
369
+ * `preferredSlabId` is a persisted support host (`wall.supportSlabId`):
370
+ * while that slab is still in the candidate set (still overlaps the wall
371
+ * band with enough covered length), the elected `elevation` is pinned to
372
+ * it instead of the majority/best-coverage election. `baseSegments` /
373
+ * `baseElevation` (fill-down) still derive from ALL supporting slabs
374
+ * unchanged. A preferred slab that no longer qualifies is silently
375
+ * ignored — deliberately never cleared here, so the host resumes if the
376
+ * slab's polygon returns (only slab deletion strips the stored field).
377
+ *
378
+ * `maxElevation` is the pointer-decided support cap (level-local Y, same
379
+ * semantics as the item election): when set, elevation groups whose
380
+ * walking surface sits above `maxElevation + SUPPORT_ELEVATION_EPSILON`
381
+ * are excluded from the majority/best election — a deck hanging above the
382
+ * surface the cursor ray actually hit never captures the elected base.
383
+ * `baseSegments` / `baseElevation` stay uncapped (geometry fill-down), and
384
+ * an explicit `preferredSlabId` still wins over the cap.
385
+ */
386
+ export function computeWallSlabSupport(wallLike, slabs, levelWalls, preferredSlabId, maxElevation) {
387
+ const { start, end, curveOffset = 0, thickness = DEFAULT_WALL_THICKNESS } = wallLike;
388
+ const halfThickness = Math.max(thickness / 2, 0);
389
+ const polylines = wallTestPolylines(start, end, curveOffset, halfThickness);
390
+ const polylineLengths = polylines.map(polylineLength);
391
+ const wallLength = polylineLengths[0];
392
+ if (wallLength < 1e-9) {
393
+ return { elevation: 0, electedSlabId: null, baseElevation: 0, baseSegments: [] };
394
+ }
395
+ const minSupport = Math.max(1e-3, Math.min(WALL_SLAB_MIN_OVERLAP, wallLength * 0.5));
396
+ const groups = [];
397
+ let preferredElevation = null;
398
+ let preferredElectedSlabId = null;
399
+ for (const slab of slabs) {
400
+ if (slab.polygon.length < 3)
401
+ continue;
402
+ const renderedPolygon = getRenderableSlabPolygon(slab, {
403
+ walls: levelWalls,
404
+ siblingSlabs: slabs.filter((other) => other.id !== slab.id),
405
+ });
406
+ let supported = 0;
407
+ const perPolyline = polylines.map((line) => {
408
+ let intervals = polylineInsideIntervals(line, renderedPolygon);
409
+ for (const hole of slab.holes || []) {
410
+ if (intervals.length === 0)
411
+ break;
412
+ if (hole.length < 3)
413
+ continue;
414
+ intervals = subtractIntervals(intervals, polylineInsideIntervals(line, hole, false));
415
+ }
416
+ supported = Math.max(supported, intervalsLength(intervals));
417
+ return intervals;
418
+ });
419
+ if (supported < minSupport)
420
+ continue;
421
+ const elevation = slab.elevation ?? 0.05;
422
+ if (preferredSlabId != null && slab.id === preferredSlabId) {
423
+ preferredElevation = elevation;
424
+ preferredElectedSlabId = slab.id;
425
+ }
426
+ let group = groups.find((candidate) => Math.abs(candidate.elevation - elevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON);
427
+ if (!group) {
428
+ group = { elevation, slabIds: [], perPolyline: polylines.map(() => []) };
429
+ groups.push(group);
430
+ }
431
+ group.slabIds.push(slab.id);
432
+ for (let i = 0; i < perPolyline.length; i++) {
433
+ group.perPolyline[i].push(...perPolyline[i]);
434
+ }
435
+ }
436
+ const evaluatedGroups = groups.map((group) => ({
437
+ ...group,
438
+ mergedPerPolyline: group.perPolyline.map(mergeIntervals),
439
+ }));
440
+ const normalizedIntervals = (group, polylineIndex) => {
441
+ const lineLength = polylineLengths[polylineIndex];
442
+ if (lineLength < 1e-9)
443
+ return [];
444
+ return group.mergedPerPolyline[polylineIndex].map(([intervalStart, intervalEnd]) => [intervalStart / lineLength, intervalEnd / lineLength]);
445
+ };
446
+ const normalizedByGroup = evaluatedGroups.map((group) => ({
447
+ elevation: group.elevation,
448
+ perPolyline: group.mergedPerPolyline.map((_, index) => normalizedIntervals(group, index)),
449
+ }));
450
+ const breakpoints = [0, 1];
451
+ for (const group of normalizedByGroup) {
452
+ for (const intervals of group.perPolyline) {
453
+ for (const [intervalStart, intervalEnd] of intervals) {
454
+ breakpoints.push(intervalStart, intervalEnd);
455
+ }
456
+ }
457
+ }
458
+ breakpoints.sort((left, right) => left - right);
459
+ const uniqueBreakpoints = breakpoints.filter((value, index) => index === 0 || value - breakpoints[index - 1] > 1e-7);
460
+ const highestAt = (groupList, polylineIndex, t) => {
461
+ let highest = Number.NEGATIVE_INFINITY;
462
+ for (const group of groupList) {
463
+ if (group.perPolyline[polylineIndex]?.some(([intervalStart, intervalEnd]) => t >= intervalStart - 1e-7 && t <= intervalEnd + 1e-7)) {
464
+ highest = Math.max(highest, group.elevation);
465
+ }
466
+ }
467
+ return highest;
468
+ };
469
+ // The pointer cap filters the ELECTION's carrying profile, not the base
470
+ // profile: with a deck capped away, the floor that also carries the wall
471
+ // must still win (geometry fill-down stays uncapped).
472
+ const electableNormalizedGroups = maxElevation == null
473
+ ? normalizedByGroup
474
+ : normalizedByGroup.filter((group) => group.elevation <= maxElevation + SUPPORT_ELEVATION_EPSILON);
475
+ const baseSegments = [];
476
+ const carryCandidates = [];
477
+ const accumulateCarry = (elevation, length) => {
478
+ let candidate = carryCandidates.find((existing) => Math.abs(existing.elevation - elevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON);
479
+ if (!candidate) {
480
+ candidate = { elevation, length: 0 };
481
+ carryCandidates.push(candidate);
482
+ }
483
+ candidate.length += length;
484
+ };
485
+ for (let index = 1; index < uniqueBreakpoints.length; index++) {
486
+ const start = uniqueBreakpoints[index - 1];
487
+ const end = uniqueBreakpoints[index];
488
+ if (end - start < 1e-7)
489
+ continue;
490
+ const midpoint = (start + end) / 2;
491
+ const centerElevation = highestAt(normalizedByGroup, 0, midpoint);
492
+ const leftElevation = polylines.length >= 3 ? highestAt(normalizedByGroup, 1, midpoint) : Number.NEGATIVE_INFINITY;
493
+ const rightElevation = polylines.length >= 3 ? highestAt(normalizedByGroup, 2, midpoint) : Number.NEGATIVE_INFINITY;
494
+ const faceElevations = [leftElevation, rightElevation].filter(Number.isFinite);
495
+ const segmentElevation = faceElevations.length > 0 ? Math.min(...faceElevations) : Math.max(centerElevation, 0);
496
+ if (electableNormalizedGroups === normalizedByGroup) {
497
+ if (faceElevations.length > 0 || Number.isFinite(centerElevation)) {
498
+ accumulateCarry(segmentElevation, end - start);
499
+ }
500
+ }
501
+ else {
502
+ const electCenter = highestAt(electableNormalizedGroups, 0, midpoint);
503
+ const electLeft = polylines.length >= 3
504
+ ? highestAt(electableNormalizedGroups, 1, midpoint)
505
+ : Number.NEGATIVE_INFINITY;
506
+ const electRight = polylines.length >= 3
507
+ ? highestAt(electableNormalizedGroups, 2, midpoint)
508
+ : Number.NEGATIVE_INFINITY;
509
+ const electFaces = [electLeft, electRight].filter(Number.isFinite);
510
+ if (electFaces.length > 0 || Number.isFinite(electCenter)) {
511
+ accumulateCarry(electFaces.length > 0 ? Math.min(...electFaces) : Math.max(electCenter, 0), end - start);
512
+ }
513
+ }
514
+ const previous = baseSegments[baseSegments.length - 1];
515
+ if (previous &&
516
+ Math.abs(previous.elevation - segmentElevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON) {
517
+ previous.end = end;
518
+ }
519
+ else {
520
+ baseSegments.push({ start, end, elevation: segmentElevation });
521
+ }
522
+ }
523
+ let majorityElevation = Number.NEGATIVE_INFINITY;
524
+ let bestElevation = Number.NEGATIVE_INFINITY;
525
+ let bestCoverage = -1;
526
+ for (const candidate of carryCandidates) {
527
+ if (candidate.length >= WALL_SLAB_SUPPORT_MAJORITY - 1e-6) {
528
+ majorityElevation = Math.max(majorityElevation, candidate.elevation);
529
+ }
530
+ if (candidate.length > bestCoverage + 1e-6 ||
531
+ (Math.abs(candidate.length - bestCoverage) <= 1e-6 && candidate.elevation > bestElevation)) {
532
+ bestCoverage = candidate.length;
533
+ bestElevation = candidate.elevation;
534
+ }
535
+ }
536
+ const elevation = preferredElevation !== null
537
+ ? preferredElevation
538
+ : majorityElevation !== Number.NEGATIVE_INFINITY
539
+ ? majorityElevation
540
+ : bestElevation === Number.NEGATIVE_INFINITY
541
+ ? 0
542
+ : bestElevation;
543
+ const electedSlabId = preferredElectedSlabId ??
544
+ evaluatedGroups
545
+ .filter((group) => maxElevation == null || group.elevation <= maxElevation + SUPPORT_ELEVATION_EPSILON)
546
+ .find((group) => Math.abs(group.elevation - elevation) <= WALL_SLAB_ELEVATION_POOL_EPSILON)
547
+ ?.slabIds.slice()
548
+ .sort()[0] ??
549
+ null;
550
+ if (baseSegments.length === 0)
551
+ baseSegments.push({ start: 0, end: 1, elevation });
552
+ const baseElevation = Math.min(...baseSegments.map((segment) => segment.elevation));
553
+ return { elevation, electedSlabId, baseElevation, baseSegments };
554
+ }
555
+ export function computeWallSlabElevation(wallLike, slabs, levelWalls) {
556
+ return computeWallSlabSupport(wallLike, slabs, levelWalls).elevation;
557
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"stair-footprint.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-footprint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEnF;;;;;;;;;GASG;AAEH,MAAM,MAAM,kBAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3F,KAAK,gBAAgB,GAAG;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAI9E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,EAAE,CAkDzF;AA0GD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,EAChB,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACxC,kBAAkB,GAAG,IAAI,CAK3B"}
1
+ {"version":3,"file":"stair-footprint.d.ts","sourceRoot":"","sources":["../../../src/systems/stair/stair-footprint.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAa,SAAS,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA;AAEnF;;;;;;;;;GASG;AAEH,MAAM,MAAM,kBAAkB,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3F,KAAK,gBAAgB,GAAG;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,QAAQ,EAAE,MAAM,CAAA;CACjB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAI9E;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,gBAAgB,EAAE,GAAG,gBAAgB,EAAE,CAkDzF;AAwHD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,SAAS,EAChB,KAAK,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GACxC,kBAAkB,GAAG,IAAI,CAK3B"}
@@ -115,6 +115,15 @@ function straightStairAABB(stair, nodes) {
115
115
  return finiteBox(box);
116
116
  }
117
117
  const ARC_SAMPLES = 48;
118
+ function getSpiralLandingSweep(stair, sweepAngle) {
119
+ if ((stair.topLandingMode ?? 'none') !== 'integrated')
120
+ return 0;
121
+ const innerRadius = Math.max(0.05, stair.innerRadius ?? 0.9);
122
+ const width = Math.max(stair.width ?? 1, 0.4);
123
+ const landingDepth = Math.max(0.3, stair.topLandingDepth ?? Math.max(width * 0.9, 0.8));
124
+ return (Math.min(Math.PI * 0.75, landingDepth / Math.max(innerRadius + width / 2, 0.1)) *
125
+ Math.sign(sweepAngle || 1));
126
+ }
118
127
  /** Bounding box of a curved / spiral stair's annular sector (plus the
119
128
  * integrated spiral top landing when present). */
120
129
  function arcStairAABB(stair) {
@@ -123,7 +132,8 @@ function arcStairAABB(stair) {
123
132
  const innerRadius = Math.max(minInnerRadius, stair.innerRadius ?? (isSpiral ? 0.2 : 0.9));
124
133
  const width = Math.max(stair.width ?? 1, 0.4);
125
134
  const outerRadius = innerRadius + width;
126
- let sweep = stair.sweepAngle ?? (isSpiral ? Math.PI * 2 : Math.PI / 2);
135
+ const rawSweep = stair.sweepAngle ?? (isSpiral ? Math.PI * 2 : Math.PI / 2);
136
+ let sweep = rawSweep;
127
137
  // A full revolution would make the arc degenerate; clamp just under 2π the
128
138
  // same way the floor-plan emitter does so the sampled box stays correct.
129
139
  if (Math.abs(sweep) >= Math.PI * 2)
@@ -139,17 +149,17 @@ function arcStairAABB(stair) {
139
149
  extendByLocal(box, stair, cos * innerRadius, sin * innerRadius);
140
150
  extendByLocal(box, stair, cos * outerRadius, sin * outerRadius);
141
151
  }
142
- // Integrated spiral top landing a rectangle hung off the outer rim.
152
+ // Integrated spiral top landing renders as an angular extension of the
153
+ // annular stair body, not as a rectangular box outside the outer rim.
143
154
  if (isSpiral && stair.topLandingMode === 'integrated') {
144
- const depth = Math.max(stair.topLandingDepth ?? 0.9, 0.1);
145
- const halfWidth = width / 2;
146
- for (const [cornerX, cornerZ] of [
147
- [outerRadius, -halfWidth],
148
- [outerRadius + depth, -halfWidth],
149
- [outerRadius + depth, halfWidth],
150
- [outerRadius, halfWidth],
151
- ]) {
152
- extendByLocal(box, stair, cornerX, cornerZ);
155
+ const landingSweep = getSpiralLandingSweep(stair, rawSweep);
156
+ const landingSteps = Math.max(1, Math.ceil(Math.abs(landingSweep) / (Math.PI / 24)));
157
+ for (let step = 0; step <= landingSteps; step += 1) {
158
+ const angle = rawSweep / 2 + (landingSweep * step) / landingSteps;
159
+ const cos = Math.cos(angle);
160
+ const sin = Math.sin(angle);
161
+ extendByLocal(box, stair, cos * innerRadius, sin * innerRadius);
162
+ extendByLocal(box, stair, cos * outerRadius, sin * outerRadius);
153
163
  }
154
164
  }
155
165
  return finiteBox(box);