@pascal-app/core 0.9.2 → 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 (339) hide show
  1. package/README.md +17 -10
  2. package/dist/events/bus.d.ts +21 -2
  3. package/dist/events/bus.d.ts.map +1 -1
  4. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
  5. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  6. package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +116 -66
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  9. package/dist/hooks/spatial-grid/spatial-grid-manager.js +316 -441
  10. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
  11. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  12. package/dist/hooks/spatial-grid/spatial-grid-sync.js +193 -12
  13. package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
  14. package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
  15. package/dist/hooks/spatial-grid/support-host-id.js +2 -0
  16. package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
  17. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
  18. package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
  19. package/dist/index.d.ts +23 -11
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +22 -10
  22. package/dist/lib/measurement-geometry.d.ts +4 -0
  23. package/dist/lib/measurement-geometry.d.ts.map +1 -1
  24. package/dist/lib/measurement-geometry.js +27 -10
  25. package/dist/lib/slab-polygon.d.ts.map +1 -1
  26. package/dist/lib/slab-polygon.js +33 -1
  27. package/dist/lib/space-detection.d.ts +15 -4
  28. package/dist/lib/space-detection.d.ts.map +1 -1
  29. package/dist/lib/space-detection.js +174 -88
  30. package/dist/lib/terrain-brush.d.ts +163 -0
  31. package/dist/lib/terrain-brush.d.ts.map +1 -0
  32. package/dist/lib/terrain-brush.js +359 -0
  33. package/dist/lib/terrain-codec.d.ts +51 -0
  34. package/dist/lib/terrain-codec.d.ts.map +1 -0
  35. package/dist/lib/terrain-codec.js +167 -0
  36. package/dist/lib/terrain-field.d.ts +150 -0
  37. package/dist/lib/terrain-field.d.ts.map +1 -0
  38. package/dist/lib/terrain-field.js +248 -0
  39. package/dist/lib/terrain-raycast.d.ts +44 -0
  40. package/dist/lib/terrain-raycast.d.ts.map +1 -0
  41. package/dist/lib/terrain-raycast.js +188 -0
  42. package/dist/lib/terrain-source.d.ts +60 -0
  43. package/dist/lib/terrain-source.d.ts.map +1 -0
  44. package/dist/lib/terrain-source.js +90 -0
  45. package/dist/lib/terrain-support.d.ts +60 -0
  46. package/dist/lib/terrain-support.d.ts.map +1 -0
  47. package/dist/lib/terrain-support.js +150 -0
  48. package/dist/lib/zone-quantities.d.ts +1 -1
  49. package/dist/lib/zone-quantities.d.ts.map +1 -1
  50. package/dist/lib/zone-quantities.js +311 -66
  51. package/dist/material-library.d.ts +9 -1
  52. package/dist/material-library.d.ts.map +1 -1
  53. package/dist/material-library.js +56 -4
  54. package/dist/registry/handles.d.ts +16 -1
  55. package/dist/registry/handles.d.ts.map +1 -1
  56. package/dist/registry/index.d.ts +1 -1
  57. package/dist/registry/index.d.ts.map +1 -1
  58. package/dist/registry/subtree.d.ts.map +1 -1
  59. package/dist/registry/subtree.js +5 -2
  60. package/dist/registry/types.d.ts +64 -1
  61. package/dist/registry/types.d.ts.map +1 -1
  62. package/dist/schema/index.d.ts +6 -3
  63. package/dist/schema/index.d.ts.map +1 -1
  64. package/dist/schema/index.js +6 -3
  65. package/dist/schema/nodes/cabinet.d.ts +2 -0
  66. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  67. package/dist/schema/nodes/cabinet.js +4 -2
  68. package/dist/schema/nodes/ceiling.d.ts +1 -1
  69. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  70. package/dist/schema/nodes/ceiling.js +7 -1
  71. package/dist/schema/nodes/column.d.ts +1 -0
  72. package/dist/schema/nodes/column.d.ts.map +1 -1
  73. package/dist/schema/nodes/column.js +2 -0
  74. package/dist/schema/nodes/construction-dimension.d.ts +195 -0
  75. package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
  76. package/dist/schema/nodes/construction-dimension.js +151 -0
  77. package/dist/schema/nodes/door.d.ts +29 -0
  78. package/dist/schema/nodes/door.d.ts.map +1 -1
  79. package/dist/schema/nodes/door.js +20 -0
  80. package/dist/schema/nodes/duct-terminal.d.ts +1 -0
  81. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -1
  82. package/dist/schema/nodes/duct-terminal.js +2 -0
  83. package/dist/schema/nodes/fence.d.ts +2 -0
  84. package/dist/schema/nodes/fence.d.ts.map +1 -1
  85. package/dist/schema/nodes/fence.js +8 -0
  86. package/dist/schema/nodes/hvac-equipment.d.ts +1 -0
  87. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -1
  88. package/dist/schema/nodes/hvac-equipment.js +2 -0
  89. package/dist/schema/nodes/item.d.ts +1 -0
  90. package/dist/schema/nodes/item.d.ts.map +1 -1
  91. package/dist/schema/nodes/item.js +13 -0
  92. package/dist/schema/nodes/level.d.ts +2 -1
  93. package/dist/schema/nodes/level.d.ts.map +1 -1
  94. package/dist/schema/nodes/level.js +12 -33
  95. package/dist/schema/nodes/shelf.d.ts +1 -0
  96. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  97. package/dist/schema/nodes/shelf.js +2 -0
  98. package/dist/schema/nodes/site.d.ts +9 -0
  99. package/dist/schema/nodes/site.d.ts.map +1 -1
  100. package/dist/schema/nodes/site.js +8 -6
  101. package/dist/schema/nodes/slab.d.ts +5 -0
  102. package/dist/schema/nodes/slab.d.ts.map +1 -1
  103. package/dist/schema/nodes/slab.js +14 -2
  104. package/dist/schema/nodes/spawn.d.ts +1 -0
  105. package/dist/schema/nodes/spawn.d.ts.map +1 -1
  106. package/dist/schema/nodes/spawn.js +2 -0
  107. package/dist/schema/nodes/stair.d.ts +3 -1
  108. package/dist/schema/nodes/stair.d.ts.map +1 -1
  109. package/dist/schema/nodes/stair.js +8 -1
  110. package/dist/schema/nodes/structural-grid.d.ts +27 -0
  111. package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
  112. package/dist/schema/nodes/structural-grid.js +17 -0
  113. package/dist/schema/nodes/wall.d.ts +5 -2
  114. package/dist/schema/nodes/wall.d.ts.map +1 -1
  115. package/dist/schema/nodes/wall.js +13 -4
  116. package/dist/schema/nodes/window.d.ts +29 -0
  117. package/dist/schema/nodes/window.d.ts.map +1 -1
  118. package/dist/schema/nodes/window.js +18 -0
  119. package/dist/schema/nodes/zone.d.ts +20 -0
  120. package/dist/schema/nodes/zone.d.ts.map +1 -1
  121. package/dist/schema/nodes/zone.js +15 -0
  122. package/dist/schema/terrain.d.ts +28 -0
  123. package/dist/schema/terrain.d.ts.map +1 -0
  124. package/dist/schema/terrain.js +30 -0
  125. package/dist/schema/types.d.ts +211 -3
  126. package/dist/schema/types.d.ts.map +1 -1
  127. package/dist/schema/types.js +4 -0
  128. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
  129. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
  130. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
  131. package/dist/services/index.d.ts +2 -1
  132. package/dist/services/index.d.ts.map +1 -1
  133. package/dist/services/index.js +2 -1
  134. package/dist/services/level-height.d.ts +8 -9
  135. package/dist/services/level-height.d.ts.map +1 -1
  136. package/dist/services/level-height.js +48 -16
  137. package/dist/services/storey.d.ts +103 -0
  138. package/dist/services/storey.d.ts.map +1 -0
  139. package/dist/services/storey.js +296 -0
  140. package/dist/store/actions/node-actions.d.ts.map +1 -1
  141. package/dist/store/actions/node-actions.js +47 -3
  142. package/dist/store/history-control.d.ts +20 -0
  143. package/dist/store/history-control.d.ts.map +1 -1
  144. package/dist/store/history-control.js +110 -13
  145. package/dist/store/use-live-terrain.d.ts +65 -0
  146. package/dist/store/use-live-terrain.d.ts.map +1 -0
  147. package/dist/store/use-live-terrain.js +31 -0
  148. package/dist/store/use-live-transforms.d.ts +8 -0
  149. package/dist/store/use-live-transforms.d.ts.map +1 -1
  150. package/dist/store/use-scene.d.ts +31 -2
  151. package/dist/store/use-scene.d.ts.map +1 -1
  152. package/dist/store/use-scene.js +638 -8
  153. package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
  154. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
  155. package/dist/systems/elevator/elevator-opening-system.js +27 -21
  156. package/dist/systems/elevator/elevator-service.d.ts +0 -2
  157. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  158. package/dist/systems/elevator/elevator-service.js +2 -24
  159. package/dist/systems/slab/slab-placement.d.ts +11 -0
  160. package/dist/systems/slab/slab-placement.d.ts.map +1 -0
  161. package/dist/systems/slab/slab-placement.js +14 -0
  162. package/dist/systems/slab/slab-support.d.ts +137 -0
  163. package/dist/systems/slab/slab-support.d.ts.map +1 -0
  164. package/dist/systems/slab/slab-support.js +557 -0
  165. package/dist/systems/stair/stair-opening-preview.d.ts +1060 -935
  166. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  167. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  168. package/dist/systems/stair/stair-opening-sync.js +18 -15
  169. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  170. package/dist/systems/stair/stair-opening-system.js +32 -4
  171. package/dist/systems/stair/stair-rise.d.ts +22 -0
  172. package/dist/systems/stair/stair-rise.d.ts.map +1 -0
  173. package/dist/systems/stair/stair-rise.js +79 -0
  174. package/dist/systems/wall/wall-curve.d.ts +10 -0
  175. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  176. package/dist/systems/wall/wall-curve.js +1 -1
  177. package/dist/systems/wall/wall-mitering.js +6 -6
  178. package/dist/systems/wall/wall-top.d.ts +40 -0
  179. package/dist/systems/wall/wall-top.d.ts.map +1 -0
  180. package/dist/systems/wall/wall-top.js +46 -0
  181. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  182. package/dist/utils/clone-scene-graph.js +34 -3
  183. package/dist/utils/heal-scene-graph.d.ts +1 -1
  184. package/dist/utils/heal-scene-graph.d.ts.map +1 -1
  185. package/dist/utils/heal-scene-graph.js +25 -9
  186. package/dist/validation/validate-build-json.d.ts +2 -0
  187. package/dist/validation/validate-build-json.d.ts.map +1 -1
  188. package/dist/validation/validate-build-json.js +59 -3
  189. package/package.json +2 -2
  190. package/dist/hooks/scene-registry/scene-registry.test.d.ts +0 -2
  191. package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +0 -1
  192. package/dist/hooks/scene-registry/scene-registry.test.js +0 -26
  193. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
  194. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
  195. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -366
  196. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +0 -2
  197. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +0 -1
  198. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +0 -363
  199. package/dist/lib/measurement-geometry.test.d.ts +0 -2
  200. package/dist/lib/measurement-geometry.test.d.ts.map +0 -1
  201. package/dist/lib/measurement-geometry.test.js +0 -102
  202. package/dist/lib/slab-polygon.test.d.ts +0 -2
  203. package/dist/lib/slab-polygon.test.d.ts.map +0 -1
  204. package/dist/lib/slab-polygon.test.js +0 -635
  205. package/dist/lib/space-detection-pause.test.d.ts +0 -2
  206. package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
  207. package/dist/lib/space-detection-pause.test.js +0 -42
  208. package/dist/lib/space-detection.test.d.ts +0 -2
  209. package/dist/lib/space-detection.test.d.ts.map +0 -1
  210. package/dist/lib/space-detection.test.js +0 -376
  211. package/dist/lib/zone-quantities.test.d.ts +0 -2
  212. package/dist/lib/zone-quantities.test.d.ts.map +0 -1
  213. package/dist/lib/zone-quantities.test.js +0 -82
  214. package/dist/registry/registry.test.d.ts +0 -2
  215. package/dist/registry/registry.test.d.ts.map +0 -1
  216. package/dist/registry/registry.test.js +0 -196
  217. package/dist/registry/relations-resolver.test.d.ts +0 -2
  218. package/dist/registry/relations-resolver.test.d.ts.map +0 -1
  219. package/dist/registry/relations-resolver.test.js +0 -183
  220. package/dist/registry/scene-api.test.d.ts +0 -2
  221. package/dist/registry/scene-api.test.d.ts.map +0 -1
  222. package/dist/registry/scene-api.test.js +0 -183
  223. package/dist/registry/subtree.test.d.ts +0 -2
  224. package/dist/registry/subtree.test.d.ts.map +0 -1
  225. package/dist/registry/subtree.test.js +0 -163
  226. package/dist/schema/asset-url.test.d.ts +0 -2
  227. package/dist/schema/asset-url.test.d.ts.map +0 -1
  228. package/dist/schema/asset-url.test.js +0 -134
  229. package/dist/schema/material.test.d.ts +0 -2
  230. package/dist/schema/material.test.d.ts.map +0 -1
  231. package/dist/schema/material.test.js +0 -43
  232. package/dist/schema/nodes/measurement.test.d.ts +0 -2
  233. package/dist/schema/nodes/measurement.test.d.ts.map +0 -1
  234. package/dist/schema/nodes/measurement.test.js +0 -139
  235. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +0 -2
  236. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +0 -1
  237. package/dist/schema/nodes/ridge-vent-defaults.test.js +0 -205
  238. package/dist/schema/nodes/roof-segment-shape.test.d.ts +0 -2
  239. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +0 -1
  240. package/dist/schema/nodes/roof-segment-shape.test.js +0 -175
  241. package/dist/schema/nodes/roof-segment-surface.test.d.ts +0 -2
  242. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +0 -1
  243. package/dist/schema/nodes/roof-segment-surface.test.js +0 -48
  244. package/dist/schema/nodes/roof-segment-trim.test.d.ts +0 -2
  245. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +0 -1
  246. package/dist/schema/nodes/roof-segment-trim.test.js +0 -107
  247. package/dist/schema/nodes/roof-segment-walls.test.d.ts +0 -2
  248. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +0 -1
  249. package/dist/schema/nodes/roof-segment-walls.test.js +0 -92
  250. package/dist/schema/nodes/wall.test.d.ts +0 -2
  251. package/dist/schema/nodes/wall.test.d.ts.map +0 -1
  252. package/dist/schema/nodes/wall.test.js +0 -208
  253. package/dist/services/alignment-anchors.test.d.ts +0 -2
  254. package/dist/services/alignment-anchors.test.d.ts.map +0 -1
  255. package/dist/services/alignment-anchors.test.js +0 -383
  256. package/dist/services/alignment.test.d.ts +0 -2
  257. package/dist/services/alignment.test.d.ts.map +0 -1
  258. package/dist/services/alignment.test.js +0 -92
  259. package/dist/services/drag-session.test.d.ts +0 -2
  260. package/dist/services/drag-session.test.d.ts.map +0 -1
  261. package/dist/services/drag-session.test.js +0 -219
  262. package/dist/services/hosting.test.d.ts +0 -2
  263. package/dist/services/hosting.test.d.ts.map +0 -1
  264. package/dist/services/hosting.test.js +0 -230
  265. package/dist/services/movement.test.d.ts +0 -2
  266. package/dist/services/movement.test.d.ts.map +0 -1
  267. package/dist/services/movement.test.js +0 -133
  268. package/dist/services/opening-guides.test.d.ts +0 -2
  269. package/dist/services/opening-guides.test.d.ts.map +0 -1
  270. package/dist/services/opening-guides.test.js +0 -208
  271. package/dist/services/port-connectivity.test.d.ts +0 -2
  272. package/dist/services/port-connectivity.test.d.ts.map +0 -1
  273. package/dist/services/port-connectivity.test.js +0 -297
  274. package/dist/services/riser-diagram.test.d.ts +0 -2
  275. package/dist/services/riser-diagram.test.d.ts.map +0 -1
  276. package/dist/services/riser-diagram.test.js +0 -66
  277. package/dist/services/single-undo-dance.test.d.ts +0 -2
  278. package/dist/services/single-undo-dance.test.d.ts.map +0 -1
  279. package/dist/services/single-undo-dance.test.js +0 -210
  280. package/dist/services/snap.test.d.ts +0 -2
  281. package/dist/services/snap.test.d.ts.map +0 -1
  282. package/dist/services/snap.test.js +0 -137
  283. package/dist/services/system-graph.test.d.ts +0 -2
  284. package/dist/services/system-graph.test.d.ts.map +0 -1
  285. package/dist/services/system-graph.test.js +0 -138
  286. package/dist/services/validate-dwv.test.d.ts +0 -2
  287. package/dist/services/validate-dwv.test.d.ts.map +0 -1
  288. package/dist/services/validate-dwv.test.js +0 -109
  289. package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
  290. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
  291. package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
  292. package/dist/store/actions/reparent.test.d.ts +0 -2
  293. package/dist/store/actions/reparent.test.d.ts.map +0 -1
  294. package/dist/store/actions/reparent.test.js +0 -212
  295. package/dist/store/actions/ridge-vent-update.test.d.ts +0 -2
  296. package/dist/store/actions/ridge-vent-update.test.d.ts.map +0 -1
  297. package/dist/store/actions/ridge-vent-update.test.js +0 -264
  298. package/dist/store/use-scene-dirty-tracking.test.d.ts +0 -2
  299. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +0 -1
  300. package/dist/store/use-scene-dirty-tracking.test.js +0 -68
  301. package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
  302. package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
  303. package/dist/store/use-scene-elevator-migration.test.js +0 -109
  304. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +0 -2
  305. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +0 -1
  306. package/dist/store/use-scene-measurement-roundtrip.test.js +0 -97
  307. package/dist/store/use-scene-plugins.test.d.ts +0 -2
  308. package/dist/store/use-scene-plugins.test.d.ts.map +0 -1
  309. package/dist/store/use-scene-plugins.test.js +0 -51
  310. package/dist/store/use-scene-wall-slot-migration.test.d.ts +0 -2
  311. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +0 -1
  312. package/dist/store/use-scene-wall-slot-migration.test.js +0 -210
  313. package/dist/store/use-scene-window-migration.test.d.ts +0 -2
  314. package/dist/store/use-scene-window-migration.test.d.ts.map +0 -1
  315. package/dist/store/use-scene-window-migration.test.js +0 -85
  316. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
  317. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
  318. package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
  319. package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
  320. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
  321. package/dist/systems/elevator/elevator-runtime.test.js +0 -64
  322. package/dist/systems/fence/fence-spline.test.d.ts +0 -2
  323. package/dist/systems/fence/fence-spline.test.d.ts.map +0 -1
  324. package/dist/systems/fence/fence-spline.test.js +0 -136
  325. package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
  326. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
  327. package/dist/systems/stair/stair-opening-preview.test.js +0 -80
  328. package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
  329. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
  330. package/dist/systems/stair/stair-opening-sync.test.js +0 -485
  331. package/dist/systems/wall/wall-mitering.test.d.ts +0 -2
  332. package/dist/systems/wall/wall-mitering.test.d.ts.map +0 -1
  333. package/dist/systems/wall/wall-mitering.test.js +0 -63
  334. package/dist/utils/clone-scene-graph.test.d.ts +0 -2
  335. package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
  336. package/dist/utils/clone-scene-graph.test.js +0 -62
  337. package/dist/utils/heal-scene-graph.test.d.ts +0 -2
  338. package/dist/utils/heal-scene-graph.test.d.ts.map +0 -1
  339. package/dist/utils/heal-scene-graph.test.js +0 -88
@@ -1,9 +1,14 @@
1
+ import { GROUND_SUPPORT_ID } from '../hooks/spatial-grid/support-host-id';
1
2
  import { CeilingNode, SlabNode, ZoneNode, } from '../schema';
3
+ import { DEFAULT_LEVEL_HEIGHT } from '../services/level-height';
4
+ import { CEILING_CLAMP_MARGIN, findLevelAboveId, getCeilingClampBound, getLevelElevations, getStoredLevelHeight, } from '../services/storey';
2
5
  import { getSceneHistoryPauseDepth, pauseSceneHistory, resumeSceneHistory, } from '../store/history-control';
6
+ import { computeWallSlabSupport } from '../systems/slab/slab-support';
3
7
  import { getClampedWallCurveOffset, getWallCurveFrameAt, isCurvedWall, } from '../systems/wall/wall-curve';
8
+ import { resolveWallTop } from '../systems/wall/wall-top';
4
9
  import { simplifyClosedPolygon } from './polygon-geometry';
10
+ import { terrainSupportLift } from './terrain-support';
5
11
  const DEFAULT_AUTO_SLAB_ELEVATION = 0.05;
6
- const DEFAULT_AUTO_CEILING_HEIGHT = 2.5;
7
12
  const CEILING_HEIGHT_EPSILON = 1e-6;
8
13
  const ROOM_CURVE_TOLERANCE = 0.04;
9
14
  const MAX_CURVE_SUBDIVISION_DEPTH = 6;
@@ -19,6 +24,7 @@ const WALL_JUNCTION_TOLERANCE = 0.08;
19
24
  // deleted) and the node is demoted to manual so user data survives.
20
25
  const ORPHAN_MERGE_COVERAGE_THRESHOLD = 0.6;
21
26
  const COVERAGE_SAMPLE_STEPS = 12;
27
+ const ROOM_VERTICAL_PLANE_EPSILON = 1e-3;
22
28
  function pointFromTuple(point) {
23
29
  return { x: point[0], y: point[1] };
24
30
  }
@@ -194,51 +200,56 @@ function wallBoundsRoom(wall, roomPolygon) {
194
200
  const matchingPoints = candidates.filter((point) => pointDistanceToPolygonBoundary(point, roomPolygon) <= WALL_ROOM_BOUNDARY_TOLERANCE);
195
201
  return matchingPoints.length >= 2;
196
202
  }
197
- function pointIsOnSlab(point, slab) {
198
- if (slab.polygon.length < 3)
199
- return false;
200
- const slabPolygon = slab.polygon.map(pointFromTuple);
201
- if (!pointInPolygon(point, slabPolygon))
202
- return false;
203
- for (const hole of slab.holes ?? []) {
204
- if (hole.length >= 3 && pointInPolygon(point, hole.map(pointFromTuple))) {
205
- return false;
206
- }
207
- }
208
- return true;
203
+ /**
204
+ * The clamp bound for a ceiling polygon under this planning context —
205
+ * the context's cross-level resolver when provided, else the plane-only
206
+ * `storeyHeight - CEILING_CLAMP_MARGIN` degradation.
207
+ */
208
+ function resolveCeilingClampBound(polygon, context) {
209
+ if (context.ceilingClampBound)
210
+ return context.ceilingClampBound(polygon);
211
+ return (context.storeyHeight ?? DEFAULT_LEVEL_HEIGHT) - CEILING_CLAMP_MARGIN;
209
212
  }
210
- function slabSupportsRoom(roomPolygon, slab) {
211
- if (slab.polygon.length < 3)
212
- return false;
213
- if (polygonSignature(slab.polygon.map(pointFromTuple)) === polygonSignature(roomPolygon)) {
214
- return true;
215
- }
216
- return pointIsOnSlab(polygonCentroid(roomPolygon), slab);
213
+ function consensusElevation(values) {
214
+ if (values.length === 0 || values.some((value) => !Number.isFinite(value)))
215
+ return undefined;
216
+ const min = Math.min(...values);
217
+ const max = Math.max(...values);
218
+ if (max - min > ROOM_VERTICAL_PLANE_EPSILON)
219
+ return undefined;
220
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
217
221
  }
218
- function resolveRoomSlabElevation(roomPolygon, slabs = []) {
219
- let maxElevation = 0;
220
- for (const slab of slabs) {
221
- if (!slabSupportsRoom(roomPolygon, slab))
222
+ function autoRoomVerticalPlacements(spaces, walls, supportSlabs, nodes, storeyHeight) {
223
+ const wallsById = new Map(walls.map((wall) => [wall.id, wall]));
224
+ const placements = new Map();
225
+ for (const space of spaces) {
226
+ const boundaryWalls = space.wallIds.flatMap((id) => {
227
+ const wall = wallsById.get(id);
228
+ return wall ? [wall] : [];
229
+ });
230
+ if (boundaryWalls.length !== space.wallIds.length)
222
231
  continue;
223
- maxElevation = Math.max(maxElevation, slab.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION);
224
- }
225
- return maxElevation;
226
- }
227
- function resolveRoomWallHeight(roomPolygon, walls = []) {
228
- let maxHeight = 0;
229
- for (const wall of walls) {
230
- if (!wallBoundsRoom(wall, roomPolygon))
232
+ const wallBases = boundaryWalls.map((wall) => {
233
+ const offset = wall.supportOffset ?? 0;
234
+ if (wall.supportSlabId === GROUND_SUPPORT_ID) {
235
+ return ((terrainSupportLift(nodes, space.levelId, wall.start[0], wall.start[1]) ?? 0) + offset);
236
+ }
237
+ return (computeWallSlabSupport(wall, supportSlabs, walls, wall.supportSlabId ?? null).elevation +
238
+ offset);
239
+ });
240
+ const base = consensusElevation(wallBases);
241
+ if (base === undefined)
231
242
  continue;
232
- const height = wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT;
233
- if (Number.isFinite(height)) {
234
- maxHeight = Math.max(maxHeight, height);
235
- }
243
+ const wallTops = boundaryWalls.map((wall, index) => resolveWallTop(wall, storeyHeight, wallBases[index] ?? base));
244
+ const top = consensusElevation(wallTops);
245
+ if (top === undefined)
246
+ continue;
247
+ placements.set(polygonSignature(space.polygon.map(pointFromTuple)), {
248
+ slabElevation: base + DEFAULT_AUTO_SLAB_ELEVATION,
249
+ ceilingHeight: top - CEILING_CLAMP_MARGIN,
250
+ });
236
251
  }
237
- return maxHeight > 0 ? maxHeight : DEFAULT_AUTO_CEILING_HEIGHT;
238
- }
239
- function resolveAutoCeilingHeight(roomPolygon, context = {}) {
240
- return (resolveRoomSlabElevation(roomPolygon, context.slabs) +
241
- resolveRoomWallHeight(roomPolygon, context.walls));
252
+ return placements;
242
253
  }
243
254
  function getWallDirection(wall) {
244
255
  const dx = wall.end[0] - wall.start[0];
@@ -612,7 +623,12 @@ function wallGeometrySignature(wall) {
612
623
  wall.end[0].toFixed(4),
613
624
  wall.end[1].toFixed(4),
614
625
  (wall.thickness ?? 0.2).toFixed(4),
615
- (wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT).toFixed(4),
626
+ // Plane-bound (no stored height) is a distinct state, not a default
627
+ // value: it resolves to the storey plane, so it must not alias an
628
+ // explicit height of the same magnitude in the trigger signature.
629
+ wall.height == null ? 'plane' : wall.height.toFixed(4),
630
+ wall.supportSlabId ?? 'elected',
631
+ (wall.supportOffset ?? 0).toFixed(4),
616
632
  getClampedWallCurveOffset(wall).toFixed(4),
617
633
  ].join('|');
618
634
  }
@@ -627,13 +643,24 @@ function zoneGeometrySignature(zone) {
627
643
  zone.polygon.map(([x, z]) => `${x.toFixed(4)},${z.toFixed(4)}`).join(';'),
628
644
  ].join('|');
629
645
  }
630
- // Slabs and ceilings stay out of the trigger signature: including generated
631
- // surfaces caused delete/recreate feedback. Zones are included only so a newly
632
- // traced room footprint can adopt its enclosing walls without waiting for the
633
- // next remodel.
646
+ // Slab/ceiling POLYGONS stay out of the trigger signature: including
647
+ // generated footprints caused delete/recreate feedback. Zones are included
648
+ // only so a newly traced room footprint can adopt its enclosing walls
649
+ // without waiting for the next remodel. Slab ELEVATIONS and the level's
650
+ // stored storey height ARE included — both feed the explicit-ceiling
651
+ // re-clamp bound (the storey plane), and neither is rewritten by
652
+ // the sync, so regeneration triggers when they change without feedback.
653
+ // Stage 3-B adds the LEVEL-ABOVE's covering-slab undersides (elevation −
654
+ // thickness, recessed pools excluded): a deck created, lowered, or
655
+ // thickened above must re-run the sync below so ceilings re-clamp under
656
+ // it. Same polygon exclusion applies — the level-above's own auto sync
657
+ // rewrites its slab footprints, and hashing them here would re-trigger
658
+ // this level on every remodel above.
634
659
  function levelStructureSnapshots(nodes) {
635
660
  const wallsByLevel = new Map();
636
661
  const zonesByLevel = new Map();
662
+ const slabElevationsByLevel = new Map();
663
+ const coveringUndersidesByLevel = new Map();
637
664
  for (const node of Object.values(nodes)) {
638
665
  if (!(node && typeof node === 'object' && 'parentId' in node && node.parentId))
639
666
  continue;
@@ -648,13 +675,33 @@ function levelStructureSnapshots(nodes) {
648
675
  zones.push(ZoneNode.parse(node));
649
676
  zonesByLevel.set(levelId, zones);
650
677
  }
678
+ else if (node.type === 'slab') {
679
+ const elevations = slabElevationsByLevel.get(levelId) ?? [];
680
+ elevations.push(`${node.id}:${(node.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION).toFixed(4)}`);
681
+ slabElevationsByLevel.set(levelId, elevations);
682
+ if (node.recessed !== true) {
683
+ const undersides = coveringUndersidesByLevel.get(levelId) ?? [];
684
+ const elevation = node.elevation ?? 0.05;
685
+ const thickness = node.thickness ?? 0.05;
686
+ undersides.push(`${node.id}:${(elevation - thickness).toFixed(4)}`);
687
+ coveringUndersidesByLevel.set(levelId, undersides);
688
+ }
689
+ }
651
690
  }
691
+ const levelElevations = getLevelElevations(nodes);
652
692
  const snapshots = new Map();
653
693
  const levelIds = new Set([...wallsByLevel.keys(), ...zonesByLevel.keys()]);
654
694
  for (const levelId of levelIds) {
655
695
  const walls = wallsByLevel.get(levelId) ?? [];
656
696
  const zones = zonesByLevel.get(levelId) ?? [];
657
- snapshots.set(levelId, `${levelWallSnapshot(walls)}##${zones.map(zoneGeometrySignature).sort().join('||')}`);
697
+ const level = nodes[levelId];
698
+ const storeyKey = level?.type === 'level' && typeof level.height === 'number' ? level.height.toFixed(4) : '';
699
+ const slabKey = (slabElevationsByLevel.get(levelId) ?? []).sort().join(';');
700
+ const aboveId = findLevelAboveId(levelId, levelElevations);
701
+ const aboveSlabKey = aboveId
702
+ ? (coveringUndersidesByLevel.get(aboveId) ?? []).sort().join(';')
703
+ : '';
704
+ snapshots.set(levelId, `${storeyKey}#${levelWallSnapshot(walls)}##${zones.map(zoneGeometrySignature).sort().join('||')}##${slabKey}##${aboveSlabKey}`);
658
705
  }
659
706
  return snapshots;
660
707
  }
@@ -712,7 +759,7 @@ export function resolveAutoZonePolygon(zone, resolve) {
712
759
  const room = extractRooms(walls).find((candidate) => sameStringSet([...new Set(candidate.boundaryFaces.map((boundary) => boundary.wallId))], zone.boundaryWallIds));
713
760
  return room ? room.polygon.map(pointToTuple) : zone.polygon;
714
761
  }
715
- export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
762
+ export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {}) {
716
763
  const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls);
717
764
  const manualSignatures = new Set(manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))));
718
765
  const manualPolygons = manualSlabs.map((slab) => slab.polygon.map(pointFromTuple));
@@ -758,7 +805,11 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
758
805
  return;
759
806
  matchedDetectedIdx.add(index);
760
807
  matchedSlabIds.add(existing.slab.id);
761
- updatesById.set(existing.slab.id, room.poly.map(pointToTuple));
808
+ const polygon = room.poly.map(pointToTuple);
809
+ updatesById.set(existing.slab.id, {
810
+ polygon,
811
+ elevation: context.elevationForRoom?.(polygon),
812
+ });
762
813
  });
763
814
  const remainingDetected = detected
764
815
  .map((room, index) => ({ room, index }))
@@ -787,7 +838,11 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
787
838
  continue;
788
839
  matchedDetectedIdx.add(index);
789
840
  matchedSlabIds.add(bestMatch.entry.slab.id);
790
- updatesById.set(bestMatch.entry.slab.id, room.poly.map(pointToTuple));
841
+ const polygon = room.poly.map(pointToTuple);
842
+ updatesById.set(bestMatch.entry.slab.id, {
843
+ polygon,
844
+ elevation: context.elevationForRoom?.(polygon),
845
+ });
791
846
  }
792
847
  const detectedRoomPolygons = detectedAll.map((room) => room.poly);
793
848
  const slabsToDelete = [];
@@ -809,10 +864,17 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
809
864
  ...existingAuto
810
865
  .filter((slab) => updatesById.has(slab.id))
811
866
  .flatMap((slab) => {
812
- const polygon = updatesById.get(slab.id);
813
- if (!polygon)
867
+ const update = updatesById.get(slab.id);
868
+ if (!update)
814
869
  return [];
815
- return sameTuplePolygon(slab.polygon, polygon) ? [] : [{ id: slab.id, data: { polygon } }];
870
+ const data = {};
871
+ if (!sameTuplePolygon(slab.polygon, update.polygon))
872
+ data.polygon = update.polygon;
873
+ if (update.elevation !== undefined &&
874
+ Math.abs(slab.elevation - update.elevation) > ROOM_VERTICAL_PLANE_EPSILON) {
875
+ data.elevation = update.elevation;
876
+ }
877
+ return Object.keys(data).length > 0 ? [{ id: slab.id, data }] : [];
816
878
  }),
817
879
  ...slabDemotions,
818
880
  ];
@@ -826,11 +888,15 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
826
888
  continue;
827
889
  const name = nextAutoRoomName(plannedSlabsForNaming, 'Slab');
828
890
  plannedSlabsForNaming.push({ name });
891
+ const polygon = room.poly.map(pointToTuple);
892
+ const elevation = context.elevationForRoom?.(polygon);
829
893
  slabsToCreate.push(SlabNode.parse({
830
894
  name,
831
- polygon: room.poly.map(pointToTuple),
895
+ polygon,
832
896
  holes: [],
833
- elevation: DEFAULT_AUTO_SLAB_ELEVATION,
897
+ elevation: elevation !== undefined && Number.isFinite(elevation)
898
+ ? elevation
899
+ : DEFAULT_AUTO_SLAB_ELEVATION,
834
900
  autoFromWalls: true,
835
901
  }));
836
902
  }
@@ -840,8 +906,8 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
840
906
  delete: slabsToDelete,
841
907
  };
842
908
  }
843
- function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore) {
844
- const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs);
909
+ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore, context = {}) {
910
+ const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs, context);
845
911
  if (plan.delete.length > 0) {
846
912
  sceneStore.getState().deleteNodes(plan.delete);
847
913
  }
@@ -853,22 +919,6 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore)
853
919
  }
854
920
  return plan;
855
921
  }
856
- export function projectAutoSlabsForPlan(existingSlabs, plan) {
857
- const slabsById = new Map(existingSlabs.map((slab) => [slab.id, slab]));
858
- for (const id of plan.delete) {
859
- slabsById.delete(id);
860
- }
861
- for (const update of plan.update) {
862
- const slab = slabsById.get(update.id);
863
- if (!slab)
864
- continue;
865
- slabsById.set(update.id, SlabNode.parse({ ...slab, ...update.data }));
866
- }
867
- for (const slab of plan.create) {
868
- slabsById.set(slab.id, slab);
869
- }
870
- return [...slabsById.values()];
871
- }
872
922
  export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context = {}) {
873
923
  const manualCeilings = existingCeilings.filter((ceiling) => !ceiling.autoFromWalls);
874
924
  const manualSignatures = new Set(manualCeilings.map((ceiling) => polygonSignature(ceiling.polygon.map(pointFromTuple))));
@@ -887,7 +937,6 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
887
937
  centroid: polygonCentroid(room.poly),
888
938
  area: Math.abs(polygonArea(room.poly)),
889
939
  bbox: bboxOf(room.poly),
890
- ceilingHeight: resolveAutoCeilingHeight(room.poly, context),
891
940
  }));
892
941
  const detected = detectedAll.filter(({ sig, poly }) => !manualSignatures.has(sig) && !matchesManualFootprint(poly, manualPolygons));
893
942
  const existingAuto = existingCeilings.filter((ceiling) => ceiling.autoFromWalls);
@@ -916,9 +965,10 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
916
965
  return;
917
966
  matchedDetectedIdx.add(index);
918
967
  matchedCeilingIds.add(existing.ceiling.id);
968
+ const polygon = room.poly.map(pointToTuple);
919
969
  updatesById.set(existing.ceiling.id, {
920
- polygon: room.poly.map(pointToTuple),
921
- height: room.ceilingHeight,
970
+ polygon,
971
+ height: context.heightForRoom?.(polygon),
922
972
  });
923
973
  });
924
974
  const remainingDetected = detected
@@ -948,9 +998,10 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
948
998
  continue;
949
999
  matchedDetectedIdx.add(index);
950
1000
  matchedCeilingIds.add(bestMatch.entry.ceiling.id);
1001
+ const polygon = room.poly.map(pointToTuple);
951
1002
  updatesById.set(bestMatch.entry.ceiling.id, {
952
- polygon: room.poly.map(pointToTuple),
953
- height: room.ceilingHeight,
1003
+ polygon,
1004
+ height: context.heightForRoom?.(polygon),
954
1005
  });
955
1006
  }
956
1007
  const detectedRoomPolygons = detectedAll.map((room) => room.poly);
@@ -967,6 +1018,24 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
967
1018
  ceilingDemotions.push({ id: ceiling.id, data: { autoFromWalls: false } });
968
1019
  }
969
1020
  }
1021
+ // Stage 3-B reactive re-clamp (clamp-never-ask): a covering slab
1022
+ // created, moved, or thickened on the level above can leave an EXISTING
1023
+ // manual explicit-height ceiling poking into its solid. Clamp explicit
1024
+ // heights down to the bound; never raise them — a user-lowered ceiling
1025
+ // is intent, only an over-bound one is a conflict. Follows-mode
1026
+ // ceilings (absent height) derive under the bound by construction and
1027
+ // are skipped, so the clamp can never convert one to an explicit
1028
+ // height.
1029
+ const manualClamps = manualCeilings.flatMap((ceiling) => {
1030
+ if (ceiling.height == null)
1031
+ return [];
1032
+ const bound = resolveCeilingClampBound(ceiling.polygon, context);
1033
+ if (!Number.isFinite(bound))
1034
+ return [];
1035
+ return ceiling.height > bound + CEILING_HEIGHT_EPSILON
1036
+ ? [{ id: ceiling.id, data: { height: bound } }]
1037
+ : [];
1038
+ });
970
1039
  const ceilingsToUpdate = [
971
1040
  ...existingAuto
972
1041
  .filter((ceiling) => updatesById.has(ceiling.id))
@@ -975,16 +1044,17 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
975
1044
  if (!update)
976
1045
  return [];
977
1046
  const data = {};
978
- if (!sameTuplePolygon(ceiling.polygon, update.polygon)) {
1047
+ if (!sameTuplePolygon(ceiling.polygon, update.polygon))
979
1048
  data.polygon = update.polygon;
980
- }
981
- if (Math.abs((ceiling.height ?? DEFAULT_AUTO_CEILING_HEIGHT) - update.height) >
982
- CEILING_HEIGHT_EPSILON) {
1049
+ if (update.height !== undefined &&
1050
+ (ceiling.height === undefined ||
1051
+ Math.abs(ceiling.height - update.height) > ROOM_VERTICAL_PLANE_EPSILON)) {
983
1052
  data.height = update.height;
984
1053
  }
985
- return Object.keys(data).length === 0 ? [] : [{ id: ceiling.id, data }];
1054
+ return Object.keys(data).length > 0 ? [{ id: ceiling.id, data }] : [];
986
1055
  }),
987
1056
  ...ceilingDemotions,
1057
+ ...manualClamps,
988
1058
  ];
989
1059
  const plannedCeilingsForNaming = [...existingCeilings];
990
1060
  const ceilingsToCreate = [];
@@ -996,11 +1066,13 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
996
1066
  continue;
997
1067
  const name = nextAutoRoomName(plannedCeilingsForNaming, 'Ceiling');
998
1068
  plannedCeilingsForNaming.push({ name });
1069
+ const polygon = room.poly.map(pointToTuple);
1070
+ const height = context.heightForRoom?.(polygon);
999
1071
  ceilingsToCreate.push(CeilingNode.parse({
1000
1072
  name,
1001
- polygon: room.poly.map(pointToTuple),
1073
+ polygon,
1002
1074
  holes: [],
1003
- height: room.ceilingHeight,
1075
+ ...(height !== undefined && Number.isFinite(height) ? { height } : {}),
1004
1076
  autoFromWalls: true,
1005
1077
  }));
1006
1078
  }
@@ -1066,10 +1138,24 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
1066
1138
  },
1067
1139
  })));
1068
1140
  }
1141
+ const levelNode = nodes[levelId];
1142
+ const storeyHeight = levelNode?.type === 'level'
1143
+ ? getStoredLevelHeight(levelNode)
1144
+ : DEFAULT_LEVEL_HEIGHT;
1069
1145
  const parsedSlabs = slabs.map((slab) => SlabNode.parse(slab));
1070
- const slabPlan = syncAutoSlabsForLevel(levelId, roomPolygons, parsedSlabs, sceneStore);
1071
- const projectedSlabs = projectAutoSlabsForPlan(parsedSlabs, slabPlan);
1072
- syncAutoCeilingsForLevel(levelId, roomPolygons, ceilings.map((ceiling) => CeilingNode.parse(ceiling)), sceneStore, { walls, slabs: projectedSlabs });
1146
+ const verticalPlacements = autoRoomVerticalPlacements(spaces, walls,
1147
+ // A derived floor cannot be evidence for its own next elevation: that
1148
+ // would lift unpinned walls, then lift the floor again on every pass.
1149
+ parsedSlabs.filter((slab) => !slab.autoFromWalls), nodes, storeyHeight);
1150
+ const placementFor = (polygon) => verticalPlacements.get(polygonSignature(polygon.map(pointFromTuple)));
1151
+ syncAutoSlabsForLevel(levelId, roomPolygons, parsedSlabs, sceneStore, {
1152
+ elevationForRoom: (polygon) => placementFor(polygon)?.slabElevation,
1153
+ });
1154
+ syncAutoCeilingsForLevel(levelId, roomPolygons, ceilings.map((ceiling) => CeilingNode.parse(ceiling)), sceneStore, {
1155
+ storeyHeight,
1156
+ ceilingClampBound: (polygon) => getCeilingClampBound(levelId, nodes, polygon),
1157
+ heightForRoom: (polygon) => placementFor(polygon)?.ceilingHeight,
1158
+ });
1073
1159
  const zonePlan = planAutoZonesForLevel(spaces, zones.map((zone) => ZoneNode.parse(zone)));
1074
1160
  if (zonePlan.update.length > 0)
1075
1161
  updateNodes(zonePlan.update);
@@ -0,0 +1,163 @@
1
+ /**
2
+ * The sculpt brush: pointer motion → `HeightPatch`.
3
+ *
4
+ * The obvious implementation — `h += strength * falloff` every tick — is wrong,
5
+ * and it is wrong in a way that is well documented by the people who ship it.
6
+ * Dwelling in one spot accumulates without bound, overlapping dabs double-apply,
7
+ * and a fast drag deposits less material than a slow one. That is exactly the
8
+ * "hash edges and jagged" result expert Sims builders complain about and route
9
+ * around. This module implements the model the mature tools converge on instead:
10
+ *
11
+ * 1. **Freeze a snapshot at pointer-down.** Every dab is evaluated against the
12
+ * snapshot, never against the running result.
13
+ * 2. **Saturating coverage mask.** A dab raises coverage to `max(mask, falloff)`
14
+ * rather than adding to it, and the height is always
15
+ * `snapshot + delta * mask`. Re-crossing your own stroke therefore cannot
16
+ * compound — the second pass recomputes the same value from the same
17
+ * snapshot.
18
+ * 3. **Arc-length dab spacing.** `advanceStroke` walks from the previous dab to
19
+ * the new pointer position in fixed metre steps, so stroke strength is a
20
+ * function of distance travelled, not of mouse speed or frame rate.
21
+ * 4. **Cosine-bell falloff.** Of the standard sculpt kernels this is one of only
22
+ * two that are C¹ at *both* centre and rim, which is what prevents a visible
23
+ * ridge at the brush boundary.
24
+ *
25
+ * The consequence users feel: one stroke is one bounded, repeatable edit. Hold
26
+ * still and nothing runs away; press again to go further.
27
+ *
28
+ * The `TerrainStroke` returned by `beginStroke` is a mutable accumulator — the
29
+ * one mutable thing in this file, and deliberately so. It is owned by exactly
30
+ * one in-flight gesture and dies with it, which is cheaper and clearer than
31
+ * rebuilding a coverage map per pointer-move.
32
+ */
33
+ import { type HeightPatch, type TerrainField } from './terrain-field';
34
+ export type BrushShape = 'round' | 'square';
35
+ /**
36
+ * `raise`/`lower` are one verb with an inverted sign at the call site, matching
37
+ * every surveyed tool's modifier-inverts convention. `flatten` is absolute:
38
+ * it converges on a target height, which is the operation that makes two
39
+ * disjoint building pads provably equal — the thing freehand push/pull cannot do.
40
+ */
41
+ export type TerrainVerb = 'raise' | 'lower' | 'flatten' | 'smooth';
42
+ export type BrushSettings = {
43
+ /** Brush radius in metres. */
44
+ radius: number;
45
+ /** 0–1. Scales how far one full stroke pass gets toward its goal. */
46
+ strength: number;
47
+ /**
48
+ * 0–1 softness. 0 is a hard-edged stamp; 1 puts the cosine bell across the
49
+ * whole radius. The inner `1 - falloff` fraction stays at full coverage.
50
+ */
51
+ falloff: number;
52
+ shape: BrushShape;
53
+ };
54
+ export declare const DEFAULT_BRUSH_SETTINGS: BrushSettings;
55
+ /**
56
+ * Metres a single raise/lower stroke moves the ground at full strength and full
57
+ * coverage. Bounding it per stroke is what makes the verb aimable: the user
58
+ * knows a pass is worth at most this much, and repeats for more.
59
+ */
60
+ export declare const RAISE_METRES_PER_STROKE = 1;
61
+ /**
62
+ * Smallest brush radius that still bites, as a multiple of the field's spacing.
63
+ *
64
+ * A dab only moves samples that fall *inside* its radius, so a brush narrower
65
+ * than the gap between samples can land entirely between four of them and change
66
+ * nothing: `advanceStroke` returns `null`, and the user drags with no mark, no
67
+ * cursor change, and no error. 1.5 rather than the ~0.71 where a round brush
68
+ * first catches a corner, because a brush that only bites when it happens to
69
+ * straddle a sample is worse than one that is honestly too small — at 1.5 the
70
+ * worst-case coverage is 0.97, so the dab lands wherever it is aimed.
71
+ *
72
+ * A *multiple* of spacing rather than a constant because spacing is not fixed:
73
+ * `fieldExtentForSite` stretches it once a lot exceeds what 257² samples cover
74
+ * at the default, so on a 300 m lot the old flat 0.5 m floor sat below half the
75
+ * sample gap and the brush silently stopped working at the bottom of its range.
76
+ */
77
+ export declare const MIN_BRUSH_RADIUS_IN_SPACINGS = 1.5;
78
+ /**
79
+ * Clamp a radius to what `field` can actually resolve.
80
+ *
81
+ * Lives here rather than in the editor's UI layer because it is a fact about the
82
+ * footprint math above, not a preference: every producer of a radius (slider,
83
+ * `[`/`]`, a future import preset) has to respect it or it produces a brush that
84
+ * does nothing.
85
+ */
86
+ export declare function minBrushRadius(field: Pick<TerrainField, 'spacing'>): number;
87
+ export type TerrainStroke = {
88
+ readonly verb: TerrainVerb;
89
+ readonly settings: BrushSettings;
90
+ /** The field at pointer-down. Never mutated; every dab reads from it. */
91
+ readonly snapshot: TerrainField;
92
+ /** Absolute target height in metres. Only meaningful for `flatten`. */
93
+ readonly target: number;
94
+ /** Sample index → saturating coverage in 0–1. */
95
+ readonly mask: Map<number, number>;
96
+ /** Lazily computed diffused snapshot, for `smooth`. */
97
+ smoothed: Int16Array | null;
98
+ /** Last dab centre, for arc-length spacing. Null until the first dab. */
99
+ lastX: number | null;
100
+ lastZ: number | null;
101
+ };
102
+ export declare function beginStroke(options: {
103
+ field: TerrainField;
104
+ verb: TerrainVerb;
105
+ settings?: Partial<BrushSettings>;
106
+ /** Absolute height for `flatten`, in metres. Ignored by the other verbs. */
107
+ target?: number;
108
+ }): TerrainStroke;
109
+ /**
110
+ * Sample the height a `flatten` stroke should aim at, from the field itself.
111
+ *
112
+ * Sampling before the stroke starts (right-click / first click, per the surveyed
113
+ * tools) is what makes "flatten this pad to match that corner" a two-step
114
+ * gesture instead of a numeric-entry chore.
115
+ */
116
+ export declare function sampleTarget(field: TerrainField, x: number, z: number): number;
117
+ /**
118
+ * Forget where the last dab landed, without ending the stroke.
119
+ *
120
+ * Needed because dab spacing is a true *arc length*: `advanceStroke` interpolates
121
+ * from the previous centre, so any gap in the pointer stream is bridged with a
122
+ * line of dabs when it resumes. That is exactly right for a dropped frame, and
123
+ * exactly wrong when the caller deliberately skipped a stretch — a camera zoom
124
+ * mid-stroke moves the ground under a held brush, and bridging back to a centre
125
+ * from before the zoom paints a stripe across everything in between.
126
+ *
127
+ * A stroke resumed after this deposits its next dab at the pointer, like the
128
+ * first one. The mask is untouched, so the stroke stays saturating across the
129
+ * break and the whole gesture is still one undo step.
130
+ */
131
+ export declare function detachStrokeAnchor(stroke: TerrainStroke): void;
132
+ /**
133
+ * Extend the stroke to the pointer's new position and return the patch to apply.
134
+ *
135
+ * Returns `null` when the motion is shorter than the dab spacing (nothing
136
+ * changed yet) or when the footprint misses the field entirely. The patch is
137
+ * absolute heights over the union of the dabs placed by *this* call, recomputed
138
+ * from the snapshot and the saturated mask — which is why applying patches in
139
+ * order converges rather than compounding.
140
+ */
141
+ export declare function advanceStroke(stroke: TerrainStroke, x: number, z: number): HeightPatch | null;
142
+ /**
143
+ * Coverage in 0–1 for a sample at offset `(dx, dz)` from a dab centre.
144
+ *
145
+ * Round uses Euclidean distance, square uses Chebyshev — the same radius means
146
+ * the square brush's inscribed circle matches the round one, so switching shape
147
+ * mid-session does not change the brush's apparent size.
148
+ */
149
+ export declare function weightAt(settings: BrushSettings, dx: number, dz: number): number;
150
+ /**
151
+ * The world-XZ height under a brush, for the readout the sculpt HUD shows.
152
+ * Thin, but it keeps the tool from importing `heightAt` just to render a number.
153
+ */
154
+ export declare function brushHeightAt(field: TerrainField, x: number, z: number): number;
155
+ /** Peak coverage the stroke reached, for tests and for the HUD's "pass" readout. */
156
+ export declare function maxCoverage(stroke: TerrainStroke): number;
157
+ /**
158
+ * Highest sample height in metres under a world-XZ rect — the seed a `flatten`
159
+ * pad uses when the user asks for "level with the high corner" rather than
160
+ * typing a number.
161
+ */
162
+ export declare function highestOver(field: TerrainField, minX: number, minZ: number, maxX: number, maxZ: number): number;
163
+ //# sourceMappingURL=terrain-brush.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terrain-brush.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-brush.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AAEH,OAAO,EACL,KAAK,WAAW,EAIhB,KAAK,YAAY,EAClB,MAAM,iBAAiB,CAAA;AAExB,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE3C;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,GAAG,QAAQ,CAAA;AAElE,MAAM,MAAM,aAAa,GAAG;IAC1B,8BAA8B;IAC9B,MAAM,EAAE,MAAM,CAAA;IACd,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,UAAU,CAAA;CAClB,CAAA;AAED,eAAO,MAAM,sBAAsB,EAAE,aAKpC,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,IAAI,CAAA;AAExC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,MAAM,CAAA;AAE/C;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,CAAC,GAAG,MAAM,CAE3E;AAaD,MAAM,MAAM,aAAa,GAAG;IAC1B,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAA;IAChC,yEAAyE;IACzE,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAA;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,iDAAiD;IACjD,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAClC,uDAAuD;IACvD,QAAQ,EAAE,UAAU,GAAG,IAAI,CAAA;IAC3B,yEAAyE;IACzE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB,CAAA;AAED,wBAAgB,WAAW,CAAC,OAAO,EAAE;IACnC,KAAK,EAAE,YAAY,CAAA;IACnB,IAAI,EAAE,WAAW,CAAA;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;IACjC,4EAA4E;IAC5E,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB,GAAG,aAAa,CAWhB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE9E;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG,IAAI,CAG9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CA8C7F;AAmID;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAehF;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAIzD;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CACzB,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,MAAM,CAeR"}