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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/README.md +17 -10
  2. package/dist/events/bus.d.ts +21 -2
  3. package/dist/events/bus.d.ts.map +1 -1
  4. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
  5. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  6. package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +116 -66
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  9. package/dist/hooks/spatial-grid/spatial-grid-manager.js +319 -441
  10. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
  11. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  12. package/dist/hooks/spatial-grid/spatial-grid-sync.js +204 -12
  13. package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
  14. package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
  15. package/dist/hooks/spatial-grid/support-host-id.js +2 -0
  16. package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
  17. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
  18. package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
  19. package/dist/index.d.ts +23 -11
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +22 -10
  22. package/dist/lib/measurement-geometry.d.ts +4 -0
  23. package/dist/lib/measurement-geometry.d.ts.map +1 -1
  24. package/dist/lib/measurement-geometry.js +27 -10
  25. package/dist/lib/slab-polygon.d.ts.map +1 -1
  26. package/dist/lib/slab-polygon.js +33 -1
  27. package/dist/lib/space-detection.d.ts +15 -4
  28. package/dist/lib/space-detection.d.ts.map +1 -1
  29. package/dist/lib/space-detection.js +232 -91
  30. package/dist/lib/terrain-brush.d.ts +163 -0
  31. package/dist/lib/terrain-brush.d.ts.map +1 -0
  32. package/dist/lib/terrain-brush.js +359 -0
  33. package/dist/lib/terrain-codec.d.ts +51 -0
  34. package/dist/lib/terrain-codec.d.ts.map +1 -0
  35. package/dist/lib/terrain-codec.js +167 -0
  36. package/dist/lib/terrain-field.d.ts +150 -0
  37. package/dist/lib/terrain-field.d.ts.map +1 -0
  38. package/dist/lib/terrain-field.js +248 -0
  39. package/dist/lib/terrain-raycast.d.ts +44 -0
  40. package/dist/lib/terrain-raycast.d.ts.map +1 -0
  41. package/dist/lib/terrain-raycast.js +188 -0
  42. package/dist/lib/terrain-source.d.ts +60 -0
  43. package/dist/lib/terrain-source.d.ts.map +1 -0
  44. package/dist/lib/terrain-source.js +90 -0
  45. package/dist/lib/terrain-support.d.ts +85 -0
  46. package/dist/lib/terrain-support.d.ts.map +1 -0
  47. package/dist/lib/terrain-support.js +218 -0
  48. package/dist/lib/zone-quantities.d.ts +1 -1
  49. package/dist/lib/zone-quantities.d.ts.map +1 -1
  50. package/dist/lib/zone-quantities.js +311 -66
  51. package/dist/material-library.d.ts +9 -1
  52. package/dist/material-library.d.ts.map +1 -1
  53. package/dist/material-library.js +56 -4
  54. package/dist/registry/handles.d.ts +16 -1
  55. package/dist/registry/handles.d.ts.map +1 -1
  56. package/dist/registry/index.d.ts +1 -1
  57. package/dist/registry/index.d.ts.map +1 -1
  58. package/dist/registry/subtree.d.ts.map +1 -1
  59. package/dist/registry/subtree.js +5 -2
  60. package/dist/registry/types.d.ts +83 -1
  61. package/dist/registry/types.d.ts.map +1 -1
  62. package/dist/schema/index.d.ts +6 -3
  63. package/dist/schema/index.d.ts.map +1 -1
  64. package/dist/schema/index.js +6 -3
  65. package/dist/schema/nodes/cabinet.d.ts +2 -0
  66. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  67. package/dist/schema/nodes/cabinet.js +4 -2
  68. package/dist/schema/nodes/ceiling.d.ts +1 -1
  69. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  70. package/dist/schema/nodes/ceiling.js +7 -1
  71. package/dist/schema/nodes/column.d.ts +1 -0
  72. package/dist/schema/nodes/column.d.ts.map +1 -1
  73. package/dist/schema/nodes/column.js +2 -0
  74. package/dist/schema/nodes/construction-dimension.d.ts +195 -0
  75. package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
  76. package/dist/schema/nodes/construction-dimension.js +151 -0
  77. package/dist/schema/nodes/door.d.ts +29 -0
  78. package/dist/schema/nodes/door.d.ts.map +1 -1
  79. package/dist/schema/nodes/door.js +20 -0
  80. package/dist/schema/nodes/duct-terminal.d.ts +1 -0
  81. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -1
  82. package/dist/schema/nodes/duct-terminal.js +2 -0
  83. package/dist/schema/nodes/fence.d.ts +2 -0
  84. package/dist/schema/nodes/fence.d.ts.map +1 -1
  85. package/dist/schema/nodes/fence.js +8 -0
  86. package/dist/schema/nodes/hvac-equipment.d.ts +1 -0
  87. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -1
  88. package/dist/schema/nodes/hvac-equipment.js +2 -0
  89. package/dist/schema/nodes/item.d.ts +1 -0
  90. package/dist/schema/nodes/item.d.ts.map +1 -1
  91. package/dist/schema/nodes/item.js +13 -0
  92. package/dist/schema/nodes/level.d.ts +2 -1
  93. package/dist/schema/nodes/level.d.ts.map +1 -1
  94. package/dist/schema/nodes/level.js +12 -33
  95. package/dist/schema/nodes/shelf.d.ts +1 -0
  96. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  97. package/dist/schema/nodes/shelf.js +2 -0
  98. package/dist/schema/nodes/site.d.ts +9 -0
  99. package/dist/schema/nodes/site.d.ts.map +1 -1
  100. package/dist/schema/nodes/site.js +8 -6
  101. package/dist/schema/nodes/slab.d.ts +5 -0
  102. package/dist/schema/nodes/slab.d.ts.map +1 -1
  103. package/dist/schema/nodes/slab.js +14 -2
  104. package/dist/schema/nodes/spawn.d.ts +1 -0
  105. package/dist/schema/nodes/spawn.d.ts.map +1 -1
  106. package/dist/schema/nodes/spawn.js +2 -0
  107. package/dist/schema/nodes/stair.d.ts +3 -1
  108. package/dist/schema/nodes/stair.d.ts.map +1 -1
  109. package/dist/schema/nodes/stair.js +8 -1
  110. package/dist/schema/nodes/structural-grid.d.ts +27 -0
  111. package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
  112. package/dist/schema/nodes/structural-grid.js +17 -0
  113. package/dist/schema/nodes/wall.d.ts +5 -2
  114. package/dist/schema/nodes/wall.d.ts.map +1 -1
  115. package/dist/schema/nodes/wall.js +13 -4
  116. package/dist/schema/nodes/window.d.ts +29 -0
  117. package/dist/schema/nodes/window.d.ts.map +1 -1
  118. package/dist/schema/nodes/window.js +18 -0
  119. package/dist/schema/nodes/zone.d.ts +20 -0
  120. package/dist/schema/nodes/zone.d.ts.map +1 -1
  121. package/dist/schema/nodes/zone.js +15 -0
  122. package/dist/schema/terrain.d.ts +28 -0
  123. package/dist/schema/terrain.d.ts.map +1 -0
  124. package/dist/schema/terrain.js +30 -0
  125. package/dist/schema/types.d.ts +211 -3
  126. package/dist/schema/types.d.ts.map +1 -1
  127. package/dist/schema/types.js +4 -0
  128. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
  129. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
  130. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
  131. package/dist/services/index.d.ts +2 -1
  132. package/dist/services/index.d.ts.map +1 -1
  133. package/dist/services/index.js +2 -1
  134. package/dist/services/level-height.d.ts +8 -9
  135. package/dist/services/level-height.d.ts.map +1 -1
  136. package/dist/services/level-height.js +48 -16
  137. package/dist/services/storey.d.ts +103 -0
  138. package/dist/services/storey.d.ts.map +1 -0
  139. package/dist/services/storey.js +296 -0
  140. package/dist/store/actions/node-actions.d.ts.map +1 -1
  141. package/dist/store/actions/node-actions.js +47 -3
  142. package/dist/store/history-control.d.ts +20 -0
  143. package/dist/store/history-control.d.ts.map +1 -1
  144. package/dist/store/history-control.js +110 -13
  145. package/dist/store/use-live-terrain.d.ts +65 -0
  146. package/dist/store/use-live-terrain.d.ts.map +1 -0
  147. package/dist/store/use-live-terrain.js +31 -0
  148. package/dist/store/use-live-transforms.d.ts +8 -0
  149. package/dist/store/use-live-transforms.d.ts.map +1 -1
  150. package/dist/store/use-scene.d.ts +31 -2
  151. package/dist/store/use-scene.d.ts.map +1 -1
  152. package/dist/store/use-scene.js +638 -8
  153. package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
  154. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
  155. package/dist/systems/elevator/elevator-opening-system.js +27 -21
  156. package/dist/systems/elevator/elevator-service.d.ts +0 -2
  157. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  158. package/dist/systems/elevator/elevator-service.js +2 -24
  159. package/dist/systems/slab/slab-placement.d.ts +11 -0
  160. package/dist/systems/slab/slab-placement.d.ts.map +1 -0
  161. package/dist/systems/slab/slab-placement.js +14 -0
  162. package/dist/systems/slab/slab-support.d.ts +150 -0
  163. package/dist/systems/slab/slab-support.d.ts.map +1 -0
  164. package/dist/systems/slab/slab-support.js +575 -0
  165. package/dist/systems/stair/stair-opening-preview.d.ts +1060 -935
  166. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  167. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  168. package/dist/systems/stair/stair-opening-sync.js +18 -15
  169. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  170. package/dist/systems/stair/stair-opening-system.js +32 -4
  171. package/dist/systems/stair/stair-rise.d.ts +22 -0
  172. package/dist/systems/stair/stair-rise.d.ts.map +1 -0
  173. package/dist/systems/stair/stair-rise.js +79 -0
  174. package/dist/systems/wall/wall-curve.d.ts +10 -0
  175. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  176. package/dist/systems/wall/wall-curve.js +1 -1
  177. package/dist/systems/wall/wall-mitering.js +6 -6
  178. package/dist/systems/wall/wall-top.d.ts +40 -0
  179. package/dist/systems/wall/wall-top.d.ts.map +1 -0
  180. package/dist/systems/wall/wall-top.js +46 -0
  181. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  182. package/dist/utils/clone-scene-graph.js +34 -3
  183. package/dist/utils/heal-scene-graph.d.ts +1 -1
  184. package/dist/utils/heal-scene-graph.d.ts.map +1 -1
  185. package/dist/utils/heal-scene-graph.js +25 -9
  186. package/dist/validation/validate-build-json.d.ts +2 -0
  187. package/dist/validation/validate-build-json.d.ts.map +1 -1
  188. package/dist/validation/validate-build-json.js +59 -3
  189. package/package.json +2 -2
  190. package/dist/hooks/scene-registry/scene-registry.test.d.ts +0 -2
  191. package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +0 -1
  192. package/dist/hooks/scene-registry/scene-registry.test.js +0 -26
  193. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
  194. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
  195. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -366
  196. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +0 -2
  197. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +0 -1
  198. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +0 -363
  199. package/dist/lib/measurement-geometry.test.d.ts +0 -2
  200. package/dist/lib/measurement-geometry.test.d.ts.map +0 -1
  201. package/dist/lib/measurement-geometry.test.js +0 -102
  202. package/dist/lib/slab-polygon.test.d.ts +0 -2
  203. package/dist/lib/slab-polygon.test.d.ts.map +0 -1
  204. package/dist/lib/slab-polygon.test.js +0 -635
  205. package/dist/lib/space-detection-pause.test.d.ts +0 -2
  206. package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
  207. package/dist/lib/space-detection-pause.test.js +0 -42
  208. package/dist/lib/space-detection.test.d.ts +0 -2
  209. package/dist/lib/space-detection.test.d.ts.map +0 -1
  210. package/dist/lib/space-detection.test.js +0 -376
  211. package/dist/lib/zone-quantities.test.d.ts +0 -2
  212. package/dist/lib/zone-quantities.test.d.ts.map +0 -1
  213. package/dist/lib/zone-quantities.test.js +0 -82
  214. package/dist/registry/registry.test.d.ts +0 -2
  215. package/dist/registry/registry.test.d.ts.map +0 -1
  216. package/dist/registry/registry.test.js +0 -196
  217. package/dist/registry/relations-resolver.test.d.ts +0 -2
  218. package/dist/registry/relations-resolver.test.d.ts.map +0 -1
  219. package/dist/registry/relations-resolver.test.js +0 -183
  220. package/dist/registry/scene-api.test.d.ts +0 -2
  221. package/dist/registry/scene-api.test.d.ts.map +0 -1
  222. package/dist/registry/scene-api.test.js +0 -183
  223. package/dist/registry/subtree.test.d.ts +0 -2
  224. package/dist/registry/subtree.test.d.ts.map +0 -1
  225. package/dist/registry/subtree.test.js +0 -163
  226. package/dist/schema/asset-url.test.d.ts +0 -2
  227. package/dist/schema/asset-url.test.d.ts.map +0 -1
  228. package/dist/schema/asset-url.test.js +0 -134
  229. package/dist/schema/material.test.d.ts +0 -2
  230. package/dist/schema/material.test.d.ts.map +0 -1
  231. package/dist/schema/material.test.js +0 -43
  232. package/dist/schema/nodes/measurement.test.d.ts +0 -2
  233. package/dist/schema/nodes/measurement.test.d.ts.map +0 -1
  234. package/dist/schema/nodes/measurement.test.js +0 -139
  235. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +0 -2
  236. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +0 -1
  237. package/dist/schema/nodes/ridge-vent-defaults.test.js +0 -205
  238. package/dist/schema/nodes/roof-segment-shape.test.d.ts +0 -2
  239. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +0 -1
  240. package/dist/schema/nodes/roof-segment-shape.test.js +0 -175
  241. package/dist/schema/nodes/roof-segment-surface.test.d.ts +0 -2
  242. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +0 -1
  243. package/dist/schema/nodes/roof-segment-surface.test.js +0 -48
  244. package/dist/schema/nodes/roof-segment-trim.test.d.ts +0 -2
  245. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +0 -1
  246. package/dist/schema/nodes/roof-segment-trim.test.js +0 -107
  247. package/dist/schema/nodes/roof-segment-walls.test.d.ts +0 -2
  248. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +0 -1
  249. package/dist/schema/nodes/roof-segment-walls.test.js +0 -92
  250. package/dist/schema/nodes/wall.test.d.ts +0 -2
  251. package/dist/schema/nodes/wall.test.d.ts.map +0 -1
  252. package/dist/schema/nodes/wall.test.js +0 -208
  253. package/dist/services/alignment-anchors.test.d.ts +0 -2
  254. package/dist/services/alignment-anchors.test.d.ts.map +0 -1
  255. package/dist/services/alignment-anchors.test.js +0 -383
  256. package/dist/services/alignment.test.d.ts +0 -2
  257. package/dist/services/alignment.test.d.ts.map +0 -1
  258. package/dist/services/alignment.test.js +0 -92
  259. package/dist/services/drag-session.test.d.ts +0 -2
  260. package/dist/services/drag-session.test.d.ts.map +0 -1
  261. package/dist/services/drag-session.test.js +0 -219
  262. package/dist/services/hosting.test.d.ts +0 -2
  263. package/dist/services/hosting.test.d.ts.map +0 -1
  264. package/dist/services/hosting.test.js +0 -230
  265. package/dist/services/movement.test.d.ts +0 -2
  266. package/dist/services/movement.test.d.ts.map +0 -1
  267. package/dist/services/movement.test.js +0 -133
  268. package/dist/services/opening-guides.test.d.ts +0 -2
  269. package/dist/services/opening-guides.test.d.ts.map +0 -1
  270. package/dist/services/opening-guides.test.js +0 -208
  271. package/dist/services/port-connectivity.test.d.ts +0 -2
  272. package/dist/services/port-connectivity.test.d.ts.map +0 -1
  273. package/dist/services/port-connectivity.test.js +0 -297
  274. package/dist/services/riser-diagram.test.d.ts +0 -2
  275. package/dist/services/riser-diagram.test.d.ts.map +0 -1
  276. package/dist/services/riser-diagram.test.js +0 -66
  277. package/dist/services/single-undo-dance.test.d.ts +0 -2
  278. package/dist/services/single-undo-dance.test.d.ts.map +0 -1
  279. package/dist/services/single-undo-dance.test.js +0 -210
  280. package/dist/services/snap.test.d.ts +0 -2
  281. package/dist/services/snap.test.d.ts.map +0 -1
  282. package/dist/services/snap.test.js +0 -137
  283. package/dist/services/system-graph.test.d.ts +0 -2
  284. package/dist/services/system-graph.test.d.ts.map +0 -1
  285. package/dist/services/system-graph.test.js +0 -138
  286. package/dist/services/validate-dwv.test.d.ts +0 -2
  287. package/dist/services/validate-dwv.test.d.ts.map +0 -1
  288. package/dist/services/validate-dwv.test.js +0 -109
  289. package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
  290. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
  291. package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
  292. package/dist/store/actions/reparent.test.d.ts +0 -2
  293. package/dist/store/actions/reparent.test.d.ts.map +0 -1
  294. package/dist/store/actions/reparent.test.js +0 -212
  295. package/dist/store/actions/ridge-vent-update.test.d.ts +0 -2
  296. package/dist/store/actions/ridge-vent-update.test.d.ts.map +0 -1
  297. package/dist/store/actions/ridge-vent-update.test.js +0 -264
  298. package/dist/store/use-scene-dirty-tracking.test.d.ts +0 -2
  299. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +0 -1
  300. package/dist/store/use-scene-dirty-tracking.test.js +0 -68
  301. package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
  302. package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
  303. package/dist/store/use-scene-elevator-migration.test.js +0 -109
  304. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +0 -2
  305. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +0 -1
  306. package/dist/store/use-scene-measurement-roundtrip.test.js +0 -97
  307. package/dist/store/use-scene-plugins.test.d.ts +0 -2
  308. package/dist/store/use-scene-plugins.test.d.ts.map +0 -1
  309. package/dist/store/use-scene-plugins.test.js +0 -51
  310. package/dist/store/use-scene-wall-slot-migration.test.d.ts +0 -2
  311. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +0 -1
  312. package/dist/store/use-scene-wall-slot-migration.test.js +0 -210
  313. package/dist/store/use-scene-window-migration.test.d.ts +0 -2
  314. package/dist/store/use-scene-window-migration.test.d.ts.map +0 -1
  315. package/dist/store/use-scene-window-migration.test.js +0 -85
  316. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
  317. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
  318. package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
  319. package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
  320. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
  321. package/dist/systems/elevator/elevator-runtime.test.js +0 -64
  322. package/dist/systems/fence/fence-spline.test.d.ts +0 -2
  323. package/dist/systems/fence/fence-spline.test.d.ts.map +0 -1
  324. package/dist/systems/fence/fence-spline.test.js +0 -136
  325. package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
  326. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
  327. package/dist/systems/stair/stair-opening-preview.test.js +0 -80
  328. package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
  329. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
  330. package/dist/systems/stair/stair-opening-sync.test.js +0 -485
  331. package/dist/systems/wall/wall-mitering.test.d.ts +0 -2
  332. package/dist/systems/wall/wall-mitering.test.d.ts.map +0 -1
  333. package/dist/systems/wall/wall-mitering.test.js +0 -63
  334. package/dist/utils/clone-scene-graph.test.d.ts +0 -2
  335. package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
  336. package/dist/utils/clone-scene-graph.test.js +0 -62
  337. package/dist/utils/heal-scene-graph.test.d.ts +0 -2
  338. package/dist/utils/heal-scene-graph.test.d.ts.map +0 -1
  339. package/dist/utils/heal-scene-graph.test.js +0 -88
@@ -1,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 { levelBaseElevationAt } 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,9 @@ 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
+ // Rewrite deadband for an existing auto surface's elevation/height: below this
28
+ // the derived plane is the same plane and writing it would churn history.
29
+ const ROOM_VERTICAL_PLANE_EPSILON = 1e-3;
22
30
  function pointFromTuple(point) {
23
31
  return { x: point[0], y: point[1] };
24
32
  }
@@ -194,51 +202,84 @@ function wallBoundsRoom(wall, roomPolygon) {
194
202
  const matchingPoints = candidates.filter((point) => pointDistanceToPolygonBoundary(point, roomPolygon) <= WALL_ROOM_BOUNDARY_TOLERANCE);
195
203
  return matchingPoints.length >= 2;
196
204
  }
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;
205
+ /**
206
+ * The clamp bound for a ceiling polygon under this planning context —
207
+ * the context's cross-level resolver when provided, else the plane-only
208
+ * `storeyHeight - CEILING_CLAMP_MARGIN` degradation.
209
+ */
210
+ function resolveCeilingClampBound(polygon, context) {
211
+ if (context.ceilingClampBound)
212
+ return context.ceilingClampBound(polygon);
213
+ return (context.storeyHeight ?? DEFAULT_LEVEL_HEIGHT) - CEILING_CLAMP_MARGIN;
209
214
  }
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);
215
+ /**
216
+ * The base a boundary wall actually stands on, in level-local metres.
217
+ *
218
+ * Resolved the same way the wall renderer resolves it — the level base under
219
+ * the wall's own start point (sculpted ground or the flat plane), the slab
220
+ * election on top of that, plus the wall's stored `supportOffset`. Reading the
221
+ * ground for `GROUND_SUPPORT_ID` walls only is what left stamped room presets
222
+ * flat: `resolveWallSupportSlabPatch` writes no host at all for a wall on bare
223
+ * terrain, so the sentinel is a hint about pointer intent, never a precondition
224
+ * for standing on the ground.
225
+ */
226
+ function boundaryWallBase(wall, walls, supportSlabs, nodes, levelId) {
227
+ const levelBase = levelBaseElevationAt(nodes, levelId, wall.start[0], wall.start[1]);
228
+ const offset = wall.supportOffset ?? 0;
229
+ if (wall.supportSlabId === GROUND_SUPPORT_ID)
230
+ return levelBase + offset;
231
+ return (computeWallSlabSupport(wall, supportSlabs, walls, wall.supportSlabId ?? null, null, levelBase)
232
+ .elevation + offset);
217
233
  }
218
- function resolveRoomSlabElevation(roomPolygon, slabs = []) {
219
- let maxElevation = 0;
220
- for (const slab of slabs) {
221
- if (!slabSupportsRoom(roomPolygon, slab))
222
- continue;
223
- maxElevation = Math.max(maxElevation, slab.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION);
224
- }
225
- return maxElevation;
234
+ /**
235
+ * The plane an auto floor/ceiling takes when its enclosing walls disagree.
236
+ *
237
+ * `floor` takes the HIGHEST wall base and `ceiling` the LOWEST wall top —
238
+ * the only pair that cannot open a hole: a floor at the lowest base would
239
+ * leave daylight under every wall standing higher, and a ceiling at the
240
+ * highest top would poke out through the shortest wall. Both surfaces stay
241
+ * flat (a slab is one scalar elevation by schema; see `vertical-model.md`),
242
+ * so a room on a slope is a level room cut into the hillside — the walls on
243
+ * the low side extend down to meet it, which is what their `baseSegments`
244
+ * fill-down already does.
245
+ *
246
+ * Non-finite inputs are the one abstain: a broken graph should keep the
247
+ * existing placement rather than move a surface to NaN.
248
+ */
249
+ function roomFloorPlane(wallBases) {
250
+ if (wallBases.length === 0 || wallBases.some((value) => !Number.isFinite(value)))
251
+ return undefined;
252
+ return Math.max(...wallBases);
226
253
  }
227
- function resolveRoomWallHeight(roomPolygon, walls = []) {
228
- let maxHeight = 0;
229
- for (const wall of walls) {
230
- if (!wallBoundsRoom(wall, roomPolygon))
254
+ function roomCeilingPlane(wallTops) {
255
+ if (wallTops.length === 0 || wallTops.some((value) => !Number.isFinite(value)))
256
+ return undefined;
257
+ return Math.min(...wallTops);
258
+ }
259
+ function autoRoomVerticalPlacements(spaces, walls, supportSlabs, nodes, storeyHeight) {
260
+ const wallsById = new Map(walls.map((wall) => [wall.id, wall]));
261
+ const placements = new Map();
262
+ for (const space of spaces) {
263
+ const boundaryWalls = space.wallIds.flatMap((id) => {
264
+ const wall = wallsById.get(id);
265
+ return wall ? [wall] : [];
266
+ });
267
+ if (boundaryWalls.length !== space.wallIds.length)
231
268
  continue;
232
- const height = wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT;
233
- if (Number.isFinite(height)) {
234
- maxHeight = Math.max(maxHeight, height);
235
- }
269
+ const wallBases = boundaryWalls.map((wall) => boundaryWallBase(wall, walls, supportSlabs, nodes, space.levelId));
270
+ const base = roomFloorPlane(wallBases);
271
+ if (base === undefined)
272
+ continue;
273
+ const wallTops = boundaryWalls.map((wall, index) => resolveWallTop(wall, storeyHeight, wallBases[index] ?? base));
274
+ const top = roomCeilingPlane(wallTops);
275
+ if (top === undefined)
276
+ continue;
277
+ placements.set(polygonSignature(space.polygon.map(pointFromTuple)), {
278
+ slabElevation: base + DEFAULT_AUTO_SLAB_ELEVATION,
279
+ ceilingHeight: top - CEILING_CLAMP_MARGIN,
280
+ });
236
281
  }
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));
282
+ return placements;
242
283
  }
243
284
  function getWallDirection(wall) {
244
285
  const dx = wall.end[0] - wall.start[0];
@@ -604,7 +645,7 @@ function sameTuplePolygon(current, next) {
604
645
  return (current.length === next.length &&
605
646
  current.every((point, index) => point[0] === next[index]?.[0] && point[1] === next[index]?.[1]));
606
647
  }
607
- function wallGeometrySignature(wall) {
648
+ function wallGeometrySignature(wall, nodes, levelId) {
608
649
  return [
609
650
  wall.id,
610
651
  wall.start[0].toFixed(4),
@@ -612,12 +653,42 @@ function wallGeometrySignature(wall) {
612
653
  wall.end[0].toFixed(4),
613
654
  wall.end[1].toFixed(4),
614
655
  (wall.thickness ?? 0.2).toFixed(4),
615
- (wall.height ?? DEFAULT_AUTO_CEILING_HEIGHT).toFixed(4),
656
+ // Plane-bound (no stored height) is a distinct state, not a default
657
+ // value: it resolves to the storey plane, so it must not alias an
658
+ // explicit height of the same magnitude in the trigger signature.
659
+ wall.height == null ? 'plane' : wall.height.toFixed(4),
660
+ wall.supportSlabId ?? 'elected',
661
+ (wall.supportOffset ?? 0).toFixed(4),
616
662
  getClampedWallCurveOffset(wall).toFixed(4),
663
+ // The ground under this wall, sampled at the SAME point
664
+ // `boundaryWallBase` samples it. Sculpting changes only `site.terrain`,
665
+ // so without a terrain term here every signature stays byte-identical
666
+ // and the sync early-exits — a room's floor and ceiling could never
667
+ // follow ground that moved beneath its walls.
668
+ //
669
+ // The sample, not the field, and not the resolved base: hashing the
670
+ // heightfield would re-trigger every level for a stroke on the far side
671
+ // of the lot, and resolving the full slab election would fold slab
672
+ // POLYGONS into the signature, which is exactly the delete/recreate
673
+ // feedback the comment below is about. Sampling where the placement
674
+ // samples means the two cannot disagree in either direction — no missed
675
+ // re-run, no spurious one.
676
+ //
677
+ // Granularity is per stroke, not per dab: live dabs publish to
678
+ // `useLiveTerrain` and never touch the scene store, so this runs once on
679
+ // release — inside the stroke's own `runAsSingleSceneHistoryStep`, which
680
+ // is what puts the moved floor and the terrain that moved it in the same
681
+ // undo step. Mid-drag the ground-hosted walls follow the brush while the
682
+ // floor waits for release; re-deriving per dab would mean a scene write
683
+ // per dab and a floor that jitters under the cursor.
684
+ levelBaseElevationAt(nodes, levelId, wall.start[0], wall.start[1]).toFixed(4),
617
685
  ].join('|');
618
686
  }
619
- function levelWallSnapshot(walls) {
620
- return walls.map(wallGeometrySignature).sort().join('||');
687
+ function levelWallSnapshot(walls, nodes, levelId) {
688
+ return walls
689
+ .map((wall) => wallGeometrySignature(wall, nodes, levelId))
690
+ .sort()
691
+ .join('||');
621
692
  }
622
693
  function zoneGeometrySignature(zone) {
623
694
  return [
@@ -627,13 +698,24 @@ function zoneGeometrySignature(zone) {
627
698
  zone.polygon.map(([x, z]) => `${x.toFixed(4)},${z.toFixed(4)}`).join(';'),
628
699
  ].join('|');
629
700
  }
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.
701
+ // Slab/ceiling POLYGONS stay out of the trigger signature: including
702
+ // generated footprints caused delete/recreate feedback. Zones are included
703
+ // only so a newly traced room footprint can adopt its enclosing walls
704
+ // without waiting for the next remodel. Slab ELEVATIONS and the level's
705
+ // stored storey height ARE included — both feed the explicit-ceiling
706
+ // re-clamp bound (the storey plane), and neither is rewritten by
707
+ // the sync, so regeneration triggers when they change without feedback.
708
+ // Stage 3-B adds the LEVEL-ABOVE's covering-slab undersides (elevation −
709
+ // thickness, recessed pools excluded): a deck created, lowered, or
710
+ // thickened above must re-run the sync below so ceilings re-clamp under
711
+ // it. Same polygon exclusion applies — the level-above's own auto sync
712
+ // rewrites its slab footprints, and hashing them here would re-trigger
713
+ // this level on every remodel above.
634
714
  function levelStructureSnapshots(nodes) {
635
715
  const wallsByLevel = new Map();
636
716
  const zonesByLevel = new Map();
717
+ const slabElevationsByLevel = new Map();
718
+ const coveringUndersidesByLevel = new Map();
637
719
  for (const node of Object.values(nodes)) {
638
720
  if (!(node && typeof node === 'object' && 'parentId' in node && node.parentId))
639
721
  continue;
@@ -648,13 +730,33 @@ function levelStructureSnapshots(nodes) {
648
730
  zones.push(ZoneNode.parse(node));
649
731
  zonesByLevel.set(levelId, zones);
650
732
  }
733
+ else if (node.type === 'slab') {
734
+ const elevations = slabElevationsByLevel.get(levelId) ?? [];
735
+ elevations.push(`${node.id}:${(node.elevation ?? DEFAULT_AUTO_SLAB_ELEVATION).toFixed(4)}`);
736
+ slabElevationsByLevel.set(levelId, elevations);
737
+ if (node.recessed !== true) {
738
+ const undersides = coveringUndersidesByLevel.get(levelId) ?? [];
739
+ const elevation = node.elevation ?? 0.05;
740
+ const thickness = node.thickness ?? 0.05;
741
+ undersides.push(`${node.id}:${(elevation - thickness).toFixed(4)}`);
742
+ coveringUndersidesByLevel.set(levelId, undersides);
743
+ }
744
+ }
651
745
  }
746
+ const levelElevations = getLevelElevations(nodes);
652
747
  const snapshots = new Map();
653
748
  const levelIds = new Set([...wallsByLevel.keys(), ...zonesByLevel.keys()]);
654
749
  for (const levelId of levelIds) {
655
750
  const walls = wallsByLevel.get(levelId) ?? [];
656
751
  const zones = zonesByLevel.get(levelId) ?? [];
657
- snapshots.set(levelId, `${levelWallSnapshot(walls)}##${zones.map(zoneGeometrySignature).sort().join('||')}`);
752
+ const level = nodes[levelId];
753
+ const storeyKey = level?.type === 'level' && typeof level.height === 'number' ? level.height.toFixed(4) : '';
754
+ const slabKey = (slabElevationsByLevel.get(levelId) ?? []).sort().join(';');
755
+ const aboveId = findLevelAboveId(levelId, levelElevations);
756
+ const aboveSlabKey = aboveId
757
+ ? (coveringUndersidesByLevel.get(aboveId) ?? []).sort().join(';')
758
+ : '';
759
+ snapshots.set(levelId, `${storeyKey}#${levelWallSnapshot(walls, nodes, levelId)}##${zones.map(zoneGeometrySignature).sort().join('||')}##${slabKey}##${aboveSlabKey}`);
658
760
  }
659
761
  return snapshots;
660
762
  }
@@ -712,7 +814,7 @@ export function resolveAutoZonePolygon(zone, resolve) {
712
814
  const room = extractRooms(walls).find((candidate) => sameStringSet([...new Set(candidate.boundaryFaces.map((boundary) => boundary.wallId))], zone.boundaryWallIds));
713
815
  return room ? room.polygon.map(pointToTuple) : zone.polygon;
714
816
  }
715
- export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
817
+ export function planAutoSlabsForLevel(roomPolygons, existingSlabs, context = {}) {
716
818
  const manualSlabs = existingSlabs.filter((slab) => !slab.autoFromWalls);
717
819
  const manualSignatures = new Set(manualSlabs.map((slab) => polygonSignature(slab.polygon.map(pointFromTuple))));
718
820
  const manualPolygons = manualSlabs.map((slab) => slab.polygon.map(pointFromTuple));
@@ -758,7 +860,11 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
758
860
  return;
759
861
  matchedDetectedIdx.add(index);
760
862
  matchedSlabIds.add(existing.slab.id);
761
- updatesById.set(existing.slab.id, room.poly.map(pointToTuple));
863
+ const polygon = room.poly.map(pointToTuple);
864
+ updatesById.set(existing.slab.id, {
865
+ polygon,
866
+ elevation: context.elevationForRoom?.(polygon),
867
+ });
762
868
  });
763
869
  const remainingDetected = detected
764
870
  .map((room, index) => ({ room, index }))
@@ -787,7 +893,11 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
787
893
  continue;
788
894
  matchedDetectedIdx.add(index);
789
895
  matchedSlabIds.add(bestMatch.entry.slab.id);
790
- updatesById.set(bestMatch.entry.slab.id, room.poly.map(pointToTuple));
896
+ const polygon = room.poly.map(pointToTuple);
897
+ updatesById.set(bestMatch.entry.slab.id, {
898
+ polygon,
899
+ elevation: context.elevationForRoom?.(polygon),
900
+ });
791
901
  }
792
902
  const detectedRoomPolygons = detectedAll.map((room) => room.poly);
793
903
  const slabsToDelete = [];
@@ -809,10 +919,17 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
809
919
  ...existingAuto
810
920
  .filter((slab) => updatesById.has(slab.id))
811
921
  .flatMap((slab) => {
812
- const polygon = updatesById.get(slab.id);
813
- if (!polygon)
922
+ const update = updatesById.get(slab.id);
923
+ if (!update)
814
924
  return [];
815
- return sameTuplePolygon(slab.polygon, polygon) ? [] : [{ id: slab.id, data: { polygon } }];
925
+ const data = {};
926
+ if (!sameTuplePolygon(slab.polygon, update.polygon))
927
+ data.polygon = update.polygon;
928
+ if (update.elevation !== undefined &&
929
+ Math.abs(slab.elevation - update.elevation) > ROOM_VERTICAL_PLANE_EPSILON) {
930
+ data.elevation = update.elevation;
931
+ }
932
+ return Object.keys(data).length > 0 ? [{ id: slab.id, data }] : [];
816
933
  }),
817
934
  ...slabDemotions,
818
935
  ];
@@ -826,11 +943,15 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
826
943
  continue;
827
944
  const name = nextAutoRoomName(plannedSlabsForNaming, 'Slab');
828
945
  plannedSlabsForNaming.push({ name });
946
+ const polygon = room.poly.map(pointToTuple);
947
+ const elevation = context.elevationForRoom?.(polygon);
829
948
  slabsToCreate.push(SlabNode.parse({
830
949
  name,
831
- polygon: room.poly.map(pointToTuple),
950
+ polygon,
832
951
  holes: [],
833
- elevation: DEFAULT_AUTO_SLAB_ELEVATION,
952
+ elevation: elevation !== undefined && Number.isFinite(elevation)
953
+ ? elevation
954
+ : DEFAULT_AUTO_SLAB_ELEVATION,
834
955
  autoFromWalls: true,
835
956
  }));
836
957
  }
@@ -840,8 +961,8 @@ export function planAutoSlabsForLevel(roomPolygons, existingSlabs) {
840
961
  delete: slabsToDelete,
841
962
  };
842
963
  }
843
- function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore) {
844
- const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs);
964
+ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore, context = {}) {
965
+ const plan = planAutoSlabsForLevel(roomPolygons, existingSlabs, context);
845
966
  if (plan.delete.length > 0) {
846
967
  sceneStore.getState().deleteNodes(plan.delete);
847
968
  }
@@ -853,22 +974,6 @@ function syncAutoSlabsForLevel(levelId, roomPolygons, existingSlabs, sceneStore)
853
974
  }
854
975
  return plan;
855
976
  }
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
977
  export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context = {}) {
873
978
  const manualCeilings = existingCeilings.filter((ceiling) => !ceiling.autoFromWalls);
874
979
  const manualSignatures = new Set(manualCeilings.map((ceiling) => polygonSignature(ceiling.polygon.map(pointFromTuple))));
@@ -887,7 +992,6 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
887
992
  centroid: polygonCentroid(room.poly),
888
993
  area: Math.abs(polygonArea(room.poly)),
889
994
  bbox: bboxOf(room.poly),
890
- ceilingHeight: resolveAutoCeilingHeight(room.poly, context),
891
995
  }));
892
996
  const detected = detectedAll.filter(({ sig, poly }) => !manualSignatures.has(sig) && !matchesManualFootprint(poly, manualPolygons));
893
997
  const existingAuto = existingCeilings.filter((ceiling) => ceiling.autoFromWalls);
@@ -916,9 +1020,10 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
916
1020
  return;
917
1021
  matchedDetectedIdx.add(index);
918
1022
  matchedCeilingIds.add(existing.ceiling.id);
1023
+ const polygon = room.poly.map(pointToTuple);
919
1024
  updatesById.set(existing.ceiling.id, {
920
- polygon: room.poly.map(pointToTuple),
921
- height: room.ceilingHeight,
1025
+ polygon,
1026
+ height: context.heightForRoom?.(polygon),
922
1027
  });
923
1028
  });
924
1029
  const remainingDetected = detected
@@ -948,9 +1053,10 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
948
1053
  continue;
949
1054
  matchedDetectedIdx.add(index);
950
1055
  matchedCeilingIds.add(bestMatch.entry.ceiling.id);
1056
+ const polygon = room.poly.map(pointToTuple);
951
1057
  updatesById.set(bestMatch.entry.ceiling.id, {
952
- polygon: room.poly.map(pointToTuple),
953
- height: room.ceilingHeight,
1058
+ polygon,
1059
+ height: context.heightForRoom?.(polygon),
954
1060
  });
955
1061
  }
956
1062
  const detectedRoomPolygons = detectedAll.map((room) => room.poly);
@@ -967,6 +1073,24 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
967
1073
  ceilingDemotions.push({ id: ceiling.id, data: { autoFromWalls: false } });
968
1074
  }
969
1075
  }
1076
+ // Stage 3-B reactive re-clamp (clamp-never-ask): a covering slab
1077
+ // created, moved, or thickened on the level above can leave an EXISTING
1078
+ // manual explicit-height ceiling poking into its solid. Clamp explicit
1079
+ // heights down to the bound; never raise them — a user-lowered ceiling
1080
+ // is intent, only an over-bound one is a conflict. Follows-mode
1081
+ // ceilings (absent height) derive under the bound by construction and
1082
+ // are skipped, so the clamp can never convert one to an explicit
1083
+ // height.
1084
+ const manualClamps = manualCeilings.flatMap((ceiling) => {
1085
+ if (ceiling.height == null)
1086
+ return [];
1087
+ const bound = resolveCeilingClampBound(ceiling.polygon, context);
1088
+ if (!Number.isFinite(bound))
1089
+ return [];
1090
+ return ceiling.height > bound + CEILING_HEIGHT_EPSILON
1091
+ ? [{ id: ceiling.id, data: { height: bound } }]
1092
+ : [];
1093
+ });
970
1094
  const ceilingsToUpdate = [
971
1095
  ...existingAuto
972
1096
  .filter((ceiling) => updatesById.has(ceiling.id))
@@ -975,16 +1099,17 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
975
1099
  if (!update)
976
1100
  return [];
977
1101
  const data = {};
978
- if (!sameTuplePolygon(ceiling.polygon, update.polygon)) {
1102
+ if (!sameTuplePolygon(ceiling.polygon, update.polygon))
979
1103
  data.polygon = update.polygon;
980
- }
981
- if (Math.abs((ceiling.height ?? DEFAULT_AUTO_CEILING_HEIGHT) - update.height) >
982
- CEILING_HEIGHT_EPSILON) {
1104
+ if (update.height !== undefined &&
1105
+ (ceiling.height === undefined ||
1106
+ Math.abs(ceiling.height - update.height) > ROOM_VERTICAL_PLANE_EPSILON)) {
983
1107
  data.height = update.height;
984
1108
  }
985
- return Object.keys(data).length === 0 ? [] : [{ id: ceiling.id, data }];
1109
+ return Object.keys(data).length > 0 ? [{ id: ceiling.id, data }] : [];
986
1110
  }),
987
1111
  ...ceilingDemotions,
1112
+ ...manualClamps,
988
1113
  ];
989
1114
  const plannedCeilingsForNaming = [...existingCeilings];
990
1115
  const ceilingsToCreate = [];
@@ -996,11 +1121,13 @@ export function planAutoCeilingsForLevel(roomPolygons, existingCeilings, context
996
1121
  continue;
997
1122
  const name = nextAutoRoomName(plannedCeilingsForNaming, 'Ceiling');
998
1123
  plannedCeilingsForNaming.push({ name });
1124
+ const polygon = room.poly.map(pointToTuple);
1125
+ const height = context.heightForRoom?.(polygon);
999
1126
  ceilingsToCreate.push(CeilingNode.parse({
1000
1127
  name,
1001
- polygon: room.poly.map(pointToTuple),
1128
+ polygon,
1002
1129
  holes: [],
1003
- height: room.ceilingHeight,
1130
+ ...(height !== undefined && Number.isFinite(height) ? { height } : {}),
1004
1131
  autoFromWalls: true,
1005
1132
  }));
1006
1133
  }
@@ -1066,10 +1193,24 @@ function runSpaceDetection(levelIds, sceneStore, editorStore, nodes) {
1066
1193
  },
1067
1194
  })));
1068
1195
  }
1196
+ const levelNode = nodes[levelId];
1197
+ const storeyHeight = levelNode?.type === 'level'
1198
+ ? getStoredLevelHeight(levelNode)
1199
+ : DEFAULT_LEVEL_HEIGHT;
1069
1200
  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 });
1201
+ const verticalPlacements = autoRoomVerticalPlacements(spaces, walls,
1202
+ // A derived floor cannot be evidence for its own next elevation: that
1203
+ // would lift unpinned walls, then lift the floor again on every pass.
1204
+ parsedSlabs.filter((slab) => !slab.autoFromWalls), nodes, storeyHeight);
1205
+ const placementFor = (polygon) => verticalPlacements.get(polygonSignature(polygon.map(pointFromTuple)));
1206
+ syncAutoSlabsForLevel(levelId, roomPolygons, parsedSlabs, sceneStore, {
1207
+ elevationForRoom: (polygon) => placementFor(polygon)?.slabElevation,
1208
+ });
1209
+ syncAutoCeilingsForLevel(levelId, roomPolygons, ceilings.map((ceiling) => CeilingNode.parse(ceiling)), sceneStore, {
1210
+ storeyHeight,
1211
+ ceilingClampBound: (polygon) => getCeilingClampBound(levelId, nodes, polygon),
1212
+ heightForRoom: (polygon) => placementFor(polygon)?.ceilingHeight,
1213
+ });
1073
1214
  const zonePlan = planAutoZonesForLevel(spaces, zones.map((zone) => ZoneNode.parse(zone)));
1074
1215
  if (zonePlan.update.length > 0)
1075
1216
  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