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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/README.md +17 -10
  2. package/dist/events/bus.d.ts +49 -2
  3. package/dist/events/bus.d.ts.map +1 -1
  4. package/dist/hooks/scene-registry/item-clip-registry.d.ts +18 -0
  5. package/dist/hooks/scene-registry/item-clip-registry.d.ts.map +1 -0
  6. package/dist/hooks/scene-registry/item-clip-registry.js +10 -0
  7. package/dist/hooks/scene-registry/scene-registry.d.ts +8 -1
  8. package/dist/hooks/scene-registry/scene-registry.d.ts.map +1 -1
  9. package/dist/hooks/scene-registry/scene-registry.js +27 -1
  10. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
  11. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  12. package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
  13. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +135 -28
  14. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  15. package/dist/hooks/spatial-grid/spatial-grid-manager.js +427 -271
  16. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
  17. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  18. package/dist/hooks/spatial-grid/spatial-grid-sync.js +208 -9
  19. package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
  20. package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
  21. package/dist/hooks/spatial-grid/support-host-id.js +2 -0
  22. package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
  23. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
  24. package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
  25. package/dist/index.d.ts +33 -11
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +32 -10
  28. package/dist/lib/level-name.d.ts +4 -0
  29. package/dist/lib/level-name.d.ts.map +1 -0
  30. package/dist/lib/level-name.js +10 -0
  31. package/dist/lib/measurement-geometry.d.ts +23 -0
  32. package/dist/lib/measurement-geometry.d.ts.map +1 -0
  33. package/dist/lib/measurement-geometry.js +249 -0
  34. package/dist/lib/polygon-geometry.d.ts +0 -1
  35. package/dist/lib/polygon-geometry.d.ts.map +1 -1
  36. package/dist/lib/polygon-geometry.js +0 -18
  37. package/dist/lib/selection-proxy.d.ts +4 -0
  38. package/dist/lib/selection-proxy.d.ts.map +1 -0
  39. package/dist/lib/selection-proxy.js +12 -0
  40. package/dist/lib/slab-polygon.d.ts +31 -2
  41. package/dist/lib/slab-polygon.d.ts.map +1 -1
  42. package/dist/lib/slab-polygon.js +557 -31
  43. package/dist/lib/slots.d.ts +14 -0
  44. package/dist/lib/slots.d.ts.map +1 -0
  45. package/dist/lib/slots.js +26 -0
  46. package/dist/lib/space-detection.d.ts +40 -6
  47. package/dist/lib/space-detection.d.ts.map +1 -1
  48. package/dist/lib/space-detection.js +574 -192
  49. package/dist/lib/terrain-brush.d.ts +163 -0
  50. package/dist/lib/terrain-brush.d.ts.map +1 -0
  51. package/dist/lib/terrain-brush.js +359 -0
  52. package/dist/lib/terrain-codec.d.ts +51 -0
  53. package/dist/lib/terrain-codec.d.ts.map +1 -0
  54. package/dist/lib/terrain-codec.js +167 -0
  55. package/dist/lib/terrain-field.d.ts +150 -0
  56. package/dist/lib/terrain-field.d.ts.map +1 -0
  57. package/dist/lib/terrain-field.js +248 -0
  58. package/dist/lib/terrain-raycast.d.ts +44 -0
  59. package/dist/lib/terrain-raycast.d.ts.map +1 -0
  60. package/dist/lib/terrain-raycast.js +188 -0
  61. package/dist/lib/terrain-source.d.ts +60 -0
  62. package/dist/lib/terrain-source.d.ts.map +1 -0
  63. package/dist/lib/terrain-source.js +90 -0
  64. package/dist/lib/terrain-support.d.ts +60 -0
  65. package/dist/lib/terrain-support.d.ts.map +1 -0
  66. package/dist/lib/terrain-support.js +150 -0
  67. package/dist/lib/wall-distance.d.ts +60 -0
  68. package/dist/lib/wall-distance.d.ts.map +1 -0
  69. package/dist/lib/wall-distance.js +85 -0
  70. package/dist/lib/zone-quantities.d.ts +21 -0
  71. package/dist/lib/zone-quantities.d.ts.map +1 -0
  72. package/dist/lib/zone-quantities.js +428 -0
  73. package/dist/material-library.d.ts +28 -1
  74. package/dist/material-library.d.ts.map +1 -1
  75. package/dist/material-library.js +2233 -441
  76. package/dist/registry/handles.d.ts +91 -3
  77. package/dist/registry/handles.d.ts.map +1 -1
  78. package/dist/registry/index.d.ts +3 -3
  79. package/dist/registry/index.d.ts.map +1 -1
  80. package/dist/registry/index.js +1 -1
  81. package/dist/registry/registry.d.ts +46 -3
  82. package/dist/registry/registry.d.ts.map +1 -1
  83. package/dist/registry/registry.js +82 -1
  84. package/dist/registry/subtree.d.ts.map +1 -1
  85. package/dist/registry/subtree.js +8 -1
  86. package/dist/registry/types.d.ts +781 -39
  87. package/dist/registry/types.d.ts.map +1 -1
  88. package/dist/schema/index.d.ts +27 -8
  89. package/dist/schema/index.d.ts.map +1 -1
  90. package/dist/schema/index.js +23 -6
  91. package/dist/schema/material.d.ts +3 -2
  92. package/dist/schema/material.d.ts.map +1 -1
  93. package/dist/schema/material.js +7 -5
  94. package/dist/schema/nodes/box-vent.d.ts +2 -2
  95. package/dist/schema/nodes/cabinet.d.ts +536 -0
  96. package/dist/schema/nodes/cabinet.d.ts.map +1 -0
  97. package/dist/schema/nodes/cabinet.js +139 -0
  98. package/dist/schema/nodes/ceiling.d.ts +4 -3
  99. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  100. package/dist/schema/nodes/ceiling.js +11 -1
  101. package/dist/schema/nodes/chimney.d.ts +4 -4
  102. package/dist/schema/nodes/column.d.ts +4 -2
  103. package/dist/schema/nodes/column.d.ts.map +1 -1
  104. package/dist/schema/nodes/column.js +5 -0
  105. package/dist/schema/nodes/construction-dimension.d.ts +195 -0
  106. package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
  107. package/dist/schema/nodes/construction-dimension.js +151 -0
  108. package/dist/schema/nodes/cupola.d.ts +2 -2
  109. package/dist/schema/nodes/door.d.ts +42 -5
  110. package/dist/schema/nodes/door.d.ts.map +1 -1
  111. package/dist/schema/nodes/door.js +32 -0
  112. package/dist/schema/nodes/dormer.d.ts +8 -8
  113. package/dist/schema/nodes/dormer.js +1 -1
  114. package/dist/schema/nodes/downspout.d.ts +2 -2
  115. package/dist/schema/nodes/duct-fitting.d.ts +91 -0
  116. package/dist/schema/nodes/duct-fitting.d.ts.map +1 -0
  117. package/dist/schema/nodes/duct-fitting.js +92 -0
  118. package/dist/schema/nodes/duct-segment.d.ts +61 -0
  119. package/dist/schema/nodes/duct-segment.d.ts.map +1 -0
  120. package/dist/schema/nodes/duct-segment.js +73 -0
  121. package/dist/schema/nodes/duct-terminal.d.ts +58 -0
  122. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -0
  123. package/dist/schema/nodes/duct-terminal.js +53 -0
  124. package/dist/schema/nodes/elevator.d.ts +3 -2
  125. package/dist/schema/nodes/elevator.d.ts.map +1 -1
  126. package/dist/schema/nodes/elevator.js +3 -0
  127. package/dist/schema/nodes/eyebrow-vent.d.ts +2 -2
  128. package/dist/schema/nodes/fence.d.ts +20 -2
  129. package/dist/schema/nodes/fence.d.ts.map +1 -1
  130. package/dist/schema/nodes/fence.js +33 -2
  131. package/dist/schema/nodes/gutter.d.ts +2 -2
  132. package/dist/schema/nodes/hvac-equipment.d.ts +64 -0
  133. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -0
  134. package/dist/schema/nodes/hvac-equipment.js +57 -0
  135. package/dist/schema/nodes/item.d.ts +9 -0
  136. package/dist/schema/nodes/item.d.ts.map +1 -1
  137. package/dist/schema/nodes/item.js +24 -0
  138. package/dist/schema/nodes/level.d.ts +2 -1
  139. package/dist/schema/nodes/level.d.ts.map +1 -1
  140. package/dist/schema/nodes/level.js +12 -31
  141. package/dist/schema/nodes/lineset.d.ts +42 -0
  142. package/dist/schema/nodes/lineset.d.ts.map +1 -0
  143. package/dist/schema/nodes/lineset.js +38 -0
  144. package/dist/schema/nodes/liquid-line.d.ts +34 -0
  145. package/dist/schema/nodes/liquid-line.d.ts.map +1 -0
  146. package/dist/schema/nodes/liquid-line.js +24 -0
  147. package/dist/schema/nodes/measurement.d.ts +306 -0
  148. package/dist/schema/nodes/measurement.d.ts.map +1 -0
  149. package/dist/schema/nodes/measurement.js +87 -0
  150. package/dist/schema/nodes/pipe-fitting.d.ts +56 -0
  151. package/dist/schema/nodes/pipe-fitting.d.ts.map +1 -0
  152. package/dist/schema/nodes/pipe-fitting.js +45 -0
  153. package/dist/schema/nodes/pipe-segment.d.ts +48 -0
  154. package/dist/schema/nodes/pipe-segment.d.ts.map +1 -0
  155. package/dist/schema/nodes/pipe-segment.js +36 -0
  156. package/dist/schema/nodes/pipe-trap.d.ts +43 -0
  157. package/dist/schema/nodes/pipe-trap.d.ts.map +1 -0
  158. package/dist/schema/nodes/pipe-trap.js +36 -0
  159. package/dist/schema/nodes/ridge-vent.d.ts +18 -3
  160. package/dist/schema/nodes/ridge-vent.d.ts.map +1 -1
  161. package/dist/schema/nodes/ridge-vent.js +221 -3
  162. package/dist/schema/nodes/roof-segment-shape.d.ts +90 -0
  163. package/dist/schema/nodes/roof-segment-shape.d.ts.map +1 -0
  164. package/dist/schema/nodes/roof-segment-shape.js +318 -0
  165. package/dist/schema/nodes/roof-segment-walls.d.ts +101 -0
  166. package/dist/schema/nodes/roof-segment-walls.d.ts.map +1 -0
  167. package/dist/schema/nodes/roof-segment-walls.js +330 -0
  168. package/dist/schema/nodes/roof-segment.d.ts +84 -8
  169. package/dist/schema/nodes/roof-segment.d.ts.map +1 -1
  170. package/dist/schema/nodes/roof-segment.js +290 -5
  171. package/dist/schema/nodes/roof.d.ts +8 -8
  172. package/dist/schema/nodes/shelf.d.ts +4 -2
  173. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  174. package/dist/schema/nodes/shelf.js +8 -0
  175. package/dist/schema/nodes/site.d.ts +9 -0
  176. package/dist/schema/nodes/site.d.ts.map +1 -1
  177. package/dist/schema/nodes/site.js +8 -6
  178. package/dist/schema/nodes/skylight.d.ts +10 -10
  179. package/dist/schema/nodes/slab.d.ts +8 -2
  180. package/dist/schema/nodes/slab.d.ts.map +1 -1
  181. package/dist/schema/nodes/slab.js +18 -2
  182. package/dist/schema/nodes/solar-panel.d.ts +4 -4
  183. package/dist/schema/nodes/spawn.d.ts +1 -0
  184. package/dist/schema/nodes/spawn.d.ts.map +1 -1
  185. package/dist/schema/nodes/spawn.js +2 -0
  186. package/dist/schema/nodes/stair-segment.d.ts +4 -4
  187. package/dist/schema/nodes/stair.d.ts +16 -13
  188. package/dist/schema/nodes/stair.d.ts.map +1 -1
  189. package/dist/schema/nodes/stair.js +11 -1
  190. package/dist/schema/nodes/structural-grid.d.ts +27 -0
  191. package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
  192. package/dist/schema/nodes/structural-grid.js +17 -0
  193. package/dist/schema/nodes/turbine-vent.d.ts +2 -2
  194. package/dist/schema/nodes/wall.d.ts +248 -6
  195. package/dist/schema/nodes/wall.d.ts.map +1 -1
  196. package/dist/schema/nodes/wall.js +266 -0
  197. package/dist/schema/nodes/window.d.ts +40 -3
  198. package/dist/schema/nodes/window.d.ts.map +1 -1
  199. package/dist/schema/nodes/window.js +30 -0
  200. package/dist/schema/nodes/zone.d.ts +22 -0
  201. package/dist/schema/nodes/zone.d.ts.map +1 -1
  202. package/dist/schema/nodes/zone.js +21 -0
  203. package/dist/schema/scene-material.d.ts +41 -0
  204. package/dist/schema/scene-material.d.ts.map +1 -0
  205. package/dist/schema/scene-material.js +9 -0
  206. package/dist/schema/terrain.d.ts +28 -0
  207. package/dist/schema/terrain.d.ts.map +1 -0
  208. package/dist/schema/terrain.js +30 -0
  209. package/dist/schema/types.d.ts +1300 -97
  210. package/dist/schema/types.d.ts.map +1 -1
  211. package/dist/schema/types.js +27 -0
  212. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
  213. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
  214. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
  215. package/dist/services/alignment-anchors.d.ts.map +1 -1
  216. package/dist/services/alignment-anchors.js +30 -1
  217. package/dist/services/alignment.d.ts +5 -0
  218. package/dist/services/alignment.d.ts.map +1 -1
  219. package/dist/services/alignment.js +2 -0
  220. package/dist/services/hosting.d.ts +8 -0
  221. package/dist/services/hosting.d.ts.map +1 -1
  222. package/dist/services/hosting.js +13 -4
  223. package/dist/services/index.d.ts +9 -2
  224. package/dist/services/index.d.ts.map +1 -1
  225. package/dist/services/index.js +9 -2
  226. package/dist/services/level-height.d.ts +27 -0
  227. package/dist/services/level-height.d.ts.map +1 -0
  228. package/dist/services/level-height.js +94 -0
  229. package/dist/services/opening-guides.d.ts +141 -0
  230. package/dist/services/opening-guides.d.ts.map +1 -0
  231. package/dist/services/opening-guides.js +267 -0
  232. package/dist/services/port-connectivity.d.ts +115 -0
  233. package/dist/services/port-connectivity.d.ts.map +1 -0
  234. package/dist/services/port-connectivity.js +300 -0
  235. package/dist/services/riser-diagram.d.ts +40 -0
  236. package/dist/services/riser-diagram.d.ts.map +1 -0
  237. package/dist/services/riser-diagram.js +110 -0
  238. package/dist/services/snap.d.ts +9 -1
  239. package/dist/services/snap.d.ts.map +1 -1
  240. package/dist/services/snap.js +21 -1
  241. package/dist/services/storey.d.ts +103 -0
  242. package/dist/services/storey.d.ts.map +1 -0
  243. package/dist/services/storey.js +296 -0
  244. package/dist/services/system-graph.d.ts +30 -0
  245. package/dist/services/system-graph.d.ts.map +1 -0
  246. package/dist/services/system-graph.js +164 -0
  247. package/dist/services/validate-dwv.d.ts +32 -0
  248. package/dist/services/validate-dwv.d.ts.map +1 -0
  249. package/dist/services/validate-dwv.js +118 -0
  250. package/dist/store/actions/node-actions.d.ts.map +1 -1
  251. package/dist/store/actions/node-actions.js +141 -4
  252. package/dist/store/history-control.d.ts +31 -0
  253. package/dist/store/history-control.d.ts.map +1 -1
  254. package/dist/store/history-control.js +130 -0
  255. package/dist/store/use-live-terrain.d.ts +65 -0
  256. package/dist/store/use-live-terrain.d.ts.map +1 -0
  257. package/dist/store/use-live-terrain.js +31 -0
  258. package/dist/store/use-live-transforms.d.ts +8 -0
  259. package/dist/store/use-live-transforms.d.ts.map +1 -1
  260. package/dist/store/use-scene.d.ts +47 -3
  261. package/dist/store/use-scene.d.ts.map +1 -1
  262. package/dist/store/use-scene.js +992 -55
  263. package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
  264. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
  265. package/dist/systems/elevator/elevator-opening-system.js +27 -21
  266. package/dist/systems/elevator/elevator-service.d.ts +0 -2
  267. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  268. package/dist/systems/elevator/elevator-service.js +2 -24
  269. package/dist/systems/fence/fence-centerline.d.ts +12 -0
  270. package/dist/systems/fence/fence-centerline.d.ts.map +1 -0
  271. package/dist/systems/fence/fence-centerline.js +37 -0
  272. package/dist/systems/fence/fence-spline.d.ts +39 -0
  273. package/dist/systems/fence/fence-spline.d.ts.map +1 -0
  274. package/dist/systems/fence/fence-spline.js +196 -0
  275. package/dist/systems/slab/slab-placement.d.ts +11 -0
  276. package/dist/systems/slab/slab-placement.d.ts.map +1 -0
  277. package/dist/systems/slab/slab-placement.js +14 -0
  278. package/dist/systems/slab/slab-support.d.ts +137 -0
  279. package/dist/systems/slab/slab-support.d.ts.map +1 -0
  280. package/dist/systems/slab/slab-support.js +557 -0
  281. package/dist/systems/stair/stair-footprint.d.ts.map +1 -1
  282. package/dist/systems/stair/stair-footprint.js +21 -11
  283. package/dist/systems/stair/stair-opening-preview.d.ts +1300 -505
  284. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  285. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  286. package/dist/systems/stair/stair-opening-sync.js +19 -33
  287. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  288. package/dist/systems/stair/stair-opening-system.js +32 -4
  289. package/dist/systems/stair/stair-rise.d.ts +22 -0
  290. package/dist/systems/stair/stair-rise.d.ts.map +1 -0
  291. package/dist/systems/stair/stair-rise.js +79 -0
  292. package/dist/systems/wall/wall-curve.d.ts +10 -0
  293. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  294. package/dist/systems/wall/wall-curve.js +1 -1
  295. package/dist/systems/wall/wall-mitering.d.ts.map +1 -1
  296. package/dist/systems/wall/wall-mitering.js +44 -8
  297. package/dist/systems/wall/wall-top.d.ts +40 -0
  298. package/dist/systems/wall/wall-top.d.ts.map +1 -0
  299. package/dist/systems/wall/wall-top.js +46 -0
  300. package/dist/utils/clone-scene-graph.d.ts +1 -0
  301. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  302. package/dist/utils/clone-scene-graph.js +56 -5
  303. package/dist/utils/heal-scene-graph.d.ts +18 -0
  304. package/dist/utils/heal-scene-graph.d.ts.map +1 -0
  305. package/dist/utils/heal-scene-graph.js +106 -0
  306. package/dist/validation/validate-build-json.d.ts +3 -0
  307. package/dist/validation/validate-build-json.d.ts.map +1 -1
  308. package/dist/validation/validate-build-json.js +90 -5
  309. package/package.json +4 -4
  310. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
  311. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
  312. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -273
  313. package/dist/lib/space-detection-pause.test.d.ts +0 -2
  314. package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
  315. package/dist/lib/space-detection-pause.test.js +0 -42
  316. package/dist/lib/space-detection.test.d.ts +0 -2
  317. package/dist/lib/space-detection.test.d.ts.map +0 -1
  318. package/dist/lib/space-detection.test.js +0 -77
  319. package/dist/registry/registry.test.d.ts +0 -2
  320. package/dist/registry/registry.test.d.ts.map +0 -1
  321. package/dist/registry/registry.test.js +0 -183
  322. package/dist/registry/relations-resolver.test.d.ts +0 -2
  323. package/dist/registry/relations-resolver.test.d.ts.map +0 -1
  324. package/dist/registry/relations-resolver.test.js +0 -183
  325. package/dist/registry/scene-api.test.d.ts +0 -2
  326. package/dist/registry/scene-api.test.d.ts.map +0 -1
  327. package/dist/registry/scene-api.test.js +0 -183
  328. package/dist/registry/subtree.test.d.ts +0 -2
  329. package/dist/registry/subtree.test.d.ts.map +0 -1
  330. package/dist/registry/subtree.test.js +0 -133
  331. package/dist/schema/asset-url.test.d.ts +0 -2
  332. package/dist/schema/asset-url.test.d.ts.map +0 -1
  333. package/dist/schema/asset-url.test.js +0 -134
  334. package/dist/services/alignment-anchors.test.d.ts +0 -2
  335. package/dist/services/alignment-anchors.test.d.ts.map +0 -1
  336. package/dist/services/alignment-anchors.test.js +0 -383
  337. package/dist/services/alignment.test.d.ts +0 -2
  338. package/dist/services/alignment.test.d.ts.map +0 -1
  339. package/dist/services/alignment.test.js +0 -92
  340. package/dist/services/drag-session.test.d.ts +0 -2
  341. package/dist/services/drag-session.test.d.ts.map +0 -1
  342. package/dist/services/drag-session.test.js +0 -219
  343. package/dist/services/hosting.test.d.ts +0 -2
  344. package/dist/services/hosting.test.d.ts.map +0 -1
  345. package/dist/services/hosting.test.js +0 -199
  346. package/dist/services/movement.test.d.ts +0 -2
  347. package/dist/services/movement.test.d.ts.map +0 -1
  348. package/dist/services/movement.test.js +0 -133
  349. package/dist/services/single-undo-dance.test.d.ts +0 -2
  350. package/dist/services/single-undo-dance.test.d.ts.map +0 -1
  351. package/dist/services/single-undo-dance.test.js +0 -210
  352. package/dist/services/snap.test.d.ts +0 -2
  353. package/dist/services/snap.test.d.ts.map +0 -1
  354. package/dist/services/snap.test.js +0 -95
  355. package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
  356. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
  357. package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
  358. package/dist/store/actions/reparent.test.d.ts +0 -2
  359. package/dist/store/actions/reparent.test.d.ts.map +0 -1
  360. package/dist/store/actions/reparent.test.js +0 -212
  361. package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
  362. package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
  363. package/dist/store/use-scene-elevator-migration.test.js +0 -109
  364. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
  365. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
  366. package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
  367. package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
  368. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
  369. package/dist/systems/elevator/elevator-runtime.test.js +0 -64
  370. package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
  371. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
  372. package/dist/systems/stair/stair-opening-preview.test.js +0 -80
  373. package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
  374. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
  375. package/dist/systems/stair/stair-opening-sync.test.js +0 -446
  376. package/dist/utils/clone-scene-graph.test.d.ts +0 -2
  377. package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
  378. package/dist/utils/clone-scene-graph.test.js +0 -59
@@ -0,0 +1,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"}
@@ -0,0 +1,359 @@
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 { heightAt, heightAtSample, quantize, } from './terrain-field';
34
+ export const DEFAULT_BRUSH_SETTINGS = {
35
+ radius: 2,
36
+ strength: 0.6,
37
+ falloff: 0.6,
38
+ shape: 'round',
39
+ };
40
+ /**
41
+ * Metres a single raise/lower stroke moves the ground at full strength and full
42
+ * coverage. Bounding it per stroke is what makes the verb aimable: the user
43
+ * knows a pass is worth at most this much, and repeats for more.
44
+ */
45
+ export const RAISE_METRES_PER_STROKE = 1;
46
+ /**
47
+ * Smallest brush radius that still bites, as a multiple of the field's spacing.
48
+ *
49
+ * A dab only moves samples that fall *inside* its radius, so a brush narrower
50
+ * than the gap between samples can land entirely between four of them and change
51
+ * nothing: `advanceStroke` returns `null`, and the user drags with no mark, no
52
+ * cursor change, and no error. 1.5 rather than the ~0.71 where a round brush
53
+ * first catches a corner, because a brush that only bites when it happens to
54
+ * straddle a sample is worse than one that is honestly too small — at 1.5 the
55
+ * worst-case coverage is 0.97, so the dab lands wherever it is aimed.
56
+ *
57
+ * A *multiple* of spacing rather than a constant because spacing is not fixed:
58
+ * `fieldExtentForSite` stretches it once a lot exceeds what 257² samples cover
59
+ * at the default, so on a 300 m lot the old flat 0.5 m floor sat below half the
60
+ * sample gap and the brush silently stopped working at the bottom of its range.
61
+ */
62
+ export const MIN_BRUSH_RADIUS_IN_SPACINGS = 1.5;
63
+ /**
64
+ * Clamp a radius to what `field` can actually resolve.
65
+ *
66
+ * Lives here rather than in the editor's UI layer because it is a fact about the
67
+ * footprint math above, not a preference: every producer of a radius (slider,
68
+ * `[`/`]`, a future import preset) has to respect it or it produces a brush that
69
+ * does nothing.
70
+ */
71
+ export function minBrushRadius(field) {
72
+ return field.spacing * MIN_BRUSH_RADIUS_IN_SPACINGS;
73
+ }
74
+ /** Dabs are placed every `radius * this` metres along the pointer path. */
75
+ const DAB_SPACING_FRACTION = 0.25;
76
+ /**
77
+ * Jacobi iterations used to precompute the smooth target. Each is applied with
78
+ * λ = 0.25 on a 4-neighbour stencil, the stability bound for the discrete
79
+ * diffusion — above it the filter oscillates instead of converging.
80
+ */
81
+ const SMOOTH_ITERATIONS = 8;
82
+ const SMOOTH_LAMBDA = 0.25;
83
+ export function beginStroke(options) {
84
+ return {
85
+ verb: options.verb,
86
+ settings: { ...DEFAULT_BRUSH_SETTINGS, ...options.settings },
87
+ snapshot: options.field,
88
+ target: options.target ?? 0,
89
+ mask: new Map(),
90
+ smoothed: null,
91
+ lastX: null,
92
+ lastZ: null,
93
+ };
94
+ }
95
+ /**
96
+ * Sample the height a `flatten` stroke should aim at, from the field itself.
97
+ *
98
+ * Sampling before the stroke starts (right-click / first click, per the surveyed
99
+ * tools) is what makes "flatten this pad to match that corner" a two-step
100
+ * gesture instead of a numeric-entry chore.
101
+ */
102
+ export function sampleTarget(field, x, z) {
103
+ return heightAt(field, x, z);
104
+ }
105
+ /**
106
+ * Forget where the last dab landed, without ending the stroke.
107
+ *
108
+ * Needed because dab spacing is a true *arc length*: `advanceStroke` interpolates
109
+ * from the previous centre, so any gap in the pointer stream is bridged with a
110
+ * line of dabs when it resumes. That is exactly right for a dropped frame, and
111
+ * exactly wrong when the caller deliberately skipped a stretch — a camera zoom
112
+ * mid-stroke moves the ground under a held brush, and bridging back to a centre
113
+ * from before the zoom paints a stripe across everything in between.
114
+ *
115
+ * A stroke resumed after this deposits its next dab at the pointer, like the
116
+ * first one. The mask is untouched, so the stroke stays saturating across the
117
+ * break and the whole gesture is still one undo step.
118
+ */
119
+ export function detachStrokeAnchor(stroke) {
120
+ stroke.lastX = null;
121
+ stroke.lastZ = null;
122
+ }
123
+ /**
124
+ * Extend the stroke to the pointer's new position and return the patch to apply.
125
+ *
126
+ * Returns `null` when the motion is shorter than the dab spacing (nothing
127
+ * changed yet) or when the footprint misses the field entirely. The patch is
128
+ * absolute heights over the union of the dabs placed by *this* call, recomputed
129
+ * from the snapshot and the saturated mask — which is why applying patches in
130
+ * order converges rather than compounding.
131
+ */
132
+ export function advanceStroke(stroke, x, z) {
133
+ const dabs = dabPositions(stroke, x, z);
134
+ if (dabs.length === 0)
135
+ return null;
136
+ const field = stroke.snapshot;
137
+ let minCol = field.cols;
138
+ let minRow = field.rows;
139
+ let maxCol = -1;
140
+ let maxRow = -1;
141
+ for (const [dx, dz] of dabs) {
142
+ const range = footprint(field, dx, dz, stroke.settings.radius);
143
+ if (!range)
144
+ continue;
145
+ if (range.col0 < minCol)
146
+ minCol = range.col0;
147
+ if (range.row0 < minRow)
148
+ minRow = range.row0;
149
+ if (range.col1 > maxCol)
150
+ maxCol = range.col1;
151
+ if (range.row1 > maxRow)
152
+ maxRow = range.row1;
153
+ for (let r = range.row0; r <= range.row1; r++) {
154
+ const sampleZ = field.origin[1] + r * field.spacing;
155
+ for (let c = range.col0; c <= range.col1; c++) {
156
+ const sampleX = field.origin[0] + c * field.spacing;
157
+ const coverage = weightAt(stroke.settings, sampleX - dx, sampleZ - dz);
158
+ if (coverage <= 0)
159
+ continue;
160
+ const index = r * field.cols + c;
161
+ const previous = stroke.mask.get(index) ?? 0;
162
+ if (coverage > previous)
163
+ stroke.mask.set(index, coverage);
164
+ }
165
+ }
166
+ }
167
+ if (maxCol < minCol || maxRow < minRow)
168
+ return null;
169
+ const cols = maxCol - minCol + 1;
170
+ const rows = maxRow - minRow + 1;
171
+ const heights = new Int16Array(cols * rows);
172
+ for (let r = 0; r < rows; r++) {
173
+ const fieldRow = minRow + r;
174
+ for (let c = 0; c < cols; c++) {
175
+ const fieldCol = minCol + c;
176
+ const index = fieldRow * field.cols + fieldCol;
177
+ heights[r * cols + c] = resolveSample(stroke, index, fieldCol, fieldRow);
178
+ }
179
+ }
180
+ return { col0: minCol, row0: minRow, cols, rows, heights };
181
+ }
182
+ /**
183
+ * The quantized height a sample should hold given the stroke's current mask.
184
+ *
185
+ * Every verb has the same shape — `snapshot + (goal - snapshot) * mask *
186
+ * strength` — which is what makes them all saturating. `raise`/`lower` express
187
+ * their goal as an offset from the snapshot, `flatten` as an absolute, and
188
+ * `smooth` as the diffused snapshot.
189
+ */
190
+ function resolveSample(stroke, index, col, row) {
191
+ const field = stroke.snapshot;
192
+ const base = field.heights[index] ?? 0;
193
+ const coverage = stroke.mask.get(index) ?? 0;
194
+ if (coverage <= 0)
195
+ return base;
196
+ const amount = coverage * stroke.settings.strength;
197
+ const baseMetres = base * field.step;
198
+ switch (stroke.verb) {
199
+ case 'raise':
200
+ return quantize(field, baseMetres + RAISE_METRES_PER_STROKE * amount);
201
+ case 'lower':
202
+ return quantize(field, baseMetres - RAISE_METRES_PER_STROKE * amount);
203
+ case 'flatten':
204
+ return quantize(field, baseMetres + (stroke.target - baseMetres) * amount);
205
+ case 'smooth': {
206
+ const smoothed = smoothedSnapshot(stroke);
207
+ const goal = (smoothed[index] ?? base) * field.step;
208
+ return quantize(field, baseMetres + (goal - baseMetres) * amount);
209
+ }
210
+ default:
211
+ // `col`/`row` are carried for future verbs that need position (terrace).
212
+ void col;
213
+ void row;
214
+ return base;
215
+ }
216
+ }
217
+ /**
218
+ * The diffused snapshot every `smooth` dab lerps toward, computed once per
219
+ * stroke.
220
+ *
221
+ * Whole-field rather than stroke-local: a local blur would need its border
222
+ * samples pinned, and pinned borders are precisely where a smooth brush leaves
223
+ * the ridge it was supposed to remove. The cost is a few passes over an array
224
+ * that is 4 225 entries at the default 65² size.
225
+ */
226
+ function smoothedSnapshot(stroke) {
227
+ if (stroke.smoothed)
228
+ return stroke.smoothed;
229
+ const field = stroke.snapshot;
230
+ let current = new Int16Array(field.heights);
231
+ let next = new Int16Array(current.length);
232
+ for (let iteration = 0; iteration < SMOOTH_ITERATIONS; iteration++) {
233
+ for (let r = 0; r < field.rows; r++) {
234
+ for (let c = 0; c < field.cols; c++) {
235
+ const index = r * field.cols + c;
236
+ const centre = current[index] ?? 0;
237
+ const laplacian = sampleClamped(current, field, c - 1, r) +
238
+ sampleClamped(current, field, c + 1, r) +
239
+ sampleClamped(current, field, c, r - 1) +
240
+ sampleClamped(current, field, c, r + 1) -
241
+ 4 * centre;
242
+ next[index] = Math.round(centre + SMOOTH_LAMBDA * laplacian);
243
+ }
244
+ }
245
+ const swap = current;
246
+ current = next;
247
+ next = swap;
248
+ }
249
+ stroke.smoothed = current;
250
+ return current;
251
+ }
252
+ function sampleClamped(heights, field, col, row) {
253
+ const c = Math.max(0, Math.min(field.cols - 1, col));
254
+ const r = Math.max(0, Math.min(field.rows - 1, row));
255
+ return heights[r * field.cols + c] ?? 0;
256
+ }
257
+ /**
258
+ * Dab centres from the last one to `(x, z)`, spaced by arc length.
259
+ *
260
+ * The first dab of a stroke is placed exactly at the pointer, so a click with no
261
+ * drag still deposits one dab. After that, motion shorter than one spacing
262
+ * deposits nothing and the stroke keeps its old anchor — which is what makes the
263
+ * spacing a true arc length rather than a per-event budget.
264
+ */
265
+ function dabPositions(stroke, x, z) {
266
+ if (stroke.lastX === null || stroke.lastZ === null) {
267
+ stroke.lastX = x;
268
+ stroke.lastZ = z;
269
+ return [[x, z]];
270
+ }
271
+ const spacing = Math.max(1e-3, stroke.settings.radius * DAB_SPACING_FRACTION);
272
+ const dx = x - stroke.lastX;
273
+ const dz = z - stroke.lastZ;
274
+ const distance = Math.hypot(dx, dz);
275
+ if (distance < spacing)
276
+ return [];
277
+ const steps = Math.floor(distance / spacing);
278
+ const dabs = [];
279
+ for (let step = 1; step <= steps; step++) {
280
+ const t = (step * spacing) / distance;
281
+ dabs.push([stroke.lastX + dx * t, stroke.lastZ + dz * t]);
282
+ }
283
+ const last = dabs[dabs.length - 1];
284
+ if (last) {
285
+ stroke.lastX = last[0];
286
+ stroke.lastZ = last[1];
287
+ }
288
+ return dabs;
289
+ }
290
+ /** Sample range a dab of `radius` at `(x, z)` touches, clamped to the field. */
291
+ function footprint(field, x, z, radius) {
292
+ const col0 = Math.max(0, Math.ceil((x - radius - field.origin[0]) / field.spacing));
293
+ const row0 = Math.max(0, Math.ceil((z - radius - field.origin[1]) / field.spacing));
294
+ const col1 = Math.min(field.cols - 1, Math.floor((x + radius - field.origin[0]) / field.spacing));
295
+ const row1 = Math.min(field.rows - 1, Math.floor((z + radius - field.origin[1]) / field.spacing));
296
+ if (col1 < col0 || row1 < row0)
297
+ return null;
298
+ return { col0, row0, col1, row1 };
299
+ }
300
+ /**
301
+ * Coverage in 0–1 for a sample at offset `(dx, dz)` from a dab centre.
302
+ *
303
+ * Round uses Euclidean distance, square uses Chebyshev — the same radius means
304
+ * the square brush's inscribed circle matches the round one, so switching shape
305
+ * mid-session does not change the brush's apparent size.
306
+ */
307
+ export function weightAt(settings, dx, dz) {
308
+ const radius = Math.max(1e-6, settings.radius);
309
+ const distance = settings.shape === 'square' ? Math.max(Math.abs(dx), Math.abs(dz)) : Math.hypot(dx, dz);
310
+ const normalized = distance / radius;
311
+ if (normalized >= 1)
312
+ return 0;
313
+ const falloff = Math.min(1, Math.max(0, settings.falloff));
314
+ if (falloff <= 0)
315
+ return 1;
316
+ const inner = 1 - falloff;
317
+ if (normalized <= inner)
318
+ return 1;
319
+ const t = (normalized - inner) / falloff;
320
+ // Cosine bell: 1 at t=0, 0 at t=1, zero derivative at both ends.
321
+ return 0.5 * (1 + Math.cos(Math.PI * t));
322
+ }
323
+ /**
324
+ * The world-XZ height under a brush, for the readout the sculpt HUD shows.
325
+ * Thin, but it keeps the tool from importing `heightAt` just to render a number.
326
+ */
327
+ export function brushHeightAt(field, x, z) {
328
+ return heightAt(field, x, z);
329
+ }
330
+ /** Peak coverage the stroke reached, for tests and for the HUD's "pass" readout. */
331
+ export function maxCoverage(stroke) {
332
+ let max = 0;
333
+ for (const value of stroke.mask.values())
334
+ if (value > max)
335
+ max = value;
336
+ return max;
337
+ }
338
+ /**
339
+ * Highest sample height in metres under a world-XZ rect — the seed a `flatten`
340
+ * pad uses when the user asks for "level with the high corner" rather than
341
+ * typing a number.
342
+ */
343
+ export function highestOver(field, minX, minZ, maxX, maxZ) {
344
+ const col0 = Math.max(0, Math.floor((minX - field.origin[0]) / field.spacing));
345
+ const row0 = Math.max(0, Math.floor((minZ - field.origin[1]) / field.spacing));
346
+ const col1 = Math.min(field.cols - 1, Math.ceil((maxX - field.origin[0]) / field.spacing));
347
+ const row1 = Math.min(field.rows - 1, Math.ceil((maxZ - field.origin[1]) / field.spacing));
348
+ if (col1 < col0 || row1 < row0)
349
+ return heightAtSample(field, col0, row0);
350
+ let highest = Number.NEGATIVE_INFINITY;
351
+ for (let r = row0; r <= row1; r++) {
352
+ for (let c = col0; c <= col1; c++) {
353
+ const value = (field.heights[r * field.cols + c] ?? 0) * field.step;
354
+ if (value > highest)
355
+ highest = value;
356
+ }
357
+ }
358
+ return highest;
359
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The wire format for a `TerrainField`.
3
+ *
4
+ * The scene graph is persisted as JSON (`projects_models.scene_graph`, a jsonb
5
+ * column), so an `Int16Array` cannot be stored directly — `JSON.stringify` turns
6
+ * a typed array into an object with numeric string keys, which round-trips
7
+ * lossily and costs ~7 bytes per sample. This module is the one place the field
8
+ * crosses that boundary.
9
+ *
10
+ * Base64 of little-endian `Int16` was chosen over the alternatives:
11
+ *
12
+ * - **A plain number array** costs 4-6× the bytes and, worse, re-parses into a
13
+ * `number[]` that every consumer would then have to copy into an `Int16Array`.
14
+ * - **`Array.from(heights)`** has the same problem and silently drops the
15
+ * quantization invariant: nothing stops a hand-edited scene from putting
16
+ * `2.5` in the array, and then `isFlatOver`'s integer equality is meaningless.
17
+ * - **Compression** (RLE, deflate) is deliberately *not* here. Imported DEMs are
18
+ * noisy and compress worst, so a compressed format would fail only on the
19
+ * messiest scenes — the ones most likely to be real. Chunking (`diffToPatches`)
20
+ * is the size mitigation, not compression.
21
+ *
22
+ * Byte order is written explicitly via `DataView` rather than relying on
23
+ * `Int16Array`'s platform order. Every realistic platform is little-endian, but
24
+ * the wire format must not depend on that being true of the machine that saved
25
+ * the scene.
26
+ */
27
+ import { type TerrainData } from '../schema/terrain';
28
+ import type { TerrainField } from './terrain-field';
29
+ export declare function encodeTerrainField(field: TerrainField): TerrainData;
30
+ /**
31
+ * Rebuild a field from persisted data, or `null` if the data is unusable.
32
+ *
33
+ * Returns `null` rather than throwing, and rather than repairing: a scene whose
34
+ * terrain fails to decode must still load (with flat ground) instead of taking
35
+ * the whole project down, and silently substituting a *different* terrain would
36
+ * be worse than showing none. The caller treats `null` as "this site has no
37
+ * terrain".
38
+ *
39
+ * Metadata and payload length are strict. Repairing a truncated payload or
40
+ * inventing missing metadata would silently substitute a different surface.
41
+ */
42
+ export declare function decodeTerrainField(data: unknown): TerrainField | null;
43
+ /**
44
+ * True when every sample is at the datum — the test for "this field is not worth
45
+ * persisting".
46
+ *
47
+ * Called before writing to the node so an untouched site keeps `terrain:
48
+ * undefined` instead of carrying ~11 KB of base64 zeroes in every saved scene.
49
+ */
50
+ export declare function isDatumField(field: TerrainField): boolean;
51
+ //# sourceMappingURL=terrain-codec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terrain-codec.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-codec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAoDnD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,YAAY,GAAG,WAAW,CAenE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CAwCrE;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,OAAO,CAKzD"}