maplibre-gl 6.3.0 → 6.4.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/dist/maplibre-gl-dev.mjs +590 -208
- package/dist/maplibre-gl-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared-dev.mjs +258 -235
- package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared.mjs +2 -2
- package/dist/maplibre-gl-shared.mjs.map +1 -1
- package/dist/maplibre-gl-worker-dev.mjs +2 -2
- package/dist/maplibre-gl-worker.mjs +2 -2
- package/dist/maplibre-gl-worker.mjs.map +1 -1
- package/dist/maplibre-gl.d.ts +258 -97
- package/dist/maplibre-gl.mjs +18 -18
- package/dist/maplibre-gl.mjs.map +1 -1
- package/package.json +7 -7
- package/src/data/bucket/fill_extrusion_bucket.ts +1 -13
- package/src/data/bucket/round_polygon_corners.test.ts +84 -36
- package/src/data/bucket/round_polygon_corners.ts +90 -39
- package/src/data/extent_bounds.ts +28 -1
- package/src/geo/projection/globe_transform.test.ts +129 -1
- package/src/geo/projection/globe_utils.ts +189 -2
- package/src/geo/projection/vertical_perspective_camera_helper.test.ts +24 -0
- package/src/geo/projection/vertical_perspective_camera_helper.ts +11 -10
- package/src/render/image_atlas.test.ts +110 -2
- package/src/render/image_atlas.ts +35 -4
- package/src/render/image_manager.test.ts +66 -1
- package/src/render/image_manager.ts +136 -144
- package/src/render/painter.ts +5 -2
- package/src/render/pattern_atlas.test.ts +131 -0
- package/src/render/pattern_atlas.ts +144 -0
- package/src/shaders/glsl/_prelude.vertex.glsl +8 -8
- package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/color_relief.fragment.glsl +1 -2
- package/src/shaders/glsl/color_relief.fragment.glsl.g.ts +1 -1
- package/src/shaders/glsl/hillshade_prepare.fragment.glsl +12 -12
- package/src/shaders/glsl/hillshade_prepare.fragment.glsl.g.ts +1 -1
- package/src/source/image_source.test.ts +41 -31
- package/src/source/image_source.ts +14 -15
- package/src/source/raster_dem_tile_source.test.ts +37 -3
- package/src/source/raster_dem_tile_source.ts +1 -2
- package/src/source/raster_tile_source.test.ts +5 -26
- package/src/source/raster_tile_source.ts +4 -3
- package/src/style/style.test.ts +64 -0
- package/src/style/style.ts +37 -64
- package/src/symbol/cross_tile_symbol_index.test.ts +31 -0
- package/src/symbol/cross_tile_symbol_index.ts +67 -37
- package/src/tile/tile_manager.test.ts +69 -1
- package/src/tile/tile_manager.ts +14 -9
- package/src/ui/handler/two_fingers_touch.test.ts +14 -3
- package/src/ui/handler_inertia.test.ts +91 -0
- package/src/ui/handler_inertia.ts +65 -14
- package/src/ui/map_tests/map_images.test.ts +26 -0
- package/src/ui/map_tests/map_webgl.test.ts +2 -1
- package/src/ui/marker.test.ts +151 -0
- package/src/ui/marker.ts +120 -10
- package/src/util/image_request.ts +31 -1
- package/src/webgl/draw/draw_background.ts +1 -1
- package/src/webgl/program/pattern.ts +3 -3
|
@@ -1,54 +1,32 @@
|
|
|
1
|
-
/* eslint-disable key-spacing */
|
|
2
|
-
import potpack from 'potpack';
|
|
3
|
-
|
|
4
1
|
import {ErrorEvent, Evented} from '../util/evented.ts';
|
|
5
2
|
import {MapStyleImageMissingEvent} from '../ui/events.ts';
|
|
6
3
|
import {RGBAImage} from '../util/image.ts';
|
|
7
|
-
import {ImagePosition} from './image_atlas.ts';
|
|
8
|
-
import {Texture} from '../webgl/texture.ts';
|
|
9
4
|
import {renderStyleImage} from '../style/style_image.ts';
|
|
10
5
|
import {warnOnce} from '../util/util.ts';
|
|
11
6
|
|
|
12
7
|
import type {StyleImage} from '../style/style_image.ts';
|
|
13
|
-
import type {Context} from '../webgl/context.ts';
|
|
14
|
-
import type {PotpackBox} from 'potpack';
|
|
15
8
|
import type {GetImagesResponse} from '../util/actor_messages.ts';
|
|
16
9
|
|
|
17
10
|
export type MissingImageRequestHandler = (id: string) => void | Promise<void>;
|
|
18
11
|
|
|
19
|
-
type Pattern = {
|
|
20
|
-
bin: PotpackBox;
|
|
21
|
-
position: ImagePosition;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* When copied into the atlas texture, image data is padded by one pixel on each side. Icon
|
|
26
|
-
* images are padded with fully transparent pixels, while pattern images are padded with a
|
|
27
|
-
* copy of the image data wrapped from the opposite side. In both cases, this ensures the
|
|
28
|
-
* correct behavior of GL_LINEAR texture sampling mode.
|
|
29
|
-
*/
|
|
30
|
-
const padding = 1;
|
|
31
|
-
|
|
32
12
|
type ImageManagerEventType = {
|
|
33
13
|
error: ErrorEvent;
|
|
34
14
|
styleimagemissing: MapStyleImageMissingEvent;
|
|
35
15
|
};
|
|
36
16
|
|
|
37
17
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* 2. Builds a texture atlas for pattern images.
|
|
42
|
-
* 3. Rerenders renderable images once per frame
|
|
43
|
-
*
|
|
44
|
-
* These are disparate responsibilities and should eventually be handled by different classes. When we implement
|
|
45
|
-
* data-driven support for `*-pattern`, we'll likely use per-bucket pattern atlases, and that would be a good time
|
|
46
|
-
* to refactor this.
|
|
18
|
+
* Owns every image of the style - the ones the sprites bring in as well as the ones added at
|
|
19
|
+
* runtime - and tracks requests for them from the tile workers, sending responses when the
|
|
20
|
+
* requests are fulfilled.
|
|
47
21
|
*/
|
|
48
22
|
export class ImageManager extends Evented<ImageManagerEventType> {
|
|
49
|
-
images:
|
|
50
|
-
|
|
51
|
-
|
|
23
|
+
images: Record<string, StyleImage>;
|
|
24
|
+
/**
|
|
25
|
+
* Incremented on every {@link ImageManager.updateImage} call. {@link ImageAtlas} instances
|
|
26
|
+
* compare it against the value they last patched against, so that a tile whose atlas is up to
|
|
27
|
+
* date can skip its patching work entirely.
|
|
28
|
+
*/
|
|
29
|
+
updateVersion: number;
|
|
52
30
|
loaded: boolean;
|
|
53
31
|
/**
|
|
54
32
|
* This is used to track requests for images that are not yet available. When the image is loaded,
|
|
@@ -59,40 +37,37 @@ export class ImageManager extends Evented<ImageManagerEventType> {
|
|
|
59
37
|
promiseResolve: (value: GetImagesResponse | PromiseLike<GetImagesResponse>) => void;
|
|
60
38
|
}>;
|
|
61
39
|
missingImageResolver: MissingImageRequestHandler | null;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The images whose `render` callback already ran in the current frame. A given image usually
|
|
42
|
+
* sits in the atlas of several tiles, so it would otherwise be asked to re-render once per
|
|
43
|
+
* tile that holds it.
|
|
44
|
+
*/
|
|
45
|
+
private _renderCallbacksDispatchedThisFrame: Record<string, boolean>;
|
|
46
|
+
/**
|
|
47
|
+
* The ids of the images each sprite has brought in, keyed by sprite id, so that they can be
|
|
48
|
+
* removed again when that sprite is reloaded or removed.
|
|
49
|
+
*/
|
|
50
|
+
private _spriteImagesIds: Record<string, string[]>;
|
|
51
|
+
/** Cached result of {@link ImageManager.listImages}, invalidated whenever an image is added or removed */
|
|
52
|
+
private _imagesIds: string[] | null;
|
|
67
53
|
|
|
68
54
|
constructor() {
|
|
69
55
|
super();
|
|
70
56
|
this.images = {};
|
|
71
|
-
this.
|
|
72
|
-
this.callbackDispatchedThisFrame = {};
|
|
57
|
+
this.updateVersion = 0;
|
|
73
58
|
this.loaded = false;
|
|
74
59
|
this.requestors = [];
|
|
75
60
|
this.missingImageResolver = null;
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
79
|
-
this.dirty = true;
|
|
61
|
+
this._spriteImagesIds = {};
|
|
62
|
+
this._imagesIds = null;
|
|
63
|
+
this._renderCallbacksDispatchedThisFrame = {};
|
|
80
64
|
}
|
|
81
65
|
|
|
82
66
|
destroy(): void {
|
|
83
|
-
// Destroy atlas texture if it exists
|
|
84
|
-
if (this.atlasTexture) {
|
|
85
|
-
this.atlasTexture.destroy();
|
|
86
|
-
this.atlasTexture = null;
|
|
87
|
-
}
|
|
88
|
-
// Remove all images and patterns
|
|
89
67
|
for (const id of Object.keys(this.images)) {
|
|
90
68
|
this.removeImage(id);
|
|
91
69
|
}
|
|
92
|
-
|
|
93
|
-
this.patterns = {};
|
|
94
|
-
this.atlasImage = new RGBAImage({width: 1, height: 1});
|
|
95
|
-
this.dirty = true;
|
|
70
|
+
this._spriteImagesIds = {};
|
|
96
71
|
}
|
|
97
72
|
isLoaded(): boolean {
|
|
98
73
|
return this.loaded;
|
|
@@ -136,6 +111,7 @@ export class ImageManager extends Evented<ImageManagerEventType> {
|
|
|
136
111
|
if (this.images[id]) throw new Error(`Image id ${id} already exist, use updateImage instead`);
|
|
137
112
|
if (this._validate(id, image)) {
|
|
138
113
|
this.images[id] = image;
|
|
114
|
+
this._imagesIds = null;
|
|
139
115
|
if (image.isWebGLImage) this.updateImage(id, image, false);
|
|
140
116
|
}
|
|
141
117
|
}
|
|
@@ -182,28 +158,120 @@ export class ImageManager extends Evented<ImageManagerEventType> {
|
|
|
182
158
|
return content[3] >= content[1];
|
|
183
159
|
}
|
|
184
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Replaces an image that is already known under this id, bumping its version so that the
|
|
163
|
+
* atlases holding it notice.
|
|
164
|
+
*
|
|
165
|
+
* The old record is read directly rather than through {@link ImageManager.getImage}, which
|
|
166
|
+
* would force a synchronous decode of the sprite data of the very image being replaced. Its
|
|
167
|
+
* size therefore comes from whichever of the two the pixels are still in, since an image that
|
|
168
|
+
* came from a sprite and was never rendered has not been decoded yet, and its version is
|
|
169
|
+
* defaulted, since images start out without one.
|
|
170
|
+
*
|
|
171
|
+
* @param validate - whether to reject an image of a different size than the one it replaces
|
|
172
|
+
*/
|
|
185
173
|
updateImage(id: string, image: StyleImage, validate: boolean = true): void {
|
|
186
|
-
const oldImage = this.
|
|
187
|
-
if (validate
|
|
188
|
-
|
|
174
|
+
const oldImage = this.images[id];
|
|
175
|
+
if (validate) {
|
|
176
|
+
const oldData = oldImage.data || oldImage.spriteData;
|
|
177
|
+
if (oldData.width !== image.data.width || oldData.height !== image.data.height) {
|
|
178
|
+
throw new Error(`size mismatch between old image (${oldData.width}x${oldData.height}) and new image (${image.data.width}x${image.data.height}).`);
|
|
179
|
+
}
|
|
189
180
|
}
|
|
190
|
-
image.version = oldImage.version + 1;
|
|
181
|
+
image.version = (oldImage.version ?? 0) + 1;
|
|
191
182
|
this.images[id] = image;
|
|
192
|
-
this.
|
|
183
|
+
this.updateVersion++;
|
|
193
184
|
}
|
|
194
185
|
|
|
195
186
|
removeImage(id: string): void {
|
|
196
187
|
const image = this.images[id];
|
|
188
|
+
if (!image) return;
|
|
197
189
|
delete this.images[id];
|
|
198
|
-
|
|
190
|
+
this._imagesIds = null;
|
|
199
191
|
|
|
200
192
|
if (image.userImage?.onRemove) {
|
|
201
193
|
image.userImage.onRemove();
|
|
202
194
|
}
|
|
203
195
|
}
|
|
204
196
|
|
|
197
|
+
/**
|
|
198
|
+
* @returns the ids of every image currently held, sprite images and runtime ones alike. The
|
|
199
|
+
* returned array is shared between callers and must not be modified.
|
|
200
|
+
*/
|
|
205
201
|
listImages(): string[] {
|
|
206
|
-
|
|
202
|
+
this._imagesIds ??= Object.keys(this.images);
|
|
203
|
+
return this._imagesIds;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Images of the `default` sprite keep their plain id, the ones of any other sprite are
|
|
208
|
+
* namespaced by their sprite's id.
|
|
209
|
+
*/
|
|
210
|
+
private _getSpriteImageId(spriteId: string, imageId: string): string {
|
|
211
|
+
return spriteId === 'default' ? imageId : `${spriteId}:${imageId}`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Takes over the images of a single sprite: adds the new ones, updates the ones that are
|
|
216
|
+
* already known, and removes the ones that a previous load of this same sprite had brought in
|
|
217
|
+
* but that are not part of it anymore.
|
|
218
|
+
*
|
|
219
|
+
* @param spriteId - the id of the sprite the images belong to
|
|
220
|
+
* @param images - the sprite's images, keyed by their id within the sprite
|
|
221
|
+
* @returns the ids of the images that this sprite now provides, and the ids of the ones that
|
|
222
|
+
* were removed because they are no longer part of it
|
|
223
|
+
*/
|
|
224
|
+
setSpriteImages(spriteId: string, images: Record<string, StyleImage>): {loaded: string[]; removed: string[]} {
|
|
225
|
+
const previousIds = this._spriteImagesIds[spriteId] ?? [];
|
|
226
|
+
const loaded: string[] = [];
|
|
227
|
+
|
|
228
|
+
for (const id in images) {
|
|
229
|
+
const imageId = this._getSpriteImageId(spriteId, id);
|
|
230
|
+
loaded.push(imageId);
|
|
231
|
+
|
|
232
|
+
if (imageId in this.images) {
|
|
233
|
+
this.updateImage(imageId, images[id], false);
|
|
234
|
+
} else {
|
|
235
|
+
this.addImage(imageId, images[id]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const loadedIds = new Set(loaded);
|
|
240
|
+
const removed = previousIds.filter(imageId => !loadedIds.has(imageId));
|
|
241
|
+
for (const imageId of removed) {
|
|
242
|
+
this.removeImage(imageId);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this._spriteImagesIds[spriteId] = loaded;
|
|
246
|
+
return {loaded, removed};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Removes all the images a single sprite has brought in.
|
|
251
|
+
* @returns the ids of the removed images
|
|
252
|
+
*/
|
|
253
|
+
removeSpriteImages(spriteId: string): string[] {
|
|
254
|
+
const removed = this._spriteImagesIds[spriteId] ?? [];
|
|
255
|
+
for (const imageId of removed) {
|
|
256
|
+
this.removeImage(imageId);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
delete this._spriteImagesIds[spriteId];
|
|
260
|
+
return removed;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Removes the images of every sprite, leaving the images added at runtime alone.
|
|
265
|
+
* @returns the ids of the removed images
|
|
266
|
+
*/
|
|
267
|
+
removeAllSpriteImages(): string[] {
|
|
268
|
+
const removed = Object.values(this._spriteImagesIds).flat();
|
|
269
|
+
for (const imageId of removed) {
|
|
270
|
+
this.removeImage(imageId);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this._spriteImagesIds = {};
|
|
274
|
+
return removed;
|
|
207
275
|
}
|
|
208
276
|
|
|
209
277
|
setMissingImageResolver(resolver: MissingImageRequestHandler | null): void {
|
|
@@ -237,7 +305,7 @@ export class ImageManager extends Evented<ImageManagerEventType> {
|
|
|
237
305
|
const resolver = this.missingImageResolver;
|
|
238
306
|
|
|
239
307
|
if (resolver) {
|
|
240
|
-
await Promise.
|
|
308
|
+
await Promise.allSettled(Array.from(unresolvedIds, (id) => resolver(id)));
|
|
241
309
|
}
|
|
242
310
|
|
|
243
311
|
const response: GetImagesResponse = {};
|
|
@@ -272,94 +340,18 @@ export class ImageManager extends Evented<ImageManagerEventType> {
|
|
|
272
340
|
return response;
|
|
273
341
|
}
|
|
274
342
|
|
|
275
|
-
// Pattern stuff
|
|
276
|
-
|
|
277
|
-
getPixelSize(): {width: number; height: number} {
|
|
278
|
-
const {width, height} = this.atlasImage;
|
|
279
|
-
return {width, height};
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
getPattern(id: string): ImagePosition {
|
|
283
|
-
const pattern = this.patterns[id];
|
|
284
|
-
|
|
285
|
-
const image = this.getImage(id);
|
|
286
|
-
if (!image) {
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- pattern?.position.version would be undefined when pattern is nullish, making undefined === undefined true
|
|
291
|
-
if (pattern && pattern.position.version === image.version) {
|
|
292
|
-
return pattern.position;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
if (!pattern) {
|
|
296
|
-
const w = image.data.width + padding * 2;
|
|
297
|
-
const h = image.data.height + padding * 2;
|
|
298
|
-
const bin = {w, h, x: 0, y: 0};
|
|
299
|
-
const position = new ImagePosition(bin, image);
|
|
300
|
-
this.patterns[id] = {bin, position};
|
|
301
|
-
} else {
|
|
302
|
-
pattern.position.version = image.version;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
this._updatePatternAtlas();
|
|
306
|
-
|
|
307
|
-
return this.patterns[id].position;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
bind(context: Context): void {
|
|
311
|
-
const gl = context.gl;
|
|
312
|
-
if (!this.atlasTexture) {
|
|
313
|
-
this.atlasTexture = new Texture(context, this.atlasImage, gl.RGBA);
|
|
314
|
-
} else if (this.dirty) {
|
|
315
|
-
this.atlasTexture.update(this.atlasImage);
|
|
316
|
-
this.dirty = false;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
this.atlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
_updatePatternAtlas(): void {
|
|
323
|
-
const bins = [];
|
|
324
|
-
for (const id in this.patterns) {
|
|
325
|
-
bins.push(this.patterns[id].bin);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
const {w, h} = potpack(bins);
|
|
329
|
-
|
|
330
|
-
const dst = this.atlasImage;
|
|
331
|
-
dst.resize({width: w || 1, height: h || 1});
|
|
332
|
-
|
|
333
|
-
for (const id in this.patterns) {
|
|
334
|
-
const {bin} = this.patterns[id];
|
|
335
|
-
const x = bin.x + padding;
|
|
336
|
-
const y = bin.y + padding;
|
|
337
|
-
const src = this.getImage(id).data;
|
|
338
|
-
const w = src.width;
|
|
339
|
-
const h = src.height;
|
|
340
|
-
|
|
341
|
-
RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y}, {width: w, height: h});
|
|
342
|
-
|
|
343
|
-
// Add 1 pixel wrapped padding on each side of the image.
|
|
344
|
-
RGBAImage.copy(src, dst, {x: 0, y: h - 1}, {x, y: y - 1}, {width: w, height: 1}); // T
|
|
345
|
-
RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y: y + h}, {width: w, height: 1}); // B
|
|
346
|
-
RGBAImage.copy(src, dst, {x: w - 1, y: 0}, {x: x - 1, y}, {width: 1, height: h}); // L
|
|
347
|
-
RGBAImage.copy(src, dst, {x: 0, y: 0}, {x: x + w, y}, {width: 1, height: h}); // R
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
this.dirty = true;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
343
|
beginFrame(): void {
|
|
354
|
-
this.
|
|
344
|
+
this._renderCallbacksDispatchedThisFrame = {};
|
|
355
345
|
}
|
|
356
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Re-renders the images among `ids` that were added with a `render` callback (see
|
|
349
|
+
* `StyleImageInterface`), at most once per frame each.
|
|
350
|
+
*/
|
|
357
351
|
dispatchRenderCallbacks(ids: string[]): void {
|
|
358
352
|
for (const id of ids) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
if (this.callbackDispatchedThisFrame[id]) continue;
|
|
362
|
-
this.callbackDispatchedThisFrame[id] = true;
|
|
353
|
+
if (this._renderCallbacksDispatchedThisFrame[id]) continue;
|
|
354
|
+
this._renderCallbacksDispatchedThisFrame[id] = true;
|
|
363
355
|
|
|
364
356
|
const image = this.getImage(id);
|
|
365
357
|
if (!image) warnOnce(`Image with ID: "${id}" was not found`);
|
package/src/render/painter.ts
CHANGED
|
@@ -29,6 +29,7 @@ import type {StyleLayer} from '../style/style_layer.ts';
|
|
|
29
29
|
import type {CrossFaded} from '../style/properties.ts';
|
|
30
30
|
import type {LineAtlas} from './line_atlas.ts';
|
|
31
31
|
import type {ImageManager} from './image_manager.ts';
|
|
32
|
+
import type {PatternAtlas} from './pattern_atlas.ts';
|
|
32
33
|
import type {GlyphManager} from './glyph_manager.ts';
|
|
33
34
|
import type {VertexBuffer} from '../webgl/vertex_buffer.ts';
|
|
34
35
|
import type {IndexBuffer} from '../webgl/index_buffer.ts';
|
|
@@ -140,6 +141,7 @@ export class Painter {
|
|
|
140
141
|
options: PainterOptions;
|
|
141
142
|
lineAtlas: LineAtlas;
|
|
142
143
|
imageManager: ImageManager;
|
|
144
|
+
patternAtlas: PatternAtlas;
|
|
143
145
|
glyphManager: GlyphManager;
|
|
144
146
|
depthRangeFor3D: DepthRangeType;
|
|
145
147
|
opaquePassCutoff: number;
|
|
@@ -518,6 +520,7 @@ export class Painter {
|
|
|
518
520
|
|
|
519
521
|
this.lineAtlas = style.lineAtlas;
|
|
520
522
|
this.imageManager = style.imageManager;
|
|
523
|
+
this.patternAtlas = style.patternAtlas;
|
|
521
524
|
this.glyphManager = style.glyphManager;
|
|
522
525
|
|
|
523
526
|
this.symbolFadeChange = style.placement.symbolFadeChange(now());
|
|
@@ -807,8 +810,8 @@ export class Painter {
|
|
|
807
810
|
isPatternMissing(image?: CrossFaded<ResolvedImage> | null): boolean {
|
|
808
811
|
if (!image) return false;
|
|
809
812
|
if (!image.from || !image.to) return true;
|
|
810
|
-
const imagePosA = this.
|
|
811
|
-
const imagePosB = this.
|
|
813
|
+
const imagePosA = this.patternAtlas.getPattern(image.from.toString());
|
|
814
|
+
const imagePosB = this.patternAtlas.getPattern(image.to.toString());
|
|
812
815
|
return !imagePosA || !imagePosB;
|
|
813
816
|
}
|
|
814
817
|
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {describe, expect, test, vi, beforeEach} from 'vitest';
|
|
2
|
+
import {ImageManager} from './image_manager.ts';
|
|
3
|
+
import {PatternAtlas} from './pattern_atlas.ts';
|
|
4
|
+
import {Context} from '../webgl/context.ts';
|
|
5
|
+
import {Texture} from '../webgl/texture.ts';
|
|
6
|
+
import {createNullGL} from '../util/test/null_gl.ts';
|
|
7
|
+
import {RGBAImage} from '../util/image.ts';
|
|
8
|
+
|
|
9
|
+
import type {StyleImage} from '../style/style_image.ts';
|
|
10
|
+
|
|
11
|
+
const PADDING_ON_EACH_SIDE = 2;
|
|
12
|
+
const sizeInAtlas = (size: number) => ({width: size + PADDING_ON_EACH_SIDE, height: size + PADDING_ON_EACH_SIDE});
|
|
13
|
+
|
|
14
|
+
function createStyleImage(size: number, fill: number = 0): StyleImage {
|
|
15
|
+
const data = new Uint8Array(size * size * 4).fill(fill);
|
|
16
|
+
return {
|
|
17
|
+
data: new RGBAImage({width: size, height: size}, data),
|
|
18
|
+
pixelRatio: 1,
|
|
19
|
+
sdf: false
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('PatternAtlas', () => {
|
|
24
|
+
let imageManager: ImageManager;
|
|
25
|
+
let patternAtlas: PatternAtlas;
|
|
26
|
+
let context: Context;
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.restoreAllMocks();
|
|
30
|
+
imageManager = new ImageManager();
|
|
31
|
+
patternAtlas = new PatternAtlas(imageManager);
|
|
32
|
+
context = new Context(createNullGL());
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('packs an image into a slot padded by one pixel on each side, and keeps handing back its position while it does not change', () => {
|
|
36
|
+
imageManager.addImage('pattern', createStyleImage(4));
|
|
37
|
+
|
|
38
|
+
const position = patternAtlas.getPattern('pattern');
|
|
39
|
+
|
|
40
|
+
expect(position.displaySize).toEqual([4, 4]);
|
|
41
|
+
expect(patternAtlas.getPixelSize()).toEqual(sizeInAtlas(4));
|
|
42
|
+
expect(patternAtlas.getPattern('pattern')).toBe(position);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('re-packs the slot when a sprite reload replaces the image with a bigger one', () => {
|
|
46
|
+
imageManager.addImage('pattern', createStyleImage(4));
|
|
47
|
+
patternAtlas.getPattern('pattern');
|
|
48
|
+
|
|
49
|
+
imageManager.updateImage('pattern', createStyleImage(8), false);
|
|
50
|
+
|
|
51
|
+
expect(patternAtlas.getPattern('pattern').displaySize).toEqual([8, 8]);
|
|
52
|
+
expect(patternAtlas.getPixelSize()).toEqual(sizeInAtlas(8));
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('re-packs the slot when the image is removed and another one is added under the same id, even though neither carries a version', () => {
|
|
56
|
+
imageManager.addImage('pattern', createStyleImage(4));
|
|
57
|
+
const positionOfRemovedImage = patternAtlas.getPattern('pattern');
|
|
58
|
+
|
|
59
|
+
imageManager.removeImage('pattern');
|
|
60
|
+
imageManager.addImage('pattern', createStyleImage(8, 255));
|
|
61
|
+
|
|
62
|
+
const position = patternAtlas.getPattern('pattern');
|
|
63
|
+
expect(position).not.toBe(positionOfRemovedImage);
|
|
64
|
+
expect(position.displaySize).toEqual([8, 8]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('keeps the slot of an image that was updated in place, as an animated pattern is, and follows its version', () => {
|
|
68
|
+
const image = createStyleImage(4);
|
|
69
|
+
imageManager.addImage('pattern', image);
|
|
70
|
+
const position = patternAtlas.getPattern('pattern');
|
|
71
|
+
|
|
72
|
+
imageManager.updateImage('pattern', image, false);
|
|
73
|
+
|
|
74
|
+
expect(patternAtlas.getPattern('pattern')).toBe(position);
|
|
75
|
+
expect(position.version).toBe(image.version);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('returns null for an image that was removed, and reclaims its slot on the next re-packing', () => {
|
|
79
|
+
imageManager.addImage('removedPattern', createStyleImage(4));
|
|
80
|
+
imageManager.addImage('keptPattern', createStyleImage(4));
|
|
81
|
+
patternAtlas.getPattern('removedPattern');
|
|
82
|
+
patternAtlas.getPattern('keptPattern');
|
|
83
|
+
|
|
84
|
+
imageManager.removeImage('removedPattern');
|
|
85
|
+
|
|
86
|
+
expect(patternAtlas.getPattern('removedPattern')).toBeNull();
|
|
87
|
+
|
|
88
|
+
imageManager.updateImage('keptPattern', createStyleImage(4), false);
|
|
89
|
+
expect(patternAtlas.getPattern('keptPattern').displaySize).toEqual([4, 4]);
|
|
90
|
+
expect(patternAtlas.getPixelSize()).toEqual(sizeInAtlas(4));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('uploads the atlas to its texture only once it changed', () => {
|
|
94
|
+
const update = vi.spyOn(Texture.prototype, 'update');
|
|
95
|
+
const bind = vi.spyOn(Texture.prototype, 'bind');
|
|
96
|
+
imageManager.addImage('pattern', createStyleImage(4));
|
|
97
|
+
patternAtlas.getPattern('pattern');
|
|
98
|
+
|
|
99
|
+
patternAtlas.bind(context);
|
|
100
|
+
const uploadsToCreateTheTexture = update.mock.calls.length;
|
|
101
|
+
patternAtlas.bind(context);
|
|
102
|
+
expect(update).toHaveBeenCalledTimes(uploadsToCreateTheTexture);
|
|
103
|
+
|
|
104
|
+
imageManager.addImage('anotherPattern', createStyleImage(4));
|
|
105
|
+
patternAtlas.getPattern('anotherPattern');
|
|
106
|
+
patternAtlas.bind(context);
|
|
107
|
+
patternAtlas.bind(context);
|
|
108
|
+
|
|
109
|
+
expect(update).toHaveBeenCalledTimes(uploadsToCreateTheTexture + 1);
|
|
110
|
+
expect(bind).toHaveBeenCalledTimes(4);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('destroy releases the texture and empties the atlas', () => {
|
|
114
|
+
const destroy = vi.spyOn(Texture.prototype, 'destroy');
|
|
115
|
+
imageManager.addImage('pattern', createStyleImage(4));
|
|
116
|
+
patternAtlas.getPattern('pattern');
|
|
117
|
+
patternAtlas.bind(context);
|
|
118
|
+
|
|
119
|
+
patternAtlas.destroy();
|
|
120
|
+
|
|
121
|
+
expect(destroy).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(patternAtlas.getPixelSize()).toEqual({width: 1, height: 1});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test('destroy does nothing to an atlas that was never bound', () => {
|
|
126
|
+
const destroy = vi.spyOn(Texture.prototype, 'destroy');
|
|
127
|
+
|
|
128
|
+
expect(() => patternAtlas.destroy()).not.toThrow();
|
|
129
|
+
expect(destroy).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/* eslint-disable key-spacing */
|
|
2
|
+
import potpack from 'potpack';
|
|
3
|
+
|
|
4
|
+
import {RGBAImage} from '../util/image.ts';
|
|
5
|
+
import {ImagePosition} from './image_atlas.ts';
|
|
6
|
+
import {Texture} from '../webgl/texture.ts';
|
|
7
|
+
|
|
8
|
+
import type {ImageManager} from './image_manager.ts';
|
|
9
|
+
import type {StyleImage} from '../style/style_image.ts';
|
|
10
|
+
import type {Context} from '../webgl/context.ts';
|
|
11
|
+
import type {PotpackBox} from 'potpack';
|
|
12
|
+
|
|
13
|
+
type Pattern = {
|
|
14
|
+
bin: PotpackBox;
|
|
15
|
+
position: ImagePosition;
|
|
16
|
+
/**
|
|
17
|
+
* The image this entry was packed from. Kept to notice when an image was removed and added
|
|
18
|
+
* again under the same id, in which case the entry has to be rebuilt even though the version
|
|
19
|
+
* of the new image may happen to match the one of the old.
|
|
20
|
+
*/
|
|
21
|
+
image: StyleImage;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* When copied into the atlas texture, pattern images are padded by one pixel on each side with a
|
|
26
|
+
* copy of the image data wrapped from the opposite side. This ensures the correct behavior of
|
|
27
|
+
* GL_LINEAR texture sampling mode.
|
|
28
|
+
*/
|
|
29
|
+
const PADDING = 1;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A texture atlas of the pattern images that are drawn without a tile's own {@link ImageAtlas},
|
|
33
|
+
* i.e. `background-pattern`. Entries are packed on demand and re-packed whenever one of them is
|
|
34
|
+
* added or changes, which is rare enough for the repacking cost not to matter.
|
|
35
|
+
*/
|
|
36
|
+
export class PatternAtlas {
|
|
37
|
+
private _imageManager: ImageManager;
|
|
38
|
+
private _entries: Record<string, Pattern>;
|
|
39
|
+
private _image: RGBAImage;
|
|
40
|
+
private _texture: Texture;
|
|
41
|
+
private _dirty: boolean;
|
|
42
|
+
|
|
43
|
+
constructor(imageManager: ImageManager) {
|
|
44
|
+
this._imageManager = imageManager;
|
|
45
|
+
this._entries = {};
|
|
46
|
+
this._image = new RGBAImage({width: 1, height: 1});
|
|
47
|
+
this._dirty = true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
destroy(): void {
|
|
51
|
+
if (this._texture) {
|
|
52
|
+
this._texture.destroy();
|
|
53
|
+
this._texture = null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
this._entries = {};
|
|
57
|
+
this._image = new RGBAImage({width: 1, height: 1});
|
|
58
|
+
this._dirty = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getPixelSize(): {width: number; height: number} {
|
|
62
|
+
const {width, height} = this._image;
|
|
63
|
+
return {width, height};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @returns the position of the pattern within the atlas, or `null` if there is no such image
|
|
68
|
+
*/
|
|
69
|
+
getPattern(id: string): ImagePosition {
|
|
70
|
+
const image = this._imageManager.getImage(id);
|
|
71
|
+
if (!image) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const entry = this._entries[id];
|
|
76
|
+
if (entry?.image !== image) {
|
|
77
|
+
// there is no entry yet, or the image was replaced by another one under the same id,
|
|
78
|
+
// in which case the slot has to be packed anew - the new image may have another size
|
|
79
|
+
const w = image.data.width + PADDING * 2;
|
|
80
|
+
const h = image.data.height + PADDING * 2;
|
|
81
|
+
const bin = {w, h, x: 0, y: 0};
|
|
82
|
+
this._entries[id] = {bin, position: new ImagePosition(bin, image), image};
|
|
83
|
+
} else if (entry.position.version !== image.version) {
|
|
84
|
+
entry.position.version = image.version;
|
|
85
|
+
} else {
|
|
86
|
+
return entry.position;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._update();
|
|
90
|
+
|
|
91
|
+
return this._entries[id].position;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
bind(context: Context): void {
|
|
95
|
+
const gl = context.gl;
|
|
96
|
+
if (!this._texture) {
|
|
97
|
+
this._texture = new Texture(context, this._image, gl.RGBA);
|
|
98
|
+
this._dirty = false;
|
|
99
|
+
} else if (this._dirty) {
|
|
100
|
+
this._texture.update(this._image);
|
|
101
|
+
this._dirty = false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
this._texture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private _update(): void {
|
|
108
|
+
// drop the entries whose image is gone - they hold on to a slot of the atlas for nothing
|
|
109
|
+
for (const id in this._entries) {
|
|
110
|
+
if (!this._imageManager.getImage(id)) {
|
|
111
|
+
delete this._entries[id];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const bins = [];
|
|
116
|
+
for (const id in this._entries) {
|
|
117
|
+
bins.push(this._entries[id].bin);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const {w, h} = potpack(bins);
|
|
121
|
+
|
|
122
|
+
const dst = this._image;
|
|
123
|
+
dst.resize({width: w || 1, height: h || 1});
|
|
124
|
+
|
|
125
|
+
for (const id in this._entries) {
|
|
126
|
+
const {bin} = this._entries[id];
|
|
127
|
+
const x = bin.x + PADDING;
|
|
128
|
+
const y = bin.y + PADDING;
|
|
129
|
+
const src = this._entries[id].image.data;
|
|
130
|
+
const w = src.width;
|
|
131
|
+
const h = src.height;
|
|
132
|
+
|
|
133
|
+
RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y}, {width: w, height: h});
|
|
134
|
+
|
|
135
|
+
// Add 1 pixel wrapped padding on each side of the image.
|
|
136
|
+
RGBAImage.copy(src, dst, {x: 0, y: h - 1}, {x, y: y - 1}, {width: w, height: 1}); // T
|
|
137
|
+
RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y: y + h}, {width: w, height: 1}); // B
|
|
138
|
+
RGBAImage.copy(src, dst, {x: w - 1, y: 0}, {x: x - 1, y}, {width: 1, height: h}); // L
|
|
139
|
+
RGBAImage.copy(src, dst, {x: 0, y: 0}, {x: x + w, y}, {width: 1, height: h}); // R
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
this._dirty = true;
|
|
143
|
+
}
|
|
144
|
+
}
|