@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.
- package/README.md +34 -2
- package/THIRD-PARTY-NOTICES.md +205 -0
- package/dist/geojson-worker.js +2160 -0
- package/dist/geojson-worker.js.map +6 -0
- package/dist/index.cjs +11388 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.js +11369 -0
- package/dist/index.js.map +6 -0
- package/dist/map-style.cjs +445 -0
- package/dist/map-style.cjs.map +6 -0
- package/dist/map-style.js +431 -0
- package/dist/map-style.js.map +6 -0
- package/dist/types/arabic-shaping.d.ts +7 -0
- package/dist/types/atmosphere-layer.d.ts +10 -0
- package/dist/types/basemap-layer.d.ts +150 -0
- package/dist/types/bidi.d.ts +8 -0
- package/dist/types/circle-layer.d.ts +49 -0
- package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
- package/dist/types/fill-outline-extension.d.ts +22 -0
- package/dist/types/fill-pattern.d.ts +99 -0
- package/dist/types/fill-style-extensions.d.ts +100 -0
- package/dist/types/geojson-source.d.ts +29 -0
- package/dist/types/geojson-tiler.d.ts +48 -0
- package/dist/types/geojson-worker.d.ts +40 -0
- package/dist/types/globe-horizon-extension.d.ts +61 -0
- package/dist/types/globe-layers.d.ts +239 -0
- package/dist/types/globe-line-angle-extension.d.ts +37 -0
- package/dist/types/glyph-font-renderer.d.ts +37 -0
- package/dist/types/glyph-pbf.d.ts +37 -0
- package/dist/types/glyph-store.d.ts +103 -0
- package/dist/types/grid-index.d.ts +47 -0
- package/dist/types/hillshade-layer.d.ts +154 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/is-globe-viewport.d.ts +2 -0
- package/dist/types/label-font-renderer.d.ts +18 -0
- package/dist/types/label-placement.d.ts +247 -0
- package/dist/types/label-text-layer.d.ts +231 -0
- package/dist/types/line-blur-extension.d.ts +73 -0
- package/dist/types/line-glyphs.d.ts +60 -0
- package/dist/types/line-placement.d.ts +93 -0
- package/dist/types/map-style-loader.d.ts +48 -0
- package/dist/types/map-style-schema.d.ts +72 -0
- package/dist/types/map-style.d.ts +6 -0
- package/dist/types/mapbox-style.d.ts +40 -0
- package/dist/types/mvt-label-layer.d.ts +923 -0
- package/dist/types/polylabel.d.ts +17 -0
- package/dist/types/raster-color-extension.d.ts +146 -0
- package/dist/types/shared-texture.d.ts +34 -0
- package/dist/types/sort-key.d.ts +10 -0
- package/dist/types/sprite.d.ts +82 -0
- package/dist/types/style-accessor.d.ts +45 -0
- package/dist/types/style-expression.d.ts +83 -0
- package/dist/types/style-resolver.d.ts +94 -0
- package/dist/types/text-font.d.ts +36 -0
- package/dist/types/text-halo-blur-extension.d.ts +55 -0
- package/dist/types/text-layout.d.ts +71 -0
- package/dist/types/util.d.ts +22 -0
- package/dist/types/zoom-paint-extension.d.ts +72 -0
- package/package.json +80 -3
- package/src/arabic-shaping.ts +366 -0
- package/src/atmosphere-layer.ts +19 -0
- package/src/basemap-layer.ts +533 -0
- package/src/bidi.ts +199 -0
- package/src/circle-layer.ts +191 -0
- package/src/fill-extrusion-lighting-extension.ts +206 -0
- package/src/fill-outline-extension.ts +70 -0
- package/src/fill-pattern.ts +400 -0
- package/src/fill-style-extensions.ts +177 -0
- package/src/geojson-source.ts +318 -0
- package/src/geojson-tiler.ts +185 -0
- package/src/geojson-worker.ts +98 -0
- package/src/globe-horizon-extension.ts +64 -0
- package/src/globe-layers.ts +3487 -0
- package/src/globe-line-angle-extension.ts +131 -0
- package/src/glyph-font-renderer.ts +173 -0
- package/src/glyph-pbf.ts +164 -0
- package/src/glyph-store.ts +326 -0
- package/src/grid-index.ts +177 -0
- package/src/hillshade-layer.ts +775 -0
- package/src/index.ts +18 -0
- package/src/is-globe-viewport.ts +24 -0
- package/src/label-font-renderer.ts +159 -0
- package/src/label-placement.ts +727 -0
- package/src/label-text-layer.ts +913 -0
- package/src/line-blur-extension.ts +237 -0
- package/src/line-glyphs.ts +364 -0
- package/src/line-placement.ts +446 -0
- package/src/map-style-loader.ts +56 -0
- package/src/map-style-schema.ts +83 -0
- package/src/map-style.ts +23 -0
- package/src/mapbox-style.ts +168 -0
- package/src/mvt-label-layer.ts +3744 -0
- package/src/polylabel.ts +312 -0
- package/src/raster-color-extension.ts +245 -0
- package/src/shared-texture.ts +153 -0
- package/src/sort-key.ts +63 -0
- package/src/sprite.ts +244 -0
- package/src/style-accessor.ts +95 -0
- package/src/style-expression.ts +327 -0
- package/src/style-resolver.ts +301 -0
- package/src/text-font.ts +150 -0
- package/src/text-halo-blur-extension.ts +115 -0
- package/src/text-layout.ts +441 -0
- package/src/util.ts +47 -0
- 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
|
+
});
|