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.
- package/dist/maplibre-gl-dev.mjs +1108 -427
- package/dist/maplibre-gl-dev.mjs.map +1 -1
- package/dist/maplibre-gl-shared-dev.mjs +359 -291
- 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 +23 -49
- package/dist/maplibre-gl-worker-dev.mjs.map +1 -1
- package/dist/maplibre-gl-worker.mjs +2 -2
- package/dist/maplibre-gl-worker.mjs.map +1 -1
- package/dist/maplibre-gl.css +1 -1
- package/dist/maplibre-gl.d.ts +467 -151
- package/dist/maplibre-gl.mjs +49 -49
- package/dist/maplibre-gl.mjs.map +1 -1
- package/package.json +12 -22
- 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/data/feature_index.bench.ts +21 -0
- package/src/data/segment.test.ts +2 -2
- package/src/geo/edge_insets.test.ts +1 -1
- package/src/geo/lng_lat.test.ts +7 -7
- package/src/geo/lng_lat_bounds.test.ts +4 -4
- package/src/geo/mercator_coordinate.test.ts +2 -2
- package/src/geo/projection/camera_helper.ts +5 -0
- package/src/geo/projection/globe_transform.test.ts +154 -1
- package/src/geo/projection/globe_transform.ts +7 -3
- package/src/geo/projection/globe_utils.ts +189 -2
- package/src/geo/projection/mercator_camera_helper.ts +1 -1
- package/src/geo/projection/mercator_transform.ts +21 -5
- package/src/geo/projection/vertical_perspective_camera_helper.test.ts +24 -0
- package/src/geo/projection/vertical_perspective_camera_helper.ts +71 -41
- package/src/geo/projection/vertical_perspective_transform.ts +7 -1
- package/src/geo/transform_interface.ts +13 -1
- package/src/index.ts +7 -2
- package/src/render/image_atlas.test.ts +180 -0
- package/src/render/image_atlas.ts +52 -6
- package/src/render/image_manager.test.ts +111 -0
- package/src/render/image_manager.ts +143 -144
- package/src/render/painter.ts +6 -14
- package/src/render/pattern_atlas.test.ts +131 -0
- package/src/render/pattern_atlas.ts +144 -0
- package/src/render/update_pattern_positions_in_program.test.ts +1 -1
- package/src/shaders/glsl/_prelude.vertex.glsl +10 -11
- package/src/shaders/glsl/_prelude.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/circle.vertex.glsl +4 -4
- package/src/shaders/glsl/circle.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/fill_extrusion.vertex.glsl +3 -3
- package/src/shaders/glsl/fill_extrusion.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl +5 -5
- package/src/shaders/glsl/fill_extrusion_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/heatmap.vertex.glsl +4 -4
- package/src/shaders/glsl/heatmap.vertex.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/shaders/glsl/line.vertex.glsl +7 -7
- package/src/shaders/glsl/line.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_gradient.vertex.glsl +6 -6
- package/src/shaders/glsl/line_gradient.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_gradient_sdf.vertex.glsl +7 -7
- package/src/shaders/glsl/line_gradient_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_pattern.vertex.glsl +7 -7
- package/src/shaders/glsl/line_pattern.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/line_sdf.vertex.glsl +7 -7
- package/src/shaders/glsl/line_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/raster.fragment.glsl +4 -4
- package/src/shaders/glsl/raster.fragment.glsl.g.ts +1 -1
- package/src/shaders/glsl/raster.vertex.glsl +11 -6
- package/src/shaders/glsl/raster.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_icon.vertex.glsl +5 -5
- package/src/shaders/glsl/symbol_icon.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_sdf.vertex.glsl +5 -5
- package/src/shaders/glsl/symbol_sdf.vertex.glsl.g.ts +1 -1
- package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl +6 -6
- package/src/shaders/glsl/symbol_text_and_icon.vertex.glsl.g.ts +1 -1
- package/src/source/canvas_source.test.ts +31 -0
- package/src/source/canvas_source.ts +2 -1
- package/src/source/geojson_worker_source.test.ts +88 -0
- package/src/source/geojson_worker_source.ts +22 -39
- package/src/source/image_source.test.ts +246 -38
- package/src/source/image_source.ts +132 -19
- 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_dem_tile_worker_source.test.ts +1 -1
- package/src/source/raster_tile_source.test.ts +5 -26
- package/src/source/raster_tile_source.ts +4 -3
- package/src/source/vector_tile_worker_source.test.ts +94 -2
- package/src/source/vector_tile_worker_source.ts +18 -27
- package/src/source/video_source.test.ts +12 -8
- package/src/source/worker_tile.bench.ts +10 -0
- package/src/source/worker_tile.ts +0 -3
- package/src/style/create_style_layer.bench.ts +15 -0
- package/src/style/properties.bench.ts +81 -0
- package/src/style/style.test.ts +75 -10
- package/src/style/style.ts +37 -64
- package/src/style/style_image.ts +67 -9
- package/src/style/style_layer.bench.ts +65 -0
- package/src/style/style_layer.test.ts +1 -1
- package/src/style/style_layer.ts +2 -2
- package/src/style/validate_style.bench.ts +11 -0
- package/src/style/validate_style.test.ts +2 -2
- package/src/symbol/anchor.test.ts +2 -2
- package/src/symbol/collision_index.bench.ts +116 -0
- package/src/symbol/cross_tile_symbol_index.bench.ts +49 -0
- package/src/symbol/cross_tile_symbol_index.test.ts +31 -0
- package/src/symbol/cross_tile_symbol_index.ts +67 -37
- package/src/symbol/shaping.test.ts +2 -1
- package/src/symbol/symbol_layout.bench.ts +30 -0
- package/src/tile/tile_id.test.ts +1 -1
- package/src/tile/tile_manager.test.ts +69 -1
- package/src/tile/tile_manager.ts +16 -11
- package/src/ui/control/fullscreen_control.test.ts +10 -10
- package/src/ui/control/geolocate_control.test.ts +3 -5
- package/src/ui/control/globe_control.test.ts +6 -6
- package/src/ui/events.ts +1 -1
- package/src/ui/handler/scroll_zoom.test.ts +50 -0
- package/src/ui/handler/two_fingers_touch.test.ts +17 -6
- package/src/ui/handler_inertia.test.ts +91 -0
- package/src/ui/handler_inertia.ts +65 -14
- package/src/ui/handler_manager.test.ts +243 -3
- package/src/ui/handler_manager.ts +79 -16
- package/src/ui/map.ts +17 -3
- package/src/ui/map_tests/map_cross_window.test.ts +2 -2
- package/src/ui/map_tests/map_events.test.ts +3 -3
- package/src/ui/map_tests/map_images.test.ts +55 -0
- package/src/ui/map_tests/map_style.test.ts +9 -8
- package/src/ui/map_tests/map_webgl.test.ts +30 -0
- package/src/ui/marker.test.ts +177 -42
- package/src/ui/marker.ts +120 -10
- package/src/ui/popup.test.ts +14 -14
- package/src/util/evented.test.ts +33 -28
- package/src/util/evented.ts +69 -43
- package/src/util/image_request.ts +31 -1
- package/src/util/test/null_gl.ts +10 -0
- package/src/util/test/util.ts +2 -1
- package/src/util/util.test.ts +1 -1
- package/src/util/web_worker_transfer.test.ts +5 -5
- package/src/webgl/context.ts +20 -0
- package/src/webgl/draw/draw_background.ts +1 -1
- package/src/webgl/draw/draw_raster.ts +11 -6
- package/src/webgl/program/pattern.ts +3 -3
- package/src/webgl/program/raster_program.ts +11 -0
- package/src/webgl/program.ts +31 -16
- package/src/webgl/texture.test.ts +16 -1
- package/src/webgl/texture.ts +3 -0
- package/src/webgl/vertex_buffer.test.ts +17 -2
- 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
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
type ImageManagerEventType = {
|
|
13
|
+
error: ErrorEvent;
|
|
14
|
+
styleimagemissing: MapStyleImageMissingEvent;
|
|
22
15
|
};
|
|
23
16
|
|
|
24
17
|
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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:
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
67
|
-
this.callbackDispatchedThisFrame = {};
|
|
57
|
+
this.updateVersion = 0;
|
|
68
58
|
this.loaded = false;
|
|
69
59
|
this.requestors = [];
|
|
70
60
|
this.missingImageResolver = null;
|
|
71
|
-
|
|
72
|
-
this.
|
|
73
|
-
this.
|
|
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.
|
|
181
|
-
if (validate
|
|
182
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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`);
|
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
|
|
|
@@ -857,18 +860,7 @@ export class Painter {
|
|
|
857
860
|
* in custom layers.
|
|
858
861
|
*/
|
|
859
862
|
setCustomLayerDefaults(): void {
|
|
860
|
-
|
|
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
|
+
});
|