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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (339) hide show
  1. package/README.md +17 -10
  2. package/dist/events/bus.d.ts +21 -2
  3. package/dist/events/bus.d.ts.map +1 -1
  4. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts +17 -1
  5. package/dist/hooks/spatial-grid/floor-placed-elevation.d.ts.map +1 -1
  6. package/dist/hooks/spatial-grid/floor-placed-elevation.js +61 -8
  7. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts +116 -66
  8. package/dist/hooks/spatial-grid/spatial-grid-manager.d.ts.map +1 -1
  9. package/dist/hooks/spatial-grid/spatial-grid-manager.js +316 -441
  10. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts +46 -1
  11. package/dist/hooks/spatial-grid/spatial-grid-sync.d.ts.map +1 -1
  12. package/dist/hooks/spatial-grid/spatial-grid-sync.js +193 -12
  13. package/dist/hooks/spatial-grid/support-host-id.d.ts +3 -0
  14. package/dist/hooks/spatial-grid/support-host-id.d.ts.map +1 -0
  15. package/dist/hooks/spatial-grid/support-host-id.js +2 -0
  16. package/dist/hooks/spatial-grid/support-host-patch.d.ts +51 -0
  17. package/dist/hooks/spatial-grid/support-host-patch.d.ts.map +1 -0
  18. package/dist/hooks/spatial-grid/support-host-patch.js +164 -0
  19. package/dist/index.d.ts +23 -11
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +22 -10
  22. package/dist/lib/measurement-geometry.d.ts +4 -0
  23. package/dist/lib/measurement-geometry.d.ts.map +1 -1
  24. package/dist/lib/measurement-geometry.js +27 -10
  25. package/dist/lib/slab-polygon.d.ts.map +1 -1
  26. package/dist/lib/slab-polygon.js +33 -1
  27. package/dist/lib/space-detection.d.ts +15 -4
  28. package/dist/lib/space-detection.d.ts.map +1 -1
  29. package/dist/lib/space-detection.js +174 -88
  30. package/dist/lib/terrain-brush.d.ts +163 -0
  31. package/dist/lib/terrain-brush.d.ts.map +1 -0
  32. package/dist/lib/terrain-brush.js +359 -0
  33. package/dist/lib/terrain-codec.d.ts +51 -0
  34. package/dist/lib/terrain-codec.d.ts.map +1 -0
  35. package/dist/lib/terrain-codec.js +167 -0
  36. package/dist/lib/terrain-field.d.ts +150 -0
  37. package/dist/lib/terrain-field.d.ts.map +1 -0
  38. package/dist/lib/terrain-field.js +248 -0
  39. package/dist/lib/terrain-raycast.d.ts +44 -0
  40. package/dist/lib/terrain-raycast.d.ts.map +1 -0
  41. package/dist/lib/terrain-raycast.js +188 -0
  42. package/dist/lib/terrain-source.d.ts +60 -0
  43. package/dist/lib/terrain-source.d.ts.map +1 -0
  44. package/dist/lib/terrain-source.js +90 -0
  45. package/dist/lib/terrain-support.d.ts +60 -0
  46. package/dist/lib/terrain-support.d.ts.map +1 -0
  47. package/dist/lib/terrain-support.js +150 -0
  48. package/dist/lib/zone-quantities.d.ts +1 -1
  49. package/dist/lib/zone-quantities.d.ts.map +1 -1
  50. package/dist/lib/zone-quantities.js +311 -66
  51. package/dist/material-library.d.ts +9 -1
  52. package/dist/material-library.d.ts.map +1 -1
  53. package/dist/material-library.js +56 -4
  54. package/dist/registry/handles.d.ts +16 -1
  55. package/dist/registry/handles.d.ts.map +1 -1
  56. package/dist/registry/index.d.ts +1 -1
  57. package/dist/registry/index.d.ts.map +1 -1
  58. package/dist/registry/subtree.d.ts.map +1 -1
  59. package/dist/registry/subtree.js +5 -2
  60. package/dist/registry/types.d.ts +64 -1
  61. package/dist/registry/types.d.ts.map +1 -1
  62. package/dist/schema/index.d.ts +6 -3
  63. package/dist/schema/index.d.ts.map +1 -1
  64. package/dist/schema/index.js +6 -3
  65. package/dist/schema/nodes/cabinet.d.ts +2 -0
  66. package/dist/schema/nodes/cabinet.d.ts.map +1 -1
  67. package/dist/schema/nodes/cabinet.js +4 -2
  68. package/dist/schema/nodes/ceiling.d.ts +1 -1
  69. package/dist/schema/nodes/ceiling.d.ts.map +1 -1
  70. package/dist/schema/nodes/ceiling.js +7 -1
  71. package/dist/schema/nodes/column.d.ts +1 -0
  72. package/dist/schema/nodes/column.d.ts.map +1 -1
  73. package/dist/schema/nodes/column.js +2 -0
  74. package/dist/schema/nodes/construction-dimension.d.ts +195 -0
  75. package/dist/schema/nodes/construction-dimension.d.ts.map +1 -0
  76. package/dist/schema/nodes/construction-dimension.js +151 -0
  77. package/dist/schema/nodes/door.d.ts +29 -0
  78. package/dist/schema/nodes/door.d.ts.map +1 -1
  79. package/dist/schema/nodes/door.js +20 -0
  80. package/dist/schema/nodes/duct-terminal.d.ts +1 -0
  81. package/dist/schema/nodes/duct-terminal.d.ts.map +1 -1
  82. package/dist/schema/nodes/duct-terminal.js +2 -0
  83. package/dist/schema/nodes/fence.d.ts +2 -0
  84. package/dist/schema/nodes/fence.d.ts.map +1 -1
  85. package/dist/schema/nodes/fence.js +8 -0
  86. package/dist/schema/nodes/hvac-equipment.d.ts +1 -0
  87. package/dist/schema/nodes/hvac-equipment.d.ts.map +1 -1
  88. package/dist/schema/nodes/hvac-equipment.js +2 -0
  89. package/dist/schema/nodes/item.d.ts +1 -0
  90. package/dist/schema/nodes/item.d.ts.map +1 -1
  91. package/dist/schema/nodes/item.js +13 -0
  92. package/dist/schema/nodes/level.d.ts +2 -1
  93. package/dist/schema/nodes/level.d.ts.map +1 -1
  94. package/dist/schema/nodes/level.js +12 -33
  95. package/dist/schema/nodes/shelf.d.ts +1 -0
  96. package/dist/schema/nodes/shelf.d.ts.map +1 -1
  97. package/dist/schema/nodes/shelf.js +2 -0
  98. package/dist/schema/nodes/site.d.ts +9 -0
  99. package/dist/schema/nodes/site.d.ts.map +1 -1
  100. package/dist/schema/nodes/site.js +8 -6
  101. package/dist/schema/nodes/slab.d.ts +5 -0
  102. package/dist/schema/nodes/slab.d.ts.map +1 -1
  103. package/dist/schema/nodes/slab.js +14 -2
  104. package/dist/schema/nodes/spawn.d.ts +1 -0
  105. package/dist/schema/nodes/spawn.d.ts.map +1 -1
  106. package/dist/schema/nodes/spawn.js +2 -0
  107. package/dist/schema/nodes/stair.d.ts +3 -1
  108. package/dist/schema/nodes/stair.d.ts.map +1 -1
  109. package/dist/schema/nodes/stair.js +8 -1
  110. package/dist/schema/nodes/structural-grid.d.ts +27 -0
  111. package/dist/schema/nodes/structural-grid.d.ts.map +1 -0
  112. package/dist/schema/nodes/structural-grid.js +17 -0
  113. package/dist/schema/nodes/wall.d.ts +5 -2
  114. package/dist/schema/nodes/wall.d.ts.map +1 -1
  115. package/dist/schema/nodes/wall.js +13 -4
  116. package/dist/schema/nodes/window.d.ts +29 -0
  117. package/dist/schema/nodes/window.d.ts.map +1 -1
  118. package/dist/schema/nodes/window.js +18 -0
  119. package/dist/schema/nodes/zone.d.ts +20 -0
  120. package/dist/schema/nodes/zone.d.ts.map +1 -1
  121. package/dist/schema/nodes/zone.js +15 -0
  122. package/dist/schema/terrain.d.ts +28 -0
  123. package/dist/schema/terrain.d.ts.map +1 -0
  124. package/dist/schema/terrain.js +30 -0
  125. package/dist/schema/types.d.ts +211 -3
  126. package/dist/schema/types.d.ts.map +1 -1
  127. package/dist/schema/types.js +4 -0
  128. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts +80 -0
  129. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.d.ts.map +1 -0
  130. package/dist/services/__fixtures__/wall-plane-top-boundary-repro.js +89 -0
  131. package/dist/services/index.d.ts +2 -1
  132. package/dist/services/index.d.ts.map +1 -1
  133. package/dist/services/index.js +2 -1
  134. package/dist/services/level-height.d.ts +8 -9
  135. package/dist/services/level-height.d.ts.map +1 -1
  136. package/dist/services/level-height.js +48 -16
  137. package/dist/services/storey.d.ts +103 -0
  138. package/dist/services/storey.d.ts.map +1 -0
  139. package/dist/services/storey.js +296 -0
  140. package/dist/store/actions/node-actions.d.ts.map +1 -1
  141. package/dist/store/actions/node-actions.js +47 -3
  142. package/dist/store/history-control.d.ts +20 -0
  143. package/dist/store/history-control.d.ts.map +1 -1
  144. package/dist/store/history-control.js +110 -13
  145. package/dist/store/use-live-terrain.d.ts +65 -0
  146. package/dist/store/use-live-terrain.d.ts.map +1 -0
  147. package/dist/store/use-live-terrain.js +31 -0
  148. package/dist/store/use-live-transforms.d.ts +8 -0
  149. package/dist/store/use-live-transforms.d.ts.map +1 -1
  150. package/dist/store/use-scene.d.ts +31 -2
  151. package/dist/store/use-scene.d.ts.map +1 -1
  152. package/dist/store/use-scene.js +638 -8
  153. package/dist/systems/elevator/elevator-opening-system.d.ts +1 -0
  154. package/dist/systems/elevator/elevator-opening-system.d.ts.map +1 -1
  155. package/dist/systems/elevator/elevator-opening-system.js +27 -21
  156. package/dist/systems/elevator/elevator-service.d.ts +0 -2
  157. package/dist/systems/elevator/elevator-service.d.ts.map +1 -1
  158. package/dist/systems/elevator/elevator-service.js +2 -24
  159. package/dist/systems/slab/slab-placement.d.ts +11 -0
  160. package/dist/systems/slab/slab-placement.d.ts.map +1 -0
  161. package/dist/systems/slab/slab-placement.js +14 -0
  162. package/dist/systems/slab/slab-support.d.ts +137 -0
  163. package/dist/systems/slab/slab-support.d.ts.map +1 -0
  164. package/dist/systems/slab/slab-support.js +557 -0
  165. package/dist/systems/stair/stair-opening-preview.d.ts +1060 -935
  166. package/dist/systems/stair/stair-opening-preview.d.ts.map +1 -1
  167. package/dist/systems/stair/stair-opening-sync.d.ts.map +1 -1
  168. package/dist/systems/stair/stair-opening-sync.js +18 -15
  169. package/dist/systems/stair/stair-opening-system.d.ts.map +1 -1
  170. package/dist/systems/stair/stair-opening-system.js +32 -4
  171. package/dist/systems/stair/stair-rise.d.ts +22 -0
  172. package/dist/systems/stair/stair-rise.d.ts.map +1 -0
  173. package/dist/systems/stair/stair-rise.js +79 -0
  174. package/dist/systems/wall/wall-curve.d.ts +10 -0
  175. package/dist/systems/wall/wall-curve.d.ts.map +1 -1
  176. package/dist/systems/wall/wall-curve.js +1 -1
  177. package/dist/systems/wall/wall-mitering.js +6 -6
  178. package/dist/systems/wall/wall-top.d.ts +40 -0
  179. package/dist/systems/wall/wall-top.d.ts.map +1 -0
  180. package/dist/systems/wall/wall-top.js +46 -0
  181. package/dist/utils/clone-scene-graph.d.ts.map +1 -1
  182. package/dist/utils/clone-scene-graph.js +34 -3
  183. package/dist/utils/heal-scene-graph.d.ts +1 -1
  184. package/dist/utils/heal-scene-graph.d.ts.map +1 -1
  185. package/dist/utils/heal-scene-graph.js +25 -9
  186. package/dist/validation/validate-build-json.d.ts +2 -0
  187. package/dist/validation/validate-build-json.d.ts.map +1 -1
  188. package/dist/validation/validate-build-json.js +59 -3
  189. package/package.json +2 -2
  190. package/dist/hooks/scene-registry/scene-registry.test.d.ts +0 -2
  191. package/dist/hooks/scene-registry/scene-registry.test.d.ts.map +0 -1
  192. package/dist/hooks/scene-registry/scene-registry.test.js +0 -26
  193. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts +0 -2
  194. package/dist/hooks/spatial-grid/floor-placed-elevation.test.d.ts.map +0 -1
  195. package/dist/hooks/spatial-grid/floor-placed-elevation.test.js +0 -366
  196. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts +0 -2
  197. package/dist/hooks/spatial-grid/wall-slab-overlap.test.d.ts.map +0 -1
  198. package/dist/hooks/spatial-grid/wall-slab-overlap.test.js +0 -363
  199. package/dist/lib/measurement-geometry.test.d.ts +0 -2
  200. package/dist/lib/measurement-geometry.test.d.ts.map +0 -1
  201. package/dist/lib/measurement-geometry.test.js +0 -102
  202. package/dist/lib/slab-polygon.test.d.ts +0 -2
  203. package/dist/lib/slab-polygon.test.d.ts.map +0 -1
  204. package/dist/lib/slab-polygon.test.js +0 -635
  205. package/dist/lib/space-detection-pause.test.d.ts +0 -2
  206. package/dist/lib/space-detection-pause.test.d.ts.map +0 -1
  207. package/dist/lib/space-detection-pause.test.js +0 -42
  208. package/dist/lib/space-detection.test.d.ts +0 -2
  209. package/dist/lib/space-detection.test.d.ts.map +0 -1
  210. package/dist/lib/space-detection.test.js +0 -376
  211. package/dist/lib/zone-quantities.test.d.ts +0 -2
  212. package/dist/lib/zone-quantities.test.d.ts.map +0 -1
  213. package/dist/lib/zone-quantities.test.js +0 -82
  214. package/dist/registry/registry.test.d.ts +0 -2
  215. package/dist/registry/registry.test.d.ts.map +0 -1
  216. package/dist/registry/registry.test.js +0 -196
  217. package/dist/registry/relations-resolver.test.d.ts +0 -2
  218. package/dist/registry/relations-resolver.test.d.ts.map +0 -1
  219. package/dist/registry/relations-resolver.test.js +0 -183
  220. package/dist/registry/scene-api.test.d.ts +0 -2
  221. package/dist/registry/scene-api.test.d.ts.map +0 -1
  222. package/dist/registry/scene-api.test.js +0 -183
  223. package/dist/registry/subtree.test.d.ts +0 -2
  224. package/dist/registry/subtree.test.d.ts.map +0 -1
  225. package/dist/registry/subtree.test.js +0 -163
  226. package/dist/schema/asset-url.test.d.ts +0 -2
  227. package/dist/schema/asset-url.test.d.ts.map +0 -1
  228. package/dist/schema/asset-url.test.js +0 -134
  229. package/dist/schema/material.test.d.ts +0 -2
  230. package/dist/schema/material.test.d.ts.map +0 -1
  231. package/dist/schema/material.test.js +0 -43
  232. package/dist/schema/nodes/measurement.test.d.ts +0 -2
  233. package/dist/schema/nodes/measurement.test.d.ts.map +0 -1
  234. package/dist/schema/nodes/measurement.test.js +0 -139
  235. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts +0 -2
  236. package/dist/schema/nodes/ridge-vent-defaults.test.d.ts.map +0 -1
  237. package/dist/schema/nodes/ridge-vent-defaults.test.js +0 -205
  238. package/dist/schema/nodes/roof-segment-shape.test.d.ts +0 -2
  239. package/dist/schema/nodes/roof-segment-shape.test.d.ts.map +0 -1
  240. package/dist/schema/nodes/roof-segment-shape.test.js +0 -175
  241. package/dist/schema/nodes/roof-segment-surface.test.d.ts +0 -2
  242. package/dist/schema/nodes/roof-segment-surface.test.d.ts.map +0 -1
  243. package/dist/schema/nodes/roof-segment-surface.test.js +0 -48
  244. package/dist/schema/nodes/roof-segment-trim.test.d.ts +0 -2
  245. package/dist/schema/nodes/roof-segment-trim.test.d.ts.map +0 -1
  246. package/dist/schema/nodes/roof-segment-trim.test.js +0 -107
  247. package/dist/schema/nodes/roof-segment-walls.test.d.ts +0 -2
  248. package/dist/schema/nodes/roof-segment-walls.test.d.ts.map +0 -1
  249. package/dist/schema/nodes/roof-segment-walls.test.js +0 -92
  250. package/dist/schema/nodes/wall.test.d.ts +0 -2
  251. package/dist/schema/nodes/wall.test.d.ts.map +0 -1
  252. package/dist/schema/nodes/wall.test.js +0 -208
  253. package/dist/services/alignment-anchors.test.d.ts +0 -2
  254. package/dist/services/alignment-anchors.test.d.ts.map +0 -1
  255. package/dist/services/alignment-anchors.test.js +0 -383
  256. package/dist/services/alignment.test.d.ts +0 -2
  257. package/dist/services/alignment.test.d.ts.map +0 -1
  258. package/dist/services/alignment.test.js +0 -92
  259. package/dist/services/drag-session.test.d.ts +0 -2
  260. package/dist/services/drag-session.test.d.ts.map +0 -1
  261. package/dist/services/drag-session.test.js +0 -219
  262. package/dist/services/hosting.test.d.ts +0 -2
  263. package/dist/services/hosting.test.d.ts.map +0 -1
  264. package/dist/services/hosting.test.js +0 -230
  265. package/dist/services/movement.test.d.ts +0 -2
  266. package/dist/services/movement.test.d.ts.map +0 -1
  267. package/dist/services/movement.test.js +0 -133
  268. package/dist/services/opening-guides.test.d.ts +0 -2
  269. package/dist/services/opening-guides.test.d.ts.map +0 -1
  270. package/dist/services/opening-guides.test.js +0 -208
  271. package/dist/services/port-connectivity.test.d.ts +0 -2
  272. package/dist/services/port-connectivity.test.d.ts.map +0 -1
  273. package/dist/services/port-connectivity.test.js +0 -297
  274. package/dist/services/riser-diagram.test.d.ts +0 -2
  275. package/dist/services/riser-diagram.test.d.ts.map +0 -1
  276. package/dist/services/riser-diagram.test.js +0 -66
  277. package/dist/services/single-undo-dance.test.d.ts +0 -2
  278. package/dist/services/single-undo-dance.test.d.ts.map +0 -1
  279. package/dist/services/single-undo-dance.test.js +0 -210
  280. package/dist/services/snap.test.d.ts +0 -2
  281. package/dist/services/snap.test.d.ts.map +0 -1
  282. package/dist/services/snap.test.js +0 -137
  283. package/dist/services/system-graph.test.d.ts +0 -2
  284. package/dist/services/system-graph.test.d.ts.map +0 -1
  285. package/dist/services/system-graph.test.js +0 -138
  286. package/dist/services/validate-dwv.test.d.ts +0 -2
  287. package/dist/services/validate-dwv.test.d.ts.map +0 -1
  288. package/dist/services/validate-dwv.test.js +0 -109
  289. package/dist/store/actions/node-mutation-sanitize.test.d.ts +0 -2
  290. package/dist/store/actions/node-mutation-sanitize.test.d.ts.map +0 -1
  291. package/dist/store/actions/node-mutation-sanitize.test.js +0 -147
  292. package/dist/store/actions/reparent.test.d.ts +0 -2
  293. package/dist/store/actions/reparent.test.d.ts.map +0 -1
  294. package/dist/store/actions/reparent.test.js +0 -212
  295. package/dist/store/actions/ridge-vent-update.test.d.ts +0 -2
  296. package/dist/store/actions/ridge-vent-update.test.d.ts.map +0 -1
  297. package/dist/store/actions/ridge-vent-update.test.js +0 -264
  298. package/dist/store/use-scene-dirty-tracking.test.d.ts +0 -2
  299. package/dist/store/use-scene-dirty-tracking.test.d.ts.map +0 -1
  300. package/dist/store/use-scene-dirty-tracking.test.js +0 -68
  301. package/dist/store/use-scene-elevator-migration.test.d.ts +0 -2
  302. package/dist/store/use-scene-elevator-migration.test.d.ts.map +0 -1
  303. package/dist/store/use-scene-elevator-migration.test.js +0 -109
  304. package/dist/store/use-scene-measurement-roundtrip.test.d.ts +0 -2
  305. package/dist/store/use-scene-measurement-roundtrip.test.d.ts.map +0 -1
  306. package/dist/store/use-scene-measurement-roundtrip.test.js +0 -97
  307. package/dist/store/use-scene-plugins.test.d.ts +0 -2
  308. package/dist/store/use-scene-plugins.test.d.ts.map +0 -1
  309. package/dist/store/use-scene-plugins.test.js +0 -51
  310. package/dist/store/use-scene-wall-slot-migration.test.d.ts +0 -2
  311. package/dist/store/use-scene-wall-slot-migration.test.d.ts.map +0 -1
  312. package/dist/store/use-scene-wall-slot-migration.test.js +0 -210
  313. package/dist/store/use-scene-window-migration.test.d.ts +0 -2
  314. package/dist/store/use-scene-window-migration.test.d.ts.map +0 -1
  315. package/dist/store/use-scene-window-migration.test.js +0 -85
  316. package/dist/systems/elevator/elevator-opening-sync.test.d.ts +0 -2
  317. package/dist/systems/elevator/elevator-opening-sync.test.d.ts.map +0 -1
  318. package/dist/systems/elevator/elevator-opening-sync.test.js +0 -146
  319. package/dist/systems/elevator/elevator-runtime.test.d.ts +0 -2
  320. package/dist/systems/elevator/elevator-runtime.test.d.ts.map +0 -1
  321. package/dist/systems/elevator/elevator-runtime.test.js +0 -64
  322. package/dist/systems/fence/fence-spline.test.d.ts +0 -2
  323. package/dist/systems/fence/fence-spline.test.d.ts.map +0 -1
  324. package/dist/systems/fence/fence-spline.test.js +0 -136
  325. package/dist/systems/stair/stair-opening-preview.test.d.ts +0 -2
  326. package/dist/systems/stair/stair-opening-preview.test.d.ts.map +0 -1
  327. package/dist/systems/stair/stair-opening-preview.test.js +0 -80
  328. package/dist/systems/stair/stair-opening-sync.test.d.ts +0 -2
  329. package/dist/systems/stair/stair-opening-sync.test.d.ts.map +0 -1
  330. package/dist/systems/stair/stair-opening-sync.test.js +0 -485
  331. package/dist/systems/wall/wall-mitering.test.d.ts +0 -2
  332. package/dist/systems/wall/wall-mitering.test.d.ts.map +0 -1
  333. package/dist/systems/wall/wall-mitering.test.js +0 -63
  334. package/dist/utils/clone-scene-graph.test.d.ts +0 -2
  335. package/dist/utils/clone-scene-graph.test.d.ts.map +0 -1
  336. package/dist/utils/clone-scene-graph.test.js +0 -62
  337. package/dist/utils/heal-scene-graph.test.d.ts +0 -2
  338. package/dist/utils/heal-scene-graph.test.d.ts.map +0 -1
  339. package/dist/utils/heal-scene-graph.test.js +0 -88
@@ -0,0 +1,150 @@
1
+ /**
2
+ * The terrain heightfield: one regular grid of quantized heights, and the
3
+ * only place terrain geometry is read from.
4
+ *
5
+ * Two invariants carry the whole design:
6
+ *
7
+ * 1. **Quantized to `Int16` multiples of `step`.** Heights are integers, so
8
+ * "is this ground flat?" is exact integer equality rather than an epsilon
9
+ * comparison. That is what lets the support election treat terrain as one
10
+ * discrete candidate (see `slab-support.ts`) instead of a float soup that
11
+ * never ties. `step` is metres per unit; at 0.01 the Int16 range covers
12
+ * ±327 m of relief, far past any building site.
13
+ * 2. **Every read goes through `heightAt`.** Consumers never index `heights`
14
+ * directly. One authority means the mesh, the raycast, the placement
15
+ * predicate, and the tests cannot disagree about where the ground is.
16
+ *
17
+ * Pure data — no Three.js, per the core layer rule. The mesh built from this
18
+ * lives in `@pascal-app/nodes`.
19
+ */
20
+ /**
21
+ * A regular grid of heights over the XZ plane.
22
+ *
23
+ * `origin` is the world XZ of sample [0,0]; sample [c,r] sits at
24
+ * `(origin[0] + c * spacing, origin[1] + r * spacing)`. Row-major, so index
25
+ * `r * cols + c`.
26
+ *
27
+ * Sized `2ⁿ+1` per side by convention (33, 65, 129, …) so a future LOD halving
28
+ * lands on existing samples and shares edge vertices with its neighbour rather
29
+ * than interpolating a seam.
30
+ */
31
+ export type TerrainField = {
32
+ /** World XZ of sample [0,0]. */
33
+ readonly origin: readonly [number, number];
34
+ /** Metres between adjacent samples. */
35
+ readonly spacing: number;
36
+ readonly cols: number;
37
+ readonly rows: number;
38
+ /** Metres per `heights` unit — the quantization ladder. */
39
+ readonly step: number;
40
+ /** Row-major quantized heights, length `cols * rows`. */
41
+ readonly heights: Int16Array;
42
+ };
43
+ /** A rectangular sub-block of samples — the unit of every terrain write. */
44
+ export type HeightPatch = {
45
+ readonly col0: number;
46
+ readonly row0: number;
47
+ readonly cols: number;
48
+ readonly rows: number;
49
+ /** Row-major quantized heights, length `cols * rows`. */
50
+ readonly heights: Int16Array;
51
+ };
52
+ export declare const DEFAULT_TERRAIN_STEP = 0.01;
53
+ export declare const DEFAULT_TERRAIN_SPACING = 0.5;
54
+ export declare function createTerrainField(options?: {
55
+ origin?: readonly [number, number];
56
+ spacing?: number;
57
+ cols?: number;
58
+ rows?: number;
59
+ step?: number;
60
+ }): TerrainField;
61
+ /** Quantize a metre height onto the field's ladder, clamped to Int16. */
62
+ export declare function quantize(field: TerrainField, metres: number): number;
63
+ /** Sample [c,r] in metres. Out-of-range indices clamp to the edge, so a
64
+ * consumer reading just past the boundary gets the border height rather than
65
+ * a hole — the same clamp-don't-ask rule the vertical model uses. */
66
+ export declare function heightAtSample(field: TerrainField, col: number, row: number): number;
67
+ /**
68
+ * The ground height in metres at a world XZ, on the rendered triangle plane.
69
+ *
70
+ * This is the single read authority named in the plan. Each cell uses the same
71
+ * `(c+1,r)`–`(c,r+1)` diagonal as `nodes/src/site/terrain-geometry.ts`, so
72
+ * placement, picking, collision, draping, and pixels all answer the same height.
73
+ */
74
+ export declare function heightAt(field: TerrainField, x: number, z: number): number;
75
+ /**
76
+ * Compatibility name for callers that explicitly describe a rendered surface.
77
+ * There is intentionally no second interpolation model: `heightAt` is the
78
+ * rendered triangle plane.
79
+ */
80
+ export declare function surfaceHeightAt(field: TerrainField, x: number, z: number): number;
81
+ /**
82
+ * Surface normal at a world XZ, from analytic central differences.
83
+ *
84
+ * Central differences rather than per-triangle face normals: the field is the
85
+ * authority, so the normal is a property of the *field* and stays continuous
86
+ * across triangle boundaries. Returns a unit vector, +Y up.
87
+ */
88
+ export declare function normalAt(field: TerrainField, x: number, z: number): [number, number, number];
89
+ /** Slope in radians from horizontal at a world XZ. 0 = flat, π/2 = vertical. */
90
+ export declare function slopeAt(field: TerrainField, x: number, z: number): number;
91
+ /**
92
+ * True when every sample under the world-XZ rect is at the identical quantized
93
+ * height — the placement predicate for kinds that need level ground.
94
+ *
95
+ * Exact integer equality, which is only meaningful because heights are
96
+ * quantized (invariant 1). A float field would need a tolerance here, and that
97
+ * tolerance would then have to agree with the support election's own epsilon —
98
+ * two tunables that could drift apart. Instead there is one ladder and no
99
+ * tolerance.
100
+ */
101
+ export declare function isFlatOver(field: TerrainField, minX: number, minZ: number, maxX: number, maxZ: number): boolean;
102
+ /**
103
+ * The one write API. Every terrain mutation — brush stroke, DEM import, scan
104
+ * conversion, preset, MCP tool — lands here.
105
+ *
106
+ * Returns a new field with a new `heights` buffer, so a stroke's before/after
107
+ * pair can be diffed for undo and for `diffToPatches`. Patch samples outside
108
+ * the field are ignored rather than throwing: an imported DEM is allowed to
109
+ * overhang the site, and clipping is the sane response.
110
+ */
111
+ export declare function applyHeightPatch(field: TerrainField, patch: HeightPatch): TerrainField;
112
+ /** The world-XZ sample range a rect covers, clamped to the field. Shared by
113
+ * the brushes so every tool agrees which samples a footprint touches. */
114
+ export declare function sampleRangeOver(field: TerrainField, minX: number, minZ: number, maxX: number, maxZ: number): {
115
+ col0: number;
116
+ row0: number;
117
+ col1: number;
118
+ row1: number;
119
+ } | null;
120
+ /**
121
+ * The only brush in the first slice: set every sample under a world-XZ rect to
122
+ * one absolute height.
123
+ *
124
+ * Absolute-height flatten before raise/lower is deliberate. A raise/lower brush
125
+ * answers "make this taller", which the user cannot aim; flatten-to-height
126
+ * answers "put the ground at 2.5 m here", which is the actual task when siting
127
+ * a building — and it is the one brush whose result is predictable enough to
128
+ * test. Returns null when the rect misses the field entirely.
129
+ */
130
+ export declare function flattenPatch(field: TerrainField, rect: {
131
+ minX: number;
132
+ minZ: number;
133
+ maxX: number;
134
+ maxZ: number;
135
+ }, metres: number): HeightPatch | null;
136
+ /**
137
+ * Split the difference between two fields into patches that each stay under
138
+ * `maxBytes` when serialized.
139
+ *
140
+ * This exists because scene operations are capped at
141
+ * `MAX_SCENE_OPERATION_BYTES` (64 KiB) and each mutation carries both `from`
142
+ * and `to` — so a whole-field write is over budget the moment the grid gets
143
+ * interesting. Rows are the split unit: a patch is always whole rows, which
144
+ * keeps reassembly trivial and means a single-row edit costs one row.
145
+ *
146
+ * Returns the minimal dirty row span, so a small brush stroke on a large field
147
+ * produces a small patch rather than a full-field rewrite.
148
+ */
149
+ export declare function diffToPatches(before: TerrainField, after: TerrainField, maxBytes: number): HeightPatch[];
150
+ //# sourceMappingURL=terrain-field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terrain-field.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-field.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,248 @@
1
+ /**
2
+ * The terrain heightfield: one regular grid of quantized heights, and the
3
+ * only place terrain geometry is read from.
4
+ *
5
+ * Two invariants carry the whole design:
6
+ *
7
+ * 1. **Quantized to `Int16` multiples of `step`.** Heights are integers, so
8
+ * "is this ground flat?" is exact integer equality rather than an epsilon
9
+ * comparison. That is what lets the support election treat terrain as one
10
+ * discrete candidate (see `slab-support.ts`) instead of a float soup that
11
+ * never ties. `step` is metres per unit; at 0.01 the Int16 range covers
12
+ * ±327 m of relief, far past any building site.
13
+ * 2. **Every read goes through `heightAt`.** Consumers never index `heights`
14
+ * directly. One authority means the mesh, the raycast, the placement
15
+ * predicate, and the tests cannot disagree about where the ground is.
16
+ *
17
+ * Pure data — no Three.js, per the core layer rule. The mesh built from this
18
+ * lives in `@pascal-app/nodes`.
19
+ */
20
+ export const DEFAULT_TERRAIN_STEP = 0.01;
21
+ export const DEFAULT_TERRAIN_SPACING = 0.5;
22
+ export function createTerrainField(options) {
23
+ const cols = options?.cols ?? 65;
24
+ const rows = options?.rows ?? 65;
25
+ return {
26
+ origin: options?.origin ?? [0, 0],
27
+ spacing: options?.spacing ?? DEFAULT_TERRAIN_SPACING,
28
+ cols,
29
+ rows,
30
+ step: options?.step ?? DEFAULT_TERRAIN_STEP,
31
+ heights: new Int16Array(cols * rows),
32
+ };
33
+ }
34
+ /** Quantize a metre height onto the field's ladder, clamped to Int16. */
35
+ export function quantize(field, metres) {
36
+ if (!Number.isFinite(metres))
37
+ return 0;
38
+ const raw = Math.round(metres / field.step);
39
+ return Math.max(-32768, Math.min(32767, raw));
40
+ }
41
+ /** Sample [c,r] in metres. Out-of-range indices clamp to the edge, so a
42
+ * consumer reading just past the boundary gets the border height rather than
43
+ * a hole — the same clamp-don't-ask rule the vertical model uses. */
44
+ export function heightAtSample(field, col, row) {
45
+ const c = Math.max(0, Math.min(field.cols - 1, Math.trunc(col)));
46
+ const r = Math.max(0, Math.min(field.rows - 1, Math.trunc(row)));
47
+ return (field.heights[r * field.cols + c] ?? 0) * field.step;
48
+ }
49
+ /**
50
+ * The ground height in metres at a world XZ, on the rendered triangle plane.
51
+ *
52
+ * This is the single read authority named in the plan. Each cell uses the same
53
+ * `(c+1,r)`–`(c,r+1)` diagonal as `nodes/src/site/terrain-geometry.ts`, so
54
+ * placement, picking, collision, draping, and pixels all answer the same height.
55
+ */
56
+ export function heightAt(field, x, z) {
57
+ if (field.cols < 2 || field.rows < 2)
58
+ return heightAtSample(field, 0, 0);
59
+ const u = Math.min(field.cols - 1, Math.max(0, (x - field.origin[0]) / field.spacing));
60
+ const v = Math.min(field.rows - 1, Math.max(0, (z - field.origin[1]) / field.spacing));
61
+ const col = Math.min(field.cols - 2, Math.floor(u));
62
+ const row = Math.min(field.rows - 2, Math.floor(v));
63
+ const tc = u - col;
64
+ const tr = v - row;
65
+ const h00 = heightAtSample(field, col, row);
66
+ const h10 = heightAtSample(field, col + 1, row);
67
+ const h01 = heightAtSample(field, col, row + 1);
68
+ const h11 = heightAtSample(field, col + 1, row + 1);
69
+ if (tc + tr <= 1)
70
+ return h00 + (h10 - h00) * tc + (h01 - h00) * tr;
71
+ return h11 + (h01 - h11) * (1 - tc) + (h10 - h11) * (1 - tr);
72
+ }
73
+ /**
74
+ * Compatibility name for callers that explicitly describe a rendered surface.
75
+ * There is intentionally no second interpolation model: `heightAt` is the
76
+ * rendered triangle plane.
77
+ */
78
+ export function surfaceHeightAt(field, x, z) {
79
+ return heightAt(field, x, z);
80
+ }
81
+ /**
82
+ * Surface normal at a world XZ, from analytic central differences.
83
+ *
84
+ * Central differences rather than per-triangle face normals: the field is the
85
+ * authority, so the normal is a property of the *field* and stays continuous
86
+ * across triangle boundaries. Returns a unit vector, +Y up.
87
+ */
88
+ export function normalAt(field, x, z) {
89
+ const d = field.spacing;
90
+ const dhdx = (heightAt(field, x + d, z) - heightAt(field, x - d, z)) / (2 * d);
91
+ const dhdz = (heightAt(field, x, z + d) - heightAt(field, x, z - d)) / (2 * d);
92
+ // Gradient (dh/dx, dh/dz) → normal (-dh/dx, 1, -dh/dz), normalized.
93
+ const len = Math.hypot(dhdx, 1, dhdz);
94
+ return [-dhdx / len, 1 / len, -dhdz / len];
95
+ }
96
+ /** Slope in radians from horizontal at a world XZ. 0 = flat, π/2 = vertical. */
97
+ export function slopeAt(field, x, z) {
98
+ const d = field.spacing;
99
+ const dhdx = (heightAt(field, x + d, z) - heightAt(field, x - d, z)) / (2 * d);
100
+ const dhdz = (heightAt(field, x, z + d) - heightAt(field, x, z - d)) / (2 * d);
101
+ return Math.atan(Math.hypot(dhdx, dhdz));
102
+ }
103
+ /**
104
+ * True when every sample under the world-XZ rect is at the identical quantized
105
+ * height — the placement predicate for kinds that need level ground.
106
+ *
107
+ * Exact integer equality, which is only meaningful because heights are
108
+ * quantized (invariant 1). A float field would need a tolerance here, and that
109
+ * tolerance would then have to agree with the support election's own epsilon —
110
+ * two tunables that could drift apart. Instead there is one ladder and no
111
+ * tolerance.
112
+ */
113
+ export function isFlatOver(field, minX, minZ, maxX, maxZ) {
114
+ const c0 = Math.max(0, Math.floor((minX - field.origin[0]) / field.spacing));
115
+ const r0 = Math.max(0, Math.floor((minZ - field.origin[1]) / field.spacing));
116
+ const c1 = Math.min(field.cols - 1, Math.ceil((maxX - field.origin[0]) / field.spacing));
117
+ const r1 = Math.min(field.rows - 1, Math.ceil((maxZ - field.origin[1]) / field.spacing));
118
+ if (c1 < c0 || r1 < r0)
119
+ return true;
120
+ const first = field.heights[r0 * field.cols + c0] ?? 0;
121
+ for (let r = r0; r <= r1; r++) {
122
+ const rowBase = r * field.cols;
123
+ for (let c = c0; c <= c1; c++) {
124
+ if ((field.heights[rowBase + c] ?? 0) !== first)
125
+ return false;
126
+ }
127
+ }
128
+ return true;
129
+ }
130
+ /**
131
+ * The one write API. Every terrain mutation — brush stroke, DEM import, scan
132
+ * conversion, preset, MCP tool — lands here.
133
+ *
134
+ * Returns a new field with a new `heights` buffer, so a stroke's before/after
135
+ * pair can be diffed for undo and for `diffToPatches`. Patch samples outside
136
+ * the field are ignored rather than throwing: an imported DEM is allowed to
137
+ * overhang the site, and clipping is the sane response.
138
+ */
139
+ export function applyHeightPatch(field, patch) {
140
+ const heights = new Int16Array(field.heights);
141
+ for (let pr = 0; pr < patch.rows; pr++) {
142
+ const row = patch.row0 + pr;
143
+ if (row < 0 || row >= field.rows)
144
+ continue;
145
+ const destBase = row * field.cols;
146
+ const srcBase = pr * patch.cols;
147
+ for (let pc = 0; pc < patch.cols; pc++) {
148
+ const col = patch.col0 + pc;
149
+ if (col < 0 || col >= field.cols)
150
+ continue;
151
+ heights[destBase + col] = patch.heights[srcBase + pc] ?? 0;
152
+ }
153
+ }
154
+ return { ...field, heights };
155
+ }
156
+ /** The world-XZ sample range a rect covers, clamped to the field. Shared by
157
+ * the brushes so every tool agrees which samples a footprint touches. */
158
+ export function sampleRangeOver(field, minX, minZ, maxX, maxZ) {
159
+ const col0 = Math.max(0, Math.floor((minX - field.origin[0]) / field.spacing));
160
+ const row0 = Math.max(0, Math.floor((minZ - field.origin[1]) / field.spacing));
161
+ const col1 = Math.min(field.cols - 1, Math.ceil((maxX - field.origin[0]) / field.spacing));
162
+ const row1 = Math.min(field.rows - 1, Math.ceil((maxZ - field.origin[1]) / field.spacing));
163
+ if (col1 < col0 || row1 < row0)
164
+ return null;
165
+ return { col0, row0, col1, row1 };
166
+ }
167
+ /**
168
+ * The only brush in the first slice: set every sample under a world-XZ rect to
169
+ * one absolute height.
170
+ *
171
+ * Absolute-height flatten before raise/lower is deliberate. A raise/lower brush
172
+ * answers "make this taller", which the user cannot aim; flatten-to-height
173
+ * answers "put the ground at 2.5 m here", which is the actual task when siting
174
+ * a building — and it is the one brush whose result is predictable enough to
175
+ * test. Returns null when the rect misses the field entirely.
176
+ */
177
+ export function flattenPatch(field, rect, metres) {
178
+ const range = sampleRangeOver(field, rect.minX, rect.minZ, rect.maxX, rect.maxZ);
179
+ if (!range)
180
+ return null;
181
+ const cols = range.col1 - range.col0 + 1;
182
+ const rows = range.row1 - range.row0 + 1;
183
+ const value = quantize(field, metres);
184
+ const heights = new Int16Array(cols * rows);
185
+ heights.fill(value);
186
+ return { col0: range.col0, row0: range.row0, cols, rows, heights };
187
+ }
188
+ /**
189
+ * Split the difference between two fields into patches that each stay under
190
+ * `maxBytes` when serialized.
191
+ *
192
+ * This exists because scene operations are capped at
193
+ * `MAX_SCENE_OPERATION_BYTES` (64 KiB) and each mutation carries both `from`
194
+ * and `to` — so a whole-field write is over budget the moment the grid gets
195
+ * interesting. Rows are the split unit: a patch is always whole rows, which
196
+ * keeps reassembly trivial and means a single-row edit costs one row.
197
+ *
198
+ * Returns the minimal dirty row span, so a small brush stroke on a large field
199
+ * produces a small patch rather than a full-field rewrite.
200
+ */
201
+ export function diffToPatches(before, after, maxBytes) {
202
+ if (before.cols !== after.cols || before.rows !== after.rows) {
203
+ // Shape changed — the whole field is dirty; chunk it wholesale.
204
+ return chunkRows(after, 0, after.rows - 1, maxBytes);
205
+ }
206
+ const patches = [];
207
+ let runStart = -1;
208
+ for (let r = 0; r < after.rows; r++) {
209
+ const base = r * after.cols;
210
+ let dirty = false;
211
+ for (let c = 0; c < after.cols; c++) {
212
+ if (before.heights[base + c] !== after.heights[base + c]) {
213
+ dirty = true;
214
+ break;
215
+ }
216
+ }
217
+ if (dirty && runStart < 0)
218
+ runStart = r;
219
+ if (!dirty && runStart >= 0) {
220
+ patches.push(...chunkRows(after, runStart, r - 1, maxBytes));
221
+ runStart = -1;
222
+ }
223
+ }
224
+ if (runStart >= 0)
225
+ patches.push(...chunkRows(after, runStart, after.rows - 1, maxBytes));
226
+ return patches;
227
+ }
228
+ /** Slice a whole-row span into patches that each fit `maxBytes`. */
229
+ function chunkRows(field, firstRow, lastRow, maxBytes) {
230
+ // 2 bytes per Int16 sample, and the transport carries `from` + `to`, so a
231
+ // row costs twice its raw size. Halving the budget up front keeps the caller
232
+ // from having to know that.
233
+ const bytesPerRow = field.cols * 2 * 2;
234
+ const rowsPerChunk = Math.max(1, Math.floor(maxBytes / Math.max(1, bytesPerRow)));
235
+ const patches = [];
236
+ for (let start = firstRow; start <= lastRow; start += rowsPerChunk) {
237
+ const end = Math.min(lastRow, start + rowsPerChunk - 1);
238
+ const rows = end - start + 1;
239
+ patches.push({
240
+ col0: 0,
241
+ row0: start,
242
+ cols: field.cols,
243
+ rows,
244
+ heights: field.heights.slice(start * field.cols, (end + 1) * field.cols),
245
+ });
246
+ }
247
+ return patches;
248
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Ray → terrain intersection.
3
+ *
4
+ * This is the function that replaces `t = -oy / dy` (the flat-ground plane
5
+ * solve) everywhere the editor asks "where on the ground is the pointer?".
6
+ * It lives in `core` with no Three.js so the 2D view, the tools, the placement
7
+ * coordinator, and the tests all call the same code — the failure mode this
8
+ * avoids is each surface having its own idea of where the ground is, which on
9
+ * flat ground is invisible and on a slope is a perspective-skewed offset that
10
+ * only some tools correct for.
11
+ *
12
+ * Method: march the ray in `spacing`-sized steps, watching the sign of
13
+ * `rayY - terrainY`, then bisect the bracketing interval. Marching (rather than
14
+ * per-triangle intersection over the whole grid) is what keeps this O(path
15
+ * length) instead of O(samples) — a 129² field is 32 768 triangles, and testing
16
+ * all of them per pointer-move is not affordable at 50 FPS.
17
+ *
18
+ * The bisection is exact to `spacing / 2^ITERATIONS`; at 0.5 m spacing and 20
19
+ * iterations that is under half a micrometre, i.e. far below the quantization
20
+ * ladder, so the returned point is on the same rendered triangle plane
21
+ * `heightAt` reports.
22
+ */
23
+ import { type TerrainField } from './terrain-field';
24
+ export type TerrainHit = {
25
+ /** Distance along the (unnormalized) direction vector. */
26
+ readonly t: number;
27
+ readonly x: number;
28
+ readonly y: number;
29
+ readonly z: number;
30
+ };
31
+ /**
32
+ * First intersection of a ray with the terrain surface, or `null` if it misses.
33
+ *
34
+ * `direction` need not be normalized; `t` is in units of `direction`'s length,
35
+ * matching `Ray.at(t)` semantics so a viewer-side caller can pass a three.js ray
36
+ * through unchanged.
37
+ *
38
+ * The march tracks the *sign change* of `rayY - surfaceY` rather than assuming
39
+ * the ray starts above the ground, so a ray fired from inside a hill or from a
40
+ * basement finds the surface it exits through. A ray whose origin is already
41
+ * exactly on the surface returns `t = 0`.
42
+ */
43
+ export declare function raycastTerrain(field: TerrainField, origin: readonly [number, number, number], direction: readonly [number, number, number]): TerrainHit | null;
44
+ //# sourceMappingURL=terrain-raycast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terrain-raycast.d.ts","sourceRoot":"","sources":["../../src/lib/terrain-raycast.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAY,KAAK,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAE7D,MAAM,MAAM,UAAU,GAAG;IACvB,0DAA0D;IAC1D,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;IAClB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAiHD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,YAAY,EACnB,MAAM,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EACzC,SAAS,EAAE,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,GAC3C,UAAU,GAAG,IAAI,CAuDnB"}
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Ray → terrain intersection.
3
+ *
4
+ * This is the function that replaces `t = -oy / dy` (the flat-ground plane
5
+ * solve) everywhere the editor asks "where on the ground is the pointer?".
6
+ * It lives in `core` with no Three.js so the 2D view, the tools, the placement
7
+ * coordinator, and the tests all call the same code — the failure mode this
8
+ * avoids is each surface having its own idea of where the ground is, which on
9
+ * flat ground is invisible and on a slope is a perspective-skewed offset that
10
+ * only some tools correct for.
11
+ *
12
+ * Method: march the ray in `spacing`-sized steps, watching the sign of
13
+ * `rayY - terrainY`, then bisect the bracketing interval. Marching (rather than
14
+ * per-triangle intersection over the whole grid) is what keeps this O(path
15
+ * length) instead of O(samples) — a 129² field is 32 768 triangles, and testing
16
+ * all of them per pointer-move is not affordable at 50 FPS.
17
+ *
18
+ * The bisection is exact to `spacing / 2^ITERATIONS`; at 0.5 m spacing and 20
19
+ * iterations that is under half a micrometre, i.e. far below the quantization
20
+ * ladder, so the returned point is on the same rendered triangle plane
21
+ * `heightAt` reports.
22
+ */
23
+ import { heightAt } from './terrain-field';
24
+ /** Bisection depth. 20 halvings of one cell is sub-micrometre at any sane spacing. */
25
+ const REFINE_ITERATIONS = 20;
26
+ /**
27
+ * How far to march before giving up, in multiples of `spacing`.
28
+ *
29
+ * A ray aimed just above a distant ridge can travel a long way before it dips
30
+ * below the surface, but an unbounded march is a hang risk if a caller passes a
31
+ * near-horizontal ray. The bound is generous relative to any site: at 0.5 m
32
+ * spacing this is 4 km of travel.
33
+ */
34
+ const MAX_STEPS = 8192;
35
+ /**
36
+ * World-XZ bounds of the field, in metres. The march is clipped to this so a ray
37
+ * pointing away from the site terminates immediately instead of stepping
38
+ * MAX_STEPS times through empty space.
39
+ */
40
+ function fieldBounds(field) {
41
+ return {
42
+ minX: field.origin[0],
43
+ minZ: field.origin[1],
44
+ maxX: field.origin[0] + (field.cols - 1) * field.spacing,
45
+ maxZ: field.origin[1] + (field.rows - 1) * field.spacing,
46
+ };
47
+ }
48
+ /**
49
+ * Min/max height in metres, cached per `heights` buffer.
50
+ *
51
+ * The vertical extent is what bounds the march for rays that never leave the
52
+ * field in XZ — a straight-down pointer ray, which is the single most common
53
+ * case, has `dx = dz = 0` and therefore an infinite XZ exit. Without a Y bound
54
+ * such a ray relies on `MAX_STEPS` to terminate, and an *upward* one burns all
55
+ * 8192 steps before returning null.
56
+ *
57
+ * The scan is O(samples), so it is cached on the buffer rather than repeated per
58
+ * ray. `applyHeightPatch` allocates a new `Int16Array` for every mutation, which
59
+ * makes the buffer identity a correct version key: a patched field misses the
60
+ * cache and rescans exactly once, and a stale entry is unreachable. `WeakMap`
61
+ * keeps it from retaining fields past their use.
62
+ */
63
+ const heightRangeCache = new WeakMap();
64
+ function heightRange(field) {
65
+ const cached = heightRangeCache.get(field.heights);
66
+ if (cached)
67
+ return cached;
68
+ let min = 0;
69
+ let max = 0;
70
+ if (field.heights.length > 0) {
71
+ min = field.heights[0] ?? 0;
72
+ max = min;
73
+ for (let i = 1; i < field.heights.length; i++) {
74
+ const h = field.heights[i] ?? 0;
75
+ if (h < min)
76
+ min = h;
77
+ if (h > max)
78
+ max = h;
79
+ }
80
+ }
81
+ const range = { minY: min * field.step, maxY: max * field.step };
82
+ heightRangeCache.set(field.heights, range);
83
+ return range;
84
+ }
85
+ /**
86
+ * Slab-method clip of the ray against the field's bounding box, returning the
87
+ * `[tEnter, tExit]` interval to march. Null when the ray misses the box.
88
+ *
89
+ * All three axes are clipped, not just XZ. The Y slab is what bounds a
90
+ * straight-down ray — `dx = dz = 0` makes both XZ slabs "parallel and inside",
91
+ * so an XZ-only clip returns an infinite exit and the march has to fall back on
92
+ * `MAX_STEPS`. The Y slab is padded because the surface is bilinear between
93
+ * samples and a tangent ray must not be clipped exactly at the extremum.
94
+ */
95
+ function clipToField(field, origin, direction) {
96
+ const bounds = fieldBounds(field);
97
+ const { minY, maxY } = heightRange(field);
98
+ const pad = field.spacing;
99
+ let tEnter = 0;
100
+ let tExit = Number.POSITIVE_INFINITY;
101
+ const axes = [
102
+ [origin[0], direction[0], bounds.minX, bounds.maxX],
103
+ [origin[1], direction[1], minY - pad, maxY + pad],
104
+ [origin[2], direction[2], bounds.minZ, bounds.maxZ],
105
+ ];
106
+ for (const [o, d, lo, hi] of axes) {
107
+ if (Math.abs(d) < 1e-12) {
108
+ // Parallel to this slab: either always inside or never.
109
+ if (o < lo || o > hi)
110
+ return null;
111
+ continue;
112
+ }
113
+ const t1 = (lo - o) / d;
114
+ const t2 = (hi - o) / d;
115
+ tEnter = Math.max(tEnter, Math.min(t1, t2));
116
+ tExit = Math.min(tExit, Math.max(t1, t2));
117
+ if (tEnter > tExit)
118
+ return null;
119
+ }
120
+ return { tEnter, tExit };
121
+ }
122
+ /**
123
+ * First intersection of a ray with the terrain surface, or `null` if it misses.
124
+ *
125
+ * `direction` need not be normalized; `t` is in units of `direction`'s length,
126
+ * matching `Ray.at(t)` semantics so a viewer-side caller can pass a three.js ray
127
+ * through unchanged.
128
+ *
129
+ * The march tracks the *sign change* of `rayY - surfaceY` rather than assuming
130
+ * the ray starts above the ground, so a ray fired from inside a hill or from a
131
+ * basement finds the surface it exits through. A ray whose origin is already
132
+ * exactly on the surface returns `t = 0`.
133
+ */
134
+ export function raycastTerrain(field, origin, direction) {
135
+ const dirLength = Math.hypot(direction[0], direction[1], direction[2]);
136
+ if (dirLength < 1e-12)
137
+ return null;
138
+ const clip = clipToField(field, origin, direction);
139
+ if (!clip)
140
+ return null;
141
+ const { tEnter, tExit } = clip;
142
+ // Step in world-space multiples of one cell, converted into t units. Half a
143
+ // cell, not a full one, so a ray crossing a single-cell spike cannot step over
144
+ // its peak and report the ground beyond it.
145
+ const stepT = field.spacing / 2 / dirLength;
146
+ const at = (t) => ({
147
+ x: origin[0] + direction[0] * t,
148
+ y: origin[1] + direction[1] * t,
149
+ z: origin[2] + direction[2] * t,
150
+ });
151
+ const surfaceGap = (t) => {
152
+ const p = at(t);
153
+ return p.y - heightAt(field, p.x, p.z);
154
+ };
155
+ const hitAt = (t) => {
156
+ const p = at(t);
157
+ return { t, x: p.x, y: heightAt(field, p.x, p.z), z: p.z };
158
+ };
159
+ let prevT = tEnter;
160
+ let prevGap = surfaceGap(tEnter);
161
+ // Entering exactly on the surface counts as a hit — a pointer ray grazing the
162
+ // ground plane must not fall through to the caller's flat-plane fallback.
163
+ if (prevGap === 0)
164
+ return hitAt(tEnter);
165
+ const startedAbove = prevGap > 0;
166
+ for (let step = 1; step <= MAX_STEPS; step++) {
167
+ const t = Math.min(tExit, tEnter + step * stepT);
168
+ const gap = surfaceGap(t);
169
+ // A crossing in either direction is a surface hit.
170
+ if (gap === 0 || gap > 0 !== startedAbove) {
171
+ let lo = prevT;
172
+ let hi = t;
173
+ for (let i = 0; i < REFINE_ITERATIONS; i++) {
174
+ const mid = (lo + hi) / 2;
175
+ if (surfaceGap(mid) > 0 === startedAbove)
176
+ lo = mid;
177
+ else
178
+ hi = mid;
179
+ }
180
+ return hitAt(hi);
181
+ }
182
+ prevT = t;
183
+ prevGap = gap;
184
+ if (t >= tExit)
185
+ break;
186
+ }
187
+ return null;
188
+ }