@react-x11/components 0.7.1 → 0.9.0

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 (354) hide show
  1. package/README.md +295 -51
  2. package/dist/charts/node.d.ts +1 -1
  3. package/dist/charts/node.d.ts.map +1 -1
  4. package/dist/charts/node.js +6 -2
  5. package/dist/charts/node.js.map +1 -1
  6. package/dist/color-picker/ColorPicker.d.ts.map +1 -1
  7. package/dist/color-picker/ColorPicker.js +43 -33
  8. package/dist/color-picker/ColorPicker.js.map +1 -1
  9. package/dist/embed/client.d.ts +48 -3
  10. package/dist/embed/client.d.ts.map +1 -1
  11. package/dist/embed/client.js +79 -5
  12. package/dist/embed/client.js.map +1 -1
  13. package/dist/embed/index.d.ts +1 -1
  14. package/dist/embed/index.d.ts.map +1 -1
  15. package/dist/embed/index.js +1 -1
  16. package/dist/embed/index.js.map +1 -1
  17. package/dist/html/node.d.ts +3 -1
  18. package/dist/html/node.d.ts.map +1 -1
  19. package/dist/html/node.js +7 -5
  20. package/dist/html/node.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/{markdown → internal/markdown}/ast.d.ts +12 -0
  26. package/dist/internal/markdown/ast.d.ts.map +1 -0
  27. package/dist/internal/markdown/ast.js +22 -0
  28. package/dist/internal/markdown/ast.js.map +1 -0
  29. package/dist/internal/markdown/parse.d.ts.map +1 -0
  30. package/dist/{markdown → internal/markdown}/parse.js +112 -22
  31. package/dist/internal/markdown/parse.js.map +1 -0
  32. package/dist/internal/markdown/stringify.d.ts +4 -0
  33. package/dist/internal/markdown/stringify.d.ts.map +1 -0
  34. package/dist/internal/markdown/stringify.js +684 -0
  35. package/dist/internal/markdown/stringify.js.map +1 -0
  36. package/dist/internal/markdown/tags.d.ts.map +1 -0
  37. package/dist/internal/markdown/tags.js.map +1 -0
  38. package/dist/internal/scroll.d.ts +3 -2
  39. package/dist/internal/scroll.d.ts.map +1 -1
  40. package/dist/internal/scroll.js +33 -15
  41. package/dist/internal/scroll.js.map +1 -1
  42. package/dist/internal/units.d.ts +2 -0
  43. package/dist/internal/units.d.ts.map +1 -0
  44. package/dist/internal/units.js +35 -0
  45. package/dist/internal/units.js.map +1 -0
  46. package/dist/internal/window.d.ts.map +1 -1
  47. package/dist/internal/window.js +4 -2
  48. package/dist/internal/window.js.map +1 -1
  49. package/dist/maps/anchors.d.ts +98 -0
  50. package/dist/maps/anchors.d.ts.map +1 -0
  51. package/dist/maps/anchors.js +531 -0
  52. package/dist/maps/anchors.js.map +1 -0
  53. package/dist/maps/controller.d.ts +283 -0
  54. package/dist/maps/controller.d.ts.map +1 -0
  55. package/dist/maps/controller.js +743 -0
  56. package/dist/maps/controller.js.map +1 -0
  57. package/dist/maps/gl/buckets.d.ts +95 -0
  58. package/dist/maps/gl/buckets.d.ts.map +1 -0
  59. package/dist/maps/gl/buckets.js +362 -0
  60. package/dist/maps/gl/buckets.js.map +1 -0
  61. package/dist/maps/gl/build-worker.d.ts +13 -0
  62. package/dist/maps/gl/build-worker.d.ts.map +1 -0
  63. package/dist/maps/gl/build-worker.js +44 -0
  64. package/dist/maps/gl/build-worker.js.map +1 -0
  65. package/dist/maps/gl/color.d.ts +8 -0
  66. package/dist/maps/gl/color.d.ts.map +1 -0
  67. package/dist/maps/gl/color.js +57 -0
  68. package/dist/maps/gl/color.js.map +1 -0
  69. package/dist/maps/gl/cover.d.ts +51 -0
  70. package/dist/maps/gl/cover.d.ts.map +1 -0
  71. package/dist/maps/gl/cover.js +124 -0
  72. package/dist/maps/gl/cover.js.map +1 -0
  73. package/dist/maps/gl/index.d.ts +3 -0
  74. package/dist/maps/gl/index.d.ts.map +1 -0
  75. package/dist/maps/gl/index.js +20 -0
  76. package/dist/maps/gl/index.js.map +1 -0
  77. package/dist/maps/gl/markers.d.ts +28 -0
  78. package/dist/maps/gl/markers.d.ts.map +1 -0
  79. package/dist/maps/gl/markers.js +60 -0
  80. package/dist/maps/gl/markers.js.map +1 -0
  81. package/dist/maps/gl/overlays.d.ts +68 -0
  82. package/dist/maps/gl/overlays.d.ts.map +1 -0
  83. package/dist/maps/gl/overlays.js +300 -0
  84. package/dist/maps/gl/overlays.js.map +1 -0
  85. package/dist/maps/gl/pane.d.ts +12 -0
  86. package/dist/maps/gl/pane.d.ts.map +1 -0
  87. package/dist/maps/gl/pane.js +37 -0
  88. package/dist/maps/gl/pane.js.map +1 -0
  89. package/dist/maps/gl/placement.d.ts +111 -0
  90. package/dist/maps/gl/placement.d.ts.map +1 -0
  91. package/dist/maps/gl/placement.js +567 -0
  92. package/dist/maps/gl/placement.js.map +1 -0
  93. package/dist/maps/gl/renderer.d.ts +352 -0
  94. package/dist/maps/gl/renderer.d.ts.map +1 -0
  95. package/dist/maps/gl/renderer.js +1127 -0
  96. package/dist/maps/gl/renderer.js.map +1 -0
  97. package/dist/maps/gl/shaders.d.ts +120 -0
  98. package/dist/maps/gl/shaders.d.ts.map +1 -0
  99. package/dist/maps/gl/shaders.js +451 -0
  100. package/dist/maps/gl/shaders.js.map +1 -0
  101. package/dist/maps/gl/store.d.ts +115 -0
  102. package/dist/maps/gl/store.d.ts.map +1 -0
  103. package/dist/maps/gl/store.js +469 -0
  104. package/dist/maps/gl/store.js.map +1 -0
  105. package/dist/maps/gl/text.d.ts +154 -0
  106. package/dist/maps/gl/text.d.ts.map +1 -0
  107. package/dist/maps/gl/text.js +382 -0
  108. package/dist/maps/gl/text.js.map +1 -0
  109. package/dist/maps/gl/view.d.ts +54 -0
  110. package/dist/maps/gl/view.d.ts.map +1 -0
  111. package/dist/maps/gl/view.js +974 -0
  112. package/dist/maps/gl/view.js.map +1 -0
  113. package/dist/maps/index.d.ts +1 -1
  114. package/dist/maps/index.d.ts.map +1 -1
  115. package/dist/maps/index.js +216 -52
  116. package/dist/maps/index.js.map +1 -1
  117. package/dist/maps/labels.d.ts +37 -16
  118. package/dist/maps/labels.d.ts.map +1 -1
  119. package/dist/maps/labels.js +188 -119
  120. package/dist/maps/labels.js.map +1 -1
  121. package/dist/maps/node.d.ts +162 -56
  122. package/dist/maps/node.d.ts.map +1 -1
  123. package/dist/maps/node.js +607 -515
  124. package/dist/maps/node.js.map +1 -1
  125. package/dist/maps/overlay.d.ts +72 -9
  126. package/dist/maps/overlay.d.ts.map +1 -1
  127. package/dist/maps/overlay.js +108 -28
  128. package/dist/maps/overlay.js.map +1 -1
  129. package/dist/maps/paint.d.ts +3 -0
  130. package/dist/maps/paint.d.ts.map +1 -1
  131. package/dist/maps/paint.js.map +1 -1
  132. package/dist/maps/proj.d.ts +14 -0
  133. package/dist/maps/proj.d.ts.map +1 -1
  134. package/dist/maps/proj.js +28 -0
  135. package/dist/maps/proj.js.map +1 -1
  136. package/dist/maps/renderer.d.ts +69 -0
  137. package/dist/maps/renderer.d.ts.map +1 -0
  138. package/dist/maps/renderer.js +66 -0
  139. package/dist/maps/renderer.js.map +1 -0
  140. package/dist/maps/sources.d.ts +25 -3
  141. package/dist/maps/sources.d.ts.map +1 -1
  142. package/dist/maps/sources.js +17 -0
  143. package/dist/maps/sources.js.map +1 -1
  144. package/dist/maps/theme.d.ts +19 -0
  145. package/dist/maps/theme.d.ts.map +1 -0
  146. package/dist/maps/theme.js +44 -0
  147. package/dist/maps/theme.js.map +1 -0
  148. package/dist/maps/tiles.d.ts +117 -17
  149. package/dist/maps/tiles.d.ts.map +1 -1
  150. package/dist/maps/tiles.js +163 -30
  151. package/dist/maps/tiles.js.map +1 -1
  152. package/dist/maps/types.d.ts +188 -20
  153. package/dist/maps/types.d.ts.map +1 -1
  154. package/dist/markdown/expressions.d.ts +1 -1
  155. package/dist/markdown/expressions.d.ts.map +1 -1
  156. package/dist/markdown/expressions.js.map +1 -1
  157. package/dist/markdown/index.d.ts +7 -6
  158. package/dist/markdown/index.d.ts.map +1 -1
  159. package/dist/markdown/index.js +9 -7
  160. package/dist/markdown/index.js.map +1 -1
  161. package/dist/markdown/spans.d.ts +1 -1
  162. package/dist/markdown/spans.d.ts.map +1 -1
  163. package/dist/media-player/index.d.ts +11 -3
  164. package/dist/media-player/index.d.ts.map +1 -1
  165. package/dist/media-player/index.js +13 -2
  166. package/dist/media-player/index.js.map +1 -1
  167. package/dist/rich-text-editor/clipboard.d.ts +23 -0
  168. package/dist/rich-text-editor/clipboard.d.ts.map +1 -0
  169. package/dist/rich-text-editor/clipboard.js +307 -0
  170. package/dist/rich-text-editor/clipboard.js.map +1 -0
  171. package/dist/rich-text-editor/collab.d.ts +32 -0
  172. package/dist/rich-text-editor/collab.d.ts.map +1 -0
  173. package/dist/rich-text-editor/collab.js +55 -0
  174. package/dist/rich-text-editor/collab.js.map +1 -0
  175. package/dist/rich-text-editor/commands.d.ts +50 -0
  176. package/dist/rich-text-editor/commands.d.ts.map +1 -0
  177. package/dist/rich-text-editor/commands.js +376 -0
  178. package/dist/rich-text-editor/commands.js.map +1 -0
  179. package/dist/rich-text-editor/drag.d.ts +68 -0
  180. package/dist/rich-text-editor/drag.d.ts.map +1 -0
  181. package/dist/rich-text-editor/drag.js +148 -0
  182. package/dist/rich-text-editor/drag.js.map +1 -0
  183. package/dist/rich-text-editor/html.d.ts +145 -0
  184. package/dist/rich-text-editor/html.d.ts.map +1 -0
  185. package/dist/rich-text-editor/html.js +516 -0
  186. package/dist/rich-text-editor/html.js.map +1 -0
  187. package/dist/rich-text-editor/index.d.ts +263 -0
  188. package/dist/rich-text-editor/index.d.ts.map +1 -0
  189. package/dist/rich-text-editor/index.js +822 -0
  190. package/dist/rich-text-editor/index.js.map +1 -0
  191. package/dist/rich-text-editor/inline.d.ts +94 -0
  192. package/dist/rich-text-editor/inline.d.ts.map +1 -0
  193. package/dist/rich-text-editor/inline.js +252 -0
  194. package/dist/rich-text-editor/inline.js.map +1 -0
  195. package/dist/rich-text-editor/keymap.d.ts +71 -0
  196. package/dist/rich-text-editor/keymap.d.ts.map +1 -0
  197. package/dist/rich-text-editor/keymap.js +393 -0
  198. package/dist/rich-text-editor/keymap.js.map +1 -0
  199. package/dist/rich-text-editor/keys.d.ts +29 -0
  200. package/dist/rich-text-editor/keys.d.ts.map +1 -0
  201. package/dist/rich-text-editor/keys.js +93 -0
  202. package/dist/rich-text-editor/keys.js.map +1 -0
  203. package/dist/rich-text-editor/look.d.ts +71 -0
  204. package/dist/rich-text-editor/look.d.ts.map +1 -0
  205. package/dist/rich-text-editor/look.js +342 -0
  206. package/dist/rich-text-editor/look.js.map +1 -0
  207. package/dist/rich-text-editor/markdown.d.ts +18 -0
  208. package/dist/rich-text-editor/markdown.d.ts.map +1 -0
  209. package/dist/rich-text-editor/markdown.js +496 -0
  210. package/dist/rich-text-editor/markdown.js.map +1 -0
  211. package/dist/rich-text-editor/nodes.d.ts +135 -0
  212. package/dist/rich-text-editor/nodes.d.ts.map +1 -0
  213. package/dist/rich-text-editor/nodes.js +397 -0
  214. package/dist/rich-text-editor/nodes.js.map +1 -0
  215. package/dist/rich-text-editor/render.d.ts +68 -0
  216. package/dist/rich-text-editor/render.d.ts.map +1 -0
  217. package/dist/rich-text-editor/render.js +500 -0
  218. package/dist/rich-text-editor/render.js.map +1 -0
  219. package/dist/rich-text-editor/schema.d.ts +16 -0
  220. package/dist/rich-text-editor/schema.d.ts.map +1 -0
  221. package/dist/rich-text-editor/schema.js +386 -0
  222. package/dist/rich-text-editor/schema.js.map +1 -0
  223. package/dist/rich-text-editor/store.d.ts +11 -0
  224. package/dist/rich-text-editor/store.d.ts.map +1 -0
  225. package/dist/rich-text-editor/store.js +43 -0
  226. package/dist/rich-text-editor/store.js.map +1 -0
  227. package/dist/rich-text-editor/suggest.d.ts +127 -0
  228. package/dist/rich-text-editor/suggest.d.ts.map +1 -0
  229. package/dist/rich-text-editor/suggest.js +0 -0
  230. package/dist/rich-text-editor/suggest.js.map +1 -0
  231. package/dist/rich-text-editor/tables.d.ts +41 -0
  232. package/dist/rich-text-editor/tables.d.ts.map +1 -0
  233. package/dist/rich-text-editor/tables.js +204 -0
  234. package/dist/rich-text-editor/tables.js.map +1 -0
  235. package/dist/rich-text-editor/toolbar.d.ts +58 -0
  236. package/dist/rich-text-editor/toolbar.d.ts.map +1 -0
  237. package/dist/rich-text-editor/toolbar.js +548 -0
  238. package/dist/rich-text-editor/toolbar.js.map +1 -0
  239. package/dist/rich-text-editor/view.d.ts +368 -0
  240. package/dist/rich-text-editor/view.d.ts.map +1 -0
  241. package/dist/rich-text-editor/view.js +1968 -0
  242. package/dist/rich-text-editor/view.js.map +1 -0
  243. package/dist/rich-text-editor/virtual.d.ts +54 -0
  244. package/dist/rich-text-editor/virtual.d.ts.map +1 -0
  245. package/dist/rich-text-editor/virtual.js +262 -0
  246. package/dist/rich-text-editor/virtual.js.map +1 -0
  247. package/dist/richtext/node.d.ts +25 -2
  248. package/dist/richtext/node.d.ts.map +1 -1
  249. package/dist/richtext/node.js +46 -24
  250. package/dist/richtext/node.js.map +1 -1
  251. package/dist/table/index.d.ts.map +1 -1
  252. package/dist/table/index.js +14 -5
  253. package/dist/table/index.js.map +1 -1
  254. package/dist/terminal/backends.d.ts +4 -1
  255. package/dist/terminal/backends.d.ts.map +1 -1
  256. package/dist/terminal/backends.js.map +1 -1
  257. package/dist/terminal/index.d.ts +18 -7
  258. package/dist/terminal/index.d.ts.map +1 -1
  259. package/dist/terminal/index.js +28 -11
  260. package/dist/terminal/index.js.map +1 -1
  261. package/dist/terminal/vt/node.d.ts +56 -3
  262. package/dist/terminal/vt/node.d.ts.map +1 -1
  263. package/dist/terminal/vt/node.js +106 -26
  264. package/dist/terminal/vt/node.js.map +1 -1
  265. package/dist/terminal/vt/renderer.d.ts +4 -0
  266. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  267. package/dist/terminal/vt/renderer.js +14 -5
  268. package/dist/terminal/vt/renderer.js.map +1 -1
  269. package/dist/tree/index.d.ts.map +1 -1
  270. package/dist/tree/index.js +6 -4
  271. package/dist/tree/index.js.map +1 -1
  272. package/package.json +23 -4
  273. package/src/charts/node.ts +6 -2
  274. package/src/color-picker/ColorPicker.ts +107 -81
  275. package/src/embed/client.ts +102 -8
  276. package/src/embed/index.ts +2 -0
  277. package/src/html/node.ts +7 -5
  278. package/src/index.ts +2 -0
  279. package/src/{markdown → internal/markdown}/ast.ts +24 -7
  280. package/src/{markdown → internal/markdown}/parse.ts +134 -24
  281. package/src/internal/markdown/stringify.ts +772 -0
  282. package/src/internal/scroll.ts +39 -18
  283. package/src/internal/units.ts +34 -0
  284. package/src/internal/window.ts +4 -2
  285. package/src/maps/anchors.ts +607 -0
  286. package/src/maps/controller.ts +999 -0
  287. package/src/maps/gl/buckets.ts +457 -0
  288. package/src/maps/gl/build-worker.ts +55 -0
  289. package/src/maps/gl/color.ts +62 -0
  290. package/src/maps/gl/cover.ts +170 -0
  291. package/src/maps/gl/index.ts +23 -0
  292. package/src/maps/gl/markers.ts +83 -0
  293. package/src/maps/gl/overlays.ts +379 -0
  294. package/src/maps/gl/pane.ts +42 -0
  295. package/src/maps/gl/placement.ts +816 -0
  296. package/src/maps/gl/renderer.ts +1576 -0
  297. package/src/maps/gl/shaders.ts +486 -0
  298. package/src/maps/gl/store.ts +566 -0
  299. package/src/maps/gl/text.ts +495 -0
  300. package/src/maps/gl/view.ts +1224 -0
  301. package/src/maps/index.ts +270 -81
  302. package/src/maps/labels.ts +214 -121
  303. package/src/maps/node.ts +763 -629
  304. package/src/maps/overlay.ts +148 -43
  305. package/src/maps/paint.ts +3 -0
  306. package/src/maps/proj.ts +31 -0
  307. package/src/maps/renderer.ts +155 -0
  308. package/src/maps/sources.ts +38 -3
  309. package/src/maps/theme.ts +51 -0
  310. package/src/maps/tiles.ts +191 -33
  311. package/src/maps/types.ts +188 -20
  312. package/src/markdown/expressions.ts +5 -1
  313. package/src/markdown/index.ts +14 -11
  314. package/src/markdown/spans.ts +1 -1
  315. package/src/media-player/index.ts +18 -4
  316. package/src/rich-text-editor/clipboard.ts +439 -0
  317. package/src/rich-text-editor/collab.ts +91 -0
  318. package/src/rich-text-editor/commands.ts +448 -0
  319. package/src/rich-text-editor/dom-globals.d.ts +23 -0
  320. package/src/rich-text-editor/drag.ts +211 -0
  321. package/src/rich-text-editor/html.ts +619 -0
  322. package/src/rich-text-editor/index.ts +1331 -0
  323. package/src/rich-text-editor/inline.ts +345 -0
  324. package/src/rich-text-editor/keymap.ts +510 -0
  325. package/src/rich-text-editor/keys.ts +119 -0
  326. package/src/rich-text-editor/look.ts +427 -0
  327. package/src/rich-text-editor/markdown.ts +653 -0
  328. package/src/rich-text-editor/nodes.ts +497 -0
  329. package/src/rich-text-editor/render.ts +800 -0
  330. package/src/rich-text-editor/schema.ts +431 -0
  331. package/src/rich-text-editor/store.ts +39 -0
  332. package/src/rich-text-editor/suggest.ts +597 -0
  333. package/src/rich-text-editor/tables.ts +272 -0
  334. package/src/rich-text-editor/toolbar.ts +724 -0
  335. package/src/rich-text-editor/view.ts +2287 -0
  336. package/src/rich-text-editor/virtual.ts +347 -0
  337. package/src/richtext/node.ts +59 -30
  338. package/src/table/index.ts +14 -5
  339. package/src/terminal/backends.ts +4 -1
  340. package/src/terminal/index.ts +43 -15
  341. package/src/terminal/vt/node.ts +128 -29
  342. package/src/terminal/vt/renderer.ts +27 -5
  343. package/src/tree/index.ts +6 -4
  344. package/dist/markdown/ast.d.ts.map +0 -1
  345. package/dist/markdown/ast.js +0 -17
  346. package/dist/markdown/ast.js.map +0 -1
  347. package/dist/markdown/parse.d.ts.map +0 -1
  348. package/dist/markdown/parse.js.map +0 -1
  349. package/dist/markdown/tags.d.ts.map +0 -1
  350. package/dist/markdown/tags.js.map +0 -1
  351. /package/dist/{markdown → internal/markdown}/parse.d.ts +0 -0
  352. /package/dist/{markdown → internal/markdown}/tags.d.ts +0 -0
  353. /package/dist/{markdown → internal/markdown}/tags.js +0 -0
  354. /package/src/{markdown → internal/markdown}/tags.ts +0 -0
@@ -0,0 +1,816 @@
1
+ // Labels, the frame's half: which of the anchors the view's tiles offer are
2
+ // drawn, and how visibly.
3
+ //
4
+ // Placement is **per frame and in screen space** — where the retained
5
+ // renderer's is per zoom level and in world pixels, because it blits a pan
6
+ // and cannot afford a placement that changes with the viewport. This one
7
+ // redraws every frame anyway, and two of the things that make a label
8
+ // unobtrusive need the viewport: a label is drawn whole or not at all (never
9
+ // cut by the pane's edge), and of a street's candidate places the ones in
10
+ // view compete. What keeps a per-frame placement from being a per-frame
11
+ // reshuffle is the rest of this file:
12
+ //
13
+ // - **A label keeps its place.** What is shown is offered again first, at
14
+ // the ground it was placed on, with a little slack — a smaller margin, a
15
+ // shorter run is enough — so that two labels at a margin's width never
16
+ // trade places frame to frame. It leaves when it has to: it no longer
17
+ // fits its street at this zoom, it would be cut by the edge, or something
18
+ // that outranks it arrives.
19
+ // - **Labels fade**, in and out, over {@link FADE_MS}. A label never pops;
20
+ // one whose raster is still being set waits, invisibly, and fades in
21
+ // when it lands.
22
+ // - **Labels do not slide.** Anchors are fixed ground, so a label moves
23
+ // exactly with the map; when a street's label leaves the view another
24
+ // anchor of it can take over, rather than one label chasing the view.
25
+ //
26
+ // The order is the map's, and deterministic: a layer's rank, then the
27
+ // feature's importance (road class, population), then what is shown, then —
28
+ // among one street's anchors — one whose label clears every intersection,
29
+ // the longest straight run, the one nearest the middle of the view.
30
+ import type { PreparedStyle } from '../paint.js';
31
+ import { resolveZoomed } from '../style.js';
32
+ import type { SymbolLayer } from '../style.js';
33
+ import { TILE_EXTENT } from './buckets.js';
34
+ import { parseColor, premultiplied } from './color.js';
35
+ import type { Rgba } from './color.js';
36
+ import {
37
+ LABEL_STRIDE,
38
+ LEVEL_SNAP,
39
+ LINE_MARGIN,
40
+ LabelField,
41
+ STRAIGHT_FRACTION,
42
+ estimateWidth,
43
+ } from '../anchors.js';
44
+ import type { GlLabelData } from '../anchors.js';
45
+ import { haloReach } from './text.js';
46
+ import type { AtlasEntry, LabelAtlas } from './text.js';
47
+
48
+ /** A rectangle in device pixels. */
49
+ interface Rect {
50
+ x: number;
51
+ y: number;
52
+ width: number;
53
+ height: number;
54
+ }
55
+
56
+ /** One tile's anchors, placed on screen as the frame draws the tile — a
57
+ * frame's `RenderTile` as it is. */
58
+ export interface LabelTile {
59
+ data: { labels?: GlLabelData };
60
+ /** The tile square's top-left and edge, device pixels. */
61
+ x: number;
62
+ y: number;
63
+ size: number;
64
+ /** The part of the view the tile stands for. An anchor outside it is
65
+ * another tile's — how a stand-in ancestor offers no duplicates. */
66
+ clip: Rect;
67
+ }
68
+
69
+ export interface PlacementFrame {
70
+ /** Device pixels. */
71
+ width: number;
72
+ height: number;
73
+ scale: number;
74
+ zoom: number;
75
+ style: PreparedStyle;
76
+ tiles: readonly LabelTile[];
77
+ /** The view's centre, normalised mercator, and the world's width in
78
+ * device pixels at this zoom. */
79
+ centerX: number;
80
+ centerY: number;
81
+ world: number;
82
+ now: number;
83
+ /** Whether labels not already shown may be placed. `view.ts` says no
84
+ * while the zoom is changing: what is shown rides along and leaves as it
85
+ * must, and what the zoom uncovers is named once it stops — not labels
86
+ * arriving and leaving at every step of it. `true` by default. */
87
+ admit?: boolean;
88
+ /** The camera is moving: a label keeps the raster it has, rather than
89
+ * asking for one at each size its zoom ramp passes through. */
90
+ moving?: boolean;
91
+ }
92
+
93
+ /** Floats per label instance; the layout `renderer.ts` binds. */
94
+ export const LABEL_INSTANCE = 18;
95
+
96
+ /** The frame's labels, ready to draw. */
97
+ export interface LabelBatch {
98
+ atlas: LabelAtlas;
99
+ /** {@link LABEL_INSTANCE} floats per label: centre x, y (device
100
+ * pixels); cos, sin of the baseline; the atlas rect x, y, w, h; ink
101
+ * and halo colour, premultiplied and faded; halo radius; `1` to set
102
+ * the label on whole pixels. */
103
+ instances: Float32Array;
104
+ count: number;
105
+ }
106
+
107
+ export interface PlacementStats {
108
+ /** Anchors considered, labels placed, labels drawn (fading ones too). */
109
+ candidates: number;
110
+ placed: number;
111
+ drawn: number;
112
+ /** Milliseconds this thread spent placing. */
113
+ ms: number;
114
+ }
115
+
116
+ /** Logical pixels between two labels, and the extra a newcomer needs. The
117
+ * extra is the hysteresis: the label already shown wins a tie by it. */
118
+ const PADDING = 4;
119
+ const NEW_PADDING = 3;
120
+ /** Logical pixels a label keeps from the view's edge. */
121
+ const EDGE = 6;
122
+ /** How long a label takes to appear or leave. */
123
+ export const FADE_MS = 220;
124
+ /** How near a new anchor must be, in label heights, to a shown label of
125
+ * the same name to be that label — across a level change, say. */
126
+ const MATCH = 2;
127
+ /** The collision grid's cell, device pixels. */
128
+ const CELL = 64;
129
+
130
+ interface LayerLabels {
131
+ id: string;
132
+ rank: number;
133
+ /** Device pixels, whole — the raster's size. */
134
+ size: number;
135
+ repeat: number;
136
+ ink: Rgba;
137
+ halo: Rgba | null;
138
+ haloPx: number;
139
+ }
140
+
141
+ /** A label on screen, or fading. Its ground is mercator, so it outlives
142
+ * the tile that offered it — a level change keeps it where it was. */
143
+ interface Shown {
144
+ key: string;
145
+ text: string;
146
+ layer: number;
147
+ size: number;
148
+ mx: number;
149
+ my: number;
150
+ angle: number;
151
+ point: boolean;
152
+ /** The anchor's run either side, intersection distance, run length and
153
+ * departure from straight — mercator units, re-judged at each zoom. */
154
+ avail: number;
155
+ clear: number;
156
+ length: number;
157
+ dev: number;
158
+ priority: number;
159
+ opacity: number;
160
+ placed: boolean;
161
+ /** The size it has a raster at — drawn until the current size's lands. */
162
+ drawn: number;
163
+ }
164
+
165
+ interface Candidate {
166
+ key: string;
167
+ text: string;
168
+ layer: number;
169
+ rank: number;
170
+ priority: number;
171
+ size: number;
172
+ point: boolean;
173
+ sx: number;
174
+ sy: number;
175
+ angle: number;
176
+ cos: number;
177
+ sin: number;
178
+ /** Device pixels. */
179
+ avail: number;
180
+ clear: number;
181
+ length: number;
182
+ /** Device pixels per mercator unit's worth of this candidate's lengths:
183
+ * what turns them back into ground for a {@link Shown}. */
184
+ toMercator: number;
185
+ rawAvail: number;
186
+ rawClear: number;
187
+ rawLength: number;
188
+ rawDev: number;
189
+ mx: number;
190
+ my: number;
191
+ shown: Shown | null;
192
+ clean: number;
193
+ centre: number;
194
+ repeat: number;
195
+ }
196
+
197
+ /**
198
+ * The frame's label state: what is shown, how visibly, and the collision
199
+ * index a pass builds.
200
+ */
201
+ export class LabelPlacer {
202
+ private readonly _shown = new Set<Shown>();
203
+ private _style: PreparedStyle | null = null;
204
+ /** The last batch's time; null before the first, which advances no fade. */
205
+ private _last: number | null = null;
206
+ private readonly _grid = new CollisionGrid();
207
+ private readonly _colors = new Map<string, Rgba | null>();
208
+ /** Estimated widths in ems, by text — a street name is asked about
209
+ * once per anchor per frame. */
210
+ private readonly _ems = new Map<string, number>();
211
+ private _instances = new Float32Array(LABEL_INSTANCE * 64);
212
+ stats: PlacementStats = { candidates: 0, placed: 0, drawn: 0, ms: 0 };
213
+
214
+ /** Labels still changing visibility: another frame is worth drawing. */
215
+ get animating(): boolean {
216
+ for (const shown of this._shown) {
217
+ if (shown.placed ? shown.opacity < 1 : shown.opacity > 0) return true;
218
+ }
219
+ return false;
220
+ }
221
+
222
+ /** Forget everything shown — the style's layers are not the same ones. */
223
+ reset(): void {
224
+ this._shown.clear();
225
+ }
226
+
227
+ /**
228
+ * Choose the frame's labels. Asks `atlas` for each contender's box, as
229
+ * the frame's budget allows, and for each winner's raster.
230
+ */
231
+ place(frame: PlacementFrame, atlas: LabelAtlas): void {
232
+ const started = now();
233
+ const { width, height, scale, zoom, style } = frame;
234
+ if (style !== this._style) {
235
+ this._shown.clear();
236
+ this._style = style;
237
+ }
238
+ const layers = this._layersAt(style, zoom, scale);
239
+ const candidates: Candidate[] = [];
240
+ const halfW = width / 2;
241
+ const halfH = height / 2;
242
+
243
+ // What is shown, first — at the ground it was placed on, for as long as
244
+ // the map still names it there: the tile under it must carry the name.
245
+ // A label is not its tile's (a level change keeps it), but it is the
246
+ // data's — a new source, a filter, a text field takes it off the map.
247
+ const named = (text: string, sx: number, sy: number): boolean => {
248
+ let covered = false;
249
+ for (const tile of frame.tiles) {
250
+ const clip = tile.clip;
251
+ if (
252
+ sx < clip.x ||
253
+ sy < clip.y ||
254
+ sx >= clip.x + clip.width ||
255
+ sy >= clip.y + clip.height
256
+ ) {
257
+ continue;
258
+ }
259
+ covered = true;
260
+ const data = tile.data.labels;
261
+ if (data && textsOf(data).has(text)) return true;
262
+ }
263
+ // Nothing drawn there yet: no tile has said otherwise.
264
+ return !covered;
265
+ };
266
+ const byKey = new Map<string, Shown[]>();
267
+ for (const shown of this._shown) {
268
+ const layer = layers[shown.layer];
269
+ if (!layer) continue;
270
+ const list = byKey.get(shown.key);
271
+ if (list) list.push(shown);
272
+ else byKey.set(shown.key, [shown]);
273
+ if (!shown.placed) continue;
274
+ let dx = shown.mx - frame.centerX;
275
+ dx -= Math.round(dx);
276
+ const sx = halfW + dx * frame.world;
277
+ const sy = halfH + (shown.my - frame.centerY) * frame.world;
278
+ if (!named(shown.text, sx, sy)) continue;
279
+ const c = this._candidate(
280
+ shown.key,
281
+ shown.text,
282
+ shown.layer,
283
+ layer,
284
+ shown.priority,
285
+ shown.point,
286
+ sx,
287
+ sy,
288
+ shown.angle,
289
+ shown.avail,
290
+ shown.clear,
291
+ shown.length,
292
+ shown.dev,
293
+ frame.world,
294
+ frame,
295
+ );
296
+ if (c) {
297
+ c.shown = shown;
298
+ candidates.push(c);
299
+ }
300
+ }
301
+
302
+ // Every anchor the view's tiles offer — unless nothing new is admitted.
303
+ const offered = frame.admit === false ? [] : frame.tiles;
304
+ for (const tile of offered) {
305
+ const data = tile.data.labels;
306
+ if (!data || data.count === 0) continue;
307
+ const ppu = tile.size / TILE_EXTENT;
308
+ const clip = tile.clip;
309
+ const a = data.anchors;
310
+ for (let i = 0; i < data.count; i++) {
311
+ const at = i * LABEL_STRIDE;
312
+ const index = a[at + LabelField.layer];
313
+ const layer = layers[index];
314
+ if (!layer) continue;
315
+ const sx = tile.x + a[at + LabelField.x] * ppu;
316
+ const sy = tile.y + a[at + LabelField.y] * ppu;
317
+ if (
318
+ sx < clip.x ||
319
+ sy < clip.y ||
320
+ sx >= clip.x + clip.width ||
321
+ sy >= clip.y + clip.height
322
+ ) {
323
+ continue;
324
+ }
325
+ const text = data.texts[a[at + LabelField.text]];
326
+ const avail = a[at + LabelField.avail];
327
+ const c = this._candidate(
328
+ `${layer.id}|${text}`,
329
+ text,
330
+ index,
331
+ layer,
332
+ a[at + LabelField.priority],
333
+ avail < 0,
334
+ sx,
335
+ sy,
336
+ a[at + LabelField.angle],
337
+ avail,
338
+ a[at + LabelField.clear],
339
+ a[at + LabelField.length],
340
+ a[at + LabelField.dev],
341
+ ppu,
342
+ frame,
343
+ );
344
+ if (c) candidates.push(c);
345
+ }
346
+ }
347
+
348
+ candidates.sort(compare);
349
+
350
+ // Greedy, in that order: a label is placed if it fits its street, is
351
+ // whole inside the view, is not a repeat of one just placed, and
352
+ // overlaps nothing placed before it.
353
+ const grid = this._grid;
354
+ grid.reset();
355
+ const placedAt = new Map<string, number[]>();
356
+ const winners: { c: Candidate; width: number; height: number }[] = [];
357
+ const inset = EDGE * scale;
358
+ for (const c of candidates) {
359
+ const repeatAt = placedAt.get(c.key);
360
+ if (repeatAt) {
361
+ const limit = c.repeat * c.repeat;
362
+ let near = false;
363
+ for (let r = 0; r < repeatAt.length; r += 2) {
364
+ const dx = repeatAt[r] - c.sx;
365
+ const dy = repeatAt[r + 1] - c.sy;
366
+ if (dx * dx + dy * dy < limit) {
367
+ near = true;
368
+ break;
369
+ }
370
+ }
371
+ if (near) continue;
372
+ }
373
+ const box = atlas.measure(c.text, c.size);
374
+ if (!box || box.width === 0) continue;
375
+ const w = box.width;
376
+ const h = box.height;
377
+ if (!c.point) {
378
+ const need = w / 2 + h * LINE_MARGIN;
379
+ if (c.avail < (c.shown ? need * 0.9 : need)) continue;
380
+ }
381
+ const hw = (Math.abs(c.cos) * w + Math.abs(c.sin) * h) / 2;
382
+ const hh = (Math.abs(c.sin) * w + Math.abs(c.cos) * h) / 2;
383
+ if (
384
+ c.sx - hw < inset ||
385
+ c.sy - hh < inset ||
386
+ c.sx + hw > width - inset ||
387
+ c.sy + hh > height - inset
388
+ ) {
389
+ continue;
390
+ }
391
+ const testPad = (PADDING / 2 + (c.shown ? 0 : NEW_PADDING)) * scale;
392
+ if (grid.hits(c, w, h, testPad)) continue;
393
+ grid.insert(c, w, h, (PADDING / 2) * scale);
394
+ winners.push({ c, width: w, height: h });
395
+ if (repeatAt) repeatAt.push(c.sx, c.sy);
396
+ else placedAt.set(c.key, [c.sx, c.sy]);
397
+ }
398
+
399
+ // What won becomes what is shown: the label it already was, one of the
400
+ // same name close by that is fading out, or a new one.
401
+ for (const shown of this._shown) shown.placed = false;
402
+ for (const { c, height } of winners) {
403
+ let shown = c.shown;
404
+ if (!shown) {
405
+ const reach = MATCH * height;
406
+ for (const other of byKey.get(c.key) ?? []) {
407
+ if (other.placed) continue;
408
+ let dx = other.mx - c.mx;
409
+ dx -= Math.round(dx);
410
+ const dist = Math.hypot(dx, other.my - c.my) * frame.world;
411
+ if (dist <= reach) {
412
+ shown = other;
413
+ break;
414
+ }
415
+ }
416
+ if (!shown || shown.opacity <= 0) {
417
+ // A label nobody can see moves to the anchor that won; one that
418
+ // can be seen keeps its ground and is judged there next pass.
419
+ const fresh =
420
+ shown ??
421
+ ({
422
+ key: c.key,
423
+ text: c.text,
424
+ layer: c.layer,
425
+ size: c.size,
426
+ opacity: 0,
427
+ placed: false,
428
+ drawn: 0,
429
+ } as Shown);
430
+ fresh.mx = c.mx;
431
+ fresh.my = c.my;
432
+ fresh.angle = c.angle;
433
+ fresh.point = c.point;
434
+ fresh.avail = c.rawAvail * c.toMercator;
435
+ fresh.clear = c.rawClear * c.toMercator;
436
+ fresh.length = c.rawLength * c.toMercator;
437
+ fresh.dev = c.rawDev * c.toMercator;
438
+ fresh.priority = c.priority;
439
+ shown = fresh;
440
+ this._shown.add(shown);
441
+ }
442
+ }
443
+ shown.placed = true;
444
+ shown.size = c.size;
445
+ if (!frame.moving || shown.drawn === 0)
446
+ atlas.entry(shown.text, shown.size);
447
+ }
448
+
449
+ this.stats = {
450
+ candidates: candidates.length,
451
+ placed: winners.length,
452
+ drawn: this.stats.drawn,
453
+ ms: now() - started,
454
+ };
455
+ }
456
+
457
+ /**
458
+ * The frame's instances: every shown label at its ground's place on
459
+ * screen, faded by where it is in its fade. Advances the fades, and lets
460
+ * go of labels that have faded out.
461
+ */
462
+ batch(frame: PlacementFrame, atlas: LabelAtlas): LabelBatch {
463
+ const dt = this._last === null ? 0 : Math.max(0, frame.now - this._last);
464
+ this._last = frame.now;
465
+ const step = dt / FADE_MS;
466
+ const layers = this._layersAt(frame.style, frame.zoom, frame.scale);
467
+ const reach = haloReach(atlas.pad);
468
+ const halfW = frame.width / 2;
469
+ const halfH = frame.height / 2;
470
+ let count = 0;
471
+ for (const shown of [...this._shown]) {
472
+ const layer = layers[shown.layer];
473
+ let entry: AtlasEntry | null;
474
+ if (frame.moving && shown.drawn > 0 && shown.drawn !== shown.size) {
475
+ // Its zoom ramp has moved on; the size it has serves until the
476
+ // view is still.
477
+ entry = atlas.entry(shown.text, shown.drawn);
478
+ } else {
479
+ entry = atlas.entry(shown.text, shown.size);
480
+ if (entry) shown.drawn = shown.size;
481
+ else if (shown.drawn > 0) entry = atlas.entry(shown.text, shown.drawn);
482
+ }
483
+ // Nothing to show yet: wait at zero rather than fade in unseen.
484
+ if (entry && layer) {
485
+ shown.opacity = shown.placed
486
+ ? Math.min(1, shown.opacity + step)
487
+ : Math.max(0, shown.opacity - step);
488
+ } else if (!shown.placed || !layer) {
489
+ shown.opacity = 0;
490
+ }
491
+ if (!shown.placed && shown.opacity <= 0) {
492
+ this._shown.delete(shown);
493
+ continue;
494
+ }
495
+ if (!entry || !layer || shown.opacity <= 0) continue;
496
+ let dx = shown.mx - frame.centerX;
497
+ dx -= Math.round(dx);
498
+ const sx = halfW + dx * frame.world;
499
+ const sy = halfH + (shown.my - frame.centerY) * frame.world;
500
+ const eased = shown.opacity * shown.opacity * (3 - 2 * shown.opacity);
501
+ if (this._instances.length < (count + 1) * LABEL_INSTANCE) {
502
+ const grown = new Float32Array(this._instances.length * 2);
503
+ grown.set(this._instances);
504
+ this._instances = grown;
505
+ }
506
+ const d = this._instances;
507
+ const at = count * LABEL_INSTANCE;
508
+ const level = shown.angle === 0;
509
+ d[at] = sx;
510
+ d[at + 1] = sy;
511
+ d[at + 2] = level ? 1 : Math.cos(shown.angle);
512
+ d[at + 3] = level ? 0 : Math.sin(shown.angle);
513
+ d[at + 4] = entry.x;
514
+ d[at + 5] = entry.y;
515
+ d[at + 6] = entry.width;
516
+ d[at + 7] = entry.height;
517
+ const ink = premultiplied(layer.ink, eased);
518
+ d[at + 8] = ink[0];
519
+ d[at + 9] = ink[1];
520
+ d[at + 10] = ink[2];
521
+ d[at + 11] = ink[3];
522
+ const halo = layer.halo ? premultiplied(layer.halo, eased) : ZERO;
523
+ d[at + 12] = halo[0];
524
+ d[at + 13] = halo[1];
525
+ d[at + 14] = halo[2];
526
+ d[at + 15] = halo[3];
527
+ d[at + 16] = layer.halo ? Math.min(layer.haloPx, reach) : 0;
528
+ d[at + 17] = level ? 1 : 0;
529
+ count++;
530
+ }
531
+ this.stats.drawn = count;
532
+ return { atlas, instances: this._instances, count };
533
+ }
534
+
535
+ // --- internals -------------------------------------------------------------
536
+
537
+ /** One anchor as a contender, or null when it cannot be one at all at
538
+ * this zoom — off the view, or on a run too bent or too short for even
539
+ * a short name. `unit` is device pixels per unit of the lengths given. */
540
+ private _candidate(
541
+ key: string,
542
+ text: string,
543
+ index: number,
544
+ layer: LayerLabels,
545
+ priority: number,
546
+ point: boolean,
547
+ sx: number,
548
+ sy: number,
549
+ angle: number,
550
+ avail: number,
551
+ clear: number,
552
+ length: number,
553
+ dev: number,
554
+ unit: number,
555
+ frame: PlacementFrame,
556
+ ): Candidate | null {
557
+ if (sx < 0 || sy < 0 || sx >= frame.width || sy >= frame.height)
558
+ return null;
559
+ const size = layer.size;
560
+ let ems = this._ems.get(text);
561
+ if (ems === undefined) {
562
+ if (this._ems.size > 20000) this._ems.clear();
563
+ ems = estimateWidth(text, 1);
564
+ this._ems.set(text, ems);
565
+ }
566
+ const estimate = ems * size;
567
+ if (!point) {
568
+ if (dev * unit > size * STRAIGHT_FRACTION) return null;
569
+ // Well short of even an under-estimate of the name: no need to
570
+ // measure it to know.
571
+ if (avail * unit < estimate * 0.35) return null;
572
+ }
573
+ const level = point || Math.abs(angle) < LEVEL_SNAP;
574
+ const a = level ? 0 : angle;
575
+ const clearPx = clear * unit;
576
+ const mx = frame.centerX + (sx - frame.width / 2) / frame.world;
577
+ const my = frame.centerY + (sy - frame.height / 2) / frame.world;
578
+ const cx = sx - frame.width / 2;
579
+ const cy = sy - frame.height / 2;
580
+ return {
581
+ key,
582
+ text,
583
+ layer: index,
584
+ rank: layer.rank,
585
+ priority,
586
+ size,
587
+ point,
588
+ sx,
589
+ sy,
590
+ angle: a,
591
+ cos: Math.cos(a),
592
+ sin: Math.sin(a),
593
+ avail: point ? Infinity : avail * unit,
594
+ clear: clearPx,
595
+ length: length * unit,
596
+ toMercator: unit / frame.world,
597
+ rawAvail: avail,
598
+ rawClear: clear,
599
+ rawLength: length,
600
+ rawDev: dev,
601
+ mx,
602
+ my,
603
+ shown: null,
604
+ clean: point || clearPx >= estimate / 2 ? 1 : 0,
605
+ centre: cx * cx + cy * cy,
606
+ repeat: layer.repeat,
607
+ };
608
+ }
609
+
610
+ /** Each symbol layer's labelling at this zoom, by layer index — null
611
+ * where it does not label. */
612
+ private _layersAt(
613
+ style: PreparedStyle,
614
+ zoom: number,
615
+ scale: number,
616
+ ): (LayerLabels | null)[] {
617
+ return style.layers.map(({ layer }) => {
618
+ if (layer.type !== 'symbol' || layer.visible === false) return null;
619
+ if (layer.minZoom !== undefined && zoom < layer.minZoom) return null;
620
+ if (layer.maxZoom !== undefined && zoom >= layer.maxZoom) return null;
621
+ const symbol = layer as SymbolLayer;
622
+ const ink = this._color(
623
+ symbol.textColor ? resolveZoomed(symbol.textColor, zoom) : '#000000',
624
+ );
625
+ if (!ink) return null;
626
+ const halo = symbol.textHaloColor
627
+ ? this._color(resolveZoomed(symbol.textHaloColor, zoom))
628
+ : null;
629
+ const haloWidth =
630
+ symbol.textHaloWidth === undefined
631
+ ? 1
632
+ : resolveZoomed(symbol.textHaloWidth, zoom);
633
+ const size = Math.max(
634
+ 1,
635
+ Math.round(
636
+ (symbol.textSize === undefined
637
+ ? 12
638
+ : resolveZoomed(symbol.textSize, zoom)) * scale,
639
+ ),
640
+ );
641
+ return {
642
+ id: layer.id,
643
+ rank: symbol.rank ?? 0,
644
+ size,
645
+ repeat: (symbol.repeatDistance ?? 250) * scale,
646
+ ink,
647
+ halo: halo && haloWidth > 0 ? halo : null,
648
+ haloPx: haloWidth * scale,
649
+ };
650
+ });
651
+ }
652
+
653
+ private _color(value: string): Rgba | null {
654
+ let color = this._colors.get(value);
655
+ if (color === undefined) {
656
+ color = parseColor(value);
657
+ this._colors.set(value, color);
658
+ }
659
+ return color;
660
+ }
661
+ }
662
+
663
+ const ZERO: Rgba = [0, 0, 0, 0];
664
+
665
+ /** Every name a tile's label layers carry, made once per tile. */
666
+ const texts = new WeakMap<GlLabelData, Set<string>>();
667
+ function textsOf(data: GlLabelData): Set<string> {
668
+ let set = texts.get(data);
669
+ if (!set) {
670
+ set = new Set(data.texts);
671
+ texts.set(data, set);
672
+ }
673
+ return set;
674
+ }
675
+
676
+ function compare(a: Candidate, b: Candidate): number {
677
+ return (
678
+ b.rank - a.rank ||
679
+ b.priority - a.priority ||
680
+ (b.shown ? 1 : 0) - (a.shown ? 1 : 0) ||
681
+ b.clean - a.clean ||
682
+ b.length - a.length ||
683
+ a.centre - b.centre ||
684
+ (a.key < b.key ? -1 : a.key > b.key ? 1 : 0) ||
685
+ a.sx - b.sx ||
686
+ a.sy - b.sy
687
+ );
688
+ }
689
+
690
+ /**
691
+ * Collision shapes over a grid of cells. A level label is its box; a
692
+ * rotated one is a chain of circles along its baseline, each as tall as
693
+ * the text — which follows a slanted name closely where its box would
694
+ * claim the whole corner-to-corner rectangle around it.
695
+ */
696
+ class CollisionGrid {
697
+ /** Five numbers per shape: kind (0 box, 1 circle), then x0, y0, x1, y1
698
+ * or cx, cy, r, 0. */
699
+ private _shapes: number[] = [];
700
+ private readonly _cells = new Map<number, number[]>();
701
+ private readonly _scratch: number[] = [];
702
+
703
+ reset(): void {
704
+ this._shapes.length = 0;
705
+ this._cells.clear();
706
+ }
707
+
708
+ hits(c: Candidate, w: number, h: number, pad: number): boolean {
709
+ const shapes = this._shapesOf(c, w, h, pad);
710
+ for (let s = 0; s < shapes.length; s += 5) {
711
+ const [x0, y0, x1, y1] = bounds(shapes, s);
712
+ for (let gx = cell(x0); gx <= cell(x1); gx++) {
713
+ for (let gy = cell(y0); gy <= cell(y1); gy++) {
714
+ const list = this._cells.get(cellKey(gx, gy));
715
+ if (!list) continue;
716
+ for (const other of list) {
717
+ if (overlaps(shapes, s, this._shapes, other)) return true;
718
+ }
719
+ }
720
+ }
721
+ }
722
+ return false;
723
+ }
724
+
725
+ insert(c: Candidate, w: number, h: number, pad: number): void {
726
+ const shapes = this._shapesOf(c, w, h, pad);
727
+ for (let s = 0; s < shapes.length; s += 5) {
728
+ const index = this._shapes.length;
729
+ for (let k = 0; k < 5; k++) this._shapes.push(shapes[s + k]);
730
+ const [x0, y0, x1, y1] = bounds(shapes, s);
731
+ for (let gx = cell(x0); gx <= cell(x1); gx++) {
732
+ for (let gy = cell(y0); gy <= cell(y1); gy++) {
733
+ const key = cellKey(gx, gy);
734
+ const list = this._cells.get(key);
735
+ if (list) list.push(index);
736
+ else this._cells.set(key, [index]);
737
+ }
738
+ }
739
+ }
740
+ }
741
+
742
+ private _shapesOf(c: Candidate, w: number, h: number, pad: number): number[] {
743
+ const out = this._scratch;
744
+ out.length = 0;
745
+ if (c.angle === 0) {
746
+ out.push(
747
+ 0,
748
+ c.sx - w / 2 - pad,
749
+ c.sy - h / 2 - pad,
750
+ c.sx + w / 2 + pad,
751
+ c.sy + h / 2 + pad,
752
+ );
753
+ return out;
754
+ }
755
+ const r = h / 2 + pad;
756
+ const span = Math.max(0, w - h);
757
+ const n = span > 0 ? Math.ceil(span / r) + 1 : 1;
758
+ for (let i = 0; i < n; i++) {
759
+ const t = n === 1 ? 0 : -span / 2 + (span * i) / (n - 1);
760
+ out.push(1, c.sx + c.cos * t, c.sy + c.sin * t, r, 0);
761
+ }
762
+ return out;
763
+ }
764
+ }
765
+
766
+ function bounds(shapes: readonly number[], s: number): number[] {
767
+ if (shapes[s] === 0)
768
+ return [shapes[s + 1], shapes[s + 2], shapes[s + 3], shapes[s + 4]];
769
+ const r = shapes[s + 3];
770
+ return [
771
+ shapes[s + 1] - r,
772
+ shapes[s + 2] - r,
773
+ shapes[s + 1] + r,
774
+ shapes[s + 2] + r,
775
+ ];
776
+ }
777
+
778
+ function overlaps(
779
+ a: readonly number[],
780
+ i: number,
781
+ b: readonly number[],
782
+ j: number,
783
+ ): boolean {
784
+ const ka = a[i];
785
+ const kb = b[j];
786
+ if (ka === 0 && kb === 0) {
787
+ return (
788
+ a[i + 1] < b[j + 3] &&
789
+ a[i + 3] > b[j + 1] &&
790
+ a[i + 2] < b[j + 4] &&
791
+ a[i + 4] > b[j + 2]
792
+ );
793
+ }
794
+ if (ka === 1 && kb === 1) {
795
+ const dx = a[i + 1] - b[j + 1];
796
+ const dy = a[i + 2] - b[j + 2];
797
+ const r = a[i + 3] + b[j + 3];
798
+ return dx * dx + dy * dy < r * r;
799
+ }
800
+ const [box, bi, circle, ci] = ka === 0 ? [a, i, b, j] : [b, j, a, i];
801
+ const cx = circle[ci + 1];
802
+ const cy = circle[ci + 2];
803
+ const r = circle[ci + 3];
804
+ const nx = Math.max(box[bi + 1], Math.min(cx, box[bi + 3]));
805
+ const ny = Math.max(box[bi + 2], Math.min(cy, box[bi + 4]));
806
+ const dx = cx - nx;
807
+ const dy = cy - ny;
808
+ return dx * dx + dy * dy < r * r;
809
+ }
810
+
811
+ const cell = (v: number): number => Math.floor(v / CELL);
812
+ const cellKey = (gx: number, gy: number): number =>
813
+ (gx + 4096) * 16384 + (gy + 4096);
814
+
815
+ const globals = globalThis as { performance?: { now(): number } };
816
+ const now = (): number => globals.performance?.now() ?? Date.now();