@tangram.gl/basemap-layers 0.0.0-stage → 0.0.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 (105) hide show
  1. package/README.md +34 -2
  2. package/THIRD-PARTY-NOTICES.md +205 -0
  3. package/dist/geojson-worker.js +2160 -0
  4. package/dist/geojson-worker.js.map +6 -0
  5. package/dist/index.cjs +11388 -0
  6. package/dist/index.cjs.map +6 -0
  7. package/dist/index.js +11369 -0
  8. package/dist/index.js.map +6 -0
  9. package/dist/map-style.cjs +445 -0
  10. package/dist/map-style.cjs.map +6 -0
  11. package/dist/map-style.js +431 -0
  12. package/dist/map-style.js.map +6 -0
  13. package/dist/types/arabic-shaping.d.ts +7 -0
  14. package/dist/types/atmosphere-layer.d.ts +10 -0
  15. package/dist/types/basemap-layer.d.ts +150 -0
  16. package/dist/types/bidi.d.ts +8 -0
  17. package/dist/types/circle-layer.d.ts +49 -0
  18. package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
  19. package/dist/types/fill-outline-extension.d.ts +22 -0
  20. package/dist/types/fill-pattern.d.ts +99 -0
  21. package/dist/types/fill-style-extensions.d.ts +100 -0
  22. package/dist/types/geojson-source.d.ts +29 -0
  23. package/dist/types/geojson-tiler.d.ts +48 -0
  24. package/dist/types/geojson-worker.d.ts +40 -0
  25. package/dist/types/globe-horizon-extension.d.ts +61 -0
  26. package/dist/types/globe-layers.d.ts +239 -0
  27. package/dist/types/globe-line-angle-extension.d.ts +37 -0
  28. package/dist/types/glyph-font-renderer.d.ts +37 -0
  29. package/dist/types/glyph-pbf.d.ts +37 -0
  30. package/dist/types/glyph-store.d.ts +103 -0
  31. package/dist/types/grid-index.d.ts +47 -0
  32. package/dist/types/hillshade-layer.d.ts +154 -0
  33. package/dist/types/index.d.ts +9 -0
  34. package/dist/types/is-globe-viewport.d.ts +2 -0
  35. package/dist/types/label-font-renderer.d.ts +18 -0
  36. package/dist/types/label-placement.d.ts +247 -0
  37. package/dist/types/label-text-layer.d.ts +231 -0
  38. package/dist/types/line-blur-extension.d.ts +73 -0
  39. package/dist/types/line-glyphs.d.ts +60 -0
  40. package/dist/types/line-placement.d.ts +93 -0
  41. package/dist/types/map-style-loader.d.ts +48 -0
  42. package/dist/types/map-style-schema.d.ts +72 -0
  43. package/dist/types/map-style.d.ts +6 -0
  44. package/dist/types/mapbox-style.d.ts +40 -0
  45. package/dist/types/mvt-label-layer.d.ts +923 -0
  46. package/dist/types/polylabel.d.ts +17 -0
  47. package/dist/types/raster-color-extension.d.ts +146 -0
  48. package/dist/types/shared-texture.d.ts +34 -0
  49. package/dist/types/sort-key.d.ts +10 -0
  50. package/dist/types/sprite.d.ts +82 -0
  51. package/dist/types/style-accessor.d.ts +45 -0
  52. package/dist/types/style-expression.d.ts +83 -0
  53. package/dist/types/style-resolver.d.ts +94 -0
  54. package/dist/types/text-font.d.ts +36 -0
  55. package/dist/types/text-halo-blur-extension.d.ts +55 -0
  56. package/dist/types/text-layout.d.ts +71 -0
  57. package/dist/types/util.d.ts +22 -0
  58. package/dist/types/zoom-paint-extension.d.ts +72 -0
  59. package/package.json +80 -3
  60. package/src/arabic-shaping.ts +366 -0
  61. package/src/atmosphere-layer.ts +19 -0
  62. package/src/basemap-layer.ts +533 -0
  63. package/src/bidi.ts +199 -0
  64. package/src/circle-layer.ts +191 -0
  65. package/src/fill-extrusion-lighting-extension.ts +206 -0
  66. package/src/fill-outline-extension.ts +70 -0
  67. package/src/fill-pattern.ts +400 -0
  68. package/src/fill-style-extensions.ts +177 -0
  69. package/src/geojson-source.ts +318 -0
  70. package/src/geojson-tiler.ts +185 -0
  71. package/src/geojson-worker.ts +98 -0
  72. package/src/globe-horizon-extension.ts +64 -0
  73. package/src/globe-layers.ts +3487 -0
  74. package/src/globe-line-angle-extension.ts +131 -0
  75. package/src/glyph-font-renderer.ts +173 -0
  76. package/src/glyph-pbf.ts +164 -0
  77. package/src/glyph-store.ts +326 -0
  78. package/src/grid-index.ts +177 -0
  79. package/src/hillshade-layer.ts +775 -0
  80. package/src/index.ts +18 -0
  81. package/src/is-globe-viewport.ts +24 -0
  82. package/src/label-font-renderer.ts +159 -0
  83. package/src/label-placement.ts +727 -0
  84. package/src/label-text-layer.ts +913 -0
  85. package/src/line-blur-extension.ts +237 -0
  86. package/src/line-glyphs.ts +364 -0
  87. package/src/line-placement.ts +446 -0
  88. package/src/map-style-loader.ts +56 -0
  89. package/src/map-style-schema.ts +83 -0
  90. package/src/map-style.ts +23 -0
  91. package/src/mapbox-style.ts +168 -0
  92. package/src/mvt-label-layer.ts +3744 -0
  93. package/src/polylabel.ts +312 -0
  94. package/src/raster-color-extension.ts +245 -0
  95. package/src/shared-texture.ts +153 -0
  96. package/src/sort-key.ts +63 -0
  97. package/src/sprite.ts +244 -0
  98. package/src/style-accessor.ts +95 -0
  99. package/src/style-expression.ts +327 -0
  100. package/src/style-resolver.ts +301 -0
  101. package/src/text-font.ts +150 -0
  102. package/src/text-halo-blur-extension.ts +115 -0
  103. package/src/text-layout.ts +441 -0
  104. package/src/util.ts +47 -0
  105. package/src/zoom-paint-extension.ts +207 -0
@@ -0,0 +1,913 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ import {WebMercatorViewport, createIterable} from '@deck.gl/core';
10
+ import {TextLayer, _MultiIconLayer as MultiIconLayer} from '@deck.gl/layers';
11
+ import type {TextLayerProps} from '@deck.gl/layers';
12
+ import type {
13
+ Accessor,
14
+ AccessorContext,
15
+ Attribute,
16
+ DefaultProps,
17
+ FilterContext
18
+ } from '@deck.gl/core';
19
+ import {Buffer} from '@luma.gl/core';
20
+ import type {Texture} from '@luma.gl/core';
21
+ import {getLabelFontRenderer} from './label-font-renderer';
22
+ import {SharedTextureSet} from './shared-texture';
23
+
24
+ /** `text-justify`: `auto` aligns the lines of a label by its text anchor, as `TextLayer` does. */
25
+ export type LabelTextJustify = 'auto' | 'left' | 'center' | 'right';
26
+
27
+ /** Where each character of a label is drawn, as `TextLayer`'s `transformParagraph` returns it. */
28
+ export type ParagraphLayout = {
29
+ /** Character positions in the row, in font atlas pixels. */
30
+ x: number[];
31
+ y: number[];
32
+ /** Width of each character's row. */
33
+ rowWidth: number[];
34
+ /** Width and height of the label box. */
35
+ size: [number, number];
36
+ };
37
+
38
+ type _LabelTextLayerProps<DataT> = {
39
+ /** Space added after each character but the last of a line, in ems of the text size. */
40
+ getLetterSpacing?: Accessor<DataT, number>;
41
+ /** How the lines of a label are aligned within its box. */
42
+ getTextJustify?: Accessor<DataT, LabelTextJustify>;
43
+ /**
44
+ * Where each glyph of a label along a line is drawn in a viewport (see {@link LineGlyphPlacement}),
45
+ * or null to draw the label straight, turned by `getAngle`. Null for every label draws them all
46
+ * straight. Its update trigger is `updateTriggers.getLineGlyphs`. WebGL only.
47
+ */
48
+ getLineGlyphs?: ((object: DataT, viewport: any) => Float32Array | null) | null;
49
+ /**
50
+ * What `getLineGlyphs` depends on in the viewport: the glyphs are placed again when it changes,
51
+ * as the layer draws.
52
+ */
53
+ getLineGlyphKey?: ((viewport: any) => string) | null;
54
+ /**
55
+ * Mip levels of the font atlas texture (see `LABEL_ATLAS_MIP_LEVELS`). 1 samples the atlas
56
+ * without mipmaps, as MapLibre GL JS samples its 24 px glyphs.
57
+ */
58
+ atlasMipLevels?: number;
59
+ /**
60
+ * Whether the glyph edge is smoothed no more than `fontSettings.smoothing`, which keeps small
61
+ * text from the 64 px label font atlas crisp. False smooths it as MapLibre GL JS smooths its
62
+ * 24 px glyphs, which is wider on small text.
63
+ */
64
+ edgeGammaCapped?: boolean;
65
+ };
66
+
67
+ /**
68
+ * `getLineGlyphs` of a label: 4 values per character, `dx, dy, angle, mode`. With mode 1 the
69
+ * character's advance centre is drawn `dx, dy` pixels from the label's position (y down), turned by
70
+ * `angle` degrees counter-clockwise, and `getPixelOffset` is not added; with mode -1 the label is not
71
+ * drawn; with mode 0 it is drawn straight.
72
+ */
73
+ export type LineGlyphPlacement = Float32Array;
74
+
75
+ /** The vertex shader line that turns a character about the label's anchor, which curved glyphs replace. */
76
+ const TURN_CHARACTER = `pixelOffset = rotate_by_angle(pixelOffset, instanceAngles) * instanceScale;
77
+ pixelOffset += instancePixelOffset;`;
78
+
79
+ /** The shader line before which a hidden label's characters collapse. */
80
+ const HIDE_CHARACTER_BEFORE = 'vTextureCoords = mix(';
81
+
82
+ export type LabelTextLayerProps<DataT = unknown> = _LabelTextLayerProps<DataT> &
83
+ TextLayerProps<DataT>;
84
+
85
+ /** The text anchor's position across the label box: 0 at its left edge, 1 at its right edge. */
86
+ const ANCHOR_POSITION = {start: 0, middle: 0.5, end: 1} as const;
87
+
88
+ /** `text-justify` as a position across the box, as MapLibre's `justify`. */
89
+ const JUSTIFY_POSITION = {left: 0, center: 0.5, right: 1} as const;
90
+
91
+ /**
92
+ * Spaces and justifies the lines of a label laid out by `TextLayer`, as MapLibre shapes text:
93
+ * `spacing` atlas pixels follow each character of a line but the last, the label box widens to the
94
+ * widest line, and each line is placed at `justify` across the box (0 left, 0.5 center, 1 right).
95
+ * `TextLayer` places each line at the text anchor's position, `anchor`, so a line is shifted by
96
+ * `(justify - anchor)` times the space it leaves in the box. `layout` is returned as is when
97
+ * neither changes it.
98
+ */
99
+ export function spaceParagraph(
100
+ layout: ParagraphLayout,
101
+ text: string,
102
+ spacing: number,
103
+ justify: number,
104
+ anchor: number
105
+ ): ParagraphLayout {
106
+ const shift = justify - anchor;
107
+ if (!spacing && !shift) {
108
+ return layout;
109
+ }
110
+ const characters = Array.from(text);
111
+ const x = layout.x.slice();
112
+ const rowWidth = layout.rowWidth.slice();
113
+ let width = 0;
114
+ // `TextLayer` breaks lines at newlines only, as no `maxWidth` is set; a newline has no position.
115
+ let start = 0;
116
+ for (let i = 0; i <= characters.length; i++) {
117
+ if (i < characters.length && characters[i] !== '\n') {
118
+ continue;
119
+ }
120
+ if (i > start) {
121
+ const lineWidth = layout.rowWidth[start] + (i - start - 1) * spacing;
122
+ for (let j = start; j < i; j++) {
123
+ x[j] += (j - start) * spacing;
124
+ rowWidth[j] = lineWidth;
125
+ }
126
+ width = Math.max(width, lineWidth);
127
+ }
128
+ start = i + 1;
129
+ }
130
+ if (shift) {
131
+ for (let i = 0; i < characters.length; i++) {
132
+ if (characters[i] !== '\n') {
133
+ x[i] += shift * (width - rowWidth[i]);
134
+ }
135
+ }
136
+ }
137
+ return {x, y: layout.y, rowWidth, size: [width, layout.size[1]]};
138
+ }
139
+
140
+ /** Appended to the font atlas cache keys of label layers (see `initializeState`). */
141
+ const LABEL_ATLAS_KEY = 'label-glyph-overhang';
142
+
143
+ const defaultProps: DefaultProps<LabelTextLayerProps> = {
144
+ getLetterSpacing: {type: 'accessor', value: 0},
145
+ getTextJustify: {type: 'accessor', value: 'auto'},
146
+ getLineGlyphs: {type: 'function', value: null, optional: true} as any,
147
+ getLineGlyphKey: {type: 'function', value: null, optional: true} as any,
148
+ atlasMipLevels: {type: 'number', value: 2},
149
+ edgeGammaCapped: {type: 'boolean', value: true},
150
+ // Keeps the part of italic glyphs left of their origin (see `getLabelFontRenderer`).
151
+ _getFontRenderer: {type: 'function', value: getLabelFontRenderer}
152
+ };
153
+
154
+ /** Evaluates a constant or function accessor. */
155
+ function evaluate<DataT, T>(
156
+ accessor: Accessor<DataT, T>,
157
+ object: DataT,
158
+ objectInfo: AccessorContext<DataT>
159
+ ): T {
160
+ return typeof accessor === 'function'
161
+ ? (accessor as (object: DataT, objectInfo: AccessorContext<DataT>) => T)(object, objectInfo)
162
+ : accessor;
163
+ }
164
+
165
+ /** A label's packed character definitions and the inputs they were made from. */
166
+ type CachedIconDefs = {
167
+ text: string;
168
+ offsets: number[];
169
+ iconAtlas: unknown;
170
+ iconMapping: unknown;
171
+ defs: Float32Array;
172
+ };
173
+
174
+ /** The glyphs along lines of a view other than the one the glyph attribute holds. */
175
+ type ViewLineGlyphs = {
176
+ /** The `getLineGlyphKey` they were placed for. */
177
+ key: string;
178
+ /** The glyph attribute revision they were placed at. */
179
+ revision: number;
180
+ values: Float32Array;
181
+ buffer: Buffer;
182
+ };
183
+
184
+ /**
185
+ * The most views whose glyphs along lines a layer keeps beside the attribute's, as a main map and a
186
+ * minimap. The view drawn least recently is forgotten first.
187
+ */
188
+ const MAX_LINE_GLYPH_VIEWS = 3;
189
+
190
+ /** Whether two number lists hold the same values. */
191
+ function isSameNumbers(a: number[], b: number[]): boolean {
192
+ if (a.length !== b.length) {
193
+ return false;
194
+ }
195
+ for (let i = 0; i < a.length; i++) {
196
+ if (a[i] !== b[i]) {
197
+ return false;
198
+ }
199
+ }
200
+ return true;
201
+ }
202
+
203
+ /**
204
+ * Half the width, in device pixels, over which MapLibre GL JS antialiases the edges of glyphs and
205
+ * of their halos: its symbol shader's `EDGE_GAMMA` (0.105 / device pixel ratio) of a distance
206
+ * field that falls by 1/8 per glyph pixel, scaled with the text.
207
+ */
208
+ export const LABEL_EDGE_DEVICE_PIXELS = 0.84;
209
+
210
+ /**
211
+ * `distancePerAtlasPixel`: distance field units per atlas pixel of the font atlas,
212
+ * `1 / fontSettings.radius`. `foreshortening`: how much the view shortens screen y, see
213
+ * `getLabelForeshortening`. `capped`: 1 when the glyph edge is smoothed no more than `sdf.gamma`.
214
+ */
215
+ const labelEdgeUniforms = {
216
+ name: 'labelEdge',
217
+ fs: /* glsl */ `\
218
+ layout(std140) uniform labelEdgeUniforms {
219
+ float distancePerAtlasPixel;
220
+ float foreshortening;
221
+ float capped;
222
+ } labelEdge;
223
+ `,
224
+ uniformTypes: {distancePerAtlasPixel: 'f32', foreshortening: 'f32', capped: 'f32'}
225
+ } as const;
226
+
227
+ /** The cosine of 89 degrees: no label is foreshortened more, so the factor stays positive. */
228
+ const MIN_FORESHORTENING = Math.cos((89 * Math.PI) / 180);
229
+
230
+ /**
231
+ * How much the view shortens screen y for the labels of a layer, against screen x: the cosine of a
232
+ * `MapView`'s pitch for labels that lie on the map, which it tilts about screen x, as MapLibre's
233
+ * `u_gamma_scale`. Billboards face the screen, and other views tilt otherwise (a `FirstPersonView`
234
+ * measures its pitch from the horizon): their labels are taken as unforeshortened, 1.
235
+ */
236
+ export function getLabelForeshortening(viewport: unknown, billboard: boolean): number {
237
+ if (billboard || !(viewport instanceof WebMercatorViewport)) {
238
+ return 1;
239
+ }
240
+ return Math.max(Math.cos((viewport.pitch * Math.PI) / 180), MIN_FORESHORTENING);
241
+ }
242
+
243
+ /**
244
+ * `MultiIconLayer`'s signed distance field statements: the glyph edge, then the outline (halo)
245
+ * edge, both smoothed over `sdf.gamma`, a fixed distance. On screen that is a share of the text
246
+ * size: 0.0375 × size × device pixel ratio device pixels on each side for the label font atlas.
247
+ */
248
+ const SDF_EDGES =
249
+ /if \(sdf\.enabled\) \{\s*float distance = alpha;\s*alpha = smoothstep\(sdf\.buffer - sdf\.gamma, sdf\.buffer \+ sdf\.gamma, distance\);\s*if \(sdf\.outlineBuffer > 0\.0\) \{\s*float inFill = alpha;\s*float inBorder = smoothstep\(sdf\.outlineBuffer - sdf\.gamma, sdf\.outlineBuffer \+ sdf\.gamma, distance\);\s*color = mix\(sdf\.outlineColor, vColor, inFill\);\s*alpha = inBorder;\s*\}\s*\}/;
250
+
251
+ /**
252
+ * The edges as MapLibre GL JS antialiases them: over `LABEL_EDGE_DEVICE_PIXELS` device pixels on
253
+ * each side, whatever the text size and device pixel ratio. The distance a device pixel spans is
254
+ * measured from the screen-space rate of change of the atlas coordinates along x, in uniform
255
+ * control flow. Labels that lie on a pitched map are foreshortened along y alone: as MapLibre's
256
+ * `u_gamma_scale`, their edges widen by 1 / cos(pitch): across y they are about as wide as on an
257
+ * unpitched map, and across x 1 / cos(pitch) times wider. MapLibre's smoothing,
258
+ * `EDGE_GAMMA / (fontScale * u_gamma_scale) * gl_Position.w` with `u_gamma_scale` =
259
+ * cos(pitch) × `cameraToCenterDistance`, is 0.84 / cos(pitch) device pixels across x at any
260
+ * distance: its glyphs are drawn `fontScale` × `cameraToCenterDistance` / w times their atlas
261
+ * size, which cancels both. The atlas coordinates' rate of change along x holds that scale,
262
+ * perspective included, at each pixel, so no term for w is added here.
263
+ * - The glyph edge, as it would be unpitched, is never smoothed more than `TextLayer` smooths it
264
+ * (`sdf.gamma`), unless `labelEdge.capped` is 0: on small text at device pixel ratio 1,
265
+ * `TextLayer`'s edge is the narrower, which suits the 64 px label font atlas. The
266
+ * cap applies before the 1 / cos(pitch) widening, so that it narrows pitched labels only where it
267
+ * would narrow them unpitched.
268
+ * - The halo edge is smoothed as MapLibre's, also when it has no blur, unless the halo reaches so
269
+ * far out that the distance field ends within the smoothing: then it is smoothed no more than
270
+ * that allows, or than `sdf.gamma`. `TextHaloBlurExtension` widens it
271
+ * from `labelHaloGamma`, after `alpha = inBorder;`, and sets `haloGamma` to the width it fades
272
+ * over.
273
+ * - The halo is a ring, as MapLibre's: it fades out inside the glyph, from the glyph edge over
274
+ * `2 * haloGamma` (MapLibre's `inner_edge`). The halo pass draws the halo alone, and the pass that
275
+ * draws the glyphs over every halo (`sdf.outlineBuffer` at `sdf.buffer`) draws the glyphs alone,
276
+ * as MapLibre's two passes. A glyph's antialiased edge, and a translucent glyph, show the ring
277
+ * where it reaches in, and the map beyond it; `TextLayer` mixes the glyph color into the halo
278
+ * pass instead, which shows much less of the halo there than MapLibre.
279
+ */
280
+ const LABEL_SDF_EDGES = /* glsl */ `\
281
+ if (sdf.enabled) {
282
+ float distance = alpha;
283
+ vec2 atlasPixel = vTextureCoords * vec2(textureSize(iconsTexture, 0));
284
+ // Along y, as MapLibre's gamma_scale: x, which pitch does not shorten, over the pitch's
285
+ // cosine. At pitch 0, and for billboards, both directions are the same.
286
+ float atlasPixelsPerDevicePixel = length(dFdx(atlasPixel)) / labelEdge.foreshortening;
287
+ float labelEdgeGamma =
288
+ ${LABEL_EDGE_DEVICE_PIXELS.toFixed(2)} * atlasPixelsPerDevicePixel * labelEdge.distancePerAtlasPixel;
289
+ // TextLayer's smoothing caps the glyph edge as it would be on an unpitched map, before the
290
+ // 1 / cos(pitch) widening: compared after it, the cap would undo the widening on the glyphs
291
+ // that perspective draws small, toward the horizon.
292
+ float unpitchedEdgeGamma = labelEdgeGamma * labelEdge.foreshortening;
293
+ float labelFillGamma = (labelEdge.capped > 0.5 ? min(sdf.gamma, unpitchedEdgeGamma) : unpitchedEdgeGamma) /
294
+ labelEdge.foreshortening;
295
+ alpha = smoothstep(sdf.buffer - labelFillGamma, sdf.buffer + labelFillGamma, distance);
296
+ if (sdf.outlineBuffer > 0.0 && sdf.outlineBuffer < sdf.buffer) {
297
+ // Capped, as TextHaloBlurExtension caps its blur, so that the halo fades out before the
298
+ // distance field ends, at the edge of the glyph's quad, or no later than with sdf.gamma.
299
+ float labelHaloGamma = min(labelEdgeGamma, max(sdf.gamma, sdf.outlineBuffer - 4.0 / 255.0));
300
+ float inBorder = smoothstep(
301
+ sdf.outlineBuffer - labelHaloGamma, sdf.outlineBuffer + labelHaloGamma, distance
302
+ );
303
+ color = sdf.outlineColor;
304
+ float haloGamma = labelHaloGamma;
305
+ alpha = inBorder;
306
+ // The ring's inner edge, inside the glyph, fading over the halo's own width.
307
+ alpha = min(alpha, 1.0 - smoothstep(sdf.buffer, sdf.buffer + 2.0 * haloGamma, distance));
308
+ }
309
+ }`;
310
+
311
+ /**
312
+ * The character layer of a `LabelTextLayer`. `TextLayer` packs, for every character of every
313
+ * label, its place in the font atlas and its offset in the label, whenever the layer's data
314
+ * changes. The packed values of each label are kept, and reused while its text, character offsets
315
+ * and font atlas are the same, so the labels a layer held before are not packed again.
316
+ *
317
+ * On WebGL it antialiases glyph and halo edges as MapLibre GL JS (see `LABEL_SDF_EDGES`). The
318
+ * WebGPU shader is `MultiIconLayer`'s.
319
+ */
320
+ /** @internal Exported for tests. */
321
+ export class LabelCharactersLayer extends MultiIconLayer<any> {
322
+ static layerName = 'LabelCharactersLayer';
323
+ static defaultProps: DefaultProps<any> = {
324
+ sdfRadius: {type: 'number', value: 12},
325
+ sdfEdgeCapped: {type: 'boolean', value: true}
326
+ };
327
+
328
+ declare state: MultiIconLayer<any>['state'] & {
329
+ /** Each label's packed character definitions, by label object. */
330
+ iconDefs?: WeakMap<object, CachedIconDefs>;
331
+ /**
332
+ * The view whose glyphs along lines the glyph attribute holds, and the `getLineGlyphKey` they
333
+ * were placed for (null when some rows were placed for another).
334
+ */
335
+ lineGlyphView?: {id: string; key: string | null};
336
+ /** Glyphs along lines of the other views drawn, by view id, least recently drawn first. */
337
+ lineGlyphViews?: Map<string, ViewLineGlyphs>;
338
+ /** Counts the glyph attribute's updates, after which the other views' glyphs are placed again. */
339
+ lineGlyphRevision?: number;
340
+ };
341
+
342
+ /** Whether characters can be placed along lines: on WebGL, whose shader this layer extends. */
343
+ drawsLineGlyphs(): boolean {
344
+ return this.context.device.type !== 'webgpu';
345
+ }
346
+
347
+ /** Glyph and halo edges as MapLibre GL JS, then, on WebGL, glyphs along lines. */
348
+ getShaders(): any {
349
+ const iconShaders = super.getShaders();
350
+ if (!SDF_EDGES.test(iconShaders.fs)) {
351
+ throw new Error(`${this.id}: MultiIconLayer's signed distance field edges not found`);
352
+ }
353
+ const shaders = {
354
+ ...iconShaders,
355
+ modules: [...iconShaders.modules, labelEdgeUniforms],
356
+ fs: iconShaders.fs.replace(SDF_EDGES, LABEL_SDF_EDGES)
357
+ };
358
+ if (!this.drawsLineGlyphs()) {
359
+ return shaders;
360
+ }
361
+ const {vs} = shaders;
362
+ if (!vs.includes(TURN_CHARACTER) || !vs.includes(HIDE_CHARACTER_BEFORE)) {
363
+ throw new Error('LabelCharactersLayer: the text vertex shader has changed');
364
+ }
365
+ return {
366
+ ...shaders,
367
+ vs: vs
368
+ .replace(
369
+ 'in vec4 instanceClipRect;',
370
+ 'in vec4 instanceClipRect;\nin vec4 instanceLineGlyphs;\nin float instanceGlyphCentres;'
371
+ )
372
+ .replace(
373
+ TURN_CHARACTER,
374
+ `if (instanceLineGlyphs.w > 0.5) {
375
+ // A glyph along a line: turned about its advance centre, placed on the line.
376
+ pixelOffset.x -= instanceGlyphCentres;
377
+ pixelOffset = rotate_by_angle(pixelOffset, instanceLineGlyphs.z) * instanceScale + instanceLineGlyphs.xy;
378
+ } else {
379
+ ${TURN_CHARACTER}
380
+ }`
381
+ )
382
+ .replace(
383
+ HIDE_CHARACTER_BEFORE,
384
+ `if (instanceLineGlyphs.w < -0.5) {
385
+ gl_Position = vec4(0.0);
386
+ }
387
+ ${HIDE_CHARACTER_BEFORE}`
388
+ )
389
+ };
390
+ }
391
+
392
+ initializeState(): void {
393
+ super.initializeState();
394
+ if (!this.drawsLineGlyphs()) {
395
+ return;
396
+ }
397
+ this.getAttributeManager()!.addInstanced({
398
+ instanceLineGlyphs: {
399
+ size: 4,
400
+ accessor: 'getLineGlyphs',
401
+ update: this.calculateInstanceLineGlyphs
402
+ },
403
+ instanceGlyphCentres: {
404
+ size: 1,
405
+ accessor: ['getIcon', 'getIconOffsets', 'getLineGlyphs'],
406
+ update: this.calculateInstanceGlyphCentres
407
+ }
408
+ });
409
+ }
410
+
411
+ updateState(params: any): void {
412
+ super.updateState(params);
413
+ const {props, oldProps} = params;
414
+ const attributes = this.getAttributeManager();
415
+ if (attributes && this.drawsLineGlyphs()) {
416
+ if (props.iconMapping !== oldProps.iconMapping) {
417
+ attributes.invalidate('instanceGlyphCentres');
418
+ }
419
+ // Without glyphs along lines, both attributes are constant; with them, the centres are needed.
420
+ if (Boolean(props.getLineGlyphs) !== Boolean(oldProps.getLineGlyphs)) {
421
+ attributes.invalidate('instanceGlyphCentres');
422
+ }
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Places the glyphs along lines again before drawing when the view has moved to another zoom or
428
+ * bearing step (`getLineGlyphKey`). The rows and every other attribute stay as they are. A layer
429
+ * drawn in several views, as a main map and a minimap, keeps the glyphs of each: the attribute
430
+ * holds one view's, the others are kept in buffers of their own, bound in their place as each
431
+ * view is drawn.
432
+ */
433
+ draw(params: any): void {
434
+ const radius = Number((this.props as any).sdfRadius) || 12;
435
+ const foreshortening = getLabelForeshortening(this.context.viewport, this.props.billboard);
436
+ const capped = (this.props as any).sdfEdgeCapped === false ? 0 : 1;
437
+ this.setShaderModuleProps({
438
+ labelEdge: {distancePerAtlasPixel: 1 / radius, foreshortening, capped}
439
+ });
440
+ const {getLineGlyphs, getLineGlyphKey} = this.props as any;
441
+ const attribute = this.getAttributeManager()?.attributes.instanceLineGlyphs;
442
+ if (getLineGlyphs && getLineGlyphKey && attribute && !attribute.isConstant && attribute.value) {
443
+ const {viewport} = this.context;
444
+ const id = getViewId(viewport);
445
+ const key = getLineGlyphKey(viewport);
446
+ const held = this.state.lineGlyphView;
447
+ let buffer: Buffer | null = null;
448
+ if (!held || held.id === id) {
449
+ if (held?.key !== key) {
450
+ this.state.lineGlyphView = {id, key};
451
+ this.fillLineGlyphs(attribute, 0, Infinity);
452
+ (attribute as any).updateSubBuffer({startOffset: 0, endOffset: attribute.value.length});
453
+ LINE_GLYPH_ATTRIBUTE_STATS.refreshes++;
454
+ }
455
+ buffer = attribute.getBuffer();
456
+ } else {
457
+ buffer = this.getViewLineGlyphs(attribute, id, key).buffer;
458
+ }
459
+ // With one view, the attribute stays bound; with more, each view binds its own glyphs.
460
+ if (this.state.lineGlyphViews?.size && buffer) {
461
+ this.state.model?.setAttributes({instanceLineGlyphs: buffer});
462
+ }
463
+ }
464
+ super.draw(params);
465
+ }
466
+
467
+ /**
468
+ * The glyphs along lines of view `id`, which the glyph attribute does not hold: kept while its
469
+ * key and the attribute's rows are the same, placed again and uploaded otherwise.
470
+ */
471
+ getViewLineGlyphs(attribute: Attribute, id: string, key: string): ViewLineGlyphs {
472
+ if (!this.state.lineGlyphViews) {
473
+ this.state.lineGlyphViews = new Map();
474
+ }
475
+ const views = this.state.lineGlyphViews;
476
+ const revision = this.state.lineGlyphRevision ?? 0;
477
+ const length = (attribute.value as Float32Array).length;
478
+ let entry = views.get(id);
479
+ if (entry) {
480
+ // The most recently drawn view last.
481
+ views.delete(id);
482
+ }
483
+ if (!entry || entry.key !== key || entry.revision !== revision) {
484
+ if (!entry || entry.values.length !== length) {
485
+ entry?.buffer.destroy();
486
+ const values = new Float32Array(length);
487
+ const buffer = this.context.device.createBuffer({
488
+ id: `${this.id}-line-glyphs-${id}`,
489
+ usage: Buffer.VERTEX | Buffer.COPY_DST,
490
+ byteLength: values.byteLength
491
+ });
492
+ entry = {key, revision, values, buffer};
493
+ }
494
+ entry.key = key;
495
+ entry.revision = revision;
496
+ this.fillLineGlyphs(attribute, 0, Infinity, entry.values);
497
+ entry.buffer.write(entry.values);
498
+ LINE_GLYPH_ATTRIBUTE_STATS.refreshes++;
499
+ }
500
+ views.set(id, entry);
501
+ for (const [oldId, old] of views) {
502
+ if (views.size <= MAX_LINE_GLYPH_VIEWS) {
503
+ break;
504
+ }
505
+ old.buffer.destroy();
506
+ views.delete(oldId);
507
+ }
508
+ return entry;
509
+ }
510
+
511
+ finalizeState(context: any): void {
512
+ super.finalizeState(context);
513
+ this.forgetViewLineGlyphs();
514
+ }
515
+
516
+ /** Releases the glyphs along lines kept for views other than the attribute's. */
517
+ forgetViewLineGlyphs(): void {
518
+ for (const entry of this.state.lineGlyphViews?.values() ?? []) {
519
+ entry.buffer.destroy();
520
+ }
521
+ this.state.lineGlyphViews?.clear();
522
+ }
523
+
524
+ /** Each character's `getLineGlyphs` values; a constant when no label is placed along a line. */
525
+ protected calculateInstanceLineGlyphs(
526
+ attribute: Attribute,
527
+ {startRow, endRow}: {startRow: number; endRow: number}
528
+ ): void {
529
+ const {getLineGlyphs, getLineGlyphKey} = this.props as any;
530
+ if (!getLineGlyphs) {
531
+ this.forgetViewLineGlyphs();
532
+ setConstant(attribute);
533
+ return;
534
+ }
535
+ // The rows changed: every view's glyphs are placed again as it is drawn.
536
+ this.state.lineGlyphRevision = (this.state.lineGlyphRevision ?? 0) + 1;
537
+ const {viewport} = this.context;
538
+ const id = getViewId(viewport);
539
+ const key = getLineGlyphKey?.(viewport) ?? null;
540
+ const held = this.state.lineGlyphView;
541
+ // Rows left as they were hold the glyphs of the view and key the attribute held.
542
+ const allRows = startRow <= 0 && !Number.isFinite(endRow);
543
+ this.state.lineGlyphView = {
544
+ id,
545
+ key: allRows || (held?.id === id && held.key === key) ? key : null
546
+ };
547
+ this.fillLineGlyphs(attribute, startRow, endRow);
548
+ }
549
+
550
+ /**
551
+ * Writes the `getLineGlyphs` values of rows `startRow` to `endRow` at the current viewport, into
552
+ * `output`, laid out as the attribute: by default, its value.
553
+ */
554
+ fillLineGlyphs(
555
+ attribute: Attribute,
556
+ startRow: number,
557
+ endRow: number,
558
+ output = attribute.value as Float32Array
559
+ ): void {
560
+ const {data, getIcon, getLineGlyphs} = this.props as any;
561
+ const {viewport} = this.context;
562
+ const {iterable, objectInfo} = createIterable(data, startRow, endRow);
563
+ for (const object of iterable) {
564
+ objectInfo.index++;
565
+ // A row's characters, as `TextLayer` counts them; a row left out of this layer has none.
566
+ const start = attribute.getVertexOffset(objectInfo.index);
567
+ const end = attribute.getVertexOffset(objectInfo.index + 1);
568
+ if (end <= start || !getIcon(object, objectInfo)) {
569
+ continue;
570
+ }
571
+ const glyphs = getLineGlyphs(object, viewport) as Float32Array | null;
572
+ if (glyphs && glyphs.length === end - start) {
573
+ output.set(glyphs, start);
574
+ } else {
575
+ output.fill(0, start, end);
576
+ }
577
+ LINE_GLYPH_ATTRIBUTE_STATS.fills++;
578
+ LINE_GLYPH_ATTRIBUTE_STATS.bytes += (end - start) * 4;
579
+ }
580
+ }
581
+
582
+ /**
583
+ * Each character's advance centre in the label, in font atlas pixels, about which a glyph along a
584
+ * line turns: its offset in the label, less its frame's anchor, plus half its advance.
585
+ */
586
+ protected calculateInstanceGlyphCentres(
587
+ attribute: Attribute,
588
+ {startRow, endRow}: {startRow: number; endRow: number}
589
+ ): void {
590
+ const {data, getIcon, getIconOffsets, getLineGlyphs, iconMapping} = this.props as any;
591
+ if (!getLineGlyphs) {
592
+ setConstant(attribute);
593
+ return;
594
+ }
595
+ const output = attribute.value as Float32Array;
596
+ const {iterable, objectInfo} = createIterable(data, startRow, endRow);
597
+ for (const object of iterable) {
598
+ objectInfo.index++;
599
+ const text = getIcon(object, objectInfo) as string;
600
+ if (!text) {
601
+ continue;
602
+ }
603
+ const offsets = getIconOffsets(object, objectInfo) as number[];
604
+ let i = attribute.getVertexOffset(objectInfo.index);
605
+ let j = 0;
606
+ for (const character of Array.from(text)) {
607
+ const frame = iconMapping?.[character];
608
+ output[i++] = frame
609
+ ? offsets[j * 2] - (frame.anchorX ?? 0) + (frame.advance ?? 0) / 2
610
+ : offsets[j * 2];
611
+ j++;
612
+ }
613
+ }
614
+ }
615
+
616
+ protected calculateInstanceIconDefs(
617
+ attribute: Attribute,
618
+ {startRow, endRow}: {startRow: number; endRow: number}
619
+ ): void {
620
+ const {data, getIcon, getIconOffsets, iconAtlas, iconMapping} = this.props as any;
621
+ const size = attribute.size;
622
+ const output = attribute.value as Float32Array;
623
+ let i = attribute.getVertexOffset(startRow);
624
+ this.state.iconDefs ||= new WeakMap();
625
+ const cache = this.state.iconDefs;
626
+ const {iterable, objectInfo} = createIterable(data, startRow, endRow);
627
+ for (const object of iterable) {
628
+ objectInfo.index++;
629
+ const text = getIcon(object, objectInfo) as string;
630
+ const offsets = getIconOffsets(object, objectInfo) as number[];
631
+ if (!text) {
632
+ continue;
633
+ }
634
+ const isObject = object && typeof object === 'object';
635
+ let entry = isObject ? cache.get(object as object) : undefined;
636
+ if (
637
+ !entry ||
638
+ entry.text !== text ||
639
+ entry.iconAtlas !== iconAtlas ||
640
+ entry.iconMapping !== iconMapping ||
641
+ !isSameNumbers(entry.offsets, offsets)
642
+ ) {
643
+ const characters = Array.from(text);
644
+ const defs = new Float32Array(characters.length * size);
645
+ characters.forEach((character, j) => {
646
+ const def = this.getInstanceIconDef(character);
647
+ def[0] = offsets[j * 2];
648
+ def[1] += offsets[j * 2 + 1];
649
+ // mask
650
+ def[6] = 1;
651
+ defs.set(def, j * size);
652
+ });
653
+ entry = {text, offsets: offsets.slice(), iconAtlas, iconMapping, defs};
654
+ if (isObject) {
655
+ cache.set(object as object, entry);
656
+ }
657
+ }
658
+ output.set(entry.defs, i);
659
+ i += entry.defs.length;
660
+ }
661
+ }
662
+ }
663
+
664
+ /**
665
+ * Glyph attribute work for benchmarks: rows filled and bytes written from `getLineGlyphs`, and
666
+ * refreshes of the whole attribute when the view crosses a zoom or bearing step.
667
+ */
668
+ export const LINE_GLYPH_ATTRIBUTE_STATS = {fills: 0, bytes: 0, refreshes: 0};
669
+
670
+ /** The id of the view `viewport` draws, by which a layer keeps each view's glyphs along lines. */
671
+ function getViewId(viewport: any): string {
672
+ return String(viewport?.id ?? '');
673
+ }
674
+
675
+ /** Makes an attribute with a custom updater a constant zero. */
676
+ function setConstant(attribute: Attribute): void {
677
+ (attribute as any).constant = true;
678
+ attribute.value = new Float32Array(attribute.size);
679
+ }
680
+
681
+ /**
682
+ * Mip levels of the font atlas texture: the atlas and one of half its size. A label atlas is drawn
683
+ * at 64 px per em and labels at 10 to 16 px, 4 to 6 times smaller at device pixel ratio 1, where a
684
+ * full mip chain samples levels of 16 or 8 px per em: averaging the distance field over that many
685
+ * atlas pixels thins and blurs strokes unevenly. MapLibre GL JS samples its 24 px glyphs without
686
+ * mipmaps. The half-size level keeps the minification at most 2 to 3 times, as MapLibre's.
687
+ */
688
+ const LABEL_ATLAS_MIP_LEVELS = 2;
689
+
690
+ /**
691
+ * A `TextLayer` with MapLibre's `text-letter-spacing` and `text-justify`. Both change where the
692
+ * characters are drawn and the label box, which the background, and so the collision box, covers.
693
+ * The box is recomputed when the `getText` (letter spacing) or `getTextAnchor` (justification)
694
+ * update triggers change.
695
+ *
696
+ * It draws its glyphs from a font atlas texture shared with every other label layer of the same
697
+ * atlas. `TextLayer` hands its glyph layer the atlas canvas, from which deck.gl creates and uploads
698
+ * a texture per layer: one per label sublayer of every tile.
699
+ */
700
+ export class LabelTextLayer<DataT = any, ExtraPropsT extends {} = {}> extends TextLayer<
701
+ DataT,
702
+ ExtraPropsT & Required<_LabelTextLayerProps<DataT>>
703
+ > {
704
+ static layerName = 'LabelTextLayer';
705
+ static defaultProps = defaultProps;
706
+
707
+ declare state: TextLayer<DataT>['state'] & {
708
+ atlasTextures?: SharedTextureSet;
709
+ /** The shared texture of the current font atlas, which the glyph layer draws from. */
710
+ atlasTexture?: Texture;
711
+ /** Each label's layout, by label object (see `transformParagraph`). */
712
+ layouts?: WeakMap<object, CachedLayout>;
713
+ };
714
+
715
+ initializeState(): void {
716
+ super.initializeState();
717
+ // deck.gl caches font atlases by their font and settings, not by the renderer that draws their
718
+ // glyphs: a TextLayer with the same font and settings would share glyphs drawn without their
719
+ // overhang (see `getLabelFontRenderer`). The atlases of label layers have keys of their own.
720
+ const manager = this.state.fontAtlasManager as any;
721
+ const getKey = manager._getKey.bind(manager);
722
+ manager._getKey = () => `${getKey()} ${LABEL_ATLAS_KEY}`;
723
+ }
724
+
725
+ renderLayers(): any {
726
+ const {atlas} = this.state.fontAtlasManager;
727
+ const {device} = this.context;
728
+ if (atlas && device) {
729
+ let {atlasTextures} = this.state;
730
+ const maxMipLevels = this.props.atlasMipLevels ?? LABEL_ATLAS_MIP_LEVELS;
731
+ if (atlasTextures?.options.maxMipLevels !== maxMipLevels) {
732
+ atlasTextures?.releaseAll();
733
+ atlasTextures = new SharedTextureSet(device, {maxMipLevels});
734
+ this.state.atlasTextures = atlasTextures;
735
+ }
736
+ // A new atlas, with more characters, is a new texture; the previous one is released.
737
+ this.state.atlasTexture = atlasTextures.get(atlas);
738
+ atlasTextures.retainOnly([atlas]);
739
+ }
740
+ return super.renderLayers();
741
+ }
742
+
743
+ /** Its characters are drawn by a `LabelCharactersLayer`. */
744
+ getSubLayerClass(subLayerId: string, DefaultLayerClass: any): any {
745
+ if (subLayerId === 'characters' && DefaultLayerClass === MultiIconLayer) {
746
+ return super.getSubLayerClass(subLayerId, LabelCharactersLayer);
747
+ }
748
+ return super.getSubLayerClass(subLayerId, DefaultLayerClass);
749
+ }
750
+
751
+ /**
752
+ * The glyph layer's props, with the shared texture in place of the atlas canvas, and the row's
753
+ * placement slot (see `LabelPlacementExtension`).
754
+ */
755
+ getSubLayerProps(sublayerProps: any = {}): any {
756
+ const props = super.getSubLayerProps(sublayerProps);
757
+ if (sublayerProps.id === 'characters') {
758
+ if (this.state.atlasTexture) {
759
+ props.iconAtlas = this.state.atlasTexture;
760
+ }
761
+ // The distance field's reach, from which the glyph layer measures its antialiasing.
762
+ props.sdfRadius = this.props.fontSettings?.radius || 12;
763
+ props.sdfEdgeCapped = this.props.edgeGammaCapped !== false;
764
+ }
765
+ // The placement slot is a row's, and the sublayers draw this layer's rows: it is handed down
766
+ // as is. deck.gl would otherwise call it with the feature a row was made from.
767
+ const {getPlacementSlot, getLineGlyphs, getLineGlyphKey, updateTriggers} = this.props as any;
768
+ if (typeof getPlacementSlot === 'function') {
769
+ props.getPlacementSlot = getPlacementSlot;
770
+ }
771
+ // Glyphs along lines are the characters' only: handed down with their trigger.
772
+ if (sublayerProps.id === 'characters') {
773
+ props.getLineGlyphs = getLineGlyphs || null;
774
+ props.getLineGlyphKey = getLineGlyphKey || null;
775
+ props.updateTriggers = {
776
+ ...props.updateTriggers,
777
+ getLineGlyphs: updateTriggers?.getLineGlyphs
778
+ };
779
+ }
780
+ // The background box stays straight, about the anchor, where glyphs along a line may not be:
781
+ // it would pick empty space beside a bend. The glyphs pick their labels.
782
+ if (sublayerProps.id === 'background' && typeof getLineGlyphs === 'function') {
783
+ props.pickable = false;
784
+ }
785
+ return props;
786
+ }
787
+
788
+ /**
789
+ * A transparent background without a border draws nothing on screen: it only marks the labels in
790
+ * the collision map and the picking buffer. It is left out of the screen pass, which saves a draw
791
+ * per label layer and frame.
792
+ */
793
+ filterSubLayer({layer, renderPass, isPicking}: FilterContext): boolean {
794
+ return !(
795
+ renderPass === 'screen' &&
796
+ !isPicking &&
797
+ layer.id === `${this.id}-background` &&
798
+ this.hasInvisibleBackground()
799
+ );
800
+ }
801
+
802
+ /**
803
+ * Whether the background box is a constant transparent color without a border, no transition
804
+ * fades it out, and no highlight colors it.
805
+ */
806
+ hasInvisibleBackground(): boolean {
807
+ const {getBackgroundColor, getBorderWidth, transitions, autoHighlight, highlightedObjectIndex} =
808
+ this.props;
809
+ return (
810
+ !autoHighlight &&
811
+ !(typeof highlightedObjectIndex === 'number' && highlightedObjectIndex >= 0) &&
812
+ Array.isArray(getBackgroundColor) &&
813
+ getBackgroundColor[3] === 0 &&
814
+ getBorderWidth === 0 &&
815
+ !transitions?.getBackgroundColor &&
816
+ !transitions?.getBorderWidth
817
+ );
818
+ }
819
+
820
+ finalizeState(context: any): void {
821
+ super.finalizeState(context);
822
+ this.state.atlasTextures?.releaseAll();
823
+ }
824
+ }
825
+
826
+ /**
827
+ * `TextLayer` lays out the characters of each label in `transformParagraph`, from which both the
828
+ * character offsets and the background box are computed. Its types declare it private, so it is
829
+ * extended here on the prototype.
830
+ */
831
+ const transformParagraph: (
832
+ this: LabelTextLayer,
833
+ object: unknown,
834
+ objectInfo: AccessorContext<unknown>
835
+ ) => ParagraphLayout = (TextLayer.prototype as any).transformParagraph;
836
+
837
+ /** A label's layout and every input it was computed from (see `transformParagraph`). */
838
+ type CachedLayout = {
839
+ text: string;
840
+ letterSpacing: number;
841
+ justify: unknown;
842
+ anchor: unknown;
843
+ atlas: unknown;
844
+ lineHeight: number;
845
+ wordBreak: unknown;
846
+ maxWidth: number;
847
+ layout: ParagraphLayout;
848
+ };
849
+
850
+ Object.defineProperty(LabelTextLayer.prototype, 'transformParagraph', {
851
+ configurable: true,
852
+ writable: true,
853
+ /**
854
+ * Lays out a label, or returns the layout last computed for the same object from the same text,
855
+ * spacing, justification, anchor, font atlas and line settings. `TextLayer` lays out each label
856
+ * twice per update, for its background box and its characters, and a layer whose data changes
857
+ * lays out every label again: the labels it held before keep their layouts.
858
+ */
859
+ value(this: LabelTextLayer, object: unknown, objectInfo: AccessorContext<unknown>) {
860
+ const {getLetterSpacing, getTextJustify, getTextAnchor, lineHeight, wordBreak, maxWidth} =
861
+ this.props;
862
+ const text = this.state.getText!(object, objectInfo) || '';
863
+ const letterSpacing = Number(evaluate(getLetterSpacing, object, objectInfo)) || 0;
864
+ const justify = evaluate(getTextJustify, object, objectInfo);
865
+ const anchor = evaluate(getTextAnchor, object, objectInfo);
866
+ const {atlas} = this.state.fontAtlasManager;
867
+ let cache = this.state.layouts;
868
+ const cached = object && typeof object === 'object' ? cache?.get(object) : undefined;
869
+ if (
870
+ cached &&
871
+ cached.text === text &&
872
+ cached.letterSpacing === letterSpacing &&
873
+ cached.justify === justify &&
874
+ cached.anchor === anchor &&
875
+ cached.atlas === atlas &&
876
+ cached.lineHeight === lineHeight &&
877
+ cached.wordBreak === wordBreak &&
878
+ cached.maxWidth === maxWidth
879
+ ) {
880
+ return cached.layout;
881
+ }
882
+ let layout = transformParagraph.call(this, object, objectInfo);
883
+ if (letterSpacing || (justify !== 'auto' && layout.x.length)) {
884
+ const anchorPosition = ANCHOR_POSITION[anchor as keyof typeof ANCHOR_POSITION] ?? 0.5;
885
+ const {fontSize} = this.state.fontAtlasManager.props;
886
+ layout = spaceParagraph(
887
+ layout,
888
+ text,
889
+ letterSpacing * fontSize,
890
+ JUSTIFY_POSITION[justify as keyof typeof JUSTIFY_POSITION] ?? anchorPosition,
891
+ anchorPosition
892
+ );
893
+ }
894
+ if (object && typeof object === 'object') {
895
+ if (!cache) {
896
+ cache = new WeakMap();
897
+ this.state.layouts = cache;
898
+ }
899
+ cache.set(object, {
900
+ text,
901
+ letterSpacing,
902
+ justify,
903
+ anchor,
904
+ atlas,
905
+ lineHeight,
906
+ wordBreak,
907
+ maxWidth,
908
+ layout
909
+ });
910
+ }
911
+ return layout;
912
+ }
913
+ });