@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +119 -1
  2. package/README.ja.md +48 -35
  3. package/README.md +52 -36
  4. package/THIRD_PARTY_NOTICES.md +29 -0
  5. package/dist/api/datasets.d.ts +5 -0
  6. package/dist/api/draw.d.ts +6 -4
  7. package/dist/api/draw.js +6 -4
  8. package/dist/api/events.d.ts +10 -0
  9. package/dist/api/impl/create-draw.js +1 -1
  10. package/dist/api/impl/datasets.js +1 -0
  11. package/dist/api/impl/events.js +2 -1
  12. package/dist/api/impl/extension-host.js +4 -1
  13. package/dist/api/impl/options.js +58 -29
  14. package/dist/dataset/dataset.js +3 -0
  15. package/dist/dataset/retained.js +19 -18
  16. package/dist/dataset/selection.d.ts +3 -3
  17. package/dist/dataset/selection.js +3 -7
  18. package/dist/dataset/table-source.js +13 -11
  19. package/dist/dataset/types.d.ts +2 -0
  20. package/dist/dispatcher/input-router.js +2 -1
  21. package/dist/dispatcher/normalizer.js +19 -2
  22. package/dist/geometry/geojson.d.ts +4 -0
  23. package/dist/geometry/geojson.js +20 -11
  24. package/dist/index.d.ts +14 -0
  25. package/dist/view/layer/attach.d.ts +11 -5
  26. package/dist/view/layer/attach.js +85 -9
  27. package/dist/view/layer/custom-layer.js +2 -1
  28. package/dist/view/layer/drape-planner.d.ts +3 -0
  29. package/dist/view/layer/drape-planner.js +9 -7
  30. package/dist/view/layer/frame-render.js +2 -3
  31. package/dist/view/layer/frame-state.d.ts +3 -0
  32. package/dist/view/layer/frame-state.js +2 -1
  33. package/dist/view/layer/gl-state.d.ts +2 -3
  34. package/dist/view/layer/gl-state.js +2 -3
  35. package/dist/view/renderers/line/dash.d.ts +27 -0
  36. package/dist/view/renderers/line/dash.js +23 -12
  37. package/dist/view/terrain/drape/bin-store.d.ts +11 -2
  38. package/dist/view/terrain/drape/bin-store.js +26 -2
  39. package/dist/view/terrain/drape/binning.d.ts +32 -0
  40. package/dist/view/terrain/drape/binning.js +15 -7
  41. package/dist/view/terrain/drape/geometry.d.ts +27 -0
  42. package/dist/view/terrain/drape/geometry.js +57 -6
  43. package/dist/view/terrain/drape/pass.d.ts +8 -3
  44. package/dist/view/terrain/drape/pass.js +39 -28
  45. package/dist/view/terrain/drape/renderer.d.ts +13 -0
  46. package/dist/view/terrain/drape/renderer.js +157 -30
  47. package/package.json +22 -10
@@ -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
  }
@@ -2,7 +2,7 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
  import { SELECTION_HIGHLIGHT_FILL_OPACITY, SELECTION_HIGHLIGHT_RGB, SELECTION_HIGHLIGHT_STROKE_EXTRA, } from '../../../shared/config/selection-highlight.js';
4
4
  import { createProgram } from '../../shaders/helpers.js';
5
- import { DRAPE_SELECTION_TEXEL, DRAPE_STYLE_TEXELS } from './bin-store.js';
5
+ import { DRAPE_DASH_TEXEL, DRAPE_SELECTION_TEXEL, DRAPE_STYLE_TEXELS, } from './bin-store.js';
6
6
  import { applyEdgeConstrainUniforms, EDGE_CONSTRAIN_GLSL, EDGE_CONSTRAIN_UNIFORMS, EDGE_UNITS, } from './edge-constrain.js';
7
7
  import { createTerrainMeshBuffers, disposeTerrainMeshBuffers, TILE_EXTENT, } from './mesh.js';
8
8
  import { applyDrapeProjectionUniforms, applyDrapeTerrainUniforms, DRAPE_PROJECTION_UNIFORMS, DRAPE_TERRAIN_UNIFORMS, TERRAIN_SAMPLE_GLSL, TERRAIN_SHADE_GLSL, } from './shared.js';
@@ -21,6 +21,15 @@ const UNIT_EDGE = 1;
21
21
  const UNIT_RUN = 2;
22
22
  const UNIT_CELL = 3;
23
23
  const UNIT_STYLE = 4;
24
+ /**
25
+ * The texture unit of the starts of the edges along their paths
26
+ *
27
+ * 5 and 7 to 9 hold the DEMs of the edges (`EDGE_UNITS`) and 6 the DEM atlas, so it comes after
28
+ * them (WebGL2 guarantees 16 units to a fragment shader).
29
+ */
30
+ export const DRAPE_EDGE_START_UNIT = 10;
31
+ /** The pixels of a 512-pixel tile, the size the dash patterns are measured in */
32
+ const DASH_TILE_PX = 512;
24
33
  /**
25
34
  * Upper limit on the sources of the factors
26
35
  *
@@ -98,6 +107,8 @@ uniform sampler2D u_edge_tex;
98
107
  uniform sampler2D u_run_tex;
99
108
  uniform sampler2D u_cell_tex;
100
109
  uniform sampler2D u_style_tex;
110
+ // Where each edge starts along its path (Mercator units), in step with u_edge_tex
111
+ uniform sampler2D u_edge_start_tex;
101
112
  uniform int u_data_width;
102
113
  uniform int u_cell_base;
103
114
  uniform float u_cells;
@@ -128,6 +139,11 @@ uniform vec2 u_paint_range;
128
139
  // Whether to compose the selection highlight in this draw (1 only for the final
129
140
  // segment)
130
141
  uniform float u_emit_selection;
142
+ // The pixels of a dash pattern per unit of the Mercator world (512 * 2^zoom) and per
143
+ // unit of this tile (512 * 2^(zoom - tile zoom)). A pattern is measured the way the
144
+ // subdividing path measures it: in pixels of the flat map at the zoom of the frame
145
+ uniform float u_dash_world_px;
146
+ uniform float u_dash_tile_px;
131
147
 
132
148
  out vec4 fragColor;
133
149
 
@@ -153,12 +169,42 @@ bool crosses(vec2 p, vec2 q, vec2 a, vec2 b) {
153
169
  return (d1 * d2 < 0.0) && (d3 * d4 < 0.0);
154
170
  }
155
171
 
156
- // The shortest vector from a point to a segment (in tile-local coordinates)
157
- vec2 closestVector(vec2 p, vec2 a, vec2 b) {
172
+ // The shortest vector from the origin to a segment
173
+ vec2 closestVector(vec2 a, vec2 b) {
158
174
  vec2 ab = b - a;
159
175
  float len2 = dot(ab, ab);
160
- float t = len2 > 0.0 ? clamp(dot(p - a, ab) / len2, 0.0, 1.0) : 0.0;
161
- return p - (a + ab * t);
176
+ float t = len2 > 0.0 ? clamp(-dot(a, ab) / len2, 0.0, 1.0) : 0.0;
177
+ return a + ab * t;
178
+ }
179
+
180
+ // The screen distance from the pixel to the dashes of a pattern on one edge
181
+ //
182
+ // sa and sb are the ends of the edge on the screen, around the pixel. start and len are
183
+ // where the edge starts along its path and how long it is, and dash and period those of
184
+ // the pattern (all in pattern pixels). The place along the edge is taken where the pixel
185
+ // is closest to it on the screen, so the dash picked is the one closest on the screen.
186
+ // The dash there and the next one are measured: between them lies a gap, and the pixel
187
+ // is closer to one of their ends. A dash is cut where the edge ends; the dash goes on
188
+ // on the next edge, which draws its own part, and the two parts meet in a round joint
189
+ // as a solid line does. Each part is a segment with round caps, so a dash gets the
190
+ // round caps of the subdividing path at its ends.
191
+ float dashDistance(vec2 sa, vec2 sb, float start, float len, float dash, float period) {
192
+ if (len <= 0.0) return 1.0e9;
193
+ vec2 ab = sb - sa;
194
+ float len2 = dot(ab, ab);
195
+ float u = len2 > 0.0 ? clamp(-dot(sa, ab) / len2, 0.0, 1.0) : 0.0;
196
+ float k = floor((start + u * len) / period);
197
+ float best = 1.0e9;
198
+ for (int i = 0; i < 2; i++) {
199
+ float from = (k + float(i)) * period;
200
+ float u0 = (from - start) / len;
201
+ float u1 = (from + dash - start) / len;
202
+ if (u1 <= 0.0 || u0 >= 1.0) continue;
203
+ u0 = clamp(u0, 0.0, 1.0);
204
+ u1 = clamp(u1, 0.0, 1.0);
205
+ best = min(best, length(closestVector(sa + ab * u0, sa + ab * u1)));
206
+ }
207
+ return best;
162
208
  }
163
209
 
164
210
  // Compose on top (premultiplied alpha)
@@ -224,20 +270,12 @@ void main() {
224
270
  > 0.5;
225
271
  if (!paints && !selected) continue;
226
272
 
227
- float minDist = 1.0e9;
228
- for (int e = 0; e < edgeCount; e++) {
229
- vec4 seg = fetchData(u_edge_tex, edgeStart + e);
230
- vec2 a = seg.xy;
231
- vec2 b = seg.zw;
232
- if (crosses(origin, p, a, b)) inside = !inside;
233
- vec2 offset = closestVector(p, a, b);
234
- minDist = min(minDist, length(jacobianInverse * offset));
235
- }
236
-
237
273
  int styleAt = elementIndex * ${DRAPE_STYLE_TEXELS};
238
274
  vec4 fill = fetchData(u_style_tex, styleAt);
239
275
  vec4 stroke = fetchData(u_style_tex, styleAt + 1);
240
276
  vec4 params = fetchData(u_style_tex, styleAt + 2);
277
+ // (a, b, c, the physical pixels of the width per CSS pixel; 0 for a solid outline)
278
+ vec4 dashSpec = fetchData(u_style_tex, styleAt + ${DRAPE_DASH_TEXEL});
241
279
  int source = int(params.z);
242
280
  vec2 factors = source >= 0 && source < ${DRAPE_MAX_SOURCES}
243
281
  ? u_source_factors[source]
@@ -248,6 +286,49 @@ void main() {
248
286
  float halfWidth = params.x * 0.5 * factors.y * widthScale;
249
287
  bool isPolygon = params.y < 0.5;
250
288
 
289
+ // A dashed outline: the dash and the gap follow from the width the line has in
290
+ // this frame (in CSS pixels), with the formulas of the subdividing path
291
+ // (dash.ts): dash = max(a, b * w), gap = max(w + 1, c * w)
292
+ bool dashed = dashSpec.w > 0.0;
293
+ float dashLength = 0.0;
294
+ float dashPeriod = 1.0;
295
+ if (dashed) {
296
+ float w = 2.0 * halfWidth / dashSpec.w;
297
+ dashLength = max(dashSpec.x, dashSpec.y * w);
298
+ dashPeriod = dashLength + max(w + 1.0, dashSpec.z * w);
299
+ }
300
+ // The distance to the whole outline is what the fill and the selection read. A
301
+ // dashed outline measures its dashes apart, and the whole outline only when
302
+ // something reads it
303
+ bool wantsEdges = !dashed || selected || (paints && isPolygon && fill.a > 0.0);
304
+ bool wantsDashes = dashed && paints;
305
+
306
+ float minDist = 1.0e9;
307
+ float dashDist = 1.0e9;
308
+ for (int e = 0; e < edgeCount; e++) {
309
+ vec4 seg = fetchData(u_edge_tex, edgeStart + e);
310
+ vec2 a = seg.xy;
311
+ vec2 b = seg.zw;
312
+ if (crosses(origin, p, a, b)) inside = !inside;
313
+ // The distance is taken on the screen: the segment is mapped to screen pixels
314
+ // around the pixel first. The closest point found on the ground and mapped
315
+ // afterwards is not the closest one on the screen where the ground is seen
316
+ // at a slant, and the line came out thinner between its vertices than at them
317
+ vec2 sa = jacobianInverse * (a - p);
318
+ vec2 sb = jacobianInverse * (b - p);
319
+ if (wantsEdges) minDist = min(minDist, length(closestVector(sa, sb)));
320
+ if (wantsDashes) {
321
+ int at = edgeStart + e;
322
+ float start = texelFetch(
323
+ u_edge_start_tex, ivec2(at % u_data_width, at / u_data_width), 0
324
+ ).r * u_dash_world_px;
325
+ float len = length(b - a) * u_dash_tile_px;
326
+ dashDist = min(dashDist, dashDistance(sa, sb, start, len, dashLength, dashPeriod));
327
+ }
328
+ }
329
+ // The outline is drawn where its dashes are (the whole of it when it is solid)
330
+ float strokeDist = dashed ? dashDist : minDist;
331
+
251
332
  if (paints && isPolygon && fill.a > 0.0) {
252
333
  // A signed distance, negative inside and positive outside (screen pixels)
253
334
  float signedDist = inside ? -minDist : minDist;
@@ -261,7 +342,7 @@ void main() {
261
342
  }
262
343
 
263
344
  if (paints && halfWidth > 0.0 && stroke.a > 0.0) {
264
- float coverage = clamp(halfWidth + 0.5 - minDist, 0.0, 1.0);
345
+ float coverage = clamp(halfWidth + 0.5 - strokeDist, 0.0, 1.0);
265
346
  if (coverage > 0.0) acc = over(acc, stroke.rgb, stroke.a * coverage * factors.x);
266
347
  }
267
348
 
@@ -283,7 +364,8 @@ void main() {
283
364
  // The added thickness is kept constant in screen pixels (so that "an
284
365
  // outline one size thicker" always looks the same regardless of the
285
366
  // unit of the original thickness). The highlight outline is also drawn
286
- // for features that have no outline (the same as the immediate mode)
367
+ // for features that have no outline (the same as the immediate mode), and
368
+ // it is solid under a dashed outline too
287
369
  float selHalf = halfWidth + u_selection_stroke_extra * 0.5;
288
370
  selStroke = max(selStroke, clamp(selHalf + 0.5 - minDist, 0.0, 1.0) * factors.x);
289
371
  }
@@ -339,6 +421,12 @@ export class DrapeRenderer {
339
421
  writable: true,
340
422
  value: null
341
423
  });
424
+ Object.defineProperty(this, "edgeStartTex", {
425
+ enumerable: true,
426
+ configurable: true,
427
+ writable: true,
428
+ value: null
429
+ });
342
430
  Object.defineProperty(this, "runTex", {
343
431
  enumerable: true,
344
432
  configurable: true,
@@ -363,6 +451,12 @@ export class DrapeRenderer {
363
451
  writable: true,
364
452
  value: [0, 0]
365
453
  });
454
+ Object.defineProperty(this, "edgeStartTexSize", {
455
+ enumerable: true,
456
+ configurable: true,
457
+ writable: true,
458
+ value: [0, 0]
459
+ });
366
460
  Object.defineProperty(this, "runTexSize", {
367
461
  enumerable: true,
368
462
  configurable: true,
@@ -385,7 +479,7 @@ export class DrapeRenderer {
385
479
  enumerable: true,
386
480
  configurable: true,
387
481
  writable: true,
388
- value: { builderId: -1, edges: 0, runs: 0, cells: 0, styles: 0 }
482
+ value: emptyUploadMark()
389
483
  });
390
484
  /** The selection version the transferred style table reflects (-1 = not transferred) */
391
485
  Object.defineProperty(this, "uploadedSelection", {
@@ -450,6 +544,7 @@ export class DrapeRenderer {
450
544
  'u_run_tex',
451
545
  'u_cell_tex',
452
546
  'u_style_tex',
547
+ 'u_edge_start_tex',
453
548
  'u_data_width',
454
549
  'u_cell_base',
455
550
  'u_cells',
@@ -464,6 +559,8 @@ export class DrapeRenderer {
464
559
  'u_selection_stroke_extra',
465
560
  'u_paint_range',
466
561
  'u_emit_selection',
562
+ 'u_dash_world_px',
563
+ 'u_dash_tile_px',
467
564
  ...EDGE_CONSTRAIN_UNIFORMS,
468
565
  ];
469
566
  this.locations = {};
@@ -485,24 +582,28 @@ export class DrapeRenderer {
485
582
  */
486
583
  upload(pack) {
487
584
  if (this.uploaded.builderId !== pack.id) {
488
- this.uploaded = { builderId: pack.id, edges: 0, runs: 0, cells: 0, styles: 0 };
585
+ this.uploaded = { ...emptyUploadMark(), builderId: pack.id };
489
586
  this.uploadedSelection = -1;
490
587
  }
491
588
  // The selection highlight rewrites rows that have already been sent (it is not
492
589
  // an append), so when the version moves only the style table is sent again from
493
- // the beginning. The style table is 4 texels per element, orders of magnitude
590
+ // the beginning. The style table is a few texels per element, orders of magnitude
494
591
  // smaller than the edge table
495
592
  if (this.uploadedSelection !== pack.selectionVersion) {
496
593
  this.uploaded.styles = 0;
497
594
  this.uploadedSelection = pack.selectionVersion;
498
595
  }
499
596
  this.edgeTex = this.uploadTexture(this.edgeTex, 'edgeTexSize', 'edges', DATA_TEX_WIDTH, pack.edges, pack.edgeCount);
597
+ // The starts along the paths go one per texel, in step with the edges
598
+ this.edgeStartTex = this.uploadTexture(this.edgeStartTex, 'edgeStartTexSize', 'edgeStarts', DATA_TEX_WIDTH, pack.edgeStarts, pack.edgeCount, 1);
500
599
  this.runTex = this.uploadTexture(this.runTex, 'runTexSize', 'runs', DATA_TEX_WIDTH, pack.runs, pack.runCount);
501
600
  this.cellTex = this.uploadTexture(this.cellTex, 'cellTexSize', 'cells', DATA_TEX_WIDTH, pack.cells, pack.cellCount);
502
- // The style table is laid out like the others (4 texels per element in rows of
503
- // DATA_TEX_WIDTH, a multiple of 4), so its height grows with elements / 512, not elements
601
+ // The style table is laid out like the others (the texels of the elements one after
602
+ // another in rows of DATA_TEX_WIDTH; an element may run over into the next row), so its
603
+ // height grows with the elements / DATA_TEX_WIDTH, not with the elements
504
604
  this.styleTex = this.uploadTexture(this.styleTex, 'styleTexSize', 'styles', DATA_TEX_WIDTH, pack.styles, DRAPE_STYLE_TEXELS * Math.max(1, pack.elementCount));
505
605
  return (this.edgeTex !== null &&
606
+ this.edgeStartTex !== null &&
506
607
  this.runTex !== null &&
507
608
  this.cellTex !== null &&
508
609
  this.styleTex !== null);
@@ -522,7 +623,12 @@ export class DrapeRenderer {
522
623
  const gl = this.gl;
523
624
  const program = this.program;
524
625
  const mesh = this.mesh;
525
- if (!program || !mesh || !this.edgeTex || !this.cellTex || !this.styleTex) {
626
+ if (!program ||
627
+ !mesh ||
628
+ !this.edgeTex ||
629
+ !this.edgeStartTex ||
630
+ !this.cellTex ||
631
+ !this.styleTex) {
526
632
  this.lastError = 'draw: missing resources';
527
633
  return;
528
634
  }
@@ -534,6 +640,7 @@ export class DrapeRenderer {
534
640
  this.bindData(UNIT_RUN, this.runTex, set.u_run_tex);
535
641
  this.bindData(UNIT_CELL, this.cellTex, set.u_cell_tex);
536
642
  this.bindData(UNIT_STYLE, this.styleTex, set.u_style_tex);
643
+ this.bindData(DRAPE_EDGE_START_UNIT, this.edgeStartTex, set.u_edge_start_tex);
537
644
  if (set.u_data_width)
538
645
  gl.uniform1i(set.u_data_width, DATA_TEX_WIDTH);
539
646
  if (set.u_cell_base)
@@ -544,6 +651,11 @@ export class DrapeRenderer {
544
651
  gl.uniform1f(set.u_opacity, opacity);
545
652
  if (set.u_zoom)
546
653
  gl.uniform1f(set.u_zoom, zoom);
654
+ if (set.u_dash_world_px)
655
+ gl.uniform1f(set.u_dash_world_px, DASH_TILE_PX * 2 ** zoom);
656
+ if (set.u_dash_tile_px) {
657
+ gl.uniform1f(set.u_dash_tile_px, DASH_TILE_PX * 2 ** (zoom - draw.tileZ));
658
+ }
547
659
  if (set['u_source_factors[0]']) {
548
660
  gl.uniform2fv(set['u_source_factors[0]'], sourceFactors);
549
661
  }
@@ -604,19 +716,27 @@ export class DrapeRenderer {
604
716
  disposeTerrainMeshBuffers(gl, this.mesh);
605
717
  this.mesh = null;
606
718
  }
607
- for (const texture of [this.edgeTex, this.runTex, this.cellTex, this.styleTex]) {
719
+ for (const texture of [
720
+ this.edgeTex,
721
+ this.edgeStartTex,
722
+ this.runTex,
723
+ this.cellTex,
724
+ this.styleTex,
725
+ ]) {
608
726
  if (texture)
609
727
  gl.deleteTexture(texture);
610
728
  }
611
729
  this.edgeTex = null;
730
+ this.edgeStartTex = null;
612
731
  this.runTex = null;
613
732
  this.cellTex = null;
614
733
  this.styleTex = null;
615
734
  this.edgeTexSize = [0, 0];
735
+ this.edgeStartTexSize = [0, 0];
616
736
  this.runTexSize = [0, 0];
617
737
  this.cellTexSize = [0, 0];
618
738
  this.styleTexSize = [0, 0];
619
- this.uploaded = { builderId: -1, edges: 0, runs: 0, cells: 0, styles: 0 };
739
+ this.uploaded = emptyUploadMark();
620
740
  this.uploadedSelection = -1;
621
741
  }
622
742
  bindData(unit, texture, location) {
@@ -639,8 +759,10 @@ export class DrapeRenderer {
639
759
  *
640
760
  * texStorage2D is immutable, so the texture is recreated whenever the required
641
761
  * size changes (once recreated, everything is sent again).
762
+ *
763
+ * @param channels The numbers per texel: 4 (RGBA32F) or 1 (R32F)
642
764
  */
643
- uploadTexture(texture, sizeField, markField, width, data, texelCount) {
765
+ uploadTexture(texture, sizeField, markField, width, data, texelCount, channels = 4) {
644
766
  const gl = this.gl;
645
767
  const size = this[sizeField];
646
768
  const neededRows = Math.max(1, Math.ceil(texelCount / width));
@@ -663,7 +785,7 @@ export class DrapeRenderer {
663
785
  // Allocate a few extra rows (so it is not recreated on every increase)
664
786
  const rows = Math.min(maxRows, Math.max(1, Math.ceil(neededRows * 1.5)));
665
787
  gl.bindTexture(gl.TEXTURE_2D, target);
666
- gl.texStorage2D(gl.TEXTURE_2D, 1, gl.RGBA32F, width, rows);
788
+ gl.texStorage2D(gl.TEXTURE_2D, 1, channels === 4 ? gl.RGBA32F : gl.R32F, width, rows);
667
789
  // Only the allocation is checked (it is rare; getError is a synchronous round trip)
668
790
  if (gl.getError() !== gl.NO_ERROR) {
669
791
  gl.deleteTexture(target);
@@ -688,16 +810,21 @@ export class DrapeRenderer {
688
810
  const fromRow = Math.floor(uploaded / width);
689
811
  const toRow = Math.ceil(texelCount / width);
690
812
  const rows = toRow - fromRow;
691
- const needed = rows * width * 4;
692
- const offset = fromRow * width * 4;
813
+ const needed = rows * width * channels;
814
+ const offset = fromRow * width * channels;
693
815
  const payload = data.length >= offset + needed
694
816
  ? data.subarray(offset, offset + needed)
695
817
  : padTo(data.subarray(offset), needed);
696
- gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, fromRow, width, rows, gl.RGBA, gl.FLOAT, payload);
818
+ const format = channels === 4 ? gl.RGBA : gl.RED;
819
+ gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, fromRow, width, rows, format, gl.FLOAT, payload);
697
820
  this.uploaded[markField] = texelCount;
698
821
  return target;
699
822
  }
700
823
  }
824
+ /** Nothing transferred yet */
825
+ function emptyUploadMark() {
826
+ return { builderId: -1, edges: 0, edgeStarts: 0, runs: 0, cells: 0, styles: 0 };
827
+ }
701
828
  /** Pads with 0 up to the length required for the transfer */
702
829
  function padTo(data, length) {
703
830
  const out = new Float32Array(length);