@react-x11/components 0.4.0 → 0.6.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 (248) hide show
  1. package/README.md +143 -8
  2. package/dist/charts/components.d.ts +3 -1
  3. package/dist/charts/components.d.ts.map +1 -1
  4. package/dist/charts/components.js +8 -4
  5. package/dist/charts/components.js.map +1 -1
  6. package/dist/charts/node.d.ts +13 -6
  7. package/dist/charts/node.d.ts.map +1 -1
  8. package/dist/charts/node.js +75 -24
  9. package/dist/charts/node.js.map +1 -1
  10. package/dist/charts/render.d.ts +4 -0
  11. package/dist/charts/render.d.ts.map +1 -1
  12. package/dist/charts/render.js +11 -6
  13. package/dist/charts/render.js.map +1 -1
  14. package/dist/code-editor/node.d.ts +47 -16
  15. package/dist/code-editor/node.d.ts.map +1 -1
  16. package/dist/code-editor/node.js +121 -51
  17. package/dist/code-editor/node.js.map +1 -1
  18. package/dist/flow/draw.d.ts +19 -1
  19. package/dist/flow/draw.d.ts.map +1 -1
  20. package/dist/flow/draw.js +64 -35
  21. package/dist/flow/draw.js.map +1 -1
  22. package/dist/flow/index.d.ts.map +1 -1
  23. package/dist/flow/index.js +24 -8
  24. package/dist/flow/index.js.map +1 -1
  25. package/dist/flow/model.d.ts +4 -2
  26. package/dist/flow/model.d.ts.map +1 -1
  27. package/dist/flow/model.js +4 -2
  28. package/dist/flow/model.js.map +1 -1
  29. package/dist/flow/node.d.ts +40 -9
  30. package/dist/flow/node.d.ts.map +1 -1
  31. package/dist/flow/node.js +142 -53
  32. package/dist/flow/node.js.map +1 -1
  33. package/dist/flow/types.d.ts +46 -21
  34. package/dist/flow/types.d.ts.map +1 -1
  35. package/dist/formula/index.d.ts +4 -2
  36. package/dist/formula/index.d.ts.map +1 -1
  37. package/dist/formula/index.js.map +1 -1
  38. package/dist/formula/node.d.ts +5 -1
  39. package/dist/formula/node.d.ts.map +1 -1
  40. package/dist/formula/node.js +19 -3
  41. package/dist/formula/node.js.map +1 -1
  42. package/dist/html/controls.d.ts +4 -1
  43. package/dist/html/controls.d.ts.map +1 -1
  44. package/dist/html/controls.js.map +1 -1
  45. package/dist/html/css/cascade.d.ts +11 -5
  46. package/dist/html/css/cascade.d.ts.map +1 -1
  47. package/dist/html/css/cascade.js +18 -8
  48. package/dist/html/css/cascade.js.map +1 -1
  49. package/dist/html/css/parse.d.ts +15 -10
  50. package/dist/html/css/parse.d.ts.map +1 -1
  51. package/dist/html/css/parse.js +41 -16
  52. package/dist/html/css/parse.js.map +1 -1
  53. package/dist/html/css/style.d.ts +3 -0
  54. package/dist/html/css/style.d.ts.map +1 -1
  55. package/dist/html/css/style.js +3 -1
  56. package/dist/html/css/style.js.map +1 -1
  57. package/dist/html/css/values.d.ts +14 -1
  58. package/dist/html/css/values.d.ts.map +1 -1
  59. package/dist/html/css/values.js +11 -8
  60. package/dist/html/css/values.js.map +1 -1
  61. package/dist/html/index.d.ts.map +1 -1
  62. package/dist/html/index.js +1 -0
  63. package/dist/html/index.js.map +1 -1
  64. package/dist/html/layout/boxes.d.ts +12 -5
  65. package/dist/html/layout/boxes.d.ts.map +1 -1
  66. package/dist/html/layout/boxes.js +7 -4
  67. package/dist/html/layout/boxes.js.map +1 -1
  68. package/dist/html/layout/inline.d.ts.map +1 -1
  69. package/dist/html/layout/inline.js +22 -1
  70. package/dist/html/layout/inline.js.map +1 -1
  71. package/dist/html/node.d.ts +22 -8
  72. package/dist/html/node.d.ts.map +1 -1
  73. package/dist/html/node.js +97 -15
  74. package/dist/html/node.js.map +1 -1
  75. package/dist/html/paint.d.ts +3 -0
  76. package/dist/html/paint.d.ts.map +1 -1
  77. package/dist/html/paint.js +2 -2
  78. package/dist/html/paint.js.map +1 -1
  79. package/dist/index.d.ts +8 -2
  80. package/dist/index.d.ts.map +1 -1
  81. package/dist/index.js +21 -0
  82. package/dist/index.js.map +1 -1
  83. package/dist/maps/clip.d.ts +43 -0
  84. package/dist/maps/clip.d.ts.map +1 -0
  85. package/dist/maps/clip.js +171 -0
  86. package/dist/maps/clip.js.map +1 -0
  87. package/dist/maps/gzip.d.ts +15 -0
  88. package/dist/maps/gzip.d.ts.map +1 -0
  89. package/dist/maps/gzip.js +56 -0
  90. package/dist/maps/gzip.js.map +1 -0
  91. package/dist/maps/index.d.ts +55 -0
  92. package/dist/maps/index.d.ts.map +1 -0
  93. package/dist/maps/index.js +141 -0
  94. package/dist/maps/index.js.map +1 -0
  95. package/dist/maps/labels.d.ts +123 -0
  96. package/dist/maps/labels.d.ts.map +1 -0
  97. package/dist/maps/labels.js +323 -0
  98. package/dist/maps/labels.js.map +1 -0
  99. package/dist/maps/mvt.d.ts +244 -0
  100. package/dist/maps/mvt.d.ts.map +1 -0
  101. package/dist/maps/mvt.js +820 -0
  102. package/dist/maps/mvt.js.map +1 -0
  103. package/dist/maps/node.d.ts +278 -0
  104. package/dist/maps/node.d.ts.map +1 -0
  105. package/dist/maps/node.js +1459 -0
  106. package/dist/maps/node.js.map +1 -0
  107. package/dist/maps/overlay.d.ts +157 -0
  108. package/dist/maps/overlay.d.ts.map +1 -0
  109. package/dist/maps/overlay.js +472 -0
  110. package/dist/maps/overlay.js.map +1 -0
  111. package/dist/maps/paint.d.ts +226 -0
  112. package/dist/maps/paint.d.ts.map +1 -0
  113. package/dist/maps/paint.js +705 -0
  114. package/dist/maps/paint.js.map +1 -0
  115. package/dist/maps/proj.d.ts +281 -0
  116. package/dist/maps/proj.d.ts.map +1 -0
  117. package/dist/maps/proj.js +451 -0
  118. package/dist/maps/proj.js.map +1 -0
  119. package/dist/maps/sources.d.ts +256 -0
  120. package/dist/maps/sources.d.ts.map +1 -0
  121. package/dist/maps/sources.js +210 -0
  122. package/dist/maps/sources.js.map +1 -0
  123. package/dist/maps/style.d.ts +141 -0
  124. package/dist/maps/style.d.ts.map +1 -0
  125. package/dist/maps/style.js +147 -0
  126. package/dist/maps/style.js.map +1 -0
  127. package/dist/maps/styles.d.ts +82 -0
  128. package/dist/maps/styles.d.ts.map +1 -0
  129. package/dist/maps/styles.js +898 -0
  130. package/dist/maps/styles.js.map +1 -0
  131. package/dist/maps/tiles.d.ts +283 -0
  132. package/dist/maps/tiles.d.ts.map +1 -0
  133. package/dist/maps/tiles.js +469 -0
  134. package/dist/maps/tiles.js.map +1 -0
  135. package/dist/maps/types.d.ts +244 -0
  136. package/dist/maps/types.d.ts.map +1 -0
  137. package/dist/maps/types.js +2 -0
  138. package/dist/maps/types.js.map +1 -0
  139. package/dist/markdown/ast.d.ts +80 -6
  140. package/dist/markdown/ast.d.ts.map +1 -1
  141. package/dist/markdown/ast.js +13 -4
  142. package/dist/markdown/ast.js.map +1 -1
  143. package/dist/markdown/expressions.d.ts +27 -0
  144. package/dist/markdown/expressions.d.ts.map +1 -0
  145. package/dist/markdown/expressions.js +203 -0
  146. package/dist/markdown/expressions.js.map +1 -0
  147. package/dist/markdown/index.d.ts +61 -2
  148. package/dist/markdown/index.d.ts.map +1 -1
  149. package/dist/markdown/index.js +101 -2
  150. package/dist/markdown/index.js.map +1 -1
  151. package/dist/markdown/parse.d.ts +1 -1
  152. package/dist/markdown/parse.d.ts.map +1 -1
  153. package/dist/markdown/parse.js +163 -15
  154. package/dist/markdown/parse.js.map +1 -1
  155. package/dist/markdown/spans.d.ts.map +1 -1
  156. package/dist/markdown/spans.js +4 -0
  157. package/dist/markdown/spans.js.map +1 -1
  158. package/dist/markdown/tags.d.ts +35 -0
  159. package/dist/markdown/tags.d.ts.map +1 -0
  160. package/dist/markdown/tags.js +213 -0
  161. package/dist/markdown/tags.js.map +1 -0
  162. package/dist/reorder/hx.d.ts +18 -0
  163. package/dist/reorder/hx.d.ts.map +1 -0
  164. package/dist/reorder/hx.js +10 -0
  165. package/dist/reorder/hx.js.map +1 -0
  166. package/dist/reorder/index.d.ts +314 -0
  167. package/dist/reorder/index.d.ts.map +1 -0
  168. package/dist/reorder/index.js +1539 -0
  169. package/dist/reorder/index.js.map +1 -0
  170. package/dist/reorder/model.d.ts +107 -0
  171. package/dist/reorder/model.d.ts.map +1 -0
  172. package/dist/reorder/model.js +162 -0
  173. package/dist/reorder/model.js.map +1 -0
  174. package/dist/richtext/node.d.ts +15 -5
  175. package/dist/richtext/node.d.ts.map +1 -1
  176. package/dist/richtext/node.js +40 -11
  177. package/dist/richtext/node.js.map +1 -1
  178. package/dist/richtext/runs.d.ts +35 -13
  179. package/dist/richtext/runs.d.ts.map +1 -1
  180. package/dist/richtext/runs.js +83 -27
  181. package/dist/richtext/runs.js.map +1 -1
  182. package/dist/tabs/index.d.ts +17 -0
  183. package/dist/tabs/index.d.ts.map +1 -1
  184. package/dist/tabs/index.js +849 -186
  185. package/dist/tabs/index.js.map +1 -1
  186. package/dist/terminal/vt/fonts.d.ts +45 -15
  187. package/dist/terminal/vt/fonts.d.ts.map +1 -1
  188. package/dist/terminal/vt/fonts.js +89 -19
  189. package/dist/terminal/vt/fonts.js.map +1 -1
  190. package/dist/terminal/vt/node.d.ts +42 -0
  191. package/dist/terminal/vt/node.d.ts.map +1 -1
  192. package/dist/terminal/vt/node.js +95 -11
  193. package/dist/terminal/vt/node.js.map +1 -1
  194. package/dist/terminal/vt/renderer.d.ts.map +1 -1
  195. package/dist/terminal/vt/renderer.js +13 -3
  196. package/dist/terminal/vt/renderer.js.map +1 -1
  197. package/package.json +14 -3
  198. package/src/charts/components.ts +14 -7
  199. package/src/charts/node.ts +81 -26
  200. package/src/charts/render.ts +16 -6
  201. package/src/code-editor/node.ts +147 -53
  202. package/src/flow/draw.ts +104 -35
  203. package/src/flow/index.ts +33 -13
  204. package/src/flow/model.ts +4 -2
  205. package/src/flow/node.ts +158 -59
  206. package/src/flow/types.ts +46 -21
  207. package/src/formula/index.ts +4 -2
  208. package/src/formula/node.ts +24 -5
  209. package/src/html/controls.ts +4 -1
  210. package/src/html/css/cascade.ts +18 -7
  211. package/src/html/css/parse.ts +46 -21
  212. package/src/html/css/style.ts +6 -1
  213. package/src/html/css/values.ts +25 -9
  214. package/src/html/index.ts +1 -0
  215. package/src/html/layout/boxes.ts +19 -9
  216. package/src/html/layout/inline.ts +23 -1
  217. package/src/html/node.ts +110 -17
  218. package/src/html/paint.ts +17 -2
  219. package/src/index.ts +102 -0
  220. package/src/maps/clip.ts +184 -0
  221. package/src/maps/gzip.ts +66 -0
  222. package/src/maps/index.ts +316 -0
  223. package/src/maps/labels.ts +442 -0
  224. package/src/maps/mvt.ts +857 -0
  225. package/src/maps/node.ts +1819 -0
  226. package/src/maps/overlay.ts +639 -0
  227. package/src/maps/paint.ts +978 -0
  228. package/src/maps/proj.ts +641 -0
  229. package/src/maps/sources.ts +425 -0
  230. package/src/maps/style.ts +309 -0
  231. package/src/maps/styles.ts +1040 -0
  232. package/src/maps/tiles.ts +676 -0
  233. package/src/maps/types.ts +260 -0
  234. package/src/markdown/ast.ts +85 -9
  235. package/src/markdown/expressions.ts +223 -0
  236. package/src/markdown/index.ts +190 -5
  237. package/src/markdown/parse.ts +230 -15
  238. package/src/markdown/spans.ts +4 -0
  239. package/src/markdown/tags.ts +240 -0
  240. package/src/reorder/hx.ts +31 -0
  241. package/src/reorder/index.ts +2223 -0
  242. package/src/reorder/model.ts +232 -0
  243. package/src/richtext/node.ts +49 -14
  244. package/src/richtext/runs.ts +115 -30
  245. package/src/tabs/index.ts +1040 -204
  246. package/src/terminal/vt/fonts.ts +128 -36
  247. package/src/terminal/vt/node.ts +105 -12
  248. package/src/terminal/vt/renderer.ts +13 -3
@@ -0,0 +1,442 @@
1
+ // Labels: collected from the symbol layers, placed against each other, and
2
+ // drawn into the frame.
3
+ //
4
+ // Not into the tile surfaces, and the three reasons are each a visible bug
5
+ // if it is done the other way. **Collision is global**: two labels in
6
+ // different tiles overlap exactly as readily as two in one, so a per-tile
7
+ // placement produces clutter at every seam. **Text must not be scaled**: a
8
+ // tile surface is composited at up to 2× during a fractional zoom, and text
9
+ // is the one thing on a map nobody accepts blurred. And a tile's labels
10
+ // would be **clipped at its edge**, which is where half of them sit.
11
+ //
12
+ // The placement itself is done in **world pixels** rather than screen
13
+ // pixels, and that is the load-bearing decision in this file. A placement
14
+ // computed against the viewport changes whenever the viewport does, so a
15
+ // pan would have to repaint the whole pane rather than blit it — and a
16
+ // label that was suppressed by a neighbour would pop in as that neighbour
17
+ // scrolled away. Placed in world pixels, whether a label wins depends only
18
+ // on the labels near it and the zoom, so a pan is a translation of a
19
+ // placement that is already correct, and the blit stands.
20
+ import type { MapCanvas } from './paint.js';
21
+ import { GeomType, GeometryBuffer } from './mvt.js';
22
+ import type { VectorTile } from './mvt.js';
23
+ import type { PreparedStyle, PreparedLayer } from './paint.js';
24
+ import { resolveZoomed } from './style.js';
25
+ import type { SymbolLayer, Zoomed } from './style.js';
26
+ import type { TileId, Transform } from './proj.js';
27
+ import { tileCountAt, tileKey } from './proj.js';
28
+
29
+ /** ntk's font cache, structurally — the same slice `src/flow/draw.ts`
30
+ * names, for the same reason. */
31
+ export interface FontsLike {
32
+ layout(
33
+ content: string,
34
+ style: Record<string, unknown>,
35
+ options?: Record<string, unknown>,
36
+ ): {
37
+ width: number;
38
+ height: number;
39
+ draw(ctx: unknown, x: number, y: number): void;
40
+ };
41
+ }
42
+
43
+ /** One shaped string, kept between frames. Shaping is the expensive half of
44
+ * drawing text and a label does not change between frames. */
45
+ export interface ShapedLabel {
46
+ width: number;
47
+ height: number;
48
+ layout: { draw(ctx: unknown, x: number, y: number): void };
49
+ }
50
+
51
+ /**
52
+ * A label that could be drawn.
53
+ *
54
+ * `mx`/`my` are normalized mercator, so a candidate outlives every camera
55
+ * move — it is a property of the data, and collected once per tile per
56
+ * zoom.
57
+ */
58
+ export interface LabelCandidate {
59
+ /** Stable for the life of the tile, which is what lets a placement be
60
+ * compared frame to frame. */
61
+ id: string;
62
+ /** `layer|text` — what the repeat-distance rule counts as "the same
63
+ * label". Keyed on the layer as well as the text so that a street and a
64
+ * place of the same name do not suppress each other. */
65
+ key: string;
66
+ text: string;
67
+ mx: number;
68
+ my: number;
69
+ /** Layer-wide priority; higher wins a collision. */
70
+ rank: number;
71
+ /** Logical pixels. */
72
+ size: number;
73
+ color: string;
74
+ halo: string | undefined;
75
+ haloWidth: number;
76
+ /** Pixels within which this text may not repeat. */
77
+ repeat: number;
78
+ }
79
+
80
+ /** A candidate that won its place, with the box it occupies in world
81
+ * pixels at the zoom it was placed for. */
82
+ export interface PlacedLabel extends LabelCandidate {
83
+ /** World pixels: `mercator × worldSize`. */
84
+ wx: number;
85
+ wy: number;
86
+ width: number;
87
+ height: number;
88
+ shaped: ShapedLabel;
89
+ }
90
+
91
+ function num<T extends number>(
92
+ value: Zoomed<T> | undefined,
93
+ zoom: number,
94
+ fallback: number,
95
+ ): number {
96
+ return value === undefined ? fallback : resolveZoomed(value, zoom);
97
+ }
98
+
99
+ /**
100
+ * The label candidates one tile contributes.
101
+ *
102
+ * Point geometry anchors at the point. Line and polygon geometry anchor at
103
+ * the **midpoint of the longest part** — which is not a true label
104
+ * placement (a street name should follow its street, and a country name
105
+ * should sit at the pole of inaccessibility of its border) but is the
106
+ * anchor those two would start from, and is right often enough to be worth
107
+ * having while line placement is unwritten. See `docs/prd-maps.md`.
108
+ */
109
+ export function collectLabels(
110
+ tile: VectorTile,
111
+ id: TileId,
112
+ prepared: PreparedStyle,
113
+ zoom: number,
114
+ buffer: GeometryBuffer,
115
+ ): LabelCandidate[] {
116
+ const out: LabelCandidate[] = [];
117
+ const n = tileCountAt(id.z);
118
+ const key = tileKey(id);
119
+ for (let i = 0; i < prepared.layers.length; i++) {
120
+ const { layer, filter } = prepared.layers[i];
121
+ if (layer.type !== 'symbol' || layer.visible === false) continue;
122
+ if (layer.minZoom !== undefined && zoom < layer.minZoom) continue;
123
+ if (layer.maxZoom !== undefined && zoom >= layer.maxZoom) continue;
124
+ const source = tile.layers.get(layer.sourceLayer);
125
+ if (!source || source.length === 0) continue;
126
+ const symbol = layer as SymbolLayer;
127
+ const size = num(symbol.textSize, zoom, 12);
128
+ const color = symbol.textColor
129
+ ? resolveZoomed(symbol.textColor, zoom)
130
+ : '#000000';
131
+ const halo = symbol.textHaloColor
132
+ ? resolveZoomed(symbol.textHaloColor, zoom)
133
+ : undefined;
134
+ const haloWidth = num(symbol.textHaloWidth, zoom, 1);
135
+ const rank = symbol.rank ?? 0;
136
+ const repeat = symbol.repeatDistance ?? 250;
137
+ const extent = source.extent;
138
+ const cursor = source.feature(0);
139
+ for (let f = 0; f < source.length; f++) {
140
+ source.seek(f, cursor);
141
+ if (!filter(cursor)) continue;
142
+ const raw = cursor.get(symbol.textField);
143
+ // The schema's fallback: a translated name field is absent for most
144
+ // features, and the local `name` is what a map without a translation
145
+ // shows rather than nothing.
146
+ const text =
147
+ typeof raw === 'string' && raw.length > 0
148
+ ? raw
149
+ : symbol.textField !== 'name'
150
+ ? typeof cursor.get('name') === 'string'
151
+ ? String(cursor.get('name'))
152
+ : ''
153
+ : '';
154
+ if (text.length === 0) continue;
155
+ cursor.readGeometry(buffer);
156
+ const anchor = anchorOf(buffer, cursor.type);
157
+ if (!anchor) continue;
158
+ out.push({
159
+ id: `${key}:${layer.id}:${f}`,
160
+ key: `${layer.id}|${text}`,
161
+ text,
162
+ // Tile-local to normalized mercator, the same arithmetic the
163
+ // rasterizer's transform does and for the same precision reason.
164
+ mx: (id.x + anchor.x / extent) / n,
165
+ my: (id.y + anchor.y / extent) / n,
166
+ rank,
167
+ size,
168
+ color,
169
+ halo,
170
+ haloWidth,
171
+ repeat,
172
+ });
173
+ }
174
+ }
175
+ return out;
176
+ }
177
+
178
+ function anchorOf(
179
+ buffer: GeometryBuffer,
180
+ type: GeomType,
181
+ ): { x: number; y: number } | null {
182
+ if (buffer.points === 0) return null;
183
+ if (type === GeomType.Point) {
184
+ return { x: buffer.coords[0], y: buffer.coords[1] };
185
+ }
186
+ // The longest part, by its box — a cheap stand-in for arc length that
187
+ // does not need a second pass over the vertices.
188
+ let best = 0;
189
+ let bestSpan = -1;
190
+ for (let part = 0; part < buffer.parts; part++) {
191
+ const at = part * 4;
192
+ const span =
193
+ buffer.partBounds[at + 2] -
194
+ buffer.partBounds[at] +
195
+ (buffer.partBounds[at + 3] - buffer.partBounds[at + 1]);
196
+ if (span > bestSpan) {
197
+ bestSpan = span;
198
+ best = part;
199
+ }
200
+ }
201
+ const from = buffer.starts[best];
202
+ const to = buffer.starts[best + 1];
203
+ if (to <= from) return null;
204
+ const mid = from + ((to - from) >> 1);
205
+ return { x: buffer.coords[mid * 2], y: buffer.coords[mid * 2 + 1] };
206
+ }
207
+
208
+ /** Cache of shaped strings, owned by the element. */
209
+ export class LabelShaper {
210
+ private readonly _cache = new Map<string, ShapedLabel>();
211
+ private _fonts: FontsLike | null;
212
+ private _family: string;
213
+ /** Device pixels per logical pixel. Text is shaped at the device size the
214
+ * panel wants, exactly as core shapes a `<text>` — a label on a retina
215
+ * screen is sharper, not bigger. */
216
+ private _scale: number;
217
+
218
+ constructor(fonts: FontsLike | null, family: string, scale: number) {
219
+ this._fonts = fonts;
220
+ this._family = family;
221
+ this._scale = scale;
222
+ }
223
+
224
+ /** Re-point at a new font manager, face or scale, dropping what was
225
+ * shaped for the old one. */
226
+ reconfigure(fonts: FontsLike | null, family: string, scale: number): void {
227
+ if (
228
+ this._fonts === fonts &&
229
+ this._family === family &&
230
+ this._scale === scale
231
+ ) {
232
+ return;
233
+ }
234
+ this._fonts = fonts;
235
+ this._family = family;
236
+ this._scale = scale;
237
+ this._cache.clear();
238
+ }
239
+
240
+ /** Shape one label, or null on a backend with no font manager. Widths
241
+ * come back in **logical** pixels, which is what placement is in. */
242
+ shape(text: string, size: number, color: string): ShapedLabel | null {
243
+ const fonts = this._fonts;
244
+ if (!fonts) return null;
245
+ const key = `${this._family}|${size}|${color}|${text}`;
246
+ const hit = this._cache.get(key);
247
+ if (hit) return hit;
248
+ // Bounded, so a map panned across a continent cannot turn the cache
249
+ // into a leak. Cleared wholesale rather than evicted one at a time: the
250
+ // labels on screen are re-shaped on the next frame and the cost of that
251
+ // is a few hundred strings once.
252
+ if (this._cache.size > 4000) this._cache.clear();
253
+ const layout = fonts.layout(text, {
254
+ family: this._family,
255
+ size: size * this._scale,
256
+ weight: 400,
257
+ style: 'normal',
258
+ color,
259
+ });
260
+ const shaped: ShapedLabel = {
261
+ width: layout.width / this._scale,
262
+ height: layout.height / this._scale,
263
+ layout,
264
+ };
265
+ this._cache.set(key, shaped);
266
+ return shaped;
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Place candidates against each other, in world pixels.
272
+ *
273
+ * Greedy by rank, which is what every map does: sort, and take a label if
274
+ * nothing already taken overlaps it. The order has to be **total and
275
+ * stable** — rank, then the candidate's id — because a tie broken by array
276
+ * order would flicker as tiles arrived in a different order.
277
+ */
278
+ export function placeLabels(
279
+ candidates: readonly LabelCandidate[],
280
+ worldSize: number,
281
+ shaper: LabelShaper,
282
+ options?: { padding?: number },
283
+ ): PlacedLabel[] {
284
+ const padding = options?.padding ?? 2;
285
+ const sorted = [...candidates].sort(
286
+ (a, b) => b.rank - a.rank || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
287
+ );
288
+ const placed: PlacedLabel[] = [];
289
+ // A sparse grid over world pixels. The cell is sized for the labels
290
+ // themselves, so a box touches one or two cells and a collision test is a
291
+ // handful of comparisons rather than a scan of everything placed.
292
+ const CELL = 64;
293
+ const grid = new Map<number, PlacedLabel[]>();
294
+ const cellKey = (cx: number, cy: number): number => cx * 0x40000 + cy;
295
+ /** Where each distinct text has already been placed, for the
296
+ * repeat-distance rule. */
297
+ const byText = new Map<string, { wx: number; wy: number }[]>();
298
+
299
+ for (const candidate of sorted) {
300
+ const shaped = shaper.shape(
301
+ candidate.text,
302
+ candidate.size,
303
+ candidate.color,
304
+ );
305
+ if (!shaped) return placed; // no font manager: nothing can be measured
306
+ const wx = candidate.mx * worldSize;
307
+ const wy = candidate.my * worldSize;
308
+ // The repeat test before the overlap test, because it is the cheaper
309
+ // one and because it is what rejects most of a street layer: a street
310
+ // is one feature per segment, so the same name arrives a dozen times
311
+ // strung out along the road, none of them overlapping any other.
312
+ if (candidate.repeat > 0) {
313
+ const already = byText.get(candidate.key);
314
+ if (already) {
315
+ const limit = candidate.repeat * candidate.repeat;
316
+ let tooClose = false;
317
+ for (const at of already) {
318
+ const dx = at.wx - wx;
319
+ const dy = at.wy - wy;
320
+ if (dx * dx + dy * dy < limit) {
321
+ tooClose = true;
322
+ break;
323
+ }
324
+ }
325
+ if (tooClose) continue;
326
+ }
327
+ }
328
+ const width = shaped.width + padding * 2;
329
+ const height = shaped.height + padding * 2;
330
+ const left = wx - width / 2;
331
+ const top = wy - height / 2;
332
+ const x0 = Math.floor(left / CELL);
333
+ const x1 = Math.floor((left + width) / CELL);
334
+ const y0 = Math.floor(top / CELL);
335
+ const y1 = Math.floor((top + height) / CELL);
336
+ let free = true;
337
+ for (let cx = x0; cx <= x1 && free; cx++) {
338
+ for (let cy = y0; cy <= y1 && free; cy++) {
339
+ const bucket = grid.get(cellKey(cx, cy));
340
+ if (!bucket) continue;
341
+ for (const other of bucket) {
342
+ if (
343
+ left < other.wx - other.width / 2 + other.width &&
344
+ left + width > other.wx - other.width / 2 &&
345
+ top < other.wy - other.height / 2 + other.height &&
346
+ top + height > other.wy - other.height / 2
347
+ ) {
348
+ free = false;
349
+ break;
350
+ }
351
+ }
352
+ }
353
+ }
354
+ if (!free) continue;
355
+ const entry: PlacedLabel = {
356
+ ...candidate,
357
+ wx,
358
+ wy,
359
+ width,
360
+ height,
361
+ shaped,
362
+ };
363
+ placed.push(entry);
364
+ if (candidate.repeat > 0) {
365
+ const already = byText.get(candidate.key);
366
+ if (already) already.push({ wx, wy });
367
+ else byText.set(candidate.key, [{ wx, wy }]);
368
+ }
369
+ for (let cx = x0; cx <= x1; cx++) {
370
+ for (let cy = y0; cy <= y1; cy++) {
371
+ const at = cellKey(cx, cy);
372
+ const bucket = grid.get(at);
373
+ if (bucket) bucket.push(entry);
374
+ else grid.set(at, [entry]);
375
+ }
376
+ }
377
+ }
378
+ return placed;
379
+ }
380
+
381
+ /**
382
+ * Draw the placed labels that land in the pane.
383
+ *
384
+ * The halo is the same string shaped a second time in the halo colour and
385
+ * drawn four times around the glyphs, rather than a real outline: ntk has
386
+ * no glyph-outline stroke, and both runs are cached, so a label costs five
387
+ * composites of two shaped runs. That is cheap for the few dozen labels a
388
+ * pane holds and it is the difference between a place name that is
389
+ * readable over a motorway and one that is not.
390
+ */
391
+ export function drawLabels(
392
+ ctx: MapCanvas,
393
+ placed: readonly PlacedLabel[],
394
+ transform: Transform,
395
+ pane: { x: number; y: number; width: number; height: number },
396
+ scale: number,
397
+ clip: { x: number; y: number; width: number; height: number } | null,
398
+ shaper: LabelShaper,
399
+ ): number {
400
+ let drawn = 0;
401
+ const world = transform.world;
402
+ const originX = pane.x + transform.paneX - transform.centerX * world;
403
+ const originY = pane.y + transform.paneY - transform.centerY * world;
404
+ for (const label of placed) {
405
+ const x = originX + label.wx - label.shaped.width / 2;
406
+ const y = originY + label.wy - label.shaped.height / 2;
407
+ if (
408
+ x + label.shaped.width < pane.x ||
409
+ y + label.shaped.height < pane.y ||
410
+ x > pane.x + pane.width ||
411
+ y > pane.y + pane.height
412
+ ) {
413
+ continue;
414
+ }
415
+ if (
416
+ clip &&
417
+ (x + label.shaped.width < clip.x ||
418
+ y + label.shaped.height < clip.y ||
419
+ x > clip.x + clip.width ||
420
+ y > clip.y + clip.height)
421
+ ) {
422
+ continue;
423
+ }
424
+ const dx = Math.round(x * scale);
425
+ const dy = Math.round(y * scale);
426
+ if (label.halo !== undefined && label.haloWidth > 0) {
427
+ const halo = shaper.shape(label.text, label.size, label.halo);
428
+ if (halo) {
429
+ const offset = Math.max(1, Math.round(label.haloWidth * scale));
430
+ halo.layout.draw(ctx, dx - offset, dy);
431
+ halo.layout.draw(ctx, dx + offset, dy);
432
+ halo.layout.draw(ctx, dx, dy - offset);
433
+ halo.layout.draw(ctx, dx, dy + offset);
434
+ }
435
+ }
436
+ label.shaped.layout.draw(ctx, dx, dy);
437
+ drawn++;
438
+ }
439
+ return drawn;
440
+ }
441
+
442
+ export type { PreparedLayer };