@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.
@@ -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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sakuzu/maplibre-gl-draw",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
4
4
  "description": "WebGL2 drawing and editing engine for MapLibre GL JS: points, lines, and polygons rendered in a custom layer, with selection, hit testing, and geometry utilities.",
5
5
  "keywords": [
6
6
  "maplibre",