@sakuzu/maplibre-gl-draw 2.0.0 → 2.1.1
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/CHANGELOG.md +119 -1
- package/README.ja.md +48 -35
- package/README.md +52 -36
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/dist/api/datasets.d.ts +5 -0
- package/dist/api/draw.d.ts +6 -4
- package/dist/api/draw.js +6 -4
- package/dist/api/events.d.ts +10 -0
- package/dist/api/impl/create-draw.js +1 -1
- package/dist/api/impl/datasets.js +1 -0
- package/dist/api/impl/events.js +2 -1
- package/dist/api/impl/extension-host.js +4 -1
- package/dist/api/impl/options.js +58 -29
- package/dist/dataset/dataset.js +3 -0
- package/dist/dataset/retained.js +19 -18
- package/dist/dataset/selection.d.ts +3 -3
- package/dist/dataset/selection.js +3 -7
- package/dist/dataset/table-source.js +13 -11
- package/dist/dataset/types.d.ts +2 -0
- package/dist/dispatcher/input-router.js +2 -1
- package/dist/dispatcher/normalizer.js +19 -2
- package/dist/geometry/geojson.d.ts +4 -0
- package/dist/geometry/geojson.js +20 -11
- package/dist/index.d.ts +14 -0
- package/dist/view/layer/attach.d.ts +11 -5
- package/dist/view/layer/attach.js +85 -9
- package/dist/view/layer/custom-layer.js +2 -1
- package/dist/view/layer/drape-planner.d.ts +3 -0
- package/dist/view/layer/drape-planner.js +9 -7
- package/dist/view/layer/frame-render.js +2 -3
- package/dist/view/layer/frame-state.d.ts +3 -0
- package/dist/view/layer/frame-state.js +2 -1
- package/dist/view/layer/gl-state.d.ts +2 -3
- package/dist/view/layer/gl-state.js +2 -3
- package/dist/view/renderers/line/dash.d.ts +27 -0
- package/dist/view/renderers/line/dash.js +23 -12
- package/dist/view/terrain/drape/bin-store.d.ts +11 -2
- package/dist/view/terrain/drape/bin-store.js +26 -2
- package/dist/view/terrain/drape/binning.d.ts +32 -0
- package/dist/view/terrain/drape/binning.js +15 -7
- package/dist/view/terrain/drape/geometry.d.ts +27 -0
- package/dist/view/terrain/drape/geometry.js +57 -6
- package/dist/view/terrain/drape/pass.d.ts +8 -3
- package/dist/view/terrain/drape/pass.js +39 -28
- package/dist/view/terrain/drape/renderer.d.ts +13 -0
- package/dist/view/terrain/drape/renderer.js +157 -30
- package/package.json +22 -10
|
@@ -119,7 +119,10 @@ export interface DrapeFrameInput {
|
|
|
119
119
|
resolveStyles: () => RetainedStyleResolver;
|
|
120
120
|
/** The terrain of the map (null when the terrain state of the frame is inactive) */
|
|
121
121
|
mapTerrain: MapTerrain;
|
|
122
|
+
/** The rendering pixel ratio, for widths fixed in screen pixels (the rendering scale included) */
|
|
122
123
|
dpr: number;
|
|
124
|
+
/** The rendering pixel ratio for widths that follow the zoom (the rendering scale not applied) */
|
|
125
|
+
contentDpr: number;
|
|
123
126
|
/** The style zoom (what the rendering reads) */
|
|
124
127
|
zoom: number;
|
|
125
128
|
/** The real zoom of the camera (the band is decided with it) */
|
|
@@ -281,7 +281,7 @@ export class DrapePlanner {
|
|
|
281
281
|
*/
|
|
282
282
|
planFrame(input) {
|
|
283
283
|
const { terrain } = this.deps;
|
|
284
|
-
const { mapTerrain, dpr, zoom, rawZoom } = input;
|
|
284
|
+
const { mapTerrain, dpr, contentDpr, zoom, rawZoom } = input;
|
|
285
285
|
let drapeUsable = false;
|
|
286
286
|
this.pendingWork = false;
|
|
287
287
|
// The analytic drape is used even in a wide band when building the index is light. Cutting
|
|
@@ -297,10 +297,10 @@ export class DrapePlanner {
|
|
|
297
297
|
// cost has to be known first). The collected result is folded behind a key, so there is no
|
|
298
298
|
// cost as long as the contents do not change
|
|
299
299
|
if (mapTerrain !== null)
|
|
300
|
-
this.collect(input.resolveStyles, dpr, zoom);
|
|
300
|
+
this.collect(input.resolveStyles, dpr, contentDpr, zoom);
|
|
301
301
|
const analyticBand = rawZoom > TERRAIN_ANALYTIC_MIN_ZOOM || this.lightEnough;
|
|
302
302
|
if (mapTerrain !== null && analyticBand) {
|
|
303
|
-
drapeUsable = this.prepare(input.gl, mapTerrain, input.resolveStyles, dpr, zoom);
|
|
303
|
+
drapeUsable = this.prepare(input.gl, mapTerrain, input.resolveStyles, dpr, contentDpr, zoom);
|
|
304
304
|
}
|
|
305
305
|
else if (mapTerrain !== null) {
|
|
306
306
|
// A wide band is handled by the vertex displacement path (no index is built either)
|
|
@@ -447,6 +447,7 @@ export class DrapePlanner {
|
|
|
447
447
|
cellOffset: item.plan.cellOffset,
|
|
448
448
|
grid: item.plan.grid,
|
|
449
449
|
groundMeters: item.plan.groundMeters,
|
|
450
|
+
tileZ: item.plan.tile.z,
|
|
450
451
|
edges,
|
|
451
452
|
},
|
|
452
453
|
projection: item.projection,
|
|
@@ -500,12 +501,13 @@ export class DrapePlanner {
|
|
|
500
501
|
* "polygons are not drawn at low zooms"). The collected result is folded behind a key, so from
|
|
501
502
|
* the second time on nothing happens as long as the contents do not change.
|
|
502
503
|
*/
|
|
503
|
-
collect(resolveStyles, dpr, zoom) {
|
|
504
|
+
collect(resolveStyles, dpr, contentDpr, zoom) {
|
|
504
505
|
const { store, datasets: manager } = this.deps;
|
|
505
506
|
const datasets = manager?.listInternal() ?? [];
|
|
506
507
|
const key = [
|
|
507
508
|
this.revision,
|
|
508
509
|
dpr,
|
|
510
|
+
contentDpr,
|
|
509
511
|
marginZoom(zoom),
|
|
510
512
|
store.getLayerOrder().join('/'),
|
|
511
513
|
datasets.map((c) => `${c.id}:${c.drapeRevision}:${c.visible ? 1 : 0}:${c.order}`).join(','),
|
|
@@ -513,7 +515,7 @@ export class DrapePlanner {
|
|
|
513
515
|
].join('|');
|
|
514
516
|
let collected = this.collected;
|
|
515
517
|
if (key !== this.collectKey || !collected) {
|
|
516
|
-
collected = collectDrapeElements(store, resolveStyles(), dpr, datasets);
|
|
518
|
+
collected = collectDrapeElements(store, resolveStyles(), dpr, contentDpr, datasets);
|
|
517
519
|
this.collectKey = key;
|
|
518
520
|
this.collected = collected;
|
|
519
521
|
this.lightEnough = collected.edgeCount <= DRAPE_WIDE_EDGE_BUDGET;
|
|
@@ -529,14 +531,14 @@ export class DrapePlanner {
|
|
|
529
531
|
* not collected again just because the view moved (collecting them again would mean rebuilding
|
|
530
532
|
* the whole index).
|
|
531
533
|
*/
|
|
532
|
-
prepare(gl, terrain, resolveStyles, dpr, zoom) {
|
|
534
|
+
prepare(gl, terrain, resolveStyles, dpr, contentDpr, zoom) {
|
|
533
535
|
const { map, datasets: manager } = this.deps;
|
|
534
536
|
const context = this.deps.terrain;
|
|
535
537
|
const tiles = getRenderableTerrainTiles(terrain);
|
|
536
538
|
if (tiles.length === 0)
|
|
537
539
|
return false;
|
|
538
540
|
const datasets = manager?.listInternal() ?? [];
|
|
539
|
-
const collected = this.collect(resolveStyles, dpr, zoom);
|
|
541
|
+
const collected = this.collect(resolveStyles, dpr, contentDpr, zoom);
|
|
540
542
|
// The factors per source (which follow the zoom) are rewritten every frame, because baking
|
|
541
543
|
// them into the elements would mean rebuilding the index on every zoom movement
|
|
542
544
|
const drapeSourceFactors = this.sourceFactors;
|
|
@@ -107,9 +107,8 @@ function drawDrapeSegment(deps, f, index) {
|
|
|
107
107
|
drawSegment(paintCursor, to, last);
|
|
108
108
|
}
|
|
109
109
|
f.restoreBlendState();
|
|
110
|
-
// Back to the depth state of the layer rendering. What the drape could not paint (
|
|
111
|
-
//
|
|
112
|
-
// extensions) is drawn next, by the vertex displacement path, and the terrain must hide it
|
|
110
|
+
// Back to the depth state of the layer rendering. What the drape could not paint (the
|
|
111
|
+
// geometry being drawn, the features of extensions) is drawn next, by the vertex displacement path, and the terrain must hide it
|
|
113
112
|
// as it does in a frame without the drape. Symbols switch the depth test off themselves.
|
|
114
113
|
applySegmentDepthState(gl, deps.terrainContext, f);
|
|
115
114
|
}
|
|
@@ -111,7 +111,10 @@ export interface FrameBuildInput {
|
|
|
111
111
|
zoom: number;
|
|
112
112
|
/** The real zoom of the camera */
|
|
113
113
|
rawZoom: number;
|
|
114
|
+
/** The rendering pixel ratio, for sizes fixed in screen pixels (the rendering scale included) */
|
|
114
115
|
dpr: number;
|
|
116
|
+
/** The rendering pixel ratio for sizes that follow the zoom (the rendering scale not applied) */
|
|
117
|
+
contentDpr: number;
|
|
115
118
|
/** The terrain state of the frame (already set on the terrain context) */
|
|
116
119
|
terrainState: TerrainRenderState;
|
|
117
120
|
/** Whether the retained batches of the Store rendering exist */
|
|
@@ -139,7 +139,7 @@ function sameCameraPosition(a, b) {
|
|
|
139
139
|
*/
|
|
140
140
|
export function buildFrameState(input) {
|
|
141
141
|
const { gl, renderers: r, map, store, terrainContext, drape, renderOptions } = input;
|
|
142
|
-
const { zoom, rawZoom, dpr, terrainState } = input;
|
|
142
|
+
const { zoom, rawZoom, dpr, contentDpr, terrainState } = input;
|
|
143
143
|
const { shaderData, defaultProjectionData } = renderOptions;
|
|
144
144
|
const mapTerrain = terrainState.active ? getMapTerrain(map) : null;
|
|
145
145
|
// Shader initialization
|
|
@@ -193,6 +193,7 @@ export function buildFrameState(input) {
|
|
|
193
193
|
resolveStyles: () => r.batchManager.getRetainedRenderers()?.styles ?? r.featureDrawer,
|
|
194
194
|
mapTerrain,
|
|
195
195
|
dpr,
|
|
196
|
+
contentDpr,
|
|
196
197
|
zoom,
|
|
197
198
|
rawZoom,
|
|
198
199
|
});
|
|
@@ -34,9 +34,8 @@ export declare function applySegmentGlState(gl: WebGL2RenderingContext, terrainC
|
|
|
34
34
|
* `applySegmentGlState` sets it at the start of a slot, and the drape of the segment, which
|
|
35
35
|
* changes the depth function and the offset, hands it back with this before the layers are
|
|
36
36
|
* drawn. The layers are therefore drawn in one depth state whether or not the drape was used.
|
|
37
|
-
* What the drape cannot paint (
|
|
38
|
-
* drawn
|
|
39
|
-
* drawn after it by the vertex displacement path and must be hidden by the terrain in the same
|
|
37
|
+
* What the drape cannot paint (the geometry being drawn, the features of extensions, the datasets
|
|
38
|
+
* not handed over to it) is drawn after it by the vertex displacement path and must be hidden by the terrain in the same
|
|
40
39
|
* way. Symbols switch the depth test off themselves (`drawBillboardsWithoutDepth`).
|
|
41
40
|
*/
|
|
42
41
|
export declare function applySegmentDepthState(gl: WebGL2RenderingContext, terrainContext: TerrainContext, f: Pick<SegmentGlFrame, 'terrainState' | 'wideFallback'>): void;
|
|
@@ -33,9 +33,8 @@ export function applySegmentGlState(gl, terrainContext, f) {
|
|
|
33
33
|
* `applySegmentGlState` sets it at the start of a slot, and the drape of the segment, which
|
|
34
34
|
* changes the depth function and the offset, hands it back with this before the layers are
|
|
35
35
|
* drawn. The layers are therefore drawn in one depth state whether or not the drape was used.
|
|
36
|
-
* What the drape cannot paint (
|
|
37
|
-
* drawn
|
|
38
|
-
* drawn after it by the vertex displacement path and must be hidden by the terrain in the same
|
|
36
|
+
* What the drape cannot paint (the geometry being drawn, the features of extensions, the datasets
|
|
37
|
+
* not handed over to it) is drawn after it by the vertex displacement path and must be hidden by the terrain in the same
|
|
39
38
|
* way. Symbols switch the depth test off themselves (`drawBillboardsWithoutDepth`).
|
|
40
39
|
*/
|
|
41
40
|
export function applySegmentDepthState(gl, terrainContext, f) {
|
|
@@ -18,6 +18,33 @@ export interface DashSegment {
|
|
|
18
18
|
* @returns The dashes, in order along the line. An empty array for fewer than 2 positions
|
|
19
19
|
*/
|
|
20
20
|
export declare function splitIntoDashes(coords: Coordinate[], dashLength: number, gapLength: number, zoom: number): DashSegment[];
|
|
21
|
+
/**
|
|
22
|
+
* The coefficients of a dash pattern
|
|
23
|
+
*
|
|
24
|
+
* The dash is `max(a, b * w)` and the gap `max(w + 1, c * w)` for a line `w` pixels wide (the
|
|
25
|
+
* smallest gap, `w + 1`, keeps the round caps of two dashes apart). This table is the one
|
|
26
|
+
* source of the patterns: the subdividing path computes them here on the CPU, and the analytic
|
|
27
|
+
* drape hands the same coefficients to its shader, which evaluates the same two formulas.
|
|
28
|
+
*/
|
|
29
|
+
export interface DashCoefficients {
|
|
30
|
+
/** The shortest dash (px) */
|
|
31
|
+
readonly a: number;
|
|
32
|
+
/** The dash per pixel of width */
|
|
33
|
+
readonly b: number;
|
|
34
|
+
/** The gap per pixel of width */
|
|
35
|
+
readonly c: number;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The coefficients of each line style that is not solid
|
|
39
|
+
*
|
|
40
|
+
* Dashed: dashes 4 times the width (12 px at least) and gaps twice the width. Dotted: short,
|
|
41
|
+
* nearly round dots and gaps about the width.
|
|
42
|
+
*/
|
|
43
|
+
export declare const DASH_COEFFICIENTS: Readonly<Record<'dashed' | 'dotted', DashCoefficients>>;
|
|
44
|
+
/**
|
|
45
|
+
* The coefficients of a line style, or null for a solid line
|
|
46
|
+
*/
|
|
47
|
+
export declare function dashCoefficientsOf(lineStyle: 'solid' | 'dashed' | 'dotted'): DashCoefficients | null;
|
|
21
48
|
/**
|
|
22
49
|
* Returns the dash and gap lengths in px that suit a line style at a line width.
|
|
23
50
|
*
|
|
@@ -126,6 +126,22 @@ export function splitIntoDashes(coords, dashLength, gapLength, zoom) {
|
|
|
126
126
|
}
|
|
127
127
|
return segments;
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* The coefficients of each line style that is not solid
|
|
131
|
+
*
|
|
132
|
+
* Dashed: dashes 4 times the width (12 px at least) and gaps twice the width. Dotted: short,
|
|
133
|
+
* nearly round dots and gaps about the width.
|
|
134
|
+
*/
|
|
135
|
+
export const DASH_COEFFICIENTS = {
|
|
136
|
+
dashed: { a: 12, b: 4, c: 2 },
|
|
137
|
+
dotted: { a: 1, b: 0.5, c: 1.2 },
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* The coefficients of a line style, or null for a solid line
|
|
141
|
+
*/
|
|
142
|
+
export function dashCoefficientsOf(lineStyle) {
|
|
143
|
+
return lineStyle === 'dashed' || lineStyle === 'dotted' ? DASH_COEFFICIENTS[lineStyle] : null;
|
|
144
|
+
}
|
|
129
145
|
/**
|
|
130
146
|
* Returns the dash and gap lengths in px that suit a line style at a line width.
|
|
131
147
|
*
|
|
@@ -137,16 +153,11 @@ export function splitIntoDashes(coords, dashLength, gapLength, zoom) {
|
|
|
137
153
|
* @returns [dashLength, gapLength], or null (for a solid line)
|
|
138
154
|
*/
|
|
139
155
|
export function getStrokeDashPattern(lineStyle, strokeWidth = 2) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Dotted: short dots (nearly round), short gaps (about the line width)
|
|
148
|
-
return [Math.max(1, strokeWidth * 0.5), Math.max(minGap, strokeWidth * 1.2)];
|
|
149
|
-
default:
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
156
|
+
const coefficients = dashCoefficientsOf(lineStyle);
|
|
157
|
+
if (!coefficients)
|
|
158
|
+
return null;
|
|
159
|
+
return [
|
|
160
|
+
Math.max(coefficients.a, coefficients.b * strokeWidth),
|
|
161
|
+
Math.max(strokeWidth + 1, coefficients.c * strokeWidth),
|
|
162
|
+
];
|
|
152
163
|
}
|
|
@@ -19,16 +19,23 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { DrapeElement, DrapeQuadBreak, DrapeTile, TileBins } from './binning.js';
|
|
21
21
|
/** The number of style texels per element */
|
|
22
|
-
export declare const DRAPE_STYLE_TEXELS =
|
|
22
|
+
export declare const DRAPE_STYLE_TEXELS = 5;
|
|
23
23
|
/**
|
|
24
24
|
* The index of the texel the selection highlight uses within the style table
|
|
25
25
|
*
|
|
26
26
|
* 0 = fill, 1 = stroke, 2 = specifications (width, kind, source, reference zoom), 3 =
|
|
27
|
-
* selection. The selection alone is put in a separate texel so that the index (the binning)
|
|
27
|
+
* selection, 4 = the dash pattern (`DRAPE_DASH_TEXEL`). The selection alone is put in a separate texel so that the index (the binning)
|
|
28
28
|
* need not be touched at all when the selection changes. Mixing it into the same texel as the
|
|
29
29
|
* color or the width would mean rebuilding the other values too on every rewrite.
|
|
30
30
|
*/
|
|
31
31
|
export declare const DRAPE_SELECTION_TEXEL = 3;
|
|
32
|
+
/**
|
|
33
|
+
* The index of the texel of the dash pattern within the style table
|
|
34
|
+
*
|
|
35
|
+
* (a, b, c, pixel ratio): the coefficients of the pattern (`DrapeDash` in `binning.ts`) and the
|
|
36
|
+
* physical pixels of the width per CSS pixel. A pixel ratio of 0 marks a solid outline.
|
|
37
|
+
*/
|
|
38
|
+
export declare const DRAPE_DASH_TEXEL = 4;
|
|
32
39
|
/** The upper bound on the number of edge texels (16 bytes per texel, so 1M is 16MB) */
|
|
33
40
|
export declare const DRAPE_MAX_EDGE_TEXELS = 1048576;
|
|
34
41
|
/** The key of a tile */
|
|
@@ -57,6 +64,8 @@ export declare class DrapePackBuilder {
|
|
|
57
64
|
* transferred) */
|
|
58
65
|
readonly id: number;
|
|
59
66
|
edges: Float32Array<ArrayBuffer>;
|
|
67
|
+
/** Where each edge starts along its path (one number per edge, in step with `edges`) */
|
|
68
|
+
edgeStarts: Float32Array<ArrayBuffer>;
|
|
60
69
|
edgeCount: number;
|
|
61
70
|
runs: Float32Array<ArrayBuffer>;
|
|
62
71
|
runCount: number;
|
|
@@ -2,16 +2,23 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
import { binTile, drapeCellWorkBudget } from './binning.js';
|
|
4
4
|
/** The number of style texels per element */
|
|
5
|
-
export const DRAPE_STYLE_TEXELS =
|
|
5
|
+
export const DRAPE_STYLE_TEXELS = 5;
|
|
6
6
|
/**
|
|
7
7
|
* The index of the texel the selection highlight uses within the style table
|
|
8
8
|
*
|
|
9
9
|
* 0 = fill, 1 = stroke, 2 = specifications (width, kind, source, reference zoom), 3 =
|
|
10
|
-
* selection. The selection alone is put in a separate texel so that the index (the binning)
|
|
10
|
+
* selection, 4 = the dash pattern (`DRAPE_DASH_TEXEL`). The selection alone is put in a separate texel so that the index (the binning)
|
|
11
11
|
* need not be touched at all when the selection changes. Mixing it into the same texel as the
|
|
12
12
|
* color or the width would mean rebuilding the other values too on every rewrite.
|
|
13
13
|
*/
|
|
14
14
|
export const DRAPE_SELECTION_TEXEL = 3;
|
|
15
|
+
/**
|
|
16
|
+
* The index of the texel of the dash pattern within the style table
|
|
17
|
+
*
|
|
18
|
+
* (a, b, c, pixel ratio): the coefficients of the pattern (`DrapeDash` in `binning.ts`) and the
|
|
19
|
+
* physical pixels of the width per CSS pixel. A pixel ratio of 0 marks a solid outline.
|
|
20
|
+
*/
|
|
21
|
+
export const DRAPE_DASH_TEXEL = 4;
|
|
15
22
|
/** The upper bound on the tiles retained (beyond it, those outside the view are dropped and
|
|
16
23
|
* the arrays are repacked) */
|
|
17
24
|
const MAX_CACHED_TILES = 96;
|
|
@@ -43,6 +50,13 @@ export class DrapePackBuilder {
|
|
|
43
50
|
writable: true,
|
|
44
51
|
value: new Float32Array(4 * 4096)
|
|
45
52
|
});
|
|
53
|
+
/** Where each edge starts along its path (one number per edge, in step with `edges`) */
|
|
54
|
+
Object.defineProperty(this, "edgeStarts", {
|
|
55
|
+
enumerable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
writable: true,
|
|
58
|
+
value: new Float32Array(4096)
|
|
59
|
+
});
|
|
46
60
|
Object.defineProperty(this, "edgeCount", {
|
|
47
61
|
enumerable: true,
|
|
48
62
|
configurable: true,
|
|
@@ -125,6 +139,14 @@ export class DrapePackBuilder {
|
|
|
125
139
|
styles[at + 9] = element.kind;
|
|
126
140
|
styles[at + 10] = element.source;
|
|
127
141
|
styles[at + 11] = element.widthZoom;
|
|
142
|
+
const dash = element.dash;
|
|
143
|
+
if (dash) {
|
|
144
|
+
const dashAt = at + DRAPE_DASH_TEXEL * 4;
|
|
145
|
+
styles[dashAt] = dash.a;
|
|
146
|
+
styles[dashAt + 1] = dash.b;
|
|
147
|
+
styles[dashAt + 2] = dash.c;
|
|
148
|
+
styles[dashAt + 3] = dash.pixelRatio;
|
|
149
|
+
}
|
|
128
150
|
}
|
|
129
151
|
this.styles = styles;
|
|
130
152
|
this.elementCount = elements.length;
|
|
@@ -167,6 +189,8 @@ export class DrapePackBuilder {
|
|
|
167
189
|
const edgeTexels = bins.edges.length / 4;
|
|
168
190
|
this.edges = ensureCapacity(this.edges, (this.edgeCount + edgeTexels) * 4);
|
|
169
191
|
this.edges.set(bins.edges, edgeBase * 4);
|
|
192
|
+
this.edgeStarts = ensureCapacity(this.edgeStarts, this.edgeCount + edgeTexels);
|
|
193
|
+
this.edgeStarts.set(bins.edgeStarts, edgeBase);
|
|
170
194
|
this.edgeCount += edgeTexels;
|
|
171
195
|
const runTexels = bins.runs.length / 4;
|
|
172
196
|
this.runs = ensureCapacity(this.runs, (this.runCount + runTexels) * 4);
|
|
@@ -109,6 +109,26 @@ export declare function drapeCellWorkBudget(tileCount: number): number;
|
|
|
109
109
|
export declare const DRAPE_LOD_TRIGGER = 16000;
|
|
110
110
|
/** The color (0..1, with the opacity already folded in) */
|
|
111
111
|
export type DrapeColor = readonly [number, number, number, number];
|
|
112
|
+
/**
|
|
113
|
+
* The dash pattern of an outline that is not solid
|
|
114
|
+
*
|
|
115
|
+
* The coefficients are those of `DASH_COEFFICIENTS` in `view/renderers/line/dash.ts`: the shader
|
|
116
|
+
* works the dash and the gap out of the width the line has in the frame, with the same formulas
|
|
117
|
+
* as the subdividing path.
|
|
118
|
+
*/
|
|
119
|
+
export interface DrapeDash {
|
|
120
|
+
/** The shortest dash (CSS px) */
|
|
121
|
+
readonly a: number;
|
|
122
|
+
/** The dash per pixel of width */
|
|
123
|
+
readonly b: number;
|
|
124
|
+
/** The gap per pixel of width */
|
|
125
|
+
readonly c: number;
|
|
126
|
+
/**
|
|
127
|
+
* The physical pixels of `strokeWidthPx` per CSS pixel (the pixel ratio the width was
|
|
128
|
+
* resolved with). The pattern is measured in CSS pixels, so the shader divides it back out
|
|
129
|
+
*/
|
|
130
|
+
readonly pixelRatio: number;
|
|
131
|
+
}
|
|
112
132
|
/**
|
|
113
133
|
* An element handed to the analytic drape (a feature of the Store and a feature of a
|
|
114
134
|
* dataset have the same shape)
|
|
@@ -131,6 +151,13 @@ export interface DrapeElement {
|
|
|
131
151
|
readonly stroke: DrapeColor;
|
|
132
152
|
/** The width of the outline (physical pixels). 0 means the outline is not drawn */
|
|
133
153
|
readonly strokeWidthPx: number;
|
|
154
|
+
/**
|
|
155
|
+
* The dash pattern of the outline (absent or null for a solid outline)
|
|
156
|
+
*
|
|
157
|
+
* Only an element with a pattern has the lengths along its paths worked out and binned next
|
|
158
|
+
* to its edges (`TileBins.edgeStarts`).
|
|
159
|
+
*/
|
|
160
|
+
readonly dash?: DrapeDash | null;
|
|
134
161
|
/**
|
|
135
162
|
* The origin of the coefficients (0 = the Store, 1 and above = datasets)
|
|
136
163
|
*
|
|
@@ -201,6 +228,11 @@ export interface TileBins {
|
|
|
201
228
|
readonly runs: Float32Array;
|
|
202
229
|
/** (x0, y0, x1, y1) per edge. In-tile 0..1 coordinates */
|
|
203
230
|
readonly edges: Float32Array;
|
|
231
|
+
/**
|
|
232
|
+
* Where each edge starts along its path, one per edge (Mercator units, the 0..1 world; not
|
|
233
|
+
* in-tile). 0 for an edge of an element without a dash pattern
|
|
234
|
+
*/
|
|
235
|
+
readonly edgeStarts: Float32Array;
|
|
204
236
|
/** Whether a limit was exceeded (if it was, the caller degrades) */
|
|
205
237
|
readonly overflow: boolean;
|
|
206
238
|
/** The number of cells truncated for exceeding the budget (a local degradation. Diagnostic) */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 SAKAIDA Atsushi
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
-
import { DRAPE_BLOCK_EDGES, drapeQuantizedGeometry } from './geometry.js';
|
|
3
|
+
import { DRAPE_BLOCK_EDGES, drapePathStarts, drapeQuantizedGeometry } from './geometry.js';
|
|
4
4
|
/** The minimum number of divisions of the grid (per side) */
|
|
5
5
|
export const DRAPE_MIN_CELLS = 8;
|
|
6
6
|
/** The maximum number of divisions of the grid (per side) */
|
|
@@ -245,6 +245,7 @@ function emptyBins(grid, edgeEstimate, overflow) {
|
|
|
245
245
|
cells: new Int32Array(grid * grid * 2),
|
|
246
246
|
runs: new Float32Array(0),
|
|
247
247
|
edges: new Float32Array(0),
|
|
248
|
+
edgeStarts: new Float32Array(0),
|
|
248
249
|
overflow,
|
|
249
250
|
truncatedCells: 0,
|
|
250
251
|
cellWork: 0,
|
|
@@ -264,6 +265,8 @@ function assembleBins(elements, picked, tile, scale, grid, zoom) {
|
|
|
264
265
|
const cells = new Int32Array(cellCount * 2);
|
|
265
266
|
/** The edges in in-tile coordinates (four numbers per edge). Stacked across elements */
|
|
266
267
|
const edgeXY = [];
|
|
268
|
+
/** Where each edge starts along its path (one number per edge) */
|
|
269
|
+
const edgeS = [];
|
|
267
270
|
/** The runs per cell. Triples of (elementIndex, inside, edgeListIndex) laid out in a row */
|
|
268
271
|
const cellRuns = new Array(cellCount);
|
|
269
272
|
/** The lists of edge indices that the runs point to */
|
|
@@ -299,8 +302,9 @@ function assembleBins(elements, picked, tile, scale, grid, zoom) {
|
|
|
299
302
|
scratch.clear();
|
|
300
303
|
touchedRows.length = 0;
|
|
301
304
|
rowTouched.fill(0);
|
|
305
|
+
const dashed = element.dash != null;
|
|
302
306
|
for (const path of picked.geometries[p].paths) {
|
|
303
|
-
scanPath(path, element.kind, tileX, tileY, scale, margin, grid, cellSize, edgeXY, scratch, rowDiff, rowTouched, touchedRows);
|
|
307
|
+
scanPath(path, element.kind, tileX, tileY, scale, margin, grid, cellSize, { xy: edgeXY, starts: edgeS }, dashed ? drapePathStarts(path) : null, scratch, rowDiff, rowTouched, touchedRows);
|
|
304
308
|
}
|
|
305
309
|
// Turn the cells that are inside and the cells the edges touch into runs (they are stacked
|
|
306
310
|
// in the order of the elements, so this comes out as the draw order)
|
|
@@ -328,6 +332,7 @@ function assembleBins(elements, picked, tile, scale, grid, zoom) {
|
|
|
328
332
|
// Repacking (laid out in the order cell -> run -> edge)
|
|
329
333
|
const runs = [];
|
|
330
334
|
const edges = [];
|
|
335
|
+
const edgeStarts = [];
|
|
331
336
|
for (let c = 0; c < cellCount; c++) {
|
|
332
337
|
const list = cellRuns[c];
|
|
333
338
|
cells[c * 2] = runs.length / 4;
|
|
@@ -352,6 +357,7 @@ function assembleBins(elements, picked, tile, scale, grid, zoom) {
|
|
|
352
357
|
break;
|
|
353
358
|
}
|
|
354
359
|
edges.push(edgeXY[at * 4], edgeXY[at * 4 + 1], edgeXY[at * 4 + 2], edgeXY[at * 4 + 3]);
|
|
360
|
+
edgeStarts.push(edgeS[at]);
|
|
355
361
|
edgeCount++;
|
|
356
362
|
}
|
|
357
363
|
}
|
|
@@ -371,6 +377,7 @@ function assembleBins(elements, picked, tile, scale, grid, zoom) {
|
|
|
371
377
|
cells,
|
|
372
378
|
runs: new Float32Array(runs),
|
|
373
379
|
edges: new Float32Array(edges),
|
|
380
|
+
edgeStarts: new Float32Array(edgeStarts),
|
|
374
381
|
overflow: false,
|
|
375
382
|
truncatedCells,
|
|
376
383
|
cellWork: (runs.length / 4 + edges.length / 4) / cellCount,
|
|
@@ -430,7 +437,7 @@ function pickElements(elements, candidates, tile, scale, quantizeLevel, zoom) {
|
|
|
430
437
|
* right of the origin, so an edge outside the tile on the right also counts (it becomes a flip
|
|
431
438
|
* of a whole row). An edge outside on the left flips nothing, so it can be dropped.
|
|
432
439
|
*/
|
|
433
|
-
function scanPath(path, kind, tileX, tileY, scale, margin, grid, cellSize,
|
|
440
|
+
function scanPath(path, kind, tileX, tileY, scale, margin, grid, cellSize, out, starts, scratch, rowDiff, rowTouched, touchedRows) {
|
|
434
441
|
const xy = path.xy;
|
|
435
442
|
const vertexCount = xy.length / 2;
|
|
436
443
|
const blockCount = path.blocks.length / 4;
|
|
@@ -458,7 +465,7 @@ function scanPath(path, kind, tileX, tileY, scale, margin, grid, cellSize, edgeX
|
|
|
458
465
|
if (kind === 0) {
|
|
459
466
|
accumulateInside(x0, y0, x1, y1, grid, cellSize, rowDiff, rowTouched, touchedRows);
|
|
460
467
|
}
|
|
461
|
-
scatterEdge(x0, y0, x1, y1, margin, grid, cellSize,
|
|
468
|
+
scatterEdge(x0, y0, x1, y1, starts ? starts[e] : 0, margin, grid, cellSize, out, scratch);
|
|
462
469
|
}
|
|
463
470
|
}
|
|
464
471
|
}
|
|
@@ -511,7 +518,7 @@ function accumulateInside(x0, y0, x1, y1, grid, cellSize, rowDiff, rowTouched, t
|
|
|
511
518
|
* that crosses the tile diagonally. The segment is cut per row to obtain the range of x, and
|
|
512
519
|
* only the columns in that range are touched.
|
|
513
520
|
*/
|
|
514
|
-
function scatterEdge(x0, y0, x1, y1, margin, grid, cellSize,
|
|
521
|
+
function scatterEdge(x0, y0, x1, y1, start, margin, grid, cellSize, out, scratch) {
|
|
515
522
|
const exLo = Math.min(x0, x1) - margin;
|
|
516
523
|
const exHi = Math.max(x0, x1) + margin;
|
|
517
524
|
const eyLo = Math.min(y0, y1) - margin;
|
|
@@ -526,8 +533,9 @@ function scatterEdge(x0, y0, x1, y1, margin, grid, cellSize, edgeXY, scratch) {
|
|
|
526
533
|
jHi = grid - 1;
|
|
527
534
|
if (jLo > jHi)
|
|
528
535
|
return;
|
|
529
|
-
const at =
|
|
530
|
-
|
|
536
|
+
const at = out.starts.length;
|
|
537
|
+
out.xy.push(x0, y0, x1, y1);
|
|
538
|
+
out.starts.push(start);
|
|
531
539
|
const horizontal = y0 === y1;
|
|
532
540
|
const inv = horizontal ? 0 : 1 / (y1 - y0);
|
|
533
541
|
for (let j = jLo; j <= jHi; j++) {
|
|
@@ -29,6 +29,20 @@ export interface DrapeGeometryPath {
|
|
|
29
29
|
readonly closed: boolean;
|
|
30
30
|
/** The bounding box of each block (minX, minY, maxX, maxY) */
|
|
31
31
|
readonly blocks: Float64Array;
|
|
32
|
+
/**
|
|
33
|
+
* Where the vertices of a quantized path came from (absent on a path of the original
|
|
34
|
+
* geometry)
|
|
35
|
+
*
|
|
36
|
+
* A dashed line keeps the pattern of the original path on a coarse tile: the length along
|
|
37
|
+
* the path at a vertex is that of the original vertex it was rounded from
|
|
38
|
+
* ({@link drapePathStarts}).
|
|
39
|
+
*/
|
|
40
|
+
readonly origin?: DrapePathOrigin;
|
|
41
|
+
}
|
|
42
|
+
/** The original path of a quantized path, and the original vertex of each of its vertices */
|
|
43
|
+
export interface DrapePathOrigin {
|
|
44
|
+
readonly path: DrapeGeometryPath;
|
|
45
|
+
readonly vertices: Int32Array;
|
|
32
46
|
}
|
|
33
47
|
/** The geometry of a single feature */
|
|
34
48
|
export interface DrapeGeometry {
|
|
@@ -93,3 +107,16 @@ export declare function buildDrapeGeometry(paths: ReadonlyArray<ReadonlyArray<Co
|
|
|
93
107
|
* @param level The fineness of the grid. The grid spacing is 2^-level (in Mercator units)
|
|
94
108
|
*/
|
|
95
109
|
export declare function drapeQuantizedGeometry(geometry: DrapeGeometry, level: number): DrapeGeometry;
|
|
110
|
+
/**
|
|
111
|
+
* The length along a path from its first vertex to each of its vertices (Mercator units)
|
|
112
|
+
*
|
|
113
|
+
* Entry `e` is where edge `e` starts on the path, so a dashed line can place its pattern on
|
|
114
|
+
* every edge on its own (on a closed path the last edge, back to the first vertex, starts at
|
|
115
|
+
* the last vertex). It is built only for dashed lines and polygons with a dashed outline, the
|
|
116
|
+
* first time a tile needs it, so a solid line pays nothing for it.
|
|
117
|
+
*
|
|
118
|
+
* A quantized path takes the lengths of the original vertices it was rounded from, so the
|
|
119
|
+
* pattern on a coarse tile stays in step with the pattern on the fine tiles next to it. A
|
|
120
|
+
* square standing in for a collapsed polygon has no original and measures itself.
|
|
121
|
+
*/
|
|
122
|
+
export declare function drapePathStarts(path: DrapeGeometryPath): Float64Array;
|
|
@@ -103,15 +103,18 @@ export function buildDrapeGeometry(paths, closed) {
|
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
105
|
* Assembles the geometry from paths in Mercator coordinates
|
|
106
|
+
*
|
|
107
|
+
* @param origins The origin of each path, for quantized geometry (null for a path that has none)
|
|
106
108
|
*/
|
|
107
|
-
function buildFromMercatorPaths(paths, closed) {
|
|
109
|
+
function buildFromMercatorPaths(paths, closed, origins) {
|
|
108
110
|
const out = [];
|
|
109
111
|
let minX = Number.POSITIVE_INFINITY;
|
|
110
112
|
let minY = Number.POSITIVE_INFINITY;
|
|
111
113
|
let maxX = Number.NEGATIVE_INFINITY;
|
|
112
114
|
let maxY = Number.NEGATIVE_INFINITY;
|
|
113
115
|
let edgeCount = 0;
|
|
114
|
-
for (
|
|
116
|
+
for (let p = 0; p < paths.length; p++) {
|
|
117
|
+
const xy = paths[p];
|
|
115
118
|
const count = xy.length / 2;
|
|
116
119
|
if (count < 2)
|
|
117
120
|
continue;
|
|
@@ -156,7 +159,10 @@ function buildFromMercatorPaths(paths, closed) {
|
|
|
156
159
|
blocks[b * 4 + 2] = bx1;
|
|
157
160
|
blocks[b * 4 + 3] = by1;
|
|
158
161
|
}
|
|
159
|
-
|
|
162
|
+
const origin = origins?.[p];
|
|
163
|
+
out.push(origin
|
|
164
|
+
? { xy, edgeCount: edges, closed, blocks, origin }
|
|
165
|
+
: { xy, edgeCount: edges, closed, blocks });
|
|
160
166
|
edgeCount += edges;
|
|
161
167
|
}
|
|
162
168
|
if (out.length === 0) {
|
|
@@ -193,10 +199,13 @@ export function drapeQuantizedGeometry(geometry, level) {
|
|
|
193
199
|
return hit;
|
|
194
200
|
const step = 2 ** -level;
|
|
195
201
|
const paths = [];
|
|
202
|
+
const origins = [];
|
|
196
203
|
for (const path of geometry.paths) {
|
|
197
204
|
const source = path.xy;
|
|
198
205
|
const count = source.length / 2;
|
|
199
206
|
const out = [];
|
|
207
|
+
/** The original vertex of each vertex kept */
|
|
208
|
+
const kept = [];
|
|
200
209
|
let lastX = Number.NaN;
|
|
201
210
|
let lastY = Number.NaN;
|
|
202
211
|
for (let i = 0; i < count; i++) {
|
|
@@ -205,6 +214,7 @@ export function drapeQuantizedGeometry(geometry, level) {
|
|
|
205
214
|
if (qx === lastX && qy === lastY)
|
|
206
215
|
continue;
|
|
207
216
|
out.push(qx, qy);
|
|
217
|
+
kept.push(i);
|
|
208
218
|
lastX = qx;
|
|
209
219
|
lastY = qy;
|
|
210
220
|
}
|
|
@@ -213,16 +223,20 @@ export function drapeQuantizedGeometry(geometry, level) {
|
|
|
213
223
|
// dropped
|
|
214
224
|
while (out.length >= 6 && out[out.length - 2] === out[0] && out[out.length - 1] === out[1]) {
|
|
215
225
|
out.length -= 2;
|
|
226
|
+
kept.length -= 1;
|
|
216
227
|
}
|
|
217
228
|
if (out.length / 2 < 3) {
|
|
218
229
|
// A polygon collapsed by the rounding is replaced with a square of one grid cell.
|
|
219
230
|
// Dropping it would leave the fill missing just there and produce spotty white
|
|
220
231
|
// blotches (confirmed hands-on, the city center at z8.7; it was pronounced in areas
|
|
221
232
|
// where small polygons at the chome level line up).
|
|
222
|
-
// The error stays within one grid cell (1/256 of a tile = about 2 pixels on screen)
|
|
233
|
+
// The error stays within one grid cell (1/256 of a tile = about 2 pixels on screen).
|
|
234
|
+
// It has no origin: a dash pattern on it would be smaller than a pixel anyway
|
|
223
235
|
const box = squareAt(source, step);
|
|
224
|
-
if (box)
|
|
236
|
+
if (box) {
|
|
225
237
|
paths.push(box);
|
|
238
|
+
origins.push(null);
|
|
239
|
+
}
|
|
226
240
|
continue;
|
|
227
241
|
}
|
|
228
242
|
}
|
|
@@ -230,8 +244,9 @@ export function drapeQuantizedGeometry(geometry, level) {
|
|
|
230
244
|
continue;
|
|
231
245
|
}
|
|
232
246
|
paths.push(new Float64Array(out));
|
|
247
|
+
origins.push({ path, vertices: new Int32Array(kept) });
|
|
233
248
|
}
|
|
234
|
-
const built = buildFromMercatorPaths(paths, geometry.paths[0]?.closed ?? true);
|
|
249
|
+
const built = buildFromMercatorPaths(paths, geometry.paths[0]?.closed ?? true, origins);
|
|
235
250
|
cache.set(level, built);
|
|
236
251
|
return built;
|
|
237
252
|
}
|
|
@@ -258,3 +273,39 @@ function squareAt(source, step) {
|
|
|
258
273
|
const cy = Math.round(sy / count / step) * step;
|
|
259
274
|
return new Float64Array([cx, cy, cx + step, cy, cx + step, cy + step, cx, cy + step]);
|
|
260
275
|
}
|
|
276
|
+
/** The lengths along the paths (path -> the length at each vertex) */
|
|
277
|
+
const startsCache = new WeakMap();
|
|
278
|
+
/**
|
|
279
|
+
* The length along a path from its first vertex to each of its vertices (Mercator units)
|
|
280
|
+
*
|
|
281
|
+
* Entry `e` is where edge `e` starts on the path, so a dashed line can place its pattern on
|
|
282
|
+
* every edge on its own (on a closed path the last edge, back to the first vertex, starts at
|
|
283
|
+
* the last vertex). It is built only for dashed lines and polygons with a dashed outline, the
|
|
284
|
+
* first time a tile needs it, so a solid line pays nothing for it.
|
|
285
|
+
*
|
|
286
|
+
* A quantized path takes the lengths of the original vertices it was rounded from, so the
|
|
287
|
+
* pattern on a coarse tile stays in step with the pattern on the fine tiles next to it. A
|
|
288
|
+
* square standing in for a collapsed polygon has no original and measures itself.
|
|
289
|
+
*/
|
|
290
|
+
export function drapePathStarts(path) {
|
|
291
|
+
const hit = startsCache.get(path);
|
|
292
|
+
if (hit)
|
|
293
|
+
return hit;
|
|
294
|
+
const count = path.xy.length / 2;
|
|
295
|
+
const starts = new Float64Array(count);
|
|
296
|
+
if (path.origin) {
|
|
297
|
+
const original = drapePathStarts(path.origin.path);
|
|
298
|
+
for (let i = 0; i < count; i++)
|
|
299
|
+
starts[i] = original[path.origin.vertices[i]];
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
const xy = path.xy;
|
|
303
|
+
let total = 0;
|
|
304
|
+
for (let i = 1; i < count; i++) {
|
|
305
|
+
total += Math.hypot(xy[i * 2] - xy[i * 2 - 2], xy[i * 2 + 1] - xy[i * 2 - 1]);
|
|
306
|
+
starts[i] = total;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
startsCache.set(path, starts);
|
|
310
|
+
return starts;
|
|
311
|
+
}
|