@deck.gl/extensions 9.4.0-beta.1 → 9.4.0-beta.4

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 (37) hide show
  1. package/dist/dist.dev.js +1281 -120
  2. package/dist/fill-style/fill-style-extension.d.ts +38 -6
  3. package/dist/fill-style/fill-style-extension.d.ts.map +1 -1
  4. package/dist/fill-style/fill-style-extension.js +88 -10
  5. package/dist/fill-style/fill-style-extension.js.map +1 -1
  6. package/dist/fill-style/procedural-pattern.d.ts +65 -0
  7. package/dist/fill-style/procedural-pattern.d.ts.map +1 -0
  8. package/dist/fill-style/procedural-pattern.js +135 -0
  9. package/dist/fill-style/procedural-pattern.js.map +1 -0
  10. package/dist/fill-style/shader-module.d.ts +13 -4
  11. package/dist/fill-style/shader-module.d.ts.map +1 -1
  12. package/dist/fill-style/shader-module.js +212 -21
  13. package/dist/fill-style/shader-module.js.map +1 -1
  14. package/dist/index.cjs +886 -2702
  15. package/dist/index.cjs.map +3 -3
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/mask/shader-module.d.ts +1 -0
  19. package/dist/mask/shader-module.d.ts.map +1 -1
  20. package/dist/path-style/path-style-extension.d.ts +49 -6
  21. package/dist/path-style/path-style-extension.d.ts.map +1 -1
  22. package/dist/path-style/path-style-extension.js +237 -37
  23. package/dist/path-style/path-style-extension.js.map +1 -1
  24. package/dist/path-style/shaders.glsl.d.ts +30 -1
  25. package/dist/path-style/shaders.glsl.d.ts.map +1 -1
  26. package/dist/path-style/shaders.glsl.js +136 -21
  27. package/dist/path-style/shaders.glsl.js.map +1 -1
  28. package/dist/terrain/shader-module.d.ts +1 -0
  29. package/dist/terrain/shader-module.d.ts.map +1 -1
  30. package/dist.min.js +377 -98
  31. package/package.json +7 -7
  32. package/src/fill-style/fill-style-extension.ts +135 -13
  33. package/src/fill-style/procedural-pattern.ts +203 -0
  34. package/src/fill-style/shader-module.ts +233 -23
  35. package/src/index.ts +10 -1
  36. package/src/path-style/path-style-extension.ts +350 -44
  37. package/src/path-style/shaders.glsl.ts +268 -27
@@ -2,12 +2,40 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
+ /**
6
+ * Dash shader injections for PathLayer, ScatterplotLayer and TextBackgroundLayer.
7
+ *
8
+ * PathLayer dash math crosses three coordinate spaces:
9
+ *
10
+ * 1. **Common space** — the private metric updater accumulates distance over normalized
11
+ * PathTessellator geometry, and `instanceDashOffsets` carries it to the shader. The public
12
+ * scalar `getDashOffsets` helper remains available for compatibility but does not back the
13
+ * managed attribute.
14
+ * 2. **Screen pixels** — the common basis for reconciling flat and billboard extrusion.
15
+ * At the dash injection point, `width` is in common units for a flat path but pixels for
16
+ * a billboarded path, whose conversion also includes `project.focalDistance`.
17
+ * `dashWidthPixels` is correct in both branches.
18
+ * 3. **Half-widths along the path** — the units of `vPathPosition.y`, tested by the fragment
19
+ * shader. One unit spans `dashWidthPixels` screen pixels.
20
+ *
21
+ * Convert explicitly whenever a value crosses these spaces. Mixing them directly makes dash
22
+ * period or phase depend on billboard, elevation, or offset configuration.
23
+ */
5
24
  export type Defines = {
6
25
  // Defines passed externally
7
26
  /**
8
27
  * Enable high precision dash rendering.
9
28
  */
10
29
  HIGH_PRECISION_DASH?: boolean;
30
+ /**
31
+ * Set whenever the dash shaders are injected, so that the offset shaders can adjust dash
32
+ * varyings only when they actually exist.
33
+ */
34
+ DASH_ENABLED?: boolean;
35
+ /**
36
+ * Set when PathStyleExtension offset shaders remap the path-width coordinate.
37
+ */
38
+ PATH_STYLE_OFFSET?: boolean;
11
39
  };
12
40
 
13
41
  export const dashShaders = {
@@ -15,18 +43,78 @@ export const dashShaders = {
15
43
  'vs:#decl': `
16
44
  in vec2 instanceDashArrays;
17
45
  #ifdef HIGH_PRECISION_DASH
18
- in float instanceDashOffsets;
46
+ // [distance from the start of the path, total length of the path], in common space.
47
+ in vec2 instanceDashOffsets;
19
48
  #endif
20
49
  out vec2 vDashArray;
21
50
  out float vDashOffset;
51
+ out float vDashPathLength;
52
+
53
+ // Also declared in the fragment stage. The vertex stage needs dashAlignMode so that it can
54
+ // reduce the dash phase modulo the same period the fragment stage will test against, and
55
+ // dashUnits to scale the dash array into that period's units.
56
+ layout(std140) uniform pathStyleUniforms {
57
+ float dashAlignMode;
58
+ bool dashGapPickable;
59
+ highp int dashUnits;
60
+ } pathStyle;
22
61
  `,
23
62
 
24
63
  'vs:#main-end': `
25
- vDashArray = instanceDashArrays;
64
+ // How many screen pixels one unit of vPathPosition.y covers. The two extrusion branches of
65
+ // the path layer leave this in different units, and both differences have to be undone here
66
+ // for a billboarded path to dash like a flat one.
67
+ //
68
+ // Flat paths extrude in common space, so \`width\` is in common units and a unit of
69
+ // vPathPosition.y is width.x * project.scale pixels.
70
+ //
71
+ // Billboarded paths extrude in clip space, so \`width\` is already in pixels at this point -
72
+ // the clip-space conversion happens inside getLineJoinOffset. That conversion also folds in
73
+ // project.focalDistance, which scales the half-width the segment delta is divided by but not
74
+ // the delta itself, so a unit of vPathPosition.y ends up focalDistance pixels wider than the
75
+ // stroke it is supposed to be relative to.
76
+ float dashWidthPixels = path.billboard
77
+ ? width.x * project.focalDistance
78
+ : width.x * project.scale;
79
+
80
+ // The actual half stroke width on screen, which is what 'widths' is relative to. It differs
81
+ // from dashWidthPixels by exactly the spurious focalDistance above, so expressing 'widths'
82
+ // as a ratio of the two corrects the billboard case onto the flat one - getDashArray is
83
+ // documented relative to the stroke, and flat paths already honor that.
84
+ float strokeHalfWidthPixels = path.billboard ? width.x : width.x * project.scale;
85
+
86
+ // Everything reduces to "how many screen pixels is one dash unit", divided through by the
87
+ // pixels per unit of vPathPosition.y, so the fragment shader needs no notion of units at all.
88
+ // Keep the cases in sync with DASH_UNITS in path-style-extension.ts.
89
+ float dashUnitPixels = strokeHalfWidthPixels;
90
+ if (pathStyle.dashUnits == 1) {
91
+ dashUnitPixels = 1.0;
92
+ } else if (pathStyle.dashUnits == 2) {
93
+ dashUnitPixels = project_size_to_pixel(1.0);
94
+ } else if (pathStyle.dashUnits == 3) {
95
+ dashUnitPixels = project.scale;
96
+ }
97
+ vDashArray = instanceDashArrays * (dashUnitPixels / dashWidthPixels);
98
+
26
99
  #ifdef HIGH_PRECISION_DASH
27
- vDashOffset = instanceDashOffsets / width.x;
100
+ // instanceDashOffsets accumulates common-space distance on the CPU. Convert it to pixels, then
101
+ // divide by the pixel half-width so both extrusion branches use the same dash coordinate.
102
+ vec2 dashOffsetAndLength = (instanceDashOffsets * project.scale) / dashWidthPixels;
103
+ vDashPathLength = dashOffsetAndLength.y;
104
+
105
+ // Reduce the phase into the first period here rather than in the fragment shader. On a long
106
+ // path at high zoom the raw offset reaches into the millions, and adding the much smaller
107
+ // vPathPosition.y to it in fp32 loses the latter entirely, freezing the pattern mid-segment.
108
+ // The dash function is periodic, so this discards nothing - but it has to be reduced modulo
109
+ // whichever period the fragment stage will actually test against, hence dashAlignMode here.
110
+ float dashUnitLength = vDashArray.x + vDashArray.y;
111
+ float dashPeriod = pathStyle.dashAlignMode == 0.0
112
+ ? dashUnitLength
113
+ : vDashPathLength / max(round(vDashPathLength / max(dashUnitLength, 0.0001)), 1.0);
114
+ vDashOffset = dashPeriod > 0.0 ? mod(dashOffsetAndLength.x, dashPeriod) : 0.0;
28
115
  #else
29
116
  vDashOffset = 0.0;
117
+ vDashPathLength = 0.0;
30
118
  #endif
31
119
  `,
32
120
 
@@ -34,10 +122,44 @@ vDashOffset = 0.0;
34
122
  layout(std140) uniform pathStyleUniforms {
35
123
  float dashAlignMode;
36
124
  bool dashGapPickable;
125
+ highp int dashUnits;
37
126
  } pathStyle;
38
127
 
39
128
  in vec2 vDashArray;
40
129
  in float vDashOffset;
130
+ in float vDashPathLength;
131
+
132
+ // Integral of the dash square wave from 0 to position, i.e. how much solid stroke lies before it.
133
+ float dashPatternIntegral(float position, float solidLength, float unitLength) {
134
+ return floor(position / unitLength) * solidLength +
135
+ min(mod(position, unitLength), solidLength);
136
+ }
137
+
138
+ // Fraction of the filter-width interval centered on position that is covered by solid stroke.
139
+ //
140
+ // Testing the dash pattern with a single comparison per fragment aliases as soon as one
141
+ // period approaches one pixel: the stroke breaks into moire or, when the phase happens to
142
+ // land inside a dash, reads as solid. Integrating the wave over the fragment footprint
143
+ // instead is the closed form of what a mipmapped dash texture approximates, and it degrades
144
+ // the way one wants - once a period drops below a pixel the result converges on the duty
145
+ // cycle solidLength / unitLength, so the stroke fades to a uniformly lighter line.
146
+ //
147
+ // position is reduced into the first period first. The integral satisfies
148
+ // F(position + periodIndex * unitLength) = F(position) + periodIndex * solidLength,
149
+ // so the same periodIndex cancels out of the
150
+ // difference below, and keeping the arguments small avoids subtracting two large nearly
151
+ // equal numbers in fp32 on long paths.
152
+ float dashPatternCoverage(
153
+ float position,
154
+ float solidLength,
155
+ float unitLength,
156
+ float filterWidth
157
+ ) {
158
+ float halfFilter = 0.5 * filterWidth;
159
+ float reducedPosition = mod(position, unitLength);
160
+ return (dashPatternIntegral(reducedPosition + halfFilter, solidLength, unitLength) -
161
+ dashPatternIntegral(reducedPosition - halfFilter, solidLength, unitLength)) / filterWidth;
162
+ }
41
163
  `,
42
164
 
43
165
  // if given position is in the gap part of the dashed line
@@ -49,41 +171,131 @@ in float vDashOffset;
49
171
  // 1 - stretch to fit, draw half dash at each end for nicer joints
50
172
  // o-- ---- ---- ---- --o-- --o-- ---- --o
51
173
  'fs:#main-start': `
174
+ float dashCoverage = 1.0;
175
+ bool shouldDiscardDash = false;
176
+ bool inRoundedDashGap = false;
177
+ float roundedDashResolvedCoverage = 1.0;
178
+ float roundedDashSubPixelBlend = 0.0;
179
+ float roundedDashDutyCycle = 1.0;
180
+
52
181
  float solidLength = vDashArray.x;
53
182
  float gapLength = vDashArray.y;
54
183
  float unitLength = solidLength + gapLength;
55
184
 
56
- float offset;
57
-
58
- if (unitLength > 0.0) {
185
+ if (unitLength > 0.0 && gapLength > 0.0) {
186
+ float offset;
59
187
  if (pathStyle.dashAlignMode == 0.0) {
60
188
  offset = vDashOffset;
61
189
  } else {
62
- unitLength = vPathLength / round(vPathLength / unitLength);
190
+ // Justified: stretch the period so a whole number of them spans the run, and start
191
+ // half a dash in so both ends finish on a joint. Rounding up to at least one period
192
+ // matters - a run shorter than half a period used to round to zero, which made
193
+ // unitLength infinite and rendered it solid.
194
+ #ifdef HIGH_PRECISION_DASH
195
+ // Justify across the entire path rather than each segment separately, so the gaps stay
196
+ // even instead of being stretched by a different amount on every segment. vDashOffset
197
+ // already carries the distance to the start of this segment, reduced modulo this same
198
+ // period in the vertex shader.
199
+ unitLength = vDashPathLength / max(round(vDashPathLength / unitLength), 1.0);
200
+ #else
201
+ unitLength = vPathLength / max(round(vPathLength / unitLength), 1.0);
202
+ #endif
203
+ // A short segment or path can make the fitted period shorter than the requested dash.
204
+ // Bound the solid interval to that period so coverage and duty cycle remain valid.
205
+ solidLength = min(solidLength, unitLength);
63
206
  offset = solidLength / 2.0;
207
+ #ifdef HIGH_PRECISION_DASH
208
+ offset += vDashOffset;
209
+ #endif
64
210
  }
65
211
 
66
- float unitOffset = mod(vPathPosition.y + offset, unitLength);
67
-
68
- if (gapLength > 0.0 && unitOffset > solidLength) {
69
- if (path.capType <= 0.5) {
70
- if (!(pathStyle.dashGapPickable && bool(picking.isActive))) {
71
- discard;
72
- }
73
- } else {
74
- // caps are rounded, test the distance to solid ends
75
- float distToEnd = length(vec2(
212
+ float alongPath = vPathPosition.y + offset;
213
+ float unitOffset = mod(alongPath, unitLength);
214
+ float filterWidth = max(fwidth(alongPath), 0.0001);
215
+
216
+ // Picking stays a hard in-or-out test. A blended picking colour decodes to the wrong
217
+ // index, and dashGapPickable is defined in terms of whole gaps rather than coverage.
218
+ if (bool(picking.isActive)) {
219
+ bool inGap = unitOffset > solidLength;
220
+ // Only measure the rounded end-cap distance after the hard interval test has found a
221
+ // gap. Inside a solid interval the longitudinal distance is negative; taking its
222
+ // vector length would turn it positive and incorrectly discard the middle of a dash.
223
+ if (inGap && path.capType > 0.5) {
224
+ inGap = length(vec2(
76
225
  min(unitOffset - solidLength, unitLength - unitOffset),
77
226
  vPathPosition.x
78
- ));
79
- if (distToEnd > 1.0) {
80
- if (!(pathStyle.dashGapPickable && bool(picking.isActive))) {
81
- discard;
82
- }
83
- }
227
+ )) > 1.0;
84
228
  }
229
+ if (inGap && !pathStyle.dashGapPickable) {
230
+ shouldDiscardDash = true;
231
+ }
232
+ } else if (path.capType <= 0.5) {
233
+ dashCoverage = dashPatternCoverage(alongPath, solidLength, unitLength, filterWidth);
234
+ } else {
235
+ // Rounded caps: the dash end is an arc, so resolve the 2D distance to the nearer solid
236
+ // end rather than the 1D position along the path. Only filter fragments in the gap;
237
+ // PathLayer already antialiases the ordinary sides of the solid stroke.
238
+ float distanceAlongGap = min(unitOffset - solidLength, unitLength - unitOffset);
239
+ // Clamp before taking the derivative so the distance stays continuous across the
240
+ // wrapped period boundary. Evaluate fwidth for every fragment in the quad; derivatives
241
+ // are undefined inside the non-uniform gap branch below.
242
+ float distanceToEnd = length(vec2(max(distanceAlongGap, 0.0), vPathPosition.x));
243
+ float capEdgePixels = (1.0 - distanceToEnd) / max(fwidth(distanceToEnd), 1e-6);
244
+ if (distanceAlongGap > 0.0) {
245
+ inRoundedDashGap = true;
246
+ roundedDashResolvedCoverage = smoothedge(0.0, capEdgePixels);
247
+ dashCoverage = roundedDashResolvedCoverage;
248
+ }
249
+ // That smoothstep resolves one dash end at a time, so it stops meaning anything once a
250
+ // whole period fits inside the filter footprint. Start fading only at that boundary;
251
+ // blending resolvable periods would attenuate the solid body of every rounded dash.
252
+ float subPixelBlend = smoothstep(unitLength, 2.0 * unitLength, filterWidth);
253
+ roundedDashSubPixelBlend = subPixelBlend;
254
+ // At sub-pixel scale, preserve the area of the repeated capsule rather than falling
255
+ // back to the rectangular duty cycle. Each scanline contains the solid body plus the
256
+ // two circular cap intrusions, capped when neighboring caps overlap across the gap.
257
+ float boundedSolidLength = min(solidLength, unitLength);
258
+ float effectiveGap = max(unitLength - boundedSolidLength, 0.0);
259
+ float capSpan = 2.0 * sqrt(max(1.0 - vPathPosition.x * vPathPosition.x, 0.0));
260
+ float roundedDutyCycle = clamp(
261
+ (boundedSolidLength + min(effectiveGap, capSpan)) / unitLength,
262
+ 0.0,
263
+ 1.0
264
+ );
265
+ roundedDashDutyCycle = roundedDutyCycle;
266
+ dashCoverage = mix(dashCoverage, roundedDutyCycle, subPixelBlend);
85
267
  }
268
+
269
+ dashCoverage = clamp(dashCoverage, 0.0, 1.0);
270
+ // Fully transparent fragments would still write depth and occlude whatever is behind.
271
+ shouldDiscardDash = shouldDiscardDash || dashCoverage <= 0.0;
272
+ }
273
+ `,
274
+
275
+ 'fs:#main-end': `
276
+ // PathLayer computes analytic-edge derivatives in its fragment body. A discard in
277
+ // #main-start can remove helper invocations and make those derivatives undefined, so all
278
+ // dash-related termination is deferred until the layer has completed that work.
279
+ #ifdef ANTIALIASING
280
+ if (inRoundedDashGap) {
281
+ // The rounded dash cap and PathLayer silhouette are two geometric constraints on the
282
+ // same fragment. Intersect their coverage instead of multiplying two edge ramps; at the
283
+ // cap/body shoulder both ramps are half covered and multiplication would create a dark
284
+ // quarter-covered notch. Keep the sub-pixel duty cycle separable from the path silhouette.
285
+ float pathCoverage = smoothedge(0.0, edgePixels);
286
+ float resolvedCapMultiplier =
287
+ min(pathCoverage, roundedDashResolvedCoverage) / max(pathCoverage, 1e-6);
288
+ dashCoverage = mix(
289
+ resolvedCapMultiplier,
290
+ roundedDashDutyCycle,
291
+ roundedDashSubPixelBlend
292
+ );
293
+ }
294
+ #endif
295
+ if (shouldDiscardDash) {
296
+ discard;
86
297
  }
298
+ fragColor.a *= dashCoverage;
87
299
  `
88
300
  }
89
301
  };
@@ -316,13 +528,42 @@ in float instanceOffsets;
316
528
  vPathPosition.x = (vPathPosition.x + offsetDir) * offsetWidth - offsetDir;
317
529
  vPathPosition.y *= offsetWidth;
318
530
  vPathLength *= offsetWidth;
531
+ #ifdef DASH_ENABLED
532
+ // DECKGL_FILTER_SIZE above widened the stroke by offsetWidth, so these rescalings restore
533
+ // units of the original half-width. The dash block merges ahead of this one.
534
+ if (pathStyle.dashUnits != 0) {
535
+ // Absolute dash units must not inherit the artificial width used to build offset
536
+ // geometry. Relative 'widths' units intentionally remain tied to the original stroke.
537
+ vDashArray *= offsetWidth;
538
+ }
539
+ vPathBounds *= offsetWidth;
540
+ #ifdef HIGH_PRECISION_DASH
541
+ vDashPathLength *= offsetWidth;
542
+ // The dash block reduced the CPU path offset modulo a period expressed in widened-width
543
+ // units. Multiplying that remainder cannot recover the original phase after wrapping, so
544
+ // redo the reduction with both the restored path offset and restored justified period.
545
+ float restoredDashUnitLength = vDashArray.x + vDashArray.y;
546
+ float restoredDashPeriod = pathStyle.dashAlignMode == 0.0
547
+ ? restoredDashUnitLength
548
+ : vDashPathLength /
549
+ max(round(vDashPathLength / max(restoredDashUnitLength, 0.0001)), 1.0);
550
+ vDashOffset = restoredDashPeriod > 0.0
551
+ ? mod(dashOffsetAndLength.x * offsetWidth, restoredDashPeriod)
552
+ : 0.0;
553
+ #else
554
+ vDashOffset *= offsetWidth;
555
+ #endif
556
+ #endif
319
557
  `,
320
- 'fs:#main-start': `
321
- float isInside;
322
- isInside = step(-1.0, vPathPosition.x) * step(vPathPosition.x, 1.0);
323
- if (isInside == 0.0) {
558
+ 'fs:#main-end': `
559
+ #ifndef ANTIALIASING
560
+ // With analytic antialiasing, PathLayer evaluates this boundary using the remapped
561
+ // vPathPosition and retains the complete centered coverage ramp. The hard clip remains for
562
+ // the original non-AA path.
563
+ if (abs(vPathPosition.x) > 1.0) {
324
564
  discard;
325
565
  }
566
+ #endif
326
567
  `
327
568
  }
328
569
  };