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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Plug-and-play functionalities for deck.gl layers",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
- "version": "9.4.0-beta.4",
6
+ "version": "9.4.1",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -42,14 +42,14 @@
42
42
  "prepublishOnly": "npm run build-bundle && npm run build-bundle -- --env=dev"
43
43
  },
44
44
  "dependencies": {
45
- "@luma.gl/shadertools": "^9.4.0-beta.7",
46
- "@luma.gl/webgl": "^9.4.0-beta.7",
45
+ "@luma.gl/shadertools": "^9.4.2",
46
+ "@luma.gl/webgl": "^9.4.2",
47
47
  "@math.gl/core": "^4.1.0"
48
48
  },
49
49
  "peerDependencies": {
50
- "@deck.gl/core": "~9.4.0-beta.3",
51
- "@luma.gl/core": "~9.4.0-beta.7",
52
- "@luma.gl/engine": "~9.4.0-beta.7"
50
+ "@deck.gl/core": "~9.4.0",
51
+ "@luma.gl/core": "~9.4.0",
52
+ "@luma.gl/engine": "~9.4.0"
53
53
  },
54
- "gitHead": "510649703132adbd49cbfa7d25a49363aec03265"
54
+ "gitHead": "7de3aa41176da9d6dcc11562fb5fa8b9d0cb11a4"
55
55
  }
@@ -14,6 +14,7 @@ import {
14
14
  dashShaders,
15
15
  Defines,
16
16
  offsetShaders,
17
+ pathStylePipelineShaders,
17
18
  scatterplotDashShaders,
18
19
  textBackgroundDashShaders
19
20
  } from './shaders.glsl';
@@ -318,8 +319,11 @@ export default class PathStyleExtension extends LayerExtension<ResolvedPathStyle
318
319
  return {modules: [pathStyle]};
319
320
  }
320
321
 
321
- // PathLayer: existing logic
322
+ // PathLayer: one ordered coordinate stage plus capability-specific declarations and setup.
322
323
  let result = {} as {inject: Record<string, string>};
324
+ if (extension.opts.dash || extension.opts.offset) {
325
+ result = mergeShaders(result, pathStylePipelineShaders);
326
+ }
323
327
  const defines: Defines = {};
324
328
  if (extension.opts.dash) {
325
329
  result = mergeShaders(result, dashShaders);
@@ -12,9 +12,9 @@
12
12
  * scalar `getDashOffsets` helper remains available for compatibility but does not back the
13
13
  * managed attribute.
14
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.
15
+ * At the dash injection point, the pre-offset stroke half-width is in common units for a
16
+ * flat path but pixels for a billboarded path, whose conversion also includes
17
+ * `project.focalDistance`.
18
18
  * 3. **Half-widths along the path** — the units of `vPathPosition.y`, tested by the fragment
19
19
  * shader. One unit spans `dashWidthPixels` screen pixels.
20
20
  *
@@ -28,8 +28,8 @@ export type Defines = {
28
28
  */
29
29
  HIGH_PRECISION_DASH?: boolean;
30
30
  /**
31
- * Set whenever the dash shaders are injected, so that the offset shaders can adjust dash
32
- * varyings only when they actually exist.
31
+ * Set whenever the dash shaders are injected. It guards PathLayer's additional arclength
32
+ * varyings and the dash stage of the shared PathStyle pipeline.
33
33
  */
34
34
  DASH_ENABLED?: boolean;
35
35
  /**
@@ -38,53 +38,47 @@ export type Defines = {
38
38
  PATH_STYLE_OFFSET?: boolean;
39
39
  };
40
40
 
41
- export const dashShaders = {
41
+ /**
42
+ * Ordered PathStyle coordinate pipeline shared by the dash and offset capabilities.
43
+ *
44
+ * Offset rendering temporarily widens PathLayer's geometry so the shifted stroke fits inside the
45
+ * generated mesh. This stage first maps PathLayer's varyings back to the pre-offset stroke, then
46
+ * derives dash coordinates from that width. Keeping both operations in one injection
47
+ * avoids making their result depend on the order in which the capability shaders are merged.
48
+ * It also owns their deferred fragment rejection, after PathLayer has evaluated derivatives.
49
+ */
50
+ export const pathStylePipelineShaders = {
42
51
  inject: {
43
- 'vs:#decl': `
44
- in vec2 instanceDashArrays;
45
- #ifdef HIGH_PRECISION_DASH
46
- // [distance from the start of the path, total length of the path], in common space.
47
- in vec2 instanceDashOffsets;
52
+ 'vs:#main-end': `
53
+ #ifdef PATH_STYLE_OFFSET
54
+ float offsetWidth = abs(instanceOffsets * 2.0) + 1.0;
55
+ float offsetDir = sign(instanceOffsets);
56
+ vPathPosition.x = (vPathPosition.x + offsetDir) * offsetWidth - offsetDir;
57
+ vPathPosition.y *= offsetWidth;
58
+ vPathLength *= offsetWidth;
59
+ #ifdef DASH_ENABLED
60
+ vPathBounds *= offsetWidth;
61
+ #endif
48
62
  #endif
49
- out vec2 vDashArray;
50
- out float vDashOffset;
51
- out float vDashPathLength;
52
63
 
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;
61
- `,
64
+ #ifdef DASH_ENABLED
65
+ // DECKGL_FILTER_SIZE may have widened width for offset geometry. Dash coordinates are measured
66
+ // against the pre-offset stroke, so recover its half-width before converting dash units.
67
+ float strokeHalfWidth = width.x;
68
+ #ifdef PATH_STYLE_OFFSET
69
+ strokeHalfWidth /= offsetWidth;
70
+ #endif
62
71
 
63
- 'vs:#main-end': `
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.
72
+ // How many screen pixels one unit of vPathPosition.y covers. Flat paths extrude in common space;
73
+ // billboarded paths extrude in clip space, whose conversion also includes focalDistance.
76
74
  float dashWidthPixels = path.billboard
77
- ? width.x * project.focalDistance
78
- : width.x * project.scale;
75
+ ? strokeHalfWidth * project.focalDistance
76
+ : strokeHalfWidth * project.scale;
77
+ float strokeHalfWidthPixels = path.billboard
78
+ ? strokeHalfWidth
79
+ : strokeHalfWidth * project.scale;
79
80
 
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.
81
+ // Convert every public dash unit to the half-width coordinate used by the fragment shader.
88
82
  // Keep the cases in sync with DASH_UNITS in path-style-extension.ts.
89
83
  float dashUnitPixels = strokeHalfWidthPixels;
90
84
  if (pathStyle.dashUnits == 1) {
@@ -116,6 +110,68 @@ vDashOffset = dashPeriod > 0.0 ? mod(dashOffsetAndLength.x, dashPeriod) : 0.0;
116
110
  vDashOffset = 0.0;
117
111
  vDashPathLength = 0.0;
118
112
  #endif
113
+ #endif
114
+ `,
115
+ 'fs:#main-end': `
116
+ #ifdef DASH_ENABLED
117
+ // PathLayer computes analytic-edge derivatives in its fragment body. A discard in
118
+ // #main-start can remove helper invocations and make those derivatives undefined, so all
119
+ // dash-related termination is deferred until the layer has completed that work.
120
+ #ifdef ANTIALIASING
121
+ if (inRoundedDashGap) {
122
+ // The rounded dash cap and PathLayer silhouette are two geometric constraints on the
123
+ // same fragment. Intersect their coverage instead of multiplying two edge ramps; at the
124
+ // cap/body shoulder both ramps are half covered and multiplication would create a dark
125
+ // quarter-covered notch. Keep the sub-pixel duty cycle separable from the path silhouette.
126
+ float pathCoverage = smoothedge(0.0, edgePixels);
127
+ float resolvedCapMultiplier =
128
+ min(pathCoverage, roundedDashResolvedCoverage) / max(pathCoverage, 1e-6);
129
+ dashCoverage = mix(
130
+ resolvedCapMultiplier,
131
+ roundedDashDutyCycle,
132
+ roundedDashSubPixelBlend
133
+ );
134
+ }
135
+ #endif
136
+ if (shouldDiscardDash) {
137
+ discard;
138
+ }
139
+ fragColor.a *= dashCoverage;
140
+ #endif
141
+ #ifdef PATH_STYLE_OFFSET
142
+ #ifndef ANTIALIASING
143
+ // With analytic antialiasing, PathLayer evaluates this boundary using the remapped
144
+ // vPathPosition and retains the complete centered coverage ramp. The hard clip remains for
145
+ // the original non-AA path.
146
+ if (abs(vPathPosition.x) > 1.0) {
147
+ discard;
148
+ }
149
+ #endif
150
+ #endif
151
+ `
152
+ }
153
+ };
154
+
155
+ export const dashShaders = {
156
+ inject: {
157
+ 'vs:#decl': `
158
+ in vec2 instanceDashArrays;
159
+ #ifdef HIGH_PRECISION_DASH
160
+ // [distance from the start of the path, total length of the path], in common space.
161
+ in vec2 instanceDashOffsets;
162
+ #endif
163
+ out vec2 vDashArray;
164
+ out float vDashOffset;
165
+ out float vDashPathLength;
166
+
167
+ // Also declared in the fragment stage. The vertex stage needs dashAlignMode so that it can
168
+ // reduce the dash phase modulo the same period the fragment stage will test against, and
169
+ // dashUnits to scale the dash array into that period's units.
170
+ layout(std140) uniform pathStyleUniforms {
171
+ float dashAlignMode;
172
+ bool dashGapPickable;
173
+ highp int dashUnits;
174
+ } pathStyle;
119
175
  `,
120
176
 
121
177
  'fs:#decl': `
@@ -270,32 +326,6 @@ float dashPatternCoverage(
270
326
  // Fully transparent fragments would still write depth and occlude whatever is behind.
271
327
  shouldDiscardDash = shouldDiscardDash || dashCoverage <= 0.0;
272
328
  }
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;
297
- }
298
- fragColor.a *= dashCoverage;
299
329
  `
300
330
  }
301
331
  };
@@ -521,49 +551,6 @@ in float instanceOffsets;
521
551
  'vs:DECKGL_FILTER_SIZE': `
522
552
  float offsetWidth = abs(instanceOffsets * 2.0) + 1.0;
523
553
  size *= offsetWidth;
524
- `,
525
- 'vs:#main-end': `
526
- float offsetWidth = abs(instanceOffsets * 2.0) + 1.0;
527
- float offsetDir = sign(instanceOffsets);
528
- vPathPosition.x = (vPathPosition.x + offsetDir) * offsetWidth - offsetDir;
529
- vPathPosition.y *= offsetWidth;
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
557
- `,
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) {
564
- discard;
565
- }
566
- #endif
567
554
  `
568
555
  }
569
556
  };
@@ -2,8 +2,15 @@
2
2
  // SPDX-License-Identifier: MIT
3
3
  // Copyright (c) vis.gl contributors
4
4
 
5
- import {WebMercatorViewport, OrthographicViewport} from '@deck.gl/core';
6
- import type {Layer, Viewport} from '@deck.gl/core';
5
+ import {WebMercatorViewport, OrthographicViewport, _GlobeViewport} from '@deck.gl/core';
6
+ import type {CoordinateSystem, Layer, ProjectUniforms, Viewport} from '@deck.gl/core';
7
+ import {vec3, vec4} from '@math.gl/core';
8
+ import type {NumericArray} from '@math.gl/core';
9
+
10
+ /** TILE_SIZE in the project shader module */
11
+ const TILE_SIZE = 512;
12
+ /** GLOBE_RADIUS / EARTH_RADIUS in the project shader module */
13
+ const GLOBE_METERS_TO_COMMON = 256 / 6370972;
7
14
 
8
15
  /** Bounds in CARTESIAN coordinates */
9
16
  export type Bounds = [minX: number, minY: number, maxX: number, maxY: number];
@@ -28,6 +35,127 @@ export function getMercatorReferenceViewport(viewport: Viewport): Viewport {
28
35
  return viewport.isGeospatial ? MERCATOR_REFERENCE_VIEWPORT : viewport;
29
36
  }
30
37
 
38
+ // Flat common space: Mercator (geospatial) or cartesian (OrthographicView). CPU counterparts of
39
+ // project_common_position_to_flat(). Adding a non-flat projection: update the flatten branch in
40
+ // core project.glsl.ts / project.wgsl.ts, isFlatViewport(), and test/render/view-presets.ts.
41
+
42
+ /** False only for GlobeViewport, whose common space is a sphere. The single globe check in this module. */
43
+ export function isFlatViewport(viewport: Viewport): boolean {
44
+ return !(viewport instanceof _GlobeViewport);
45
+ }
46
+
47
+ /** Per-call overrides of the layer's coordinateSystem, coordinateOrigin and modelMatrix */
48
+ export type FlatProjectOptions = {
49
+ fromCoordinateSystem?: CoordinateSystem;
50
+ fromCoordinateOrigin?: [number, number, number];
51
+ modelMatrix?: NumericArray | null;
52
+ };
53
+
54
+ /**
55
+ * CPU counterpart of `project_common_position_to_flat(project_position(position))`, relative to
56
+ * `getFlatCommonOrigin()`. Always pair with the GPU function: a raw `layer.projectPosition()`
57
+ * disagrees with it under globe or auto-offset.
58
+ */
59
+ export function projectToFlatCommon(
60
+ layer: Layer,
61
+ position: number[],
62
+ opts: FlatProjectOptions = {}
63
+ ): [number, number, number] {
64
+ // Same resolution as Layer.projectPosition so multi-view setups stay consistent
65
+ const viewport = getLayerViewport(layer);
66
+ if (isFlatViewport(viewport)) {
67
+ return layer.projectPosition(position, opts);
68
+ }
69
+ if (getSourceCoordinateSystem(layer, opts) === 'meter-offsets') {
70
+ return projectGlobeMeterOffsetsToFlatCommon(layer, viewport, position, opts);
71
+ }
72
+ return layer.projectPosition(position, {
73
+ ...opts,
74
+ viewport: getMercatorReferenceViewport(viewport),
75
+ autoOffset: false
76
+ });
77
+ }
78
+
79
+ /**
80
+ * Mirrors the shader's GLOBE + METER_OFFSETS path: a linear offset in the tangent plane at the
81
+ * origin. Do not go through lng/lat; the error grows with the square of the offset (~200 m at 300 km).
82
+ */
83
+ function projectGlobeMeterOffsetsToFlatCommon(
84
+ layer: Layer,
85
+ viewport: Viewport,
86
+ position: number[],
87
+ opts: FlatProjectOptions
88
+ ): [number, number, number] {
89
+ const origin = opts.fromCoordinateOrigin ?? layer.props.coordinateOrigin;
90
+ const modelMatrix = opts.modelMatrix === undefined ? layer.props.modelMatrix : opts.modelMatrix;
91
+ let [x, y, z = 0] = position;
92
+ if (modelMatrix) {
93
+ [x, y, z] = vec4.transformMat4([] as number[], [x, y, z, 1], modelMatrix);
94
+ }
95
+ // project.commonOrigin: sphere position of the coordinate origin
96
+ const originCommon = viewport.projectPosition(origin);
97
+ // project_get_orientation_matrix(commonOrigin)
98
+ const uz = vec3.normalize([] as number[], originCommon);
99
+ const ux = Math.abs(uz[2]) === 1 ? [1, 0, 0] : vec3.normalize([] as number[], [uz[1], -uz[0], 0]);
100
+ const uy = vec3.cross([] as number[], uz, ux);
101
+ // enuMatrix * vec3(-x, -y, z) * metersToCommon
102
+ const displaced = [0, 1, 2].map(
103
+ i => originCommon[i] + (ux[i] * -x + uy[i] * -y + uz[i] * z) * GLOBE_METERS_TO_COMMON
104
+ );
105
+ // project_globe_to_mercator_
106
+ const lngLatZ = viewport.unprojectPosition(displaced);
107
+ return MERCATOR_REFERENCE_VIEWPORT.projectPosition(lngLatZ);
108
+ }
109
+
110
+ /**
111
+ * Projects `[minX, minY, maxX, maxY]` into flat common bounds. Lng/lat bounds with
112
+ * `minX - maxX > 180` take the shorter arc across the antimeridian, so `maxX` may exceed TILE_SIZE;
113
+ * test against them with `project_common_position_to_flat_wrapped()` about the bounds centre.
114
+ * Otherwise edges are re-ordered, so `[right, top, left, bottom]` input still works.
115
+ */
116
+ export function projectBoundsToFlatCommon(
117
+ layer: Layer,
118
+ bounds: Readonly<[number, number, number, number]>,
119
+ opts?: FlatProjectOptions
120
+ ): Bounds {
121
+ const a = projectToFlatCommon(layer, [bounds[0], bounds[1], 0], opts);
122
+ const b = projectToFlatCommon(layer, [bounds[2], bounds[3], 0], opts);
123
+ if (
124
+ bounds[0] - bounds[2] > 180 &&
125
+ getLayerViewport(layer).isGeospatial &&
126
+ getSourceCoordinateSystem(layer, opts) === 'lnglat'
127
+ ) {
128
+ b[0] += TILE_SIZE;
129
+ }
130
+ return [Math.min(a[0], b[0]), Math.min(a[1], b[1]), Math.max(a[0], b[0]), Math.max(a[1], b[1])];
131
+ }
132
+
133
+ function getLayerViewport(layer: Layer): Viewport {
134
+ return layer.internalState?.viewport || layer.context.viewport;
135
+ }
136
+
137
+ /** The coordinate system `position` is expressed in, with `default` resolved */
138
+ function getSourceCoordinateSystem(layer: Layer, opts: FlatProjectOptions = {}): CoordinateSystem {
139
+ const coordinateSystem = opts.fromCoordinateSystem ?? layer.props.coordinateSystem;
140
+ if (coordinateSystem === 'default') {
141
+ return getLayerViewport(layer).isGeospatial ? 'lnglat' : 'cartesian';
142
+ }
143
+ return coordinateSystem;
144
+ }
145
+
146
+ /**
147
+ * Origin of `project_common_position_to_flat()` results: `project.commonOrigin` on flat viewports,
148
+ * `[0, 0]` under globe, where commonOrigin is a sphere position and must never be re-added.
149
+ */
150
+ export function getFlatCommonOrigin(
151
+ projectUniforms: ProjectUniforms,
152
+ viewport: Viewport
153
+ ): [number, number] {
154
+ return isFlatViewport(viewport)
155
+ ? [projectUniforms.commonOrigin[0], projectUniforms.commonOrigin[1]]
156
+ : [0, 0];
157
+ }
158
+
31
159
  /*
32
160
  * Compute the union of bounds from multiple layers
33
161
  * Returns bounds in CARTESIAN coordinates
@@ -181,6 +309,12 @@ export function getRenderBounds(
181
309
  return [0, 0, 1, 1];
182
310
  }
183
311
 
312
+ // layerBounds are flat (Mercator) but getViewportBounds() would return sphere coordinates for a
313
+ // non-flat viewport; the two cannot be intersected. Render the full layer extent instead.
314
+ if (!isFlatViewport(viewport)) {
315
+ return layerBounds;
316
+ }
317
+
184
318
  const viewportBounds = getViewportBounds(viewport, zRange);
185
319
  // Expand viewport bounds by 2X. Heurestically chosen to avoid masking
186
320
  // errors when mask is partially out of view