maplibre-gl 6.2.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.
Files changed (150) hide show
  1. package/dist/maplibre-gl-dev.mjs +1108 -427
  2. package/dist/maplibre-gl-dev.mjs.map +1 -1
  3. package/dist/maplibre-gl-shared-dev.mjs +359 -291
  4. package/dist/maplibre-gl-shared-dev.mjs.map +1 -1
  5. package/dist/maplibre-gl-shared.mjs +2 -2
  6. package/dist/maplibre-gl-shared.mjs.map +1 -1
  7. package/dist/maplibre-gl-worker-dev.mjs +23 -49
  8. package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
  9. package/dist/maplibre-gl-worker.mjs +2 -2
  10. package/dist/maplibre-gl-worker.mjs.map +1 -1
  11. package/dist/maplibre-gl.css +1 -1
  12. package/dist/maplibre-gl.d.ts +467 -151
  13. package/dist/maplibre-gl.mjs +49 -49
  14. package/dist/maplibre-gl.mjs.map +1 -1
  15. package/package.json +12 -22
  16. package/src/data/bucket/fill_extrusion_bucket.ts +1 -13
  17. package/src/data/bucket/round_polygon_corners.test.ts +84 -36
  18. package/src/data/bucket/round_polygon_corners.ts +90 -39
  19. package/src/data/extent_bounds.ts +28 -1
  20. package/src/data/feature_index.bench.ts +21 -0
  21. package/src/data/segment.test.ts +2 -2
  22. package/src/geo/edge_insets.test.ts +1 -1
  23. package/src/geo/lng_lat.test.ts +7 -7
  24. package/src/geo/lng_lat_bounds.test.ts +4 -4
  25. package/src/geo/mercator_coordinate.test.ts +2 -2
  26. package/src/geo/projection/camera_helper.ts +5 -0
  27. package/src/geo/projection/globe_transform.test.ts +154 -1
  28. package/src/geo/projection/globe_transform.ts +7 -3
  29. package/src/geo/projection/globe_utils.ts +189 -2
  30. package/src/geo/projection/mercator_camera_helper.ts +1 -1
  31. package/src/geo/projection/mercator_transform.ts +21 -5
  32. package/src/geo/projection/vertical_perspective_camera_helper.test.ts +24 -0
  33. package/src/geo/projection/vertical_perspective_camera_helper.ts +71 -41
  34. package/src/geo/projection/vertical_perspective_transform.ts +7 -1
  35. package/src/geo/transform_interface.ts +13 -1
  36. package/src/index.ts +7 -2
  37. package/src/render/image_atlas.test.ts +180 -0
  38. package/src/render/image_atlas.ts +52 -6
  39. package/src/render/image_manager.test.ts +111 -0
  40. package/src/render/image_manager.ts +143 -144
  41. package/src/render/painter.ts +6 -14
  42. package/src/render/pattern_atlas.test.ts +131 -0
  43. package/src/render/pattern_atlas.ts +144 -0
  44. package/src/render/update_pattern_positions_in_program.test.ts +1 -1
  45. package/src/shaders/glsl/_prelude.vertex.glsl +10 -11
  46. package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
  47. package/src/shaders/glsl/circle.vertex.glsl +4 -4
  48. package/src/shaders/glsl/circle.vertex.glsl.g.ts +1 -1
  49. package/src/shaders/glsl/color_relief.fragment.glsl +1 -2
  50. package/src/shaders/glsl/color_relief.fragment.glsl.g.ts +1 -1
  51. package/src/shaders/glsl/fill_extrusion.vertex.glsl +3 -3
  52. package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
  53. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +5 -5
  54. package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
  55. package/src/shaders/glsl/heatmap.vertex.glsl +4 -4
  56. package/src/shaders/glsl/heatmap.vertex.glsl.g.ts +1 -1
  57. package/src/shaders/glsl/hillshade_prepare.fragment.glsl +12 -12
  58. package/src/shaders/glsl/hillshade_prepare.fragment.glsl.g.ts +1 -1
  59. package/src/shaders/glsl/line.vertex.glsl +7 -7
  60. package/src/shaders/glsl/line.vertex.glsl.g.ts +1 -1
  61. package/src/shaders/glsl/line_gradient.vertex.glsl +6 -6
  62. package/src/shaders/glsl/line_gradient.vertex.glsl.g.ts +1 -1
  63. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl +7 -7
  64. package/src/shaders/glsl/line_gradient_sdf.vertex.glsl.g.ts +1 -1
  65. package/src/shaders/glsl/line_pattern.vertex.glsl +7 -7
  66. package/src/shaders/glsl/line_pattern.vertex.glsl.g.ts +1 -1
  67. package/src/shaders/glsl/line_sdf.vertex.glsl +7 -7
  68. package/src/shaders/glsl/line_sdf.vertex.glsl.g.ts +1 -1
  69. package/src/shaders/glsl/raster.fragment.glsl +4 -4
  70. package/src/shaders/glsl/raster.fragment.glsl.g.ts +1 -1
  71. package/src/shaders/glsl/raster.vertex.glsl +11 -6
  72. package/src/shaders/glsl/raster.vertex.glsl.g.ts +1 -1
  73. package/src/shaders/glsl/symbol_icon.vertex.glsl +5 -5
  74. package/src/shaders/glsl/symbol_icon.vertex.glsl.g.ts +1 -1
  75. package/src/shaders/glsl/symbol_sdf.vertex.glsl +5 -5
  76. package/src/shaders/glsl/symbol_sdf.vertex.glsl.g.ts +1 -1
  77. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl +6 -6
  78. package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
  79. package/src/source/canvas_source.test.ts +31 -0
  80. package/src/source/canvas_source.ts +2 -1
  81. package/src/source/geojson_worker_source.test.ts +88 -0
  82. package/src/source/geojson_worker_source.ts +22 -39
  83. package/src/source/image_source.test.ts +246 -38
  84. package/src/source/image_source.ts +132 -19
  85. package/src/source/raster_dem_tile_source.test.ts +37 -3
  86. package/src/source/raster_dem_tile_source.ts +1 -2
  87. package/src/source/raster_dem_tile_worker_source.test.ts +1 -1
  88. package/src/source/raster_tile_source.test.ts +5 -26
  89. package/src/source/raster_tile_source.ts +4 -3
  90. package/src/source/vector_tile_worker_source.test.ts +94 -2
  91. package/src/source/vector_tile_worker_source.ts +18 -27
  92. package/src/source/video_source.test.ts +12 -8
  93. package/src/source/worker_tile.bench.ts +10 -0
  94. package/src/source/worker_tile.ts +0 -3
  95. package/src/style/create_style_layer.bench.ts +15 -0
  96. package/src/style/properties.bench.ts +81 -0
  97. package/src/style/style.test.ts +75 -10
  98. package/src/style/style.ts +37 -64
  99. package/src/style/style_image.ts +67 -9
  100. package/src/style/style_layer.bench.ts +65 -0
  101. package/src/style/style_layer.test.ts +1 -1
  102. package/src/style/style_layer.ts +2 -2
  103. package/src/style/validate_style.bench.ts +11 -0
  104. package/src/style/validate_style.test.ts +2 -2
  105. package/src/symbol/anchor.test.ts +2 -2
  106. package/src/symbol/collision_index.bench.ts +116 -0
  107. package/src/symbol/cross_tile_symbol_index.bench.ts +49 -0
  108. package/src/symbol/cross_tile_symbol_index.test.ts +31 -0
  109. package/src/symbol/cross_tile_symbol_index.ts +67 -37
  110. package/src/symbol/shaping.test.ts +2 -1
  111. package/src/symbol/symbol_layout.bench.ts +30 -0
  112. package/src/tile/tile_id.test.ts +1 -1
  113. package/src/tile/tile_manager.test.ts +69 -1
  114. package/src/tile/tile_manager.ts +16 -11
  115. package/src/ui/control/fullscreen_control.test.ts +10 -10
  116. package/src/ui/control/geolocate_control.test.ts +3 -5
  117. package/src/ui/control/globe_control.test.ts +6 -6
  118. package/src/ui/events.ts +1 -1
  119. package/src/ui/handler/scroll_zoom.test.ts +50 -0
  120. package/src/ui/handler/two_fingers_touch.test.ts +17 -6
  121. package/src/ui/handler_inertia.test.ts +91 -0
  122. package/src/ui/handler_inertia.ts +65 -14
  123. package/src/ui/handler_manager.test.ts +243 -3
  124. package/src/ui/handler_manager.ts +79 -16
  125. package/src/ui/map.ts +17 -3
  126. package/src/ui/map_tests/map_cross_window.test.ts +2 -2
  127. package/src/ui/map_tests/map_events.test.ts +3 -3
  128. package/src/ui/map_tests/map_images.test.ts +55 -0
  129. package/src/ui/map_tests/map_style.test.ts +9 -8
  130. package/src/ui/map_tests/map_webgl.test.ts +30 -0
  131. package/src/ui/marker.test.ts +177 -42
  132. package/src/ui/marker.ts +120 -10
  133. package/src/ui/popup.test.ts +14 -14
  134. package/src/util/evented.test.ts +33 -28
  135. package/src/util/evented.ts +69 -43
  136. package/src/util/image_request.ts +31 -1
  137. package/src/util/test/null_gl.ts +10 -0
  138. package/src/util/test/util.ts +2 -1
  139. package/src/util/util.test.ts +1 -1
  140. package/src/util/web_worker_transfer.test.ts +5 -5
  141. package/src/webgl/context.ts +20 -0
  142. package/src/webgl/draw/draw_background.ts +1 -1
  143. package/src/webgl/draw/draw_raster.ts +11 -6
  144. package/src/webgl/program/pattern.ts +3 -3
  145. package/src/webgl/program/raster_program.ts +11 -0
  146. package/src/webgl/program.ts +31 -16
  147. package/src/webgl/texture.test.ts +16 -1
  148. package/src/webgl/texture.ts +3 -0
  149. package/src/webgl/vertex_buffer.test.ts +17 -2
  150. package/src/webgl/vertex_buffer.ts +24 -13
@@ -0,0 +1,111 @@
1
+ import {test, expect, vi} from 'vitest';
2
+ import {ImageManager} from './image_manager.ts';
3
+ import {RGBAImage} from '../util/image.ts';
4
+
5
+ import type {StyleImage} from '../style/style_image.ts';
6
+
7
+ function webGLImage(render?: () => boolean): StyleImage {
8
+ return {
9
+ data: new RGBAImage({width: 2, height: 2}),
10
+ version: 0,
11
+ pixelRatio: 1,
12
+ sdf: false,
13
+ isWebGLImage: true,
14
+ userImage: {width: 2, height: 2, data: {renderWithWebGL: vi.fn()}, render}
15
+ };
16
+ }
17
+
18
+ function styleImage(size: number = 2): StyleImage {
19
+ return {data: new RGBAImage({width: size, height: size}), pixelRatio: 1, sdf: false};
20
+ }
21
+
22
+ /** An image of a sprite that was never rendered, so its pixels are still in the sprite sheet */
23
+ function undecodedSpriteImage(size: number): StyleImage {
24
+ return {
25
+ data: null,
26
+ pixelRatio: 1,
27
+ sdf: false,
28
+ spriteData: {width: size, height: size, x: 0, y: 0, context: null}
29
+ };
30
+ }
31
+
32
+ test('a WebGL image owes every atlas a render as soon as it is added, even without a render callback', () => {
33
+ const manager = new ImageManager();
34
+ const updateVersion = manager.updateVersion;
35
+ manager.addImage('webgl', webGLImage());
36
+
37
+ expect(manager.getImage('webgl').version).toBe(1);
38
+ expect(manager.updateVersion).toBe(updateVersion + 1);
39
+ });
40
+
41
+ test('a WebGL image is re-rendered when its render callback reports a change', () => {
42
+ const manager = new ImageManager();
43
+ manager.addImage('webgl', webGLImage(() => true));
44
+ const version = manager.getImage('webgl').version;
45
+
46
+ manager.beginFrame();
47
+ manager.dispatchRenderCallbacks(['webgl']);
48
+
49
+ expect(manager.getImage('webgl').version).toBe(version + 1);
50
+ });
51
+
52
+ test('a WebGL image is left alone when its render callback reports no change', () => {
53
+ const manager = new ImageManager();
54
+ manager.addImage('webgl', webGLImage(() => false));
55
+ const version = manager.getImage('webgl').version;
56
+
57
+ manager.beginFrame();
58
+ manager.dispatchRenderCallbacks(['webgl']);
59
+
60
+ expect(manager.getImage('webgl').version).toBe(version);
61
+ });
62
+
63
+ test('a render callback runs once per frame, however many atlases ask for it', () => {
64
+ const manager = new ImageManager();
65
+ const render = vi.fn(() => false);
66
+ manager.addImage('webgl', webGLImage(render));
67
+
68
+ manager.beginFrame();
69
+ manager.dispatchRenderCallbacks(['webgl']);
70
+ manager.dispatchRenderCallbacks(['webgl']);
71
+ expect(render).toHaveBeenCalledTimes(1);
72
+
73
+ manager.beginFrame();
74
+ manager.dispatchRenderCallbacks(['webgl']);
75
+ expect(render).toHaveBeenCalledTimes(2);
76
+ });
77
+
78
+ test('removing an image that is not there leaves the others alone', () => {
79
+ const manager = new ImageManager();
80
+ manager.addImage('kept', styleImage());
81
+
82
+ manager.removeImage('neverAdded');
83
+
84
+ expect(manager.listImages()).toEqual(['kept']);
85
+ });
86
+
87
+ test('the images a sprite brought in are removed with it, and reported as removed', () => {
88
+ const manager = new ImageManager();
89
+ manager.setSpriteImages('default', {fromSprite: styleImage(), alsoFromSprite: styleImage()});
90
+ manager.addImage('addedAtRuntime', styleImage());
91
+
92
+ expect(manager.removeSpriteImages('default')).toEqual(['fromSprite', 'alsoFromSprite']);
93
+ expect(manager.listImages()).toEqual(['addedAtRuntime']);
94
+ expect(manager.removeSpriteImages('default')).toEqual([]);
95
+ });
96
+
97
+ test('updateImage rejects an image of a different size than the one it replaces', () => {
98
+ const manager = new ImageManager();
99
+ manager.addImage('image', styleImage(2));
100
+
101
+ expect(() => manager.updateImage('image', styleImage(4))).toThrow(/size mismatch between old image \(2x2\) and new image \(4x4\)/);
102
+ expect(() => manager.updateImage('image', styleImage(2))).not.toThrow();
103
+ });
104
+
105
+ test('updateImage sizes up a sprite image that was never decoded without decoding it', () => {
106
+ const manager = new ImageManager();
107
+ manager.addImage('image', undecodedSpriteImage(2));
108
+
109
+ expect(() => manager.updateImage('image', styleImage(4))).toThrow(/size mismatch between old image \(2x2\) and new image \(4x4\)/);
110
+ expect(() => manager.updateImage('image', styleImage(2))).not.toThrow();
111
+ });
@@ -1,49 +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;
12
+ type ImageManagerEventType = {
13
+ error: ErrorEvent;
14
+ styleimagemissing: MapStyleImageMissingEvent;
22
15
  };
23
16
 
24
17
  /**
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
- /**
33
- * ImageManager does three things:
34
- *
35
- * 1. Tracks requests for icon images from tile workers and sends responses when the requests are fulfilled.
36
- * 2. Builds a texture atlas for pattern images.
37
- * 3. Rerenders renderable images once per frame
38
- *
39
- * These are disparate responsibilities and should eventually be handled by different classes. When we implement
40
- * data-driven support for `*-pattern`, we'll likely use per-bucket pattern atlases, and that would be a good time
41
- * 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.
42
21
  */
43
- export class ImageManager extends Evented {
44
- images: {[_: string]: StyleImage};
45
- updatedImages: {[_: string]: boolean};
46
- callbackDispatchedThisFrame: {[_: string]: boolean};
22
+ export class ImageManager extends Evented<ImageManagerEventType> {
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;
47
30
  loaded: boolean;
48
31
  /**
49
32
  * This is used to track requests for images that are not yet available. When the image is loaded,
@@ -54,40 +37,37 @@ export class ImageManager extends Evented {
54
37
  promiseResolve: (value: GetImagesResponse | PromiseLike<GetImagesResponse>) => void;
55
38
  }>;
56
39
  missingImageResolver: MissingImageRequestHandler | null;
57
-
58
- patterns: {[_: string]: Pattern};
59
- atlasImage: RGBAImage;
60
- atlasTexture: Texture;
61
- dirty: boolean;
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;
62
53
 
63
54
  constructor() {
64
55
  super();
65
56
  this.images = {};
66
- this.updatedImages = {};
67
- this.callbackDispatchedThisFrame = {};
57
+ this.updateVersion = 0;
68
58
  this.loaded = false;
69
59
  this.requestors = [];
70
60
  this.missingImageResolver = null;
71
-
72
- this.patterns = {};
73
- this.atlasImage = new RGBAImage({width: 1, height: 1});
74
- this.dirty = true;
61
+ this._spriteImagesIds = {};
62
+ this._imagesIds = null;
63
+ this._renderCallbacksDispatchedThisFrame = {};
75
64
  }
76
65
 
77
66
  destroy(): void {
78
- // Destroy atlas texture if it exists
79
- if (this.atlasTexture) {
80
- this.atlasTexture.destroy();
81
- this.atlasTexture = null;
82
- }
83
- // Remove all images and patterns
84
67
  for (const id of Object.keys(this.images)) {
85
68
  this.removeImage(id);
86
69
  }
87
-
88
- this.patterns = {};
89
- this.atlasImage = new RGBAImage({width: 1, height: 1});
90
- this.dirty = true;
70
+ this._spriteImagesIds = {};
91
71
  }
92
72
  isLoaded(): boolean {
93
73
  return this.loaded;
@@ -131,6 +111,8 @@ export class ImageManager extends Evented {
131
111
  if (this.images[id]) throw new Error(`Image id ${id} already exist, use updateImage instead`);
132
112
  if (this._validate(id, image)) {
133
113
  this.images[id] = image;
114
+ this._imagesIds = null;
115
+ if (image.isWebGLImage) this.updateImage(id, image, false);
134
116
  }
135
117
  }
136
118
 
@@ -176,28 +158,120 @@ export class ImageManager extends Evented {
176
158
  return content[3] >= content[1];
177
159
  }
178
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
+ */
179
173
  updateImage(id: string, image: StyleImage, validate: boolean = true): void {
180
- const oldImage = this.getImage(id);
181
- if (validate && (oldImage.data.width !== image.data.width || oldImage.data.height !== image.data.height)) {
182
- throw new Error(`size mismatch between old image (${oldImage.data.width}x${oldImage.data.height}) and new image (${image.data.width}x${image.data.height}).`);
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
+ }
183
180
  }
184
- image.version = oldImage.version + 1;
181
+ image.version = (oldImage.version ?? 0) + 1;
185
182
  this.images[id] = image;
186
- this.updatedImages[id] = true;
183
+ this.updateVersion++;
187
184
  }
188
185
 
189
186
  removeImage(id: string): void {
190
187
  const image = this.images[id];
188
+ if (!image) return;
191
189
  delete this.images[id];
192
- delete this.patterns[id];
190
+ this._imagesIds = null;
193
191
 
194
192
  if (image.userImage?.onRemove) {
195
193
  image.userImage.onRemove();
196
194
  }
197
195
  }
198
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
+ */
199
201
  listImages(): string[] {
200
- return Object.keys(this.images);
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;
201
275
  }
202
276
 
203
277
  setMissingImageResolver(resolver: MissingImageRequestHandler | null): void {
@@ -231,7 +305,7 @@ export class ImageManager extends Evented {
231
305
  const resolver = this.missingImageResolver;
232
306
 
233
307
  if (resolver) {
234
- await Promise.all(Array.from(unresolvedIds, (id) => resolver(id)));
308
+ await Promise.allSettled(Array.from(unresolvedIds, (id) => resolver(id)));
235
309
  }
236
310
 
237
311
  const response: GetImagesResponse = {};
@@ -252,7 +326,8 @@ export class ImageManager extends Evented {
252
326
  content: image.content,
253
327
  textFitWidth: image.textFitWidth,
254
328
  textFitHeight: image.textFitHeight,
255
- hasRenderCallback: Boolean(image.userImage?.render)
329
+ hasRenderCallback: Boolean(image.userImage?.render),
330
+ isWebGLImage: image.isWebGLImage
256
331
  };
257
332
  }
258
333
  }
@@ -265,94 +340,18 @@ export class ImageManager extends Evented {
265
340
  return response;
266
341
  }
267
342
 
268
- // Pattern stuff
269
-
270
- getPixelSize(): {width: number; height: number} {
271
- const {width, height} = this.atlasImage;
272
- return {width, height};
273
- }
274
-
275
- getPattern(id: string): ImagePosition {
276
- const pattern = this.patterns[id];
277
-
278
- const image = this.getImage(id);
279
- if (!image) {
280
- return null;
281
- }
282
-
283
- // eslint-disable-next-line @typescript-eslint/prefer-optional-chain -- pattern?.position.version would be undefined when pattern is nullish, making undefined === undefined true
284
- if (pattern && pattern.position.version === image.version) {
285
- return pattern.position;
286
- }
287
-
288
- if (!pattern) {
289
- const w = image.data.width + padding * 2;
290
- const h = image.data.height + padding * 2;
291
- const bin = {w, h, x: 0, y: 0};
292
- const position = new ImagePosition(bin, image);
293
- this.patterns[id] = {bin, position};
294
- } else {
295
- pattern.position.version = image.version;
296
- }
297
-
298
- this._updatePatternAtlas();
299
-
300
- return this.patterns[id].position;
301
- }
302
-
303
- bind(context: Context): void {
304
- const gl = context.gl;
305
- if (!this.atlasTexture) {
306
- this.atlasTexture = new Texture(context, this.atlasImage, gl.RGBA);
307
- } else if (this.dirty) {
308
- this.atlasTexture.update(this.atlasImage);
309
- this.dirty = false;
310
- }
311
-
312
- this.atlasTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
313
- }
314
-
315
- _updatePatternAtlas(): void {
316
- const bins = [];
317
- for (const id in this.patterns) {
318
- bins.push(this.patterns[id].bin);
319
- }
320
-
321
- const {w, h} = potpack(bins);
322
-
323
- const dst = this.atlasImage;
324
- dst.resize({width: w || 1, height: h || 1});
325
-
326
- for (const id in this.patterns) {
327
- const {bin} = this.patterns[id];
328
- const x = bin.x + padding;
329
- const y = bin.y + padding;
330
- const src = this.getImage(id).data;
331
- const w = src.width;
332
- const h = src.height;
333
-
334
- RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y}, {width: w, height: h});
335
-
336
- // Add 1 pixel wrapped padding on each side of the image.
337
- RGBAImage.copy(src, dst, {x: 0, y: h - 1}, {x, y: y - 1}, {width: w, height: 1}); // T
338
- RGBAImage.copy(src, dst, {x: 0, y: 0}, {x, y: y + h}, {width: w, height: 1}); // B
339
- RGBAImage.copy(src, dst, {x: w - 1, y: 0}, {x: x - 1, y}, {width: 1, height: h}); // L
340
- RGBAImage.copy(src, dst, {x: 0, y: 0}, {x: x + w, y}, {width: 1, height: h}); // R
341
- }
342
-
343
- this.dirty = true;
344
- }
345
-
346
343
  beginFrame(): void {
347
- this.callbackDispatchedThisFrame = {};
344
+ this._renderCallbacksDispatchedThisFrame = {};
348
345
  }
349
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
+ */
350
351
  dispatchRenderCallbacks(ids: string[]): void {
351
352
  for (const id of ids) {
352
-
353
- // the callback for the image was already dispatched for a different frame
354
- if (this.callbackDispatchedThisFrame[id]) continue;
355
- this.callbackDispatchedThisFrame[id] = true;
353
+ if (this._renderCallbacksDispatchedThisFrame[id]) continue;
354
+ this._renderCallbacksDispatchedThisFrame[id] = true;
356
355
 
357
356
  const image = this.getImage(id);
358
357
  if (!image) warnOnce(`Image with ID: "${id}" was not found`);
@@ -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.imageManager.getPattern(image.from.toString());
811
- const imagePosB = this.imageManager.getPattern(image.to.toString());
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
 
@@ -857,18 +860,7 @@ export class Painter {
857
860
  * in custom layers.
858
861
  */
859
862
  setCustomLayerDefaults(): void {
860
- // Prevent custom layers from unintentionally modify the last VAO used.
861
- // All other state is state is restored on it's own, but for VAOs it's
862
- // simpler to unbind so that we don't have to track the state of VAOs.
863
- this.context.unbindVAO();
864
-
865
- // The default values for this state is meaningful and often expected.
866
- // Leaving this state dirty could cause a lot of confusion for users.
867
- this.context.cullFace.setDefault();
868
- this.context.activeTexture.setDefault();
869
- this.context.pixelStoreUnpack.setDefault();
870
- this.context.pixelStoreUnpackPremultiplyAlpha.setDefault();
871
- this.context.pixelStoreUnpackFlipY.setDefault();
863
+ this.context.setCustomLayerDefaults();
872
864
  }
873
865
 
874
866
  /*
@@ -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
+ });