@sakuzu/maplibre-gl-draw 2.1.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.
@@ -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, edgeXY, scratch, rowDiff, rowTouched, touchedRows) {
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, edgeXY, scratch);
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, edgeXY, scratch) {
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 = edgeXY.length / 4;
530
- edgeXY.push(x0, y0, x1, y1);
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 (const xy of paths) {
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
- out.push({ xy, edgeCount: edges, closed, blocks });
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
+ }
@@ -25,8 +25,8 @@ export declare const DRAPE_MAX_VERTICES = 4000000;
25
25
  export interface DrapeCollectResult {
26
26
  /** The elements in draw order */
27
27
  readonly elements: DrapeElement[];
28
- /** The IDs of the Store features that cannot be put on the drape (points, images, dashed
29
- * lines and so on) */
28
+ /** The IDs of the Store features that cannot be put on the drape (points, images and so
29
+ * on) */
30
30
  readonly excluded: Set<string>;
31
31
  /** The breaks in the stacking order (the positions of the images; dataset order =
32
32
  * ascending afterElements) */
@@ -98,8 +98,13 @@ export interface DrapeDatasetSource {
98
98
  * It is not narrowed down by the view. Narrowing it down would change the element order every
99
99
  * time the view moves and require rebuilding the tile indices (the narrowing down within a
100
100
  * tile is done by `binTile` using the bounding boxes).
101
+ *
102
+ * @param pixelRatio The rendering pixel ratio, for widths fixed in screen pixels (the rendering
103
+ * scale included; `resolvePixelRatio`)
104
+ * @param contentPixelRatio The rendering pixel ratio for widths that follow the zoom (the
105
+ * rendering scale not applied; `resolveContentPixelRatio`)
101
106
  */
102
- export declare function collectDrapeElements(store: Store, styles: RetainedStyleResolver, pixelRatio: number, datasets: readonly DrapeDatasetSource[]): DrapeCollectResult;
107
+ export declare function collectDrapeElements(store: Store, styles: RetainedStyleResolver, pixelRatio: number, contentPixelRatio: number, datasets: readonly DrapeDatasetSource[]): DrapeCollectResult;
103
108
  /**
104
109
  * Whether the analytic drape may be used
105
110
  *
@@ -20,6 +20,7 @@ import { coordinatesOf } from '../../../shared/utils/coordinates.js';
20
20
  import { getCreatedZoom } from '../../../shared/utils/property.js';
21
21
  import { getDisplayFeatures, isLocallyHidden } from '../../../store/local-visibility.js';
22
22
  import { layerDrawFactors } from '../../renderers/draw-factors.js';
23
+ import { dashCoefficientsOf } from '../../renderers/line/dash.js';
23
24
  import { drapeSelectionKey, } from './binning.js';
24
25
  import { drapeBoundsOf, drapeGeometryOf } from './geometry.js';
25
26
  import { DRAPE_MAX_SOURCES } from './renderer.js';
@@ -74,8 +75,13 @@ export function drapeLayerSource(layerIndex, datasetCount) {
74
75
  * It is not narrowed down by the view. Narrowing it down would change the element order every
75
76
  * time the view moves and require rebuilding the tile indices (the narrowing down within a
76
77
  * tile is done by `binTile` using the bounding boxes).
78
+ *
79
+ * @param pixelRatio The rendering pixel ratio, for widths fixed in screen pixels (the rendering
80
+ * scale included; `resolvePixelRatio`)
81
+ * @param contentPixelRatio The rendering pixel ratio for widths that follow the zoom (the
82
+ * rendering scale not applied; `resolveContentPixelRatio`)
77
83
  */
78
- export function collectDrapeElements(store, styles, pixelRatio, datasets) {
84
+ export function collectDrapeElements(store, styles, pixelRatio, contentPixelRatio, datasets) {
79
85
  const visible = getDisplayFeatures(store);
80
86
  const byLayer = new Map();
81
87
  for (const feature of visible) {
@@ -97,6 +103,7 @@ export function collectDrapeElements(store, styles, pixelRatio, datasets) {
97
103
  drapedDatasets: new Set(),
98
104
  styles,
99
105
  pixelRatio,
106
+ contentPixelRatio,
100
107
  layerSources: new Map(),
101
108
  };
102
109
  const byId = new Map();
@@ -168,21 +175,13 @@ function collectStoreFeature(context, feature, layer, source, bakedOpacity) {
168
175
  }
169
176
  if (feature.type === 'Polygon' || feature.type === 'MultiPolygon') {
170
177
  const { fillColor, strokeStyle } = context.styles.getPolygonStyles(feature, layer);
171
- // Dashed lines are not put through the analytic evaluation (solving the accumulated
172
- // distance along the outline from the pixel is not implemented). Drawing them as solid
173
- // lines would change the appearance, so features with dashed lines are routed to the
174
- // previous vertex displacement path.
175
- if (strokeStyle.lineStyle !== 'solid') {
176
- context.excluded.add(feature.id);
177
- return;
178
- }
179
178
  const hasStroke = strokeStyle.opacity > 0 && strokeStyle.width > 0;
180
179
  const rings = polygonRings(feature);
181
180
  if (rings.length === 0)
182
181
  return;
183
182
  if (fillColor[3] <= 0 && !hasStroke)
184
183
  return;
185
- const polygonWidth = widthOf(feature, strokeStyle.width, context.pixelRatio);
184
+ const polygonWidth = widthOf(feature, strokeStyle.width, context);
186
185
  pushElement(context, feature, 0, rings, {
187
186
  fill: [fillColor[0], fillColor[1], fillColor[2], fillColor[3] * bakedOpacity],
188
187
  stroke: [
@@ -192,6 +191,7 @@ function collectStoreFeature(context, feature, layer, source, bakedOpacity) {
192
191
  strokeStyle.color[3] * (hasStroke ? strokeStyle.opacity : 0) * bakedOpacity,
193
192
  ],
194
193
  strokeWidthPx: hasStroke ? polygonWidth.px : 0,
194
+ dash: dashOf(strokeStyle.lineStyle, polygonWidth),
195
195
  source,
196
196
  widthZoom: polygonWidth.zoom,
197
197
  // A Store selection is expressed by the selection UI (the box and the handles). The
@@ -201,16 +201,12 @@ function collectStoreFeature(context, feature, layer, source, bakedOpacity) {
201
201
  return;
202
202
  }
203
203
  const strokeStyle = context.styles.getLineStringStrokeStyle(feature, layer);
204
- if (strokeStyle.lineStyle !== 'solid') {
205
- context.excluded.add(feature.id);
206
- return;
207
- }
208
204
  if (strokeStyle.opacity <= 0 || strokeStyle.width <= 0)
209
205
  return;
210
206
  const paths = linePaths(feature);
211
207
  if (paths.length === 0)
212
208
  return;
213
- const lineWidth = widthOf(feature, strokeStyle.width, context.pixelRatio);
209
+ const lineWidth = widthOf(feature, strokeStyle.width, context);
214
210
  pushElement(context, feature, 1, paths, {
215
211
  fill: [0, 0, 0, 0],
216
212
  stroke: [
@@ -220,6 +216,7 @@ function collectStoreFeature(context, feature, layer, source, bakedOpacity) {
220
216
  strokeStyle.color[3] * strokeStyle.opacity * bakedOpacity,
221
217
  ],
222
218
  strokeWidthPx: lineWidth.px,
219
+ dash: dashOf(strokeStyle.lineStyle, lineWidth),
223
220
  source,
224
221
  widthZoom: lineWidth.zoom,
225
222
  selectionKey: '',
@@ -243,16 +240,12 @@ function collectDataset(context, dataset, source) {
243
240
  if (feature.type === 'Polygon' || feature.type === 'MultiPolygon') {
244
241
  const { fillColor, strokeStyle } = context.styles.getPolygonStyles(feature);
245
242
  const hasStroke = strokeStyle.opacity > 0 && strokeStyle.width > 0;
246
- // A dashed outline is drawn by the immediate path on the retained batch side (the
247
- // paired test)
248
- if (hasStroke && strokeStyle.lineStyle !== 'solid')
249
- continue;
250
243
  if (!hasStroke && fillColor[3] <= 0)
251
244
  continue;
252
245
  const rings = polygonRings(feature);
253
246
  if (rings.length === 0)
254
247
  continue;
255
- const width = widthOf(feature, strokeStyle.width, context.pixelRatio);
248
+ const width = widthOf(feature, strokeStyle.width, context);
256
249
  pushElement(context, coordinatesOf(feature), 0, rings, {
257
250
  fill: fillColor,
258
251
  stroke: [
@@ -262,6 +255,7 @@ function collectDataset(context, dataset, source) {
262
255
  hasStroke ? strokeStyle.color[3] : 0,
263
256
  ],
264
257
  strokeWidthPx: hasStroke ? width.px : 0,
258
+ dash: dashOf(strokeStyle.lineStyle, width),
265
259
  source,
266
260
  widthZoom: width.zoom,
267
261
  selectionKey: drapeSelectionKey(dataset.id, feature.id),
@@ -274,14 +268,12 @@ function collectDataset(context, dataset, source) {
274
268
  continue;
275
269
  }
276
270
  const strokeStyle = context.styles.getLineStringStrokeStyle(feature);
277
- if (strokeStyle.lineStyle !== 'solid')
278
- continue;
279
271
  if (strokeStyle.opacity <= 0 || strokeStyle.width <= 0)
280
272
  continue;
281
273
  const paths = linePaths(feature);
282
274
  if (paths.length === 0)
283
275
  continue;
284
- const width = widthOf(feature, strokeStyle.width, context.pixelRatio);
276
+ const width = widthOf(feature, strokeStyle.width, context);
285
277
  pushElement(context, coordinatesOf(feature), 1, paths, {
286
278
  fill: [0, 0, 0, 0],
287
279
  stroke: [
@@ -291,6 +283,7 @@ function collectDataset(context, dataset, source) {
291
283
  strokeStyle.color[3] * strokeStyle.opacity,
292
284
  ],
293
285
  strokeWidthPx: width.px,
286
+ dash: dashOf(strokeStyle.lineStyle, width),
294
287
  source,
295
288
  widthZoom: width.zoom,
296
289
  selectionKey: drapeSelectionKey(dataset.id, feature.id),
@@ -301,15 +294,31 @@ function collectDataset(context, dataset, source) {
301
294
  * Resolves the line width with the same convention as the retained batch path
302
295
  *
303
296
  * A feature without a `createdZoom` is fixed in screen pixels (the rendering scale applies),
304
- * and a feature with one scales by `2^(zoom - createdZoom)`. So that the thickness does not
305
- * change with the presence of terrain, the drape follows the same convention.
297
+ * and a feature with one scales by `2^(zoom - createdZoom)` (the device pixel ratio applies,
298
+ * the rendering scale does not). So that the thickness does not change with the presence of
299
+ * terrain, the drape follows the same convention.
306
300
  */
307
- function widthOf(feature, width, pixelRatio) {
301
+ function widthOf(feature, width, ratios) {
308
302
  const createdZoom = getCreatedZoom(feature);
309
303
  if (createdZoom !== undefined && Number.isFinite(createdZoom)) {
310
- return { px: width, zoom: createdZoom };
304
+ const ratio = ratios.contentPixelRatio;
305
+ return { px: width * ratio, zoom: createdZoom, ratio };
311
306
  }
312
- return { px: width * pixelRatio, zoom: -1 };
307
+ return { px: width * ratios.pixelRatio, zoom: -1, ratio: ratios.pixelRatio };
308
+ }
309
+ /**
310
+ * The dash pattern of an outline, or null for a solid one
311
+ *
312
+ * The coefficients come from the table the subdividing path uses (`dash.ts`), and the
313
+ * shader turns them into the dash and the gap of the width the line has in the frame, in
314
+ * CSS pixels as the subdividing path measures them: the pixel ratio the width was resolved
315
+ * with goes along to take back out.
316
+ */
317
+ function dashOf(lineStyle, width) {
318
+ const coefficients = dashCoefficientsOf(lineStyle);
319
+ if (!coefficients || width.ratio <= 0)
320
+ return null;
321
+ return { ...coefficients, pixelRatio: width.ratio };
313
322
  }
314
323
  /** The rings of a polygon (with the closing point dropped) */
315
324
  function polygonRings(feature) {
@@ -343,6 +352,8 @@ function pushElement(context, cacheKey, kind, paths, style) {
343
352
  // is not converted)
344
353
  geometry: () => drapeGeometryOf(cacheKey, paths, kind === 0),
345
354
  ...style,
355
+ // An outline that is not drawn has no pattern to lay out
356
+ dash: style.strokeWidthPx > 0 ? style.dash : null,
346
357
  });
347
358
  context.edgeCount += edges;
348
359
  }
@@ -26,6 +26,13 @@ import { type DrapePackBuilder } from './bin-store.js';
26
26
  import { EDGE_UNITS } from './edge-constrain.js';
27
27
  import type { ResolvedTileEdges } from './stitch.js';
28
28
  export { EDGE_UNITS };
29
+ /**
30
+ * The texture unit of the starts of the edges along their paths
31
+ *
32
+ * 5 and 7 to 9 hold the DEMs of the edges (`EDGE_UNITS`) and 6 the DEM atlas, so it comes after
33
+ * them (WebGL2 guarantees 16 units to a fragment shader).
34
+ */
35
+ export declare const DRAPE_EDGE_START_UNIT = 10;
29
36
  /**
30
37
  * Upper limit on the sources of the factors
31
38
  *
@@ -92,6 +99,8 @@ export interface DrapeTileDraw {
92
99
  readonly grid: number;
93
100
  /** The ground size of this tile (meters). Used to compute the shading gradient */
94
101
  readonly groundMeters: number;
102
+ /** The zoom of this tile (it scales the lengths of the edges into dash pattern pixels) */
103
+ readonly tileZ: number;
95
104
  /**
96
105
  * The constraint for each edge (resolving T-junctions)
97
106
  *
@@ -123,10 +132,12 @@ export declare class DrapeRenderer {
123
132
  private variantName;
124
133
  private mesh;
125
134
  private edgeTex;
135
+ private edgeStartTex;
126
136
  private runTex;
127
137
  private cellTex;
128
138
  private styleTex;
129
139
  private edgeTexSize;
140
+ private edgeStartTexSize;
130
141
  private runTexSize;
131
142
  private cellTexSize;
132
143
  private styleTexSize;
@@ -179,6 +190,8 @@ export declare class DrapeRenderer {
179
190
  *
180
191
  * texStorage2D is immutable, so the texture is recreated whenever the required
181
192
  * size changes (once recreated, everything is sent again).
193
+ *
194
+ * @param channels The numbers per texel: 4 (RGBA32F) or 1 (R32F)
182
195
  */
183
196
  private uploadTexture;
184
197
  }