@tangram.gl/basemap-layers 0.0.0-stage → 0.0.0

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 (105) hide show
  1. package/README.md +34 -2
  2. package/THIRD-PARTY-NOTICES.md +205 -0
  3. package/dist/geojson-worker.js +2160 -0
  4. package/dist/geojson-worker.js.map +6 -0
  5. package/dist/index.cjs +11388 -0
  6. package/dist/index.cjs.map +6 -0
  7. package/dist/index.js +11369 -0
  8. package/dist/index.js.map +6 -0
  9. package/dist/map-style.cjs +445 -0
  10. package/dist/map-style.cjs.map +6 -0
  11. package/dist/map-style.js +431 -0
  12. package/dist/map-style.js.map +6 -0
  13. package/dist/types/arabic-shaping.d.ts +7 -0
  14. package/dist/types/atmosphere-layer.d.ts +10 -0
  15. package/dist/types/basemap-layer.d.ts +150 -0
  16. package/dist/types/bidi.d.ts +8 -0
  17. package/dist/types/circle-layer.d.ts +49 -0
  18. package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
  19. package/dist/types/fill-outline-extension.d.ts +22 -0
  20. package/dist/types/fill-pattern.d.ts +99 -0
  21. package/dist/types/fill-style-extensions.d.ts +100 -0
  22. package/dist/types/geojson-source.d.ts +29 -0
  23. package/dist/types/geojson-tiler.d.ts +48 -0
  24. package/dist/types/geojson-worker.d.ts +40 -0
  25. package/dist/types/globe-horizon-extension.d.ts +61 -0
  26. package/dist/types/globe-layers.d.ts +239 -0
  27. package/dist/types/globe-line-angle-extension.d.ts +37 -0
  28. package/dist/types/glyph-font-renderer.d.ts +37 -0
  29. package/dist/types/glyph-pbf.d.ts +37 -0
  30. package/dist/types/glyph-store.d.ts +103 -0
  31. package/dist/types/grid-index.d.ts +47 -0
  32. package/dist/types/hillshade-layer.d.ts +154 -0
  33. package/dist/types/index.d.ts +9 -0
  34. package/dist/types/is-globe-viewport.d.ts +2 -0
  35. package/dist/types/label-font-renderer.d.ts +18 -0
  36. package/dist/types/label-placement.d.ts +247 -0
  37. package/dist/types/label-text-layer.d.ts +231 -0
  38. package/dist/types/line-blur-extension.d.ts +73 -0
  39. package/dist/types/line-glyphs.d.ts +60 -0
  40. package/dist/types/line-placement.d.ts +93 -0
  41. package/dist/types/map-style-loader.d.ts +48 -0
  42. package/dist/types/map-style-schema.d.ts +72 -0
  43. package/dist/types/map-style.d.ts +6 -0
  44. package/dist/types/mapbox-style.d.ts +40 -0
  45. package/dist/types/mvt-label-layer.d.ts +923 -0
  46. package/dist/types/polylabel.d.ts +17 -0
  47. package/dist/types/raster-color-extension.d.ts +146 -0
  48. package/dist/types/shared-texture.d.ts +34 -0
  49. package/dist/types/sort-key.d.ts +10 -0
  50. package/dist/types/sprite.d.ts +82 -0
  51. package/dist/types/style-accessor.d.ts +45 -0
  52. package/dist/types/style-expression.d.ts +83 -0
  53. package/dist/types/style-resolver.d.ts +94 -0
  54. package/dist/types/text-font.d.ts +36 -0
  55. package/dist/types/text-halo-blur-extension.d.ts +55 -0
  56. package/dist/types/text-layout.d.ts +71 -0
  57. package/dist/types/util.d.ts +22 -0
  58. package/dist/types/zoom-paint-extension.d.ts +72 -0
  59. package/package.json +80 -3
  60. package/src/arabic-shaping.ts +366 -0
  61. package/src/atmosphere-layer.ts +19 -0
  62. package/src/basemap-layer.ts +533 -0
  63. package/src/bidi.ts +199 -0
  64. package/src/circle-layer.ts +191 -0
  65. package/src/fill-extrusion-lighting-extension.ts +206 -0
  66. package/src/fill-outline-extension.ts +70 -0
  67. package/src/fill-pattern.ts +400 -0
  68. package/src/fill-style-extensions.ts +177 -0
  69. package/src/geojson-source.ts +318 -0
  70. package/src/geojson-tiler.ts +185 -0
  71. package/src/geojson-worker.ts +98 -0
  72. package/src/globe-horizon-extension.ts +64 -0
  73. package/src/globe-layers.ts +3487 -0
  74. package/src/globe-line-angle-extension.ts +131 -0
  75. package/src/glyph-font-renderer.ts +173 -0
  76. package/src/glyph-pbf.ts +164 -0
  77. package/src/glyph-store.ts +326 -0
  78. package/src/grid-index.ts +177 -0
  79. package/src/hillshade-layer.ts +775 -0
  80. package/src/index.ts +18 -0
  81. package/src/is-globe-viewport.ts +24 -0
  82. package/src/label-font-renderer.ts +159 -0
  83. package/src/label-placement.ts +727 -0
  84. package/src/label-text-layer.ts +913 -0
  85. package/src/line-blur-extension.ts +237 -0
  86. package/src/line-glyphs.ts +364 -0
  87. package/src/line-placement.ts +446 -0
  88. package/src/map-style-loader.ts +56 -0
  89. package/src/map-style-schema.ts +83 -0
  90. package/src/map-style.ts +23 -0
  91. package/src/mapbox-style.ts +168 -0
  92. package/src/mvt-label-layer.ts +3744 -0
  93. package/src/polylabel.ts +312 -0
  94. package/src/raster-color-extension.ts +245 -0
  95. package/src/shared-texture.ts +153 -0
  96. package/src/sort-key.ts +63 -0
  97. package/src/sprite.ts +244 -0
  98. package/src/style-accessor.ts +95 -0
  99. package/src/style-expression.ts +327 -0
  100. package/src/style-resolver.ts +301 -0
  101. package/src/text-font.ts +150 -0
  102. package/src/text-halo-blur-extension.ts +115 -0
  103. package/src/text-layout.ts +441 -0
  104. package/src/util.ts +47 -0
  105. package/src/zoom-paint-extension.ts +207 -0
package/src/bidi.ts ADDED
@@ -0,0 +1,199 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT AND BSD-3-Clause
7
+ // Copyright (c) vis.gl contributors
8
+ // Copyright (c) 2023, MapLibre contributors
9
+
10
+ /**
11
+ * @license
12
+ * Portions of this file are adapted from MapLibre GL JS v6.13.0, src/symbol/bidi.ts,
13
+ * under this license:
14
+ *
15
+ * Copyright (c) 2023, MapLibre contributors
16
+ *
17
+ * All rights reserved.
18
+ *
19
+ * Redistribution and use in source and binary forms, with or without modification,
20
+ * are permitted provided that the following conditions are met:
21
+ *
22
+ * * Redistributions of source code must retain the above copyright notice,
23
+ * this list of conditions and the following disclaimer.
24
+ * * Redistributions in binary form must reproduce the above copyright notice,
25
+ * this list of conditions and the following disclaimer in the documentation
26
+ * and/or other materials provided with the distribution.
27
+ * * Neither the name of MapLibre GL JS nor the names of its contributors
28
+ * may be used to endorse or promote products derived from this software
29
+ * without specific prior written permission.
30
+ *
31
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
32
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
33
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
34
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
36
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
37
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
38
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
39
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
40
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
41
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42
+ */
43
+
44
+ // Reordering of bidirectional text as MapLibre GL JS 6.13 does it without a right-to-left text
45
+ // plugin, on the same Unicode Bidirectional Algorithm implementation: the bidi-js package (MIT),
46
+ // a dependency, which holds its own Unicode data.
47
+ import bidiFactory, {type Bidi, type BidiCharTypeName} from 'bidi-js';
48
+
49
+ let bidiInstance: Bidi | null = null;
50
+
51
+ /** bidi-js, built on first use: it unpacks its Unicode data when built. */
52
+ function getBidi(): Bidi {
53
+ bidiInstance ||= bidiFactory();
54
+ return bidiInstance;
55
+ }
56
+
57
+ /** Characters that steer the bidirectional algorithm, which no font draws: dropped once used. */
58
+ const BIDI_CONTROLS = /[؜‎‏‪-‮⁦-⁩]/;
59
+
60
+ /** Character types rule L1 resets to the paragraph's direction at the end of a line. */
61
+ const NEUTRAL_AT_END_OF_LINE = new Set<BidiCharTypeName>(['WS', 'FSI', 'LRI', 'RLI', 'PDI']);
62
+
63
+ /** Character types rule L1 resets wherever they appear, with the whitespace before them. */
64
+ const ALWAYS_RESET = new Set<BidiCharTypeName>(['S', 'B']);
65
+
66
+ /** Character types that make text need reordering: right-to-left letters and overrides. */
67
+ const RIGHT_TO_LEFT = new Set<BidiCharTypeName>(['R', 'AL', 'RLE', 'RLO', 'RLI']);
68
+
69
+ /** One grapheme cluster of a line, with its embedding level. */
70
+ type Cluster = {text: string; level: number};
71
+
72
+ let graphemeSegmenter: {segment(text: string): Iterable<{segment: string}>} | null | undefined;
73
+
74
+ /**
75
+ * Splits text into grapheme clusters, or code points where `Intl.Segmenter` is missing, so that a
76
+ * letter and the marks written on it move together when a right-to-left run is reversed (rule L3).
77
+ */
78
+ function toGraphemes(text: string): string[] {
79
+ if (graphemeSegmenter === undefined) {
80
+ const Segmenter = (Intl as any)?.Segmenter;
81
+ graphemeSegmenter = Segmenter ? new Segmenter(undefined, {granularity: 'grapheme'}) : null;
82
+ }
83
+ return graphemeSegmenter
84
+ ? Array.from(graphemeSegmenter.segment(text), ({segment}) => segment)
85
+ : Array.from(text);
86
+ }
87
+
88
+ /** Whether `text` has right-to-left characters, so that it needs {@link processBidirectionalText}. */
89
+ export function containsRightToLeftText(text: string): boolean {
90
+ // Latin text, the common case, skips the lookups.
91
+ if (!/[^\u0000-ɏ]/.test(text)) {
92
+ return false;
93
+ }
94
+ const bidi = getBidi();
95
+ for (const character of text) {
96
+ if (RIGHT_TO_LEFT.has(bidi.getBidiCharTypeName(character))) {
97
+ return true;
98
+ }
99
+ }
100
+ return false;
101
+ }
102
+
103
+ /** Rule L1: whitespace at the end of a line, and separators, take the paragraph's direction. */
104
+ function resetTrailingNeutrals(clusters: Cluster[], paragraphLevel: number): void {
105
+ const bidi = getBidi();
106
+ let trailing = true;
107
+ for (let i = clusters.length - 1; i >= 0; i--) {
108
+ const type = bidi.getBidiCharTypeName(clusters[i].text[0]);
109
+ if (ALWAYS_RESET.has(type)) {
110
+ clusters[i].level = paragraphLevel;
111
+ trailing = true;
112
+ } else if (trailing && NEUTRAL_AT_END_OF_LINE.has(type)) {
113
+ clusters[i].level = paragraphLevel;
114
+ } else {
115
+ trailing = false;
116
+ }
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Rule L2: from the deepest level down to the shallowest odd one, reverses every run at that level
122
+ * or deeper.
123
+ */
124
+ function reorder(clusters: Cluster[], paragraphLevel: number): Cluster[] {
125
+ let highest = paragraphLevel;
126
+ let lowestOdd = Infinity;
127
+ for (const {level} of clusters) {
128
+ if (level > highest) highest = level;
129
+ if ((level | 1) < lowestOdd) lowestOdd = level | 1;
130
+ }
131
+ const ordered = clusters.slice();
132
+ for (let level = highest; level >= lowestOdd; level--) {
133
+ for (let start = 0; start < ordered.length; start++) {
134
+ if (ordered[start].level < level) continue;
135
+ let end = start;
136
+ while (end + 1 < ordered.length && ordered[end + 1].level >= level) end++;
137
+ for (let i = start, j = end; i < j; i++, j--) {
138
+ [ordered[i], ordered[j]] = [ordered[j], ordered[i]];
139
+ }
140
+ start = end;
141
+ }
142
+ }
143
+ return ordered;
144
+ }
145
+
146
+ /** Puts the code units `[start, end)` of `text` into visual order, dropping bidi controls. */
147
+ function reorderLine(
148
+ text: string,
149
+ levels: Uint8Array,
150
+ paragraphLevel: number,
151
+ start: number,
152
+ end: number
153
+ ): string {
154
+ const bidi = getBidi();
155
+ const clusters: Cluster[] = [];
156
+ let index = start;
157
+ for (const cluster of toGraphemes(text.slice(start, end))) {
158
+ clusters.push({text: cluster, level: levels[index]});
159
+ index += cluster.length;
160
+ }
161
+ resetTrailingNeutrals(clusters, paragraphLevel);
162
+ let reordered = '';
163
+ for (const cluster of reorder(clusters, paragraphLevel)) {
164
+ if (BIDI_CONTROLS.test(cluster.text)) continue;
165
+ // A bracket read right to left is drawn as its mirror image.
166
+ reordered +=
167
+ cluster.level % 2 ? (bidi.getMirroredCharacter(cluster.text) ?? cluster.text) : cluster.text;
168
+ }
169
+ return reordered;
170
+ }
171
+
172
+ /**
173
+ * Puts text into the order it is drawn, broken into lines, as MapLibre GL JS does: the embedding
174
+ * levels are found for the whole label, then each line is reordered on its own. A label is broken
175
+ * at `lineBreakPoints` (UTF-16 code unit offsets) and wherever a paragraph ends.
176
+ */
177
+ export function processBidirectionalText(text: string, lineBreakPoints: number[]): string[] {
178
+ const {levels, paragraphs} = getBidi().getEmbeddingLevels(text);
179
+ const inside = lineBreakPoints
180
+ .concat(paragraphs.map(paragraph => paragraph.start))
181
+ .filter(point => point > 0 && point < text.length);
182
+ const bounds = [...new Set([0, ...inside, text.length])].sort((a, b) => a - b);
183
+ const lines: string[] = [];
184
+ // The bounds and the paragraphs are both in text order, so one pass finds each line's paragraph
185
+ // (tile text is untrusted: a label of many short lines must not cost lines times paragraphs).
186
+ let paragraph = 0;
187
+ for (let i = 0; i + 1 < bounds.length; i++) {
188
+ const start = bounds[i];
189
+ while (paragraph < paragraphs.length && paragraphs[paragraph].end < start) {
190
+ paragraph++;
191
+ }
192
+ const paragraphLevel =
193
+ paragraph < paragraphs.length && paragraphs[paragraph].start <= start
194
+ ? paragraphs[paragraph].level
195
+ : 0;
196
+ lines.push(reorderLine(text, levels, paragraphLevel, start, bounds[i + 1]));
197
+ }
198
+ return lines;
199
+ }
@@ -0,0 +1,191 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ import {LayerExtension} from '@deck.gl/core';
10
+ import type {Layer} from '@deck.gl/core';
11
+ import {ScatterplotLayer} from '@deck.gl/layers';
12
+ import {sortFeaturesByKey} from './sort-key';
13
+ import {getStyleAccessor, withOpacity} from './style-accessor';
14
+ import {getStylePropertyDefault} from './style-expression';
15
+ import type {ResolvedBasemapStyleLayer as BasemapStyleLayer} from './style-resolver';
16
+
17
+ const DEFAULT_RADIUS = getStylePropertyDefault('circle-radius') as number;
18
+ const DEFAULT_COLOR = getStylePropertyDefault('circle-color') as number[];
19
+ const DEFAULT_OPACITY = getStylePropertyDefault('circle-opacity') as number;
20
+ const DEFAULT_STROKE_WIDTH = getStylePropertyDefault('circle-stroke-width') as number;
21
+ const DEFAULT_STROKE_COLOR = getStylePropertyDefault('circle-stroke-color') as number[];
22
+ const DEFAULT_STROKE_OPACITY = getStylePropertyDefault('circle-stroke-opacity') as number;
23
+ const DEFAULT_BLUR = getStylePropertyDefault('circle-blur') as number;
24
+
25
+ /**
26
+ * Adds `getBlur` to a `ScatterplotLayer`: per circle, the outer fraction of the circle (stroke
27
+ * included) that fades out, as MapLibre GL JS draws `circle-blur`. WebGL only; on WebGPU circles
28
+ * draw without blur.
29
+ */
30
+ export class CircleBlurExtension extends LayerExtension {
31
+ static extensionName = 'CircleBlurExtension';
32
+ static defaultProps = {getBlur: {type: 'accessor', value: 0}};
33
+
34
+ initializeState(this: Layer) {
35
+ if (this.context.device.type === 'webgpu') {
36
+ return;
37
+ }
38
+ this.getAttributeManager()?.addInstanced({
39
+ instanceCircleBlur: {size: 1, accessor: 'getBlur', defaultValue: 0}
40
+ });
41
+ }
42
+
43
+ getShaders(this: Layer) {
44
+ if (this.context.device.type === 'webgpu') {
45
+ return {};
46
+ }
47
+ // deck.gl appends these to the layer's and other extensions' code for the same hooks.
48
+ return {
49
+ inject: {
50
+ 'vs:#decl': 'in float instanceCircleBlur;\nout float circleBlur_blur;',
51
+ 'vs:#main-end': 'circleBlur_blur = instanceCircleBlur;',
52
+ 'fs:#decl': 'in float circleBlur_blur;',
53
+ 'fs:DECKGL_FILTER_COLOR': `
54
+ if (circleBlur_blur > 0.0) {
55
+ color.a *= 1.0 - smoothstep(1.0 - circleBlur_blur, 1.0, length(geometry.uv));
56
+ }
57
+ `
58
+ }
59
+ };
60
+ }
61
+ }
62
+
63
+ /** Shared by every blurred circle sublayer, so deck.gl does not see their extensions change. */
64
+ const CIRCLE_BLUR_EXTENSION = new CircleBlurExtension();
65
+
66
+ const pointFeatureCache = new WeakMap<any[], any[]>();
67
+
68
+ function getPointFeatures(features: any[]): any[] {
69
+ const cached = pointFeatureCache.get(features);
70
+ if (cached) {
71
+ return cached;
72
+ }
73
+ const points = features.every(feature => feature.geometry?.type === 'Point')
74
+ ? features
75
+ : features.flatMap(feature => {
76
+ if (feature.geometry?.type === 'Point') {
77
+ return [feature];
78
+ }
79
+ if (feature.geometry?.type === 'MultiPoint') {
80
+ return feature.geometry.coordinates.map((position: number[]) => ({...feature, position}));
81
+ }
82
+ return [];
83
+ });
84
+ pointFeatureCache.set(features, points);
85
+ return points;
86
+ }
87
+
88
+ /**
89
+ * Draws a `circle` style layer's points (and each point of a multipoint) with MapLibre's circle
90
+ * paint: the stroke is drawn outside `circle-radius`. Under pitch, circles shrink with distance,
91
+ * as with the default `circle-pitch-scale` of `map`; `viewport` is not applied.
92
+ * `circle-translate-anchor` follows `circle-pitch-alignment`: the offset is screen-aligned when
93
+ * circles face the viewer and map-aligned when they lie on the map.
94
+ */
95
+ export function createCircleSubLayer({
96
+ baseProps,
97
+ id,
98
+ styleLayer,
99
+ features,
100
+ zoom,
101
+ globe,
102
+ parameters
103
+ }: {
104
+ baseProps: any;
105
+ id: string;
106
+ styleLayer: BasemapStyleLayer;
107
+ features: any[];
108
+ zoom: number;
109
+ /**
110
+ * On a globe, circles always face the viewer: `circle-pitch-alignment: map` is not verified
111
+ * there.
112
+ */
113
+ globe: boolean;
114
+ parameters: any;
115
+ }) {
116
+ const points = getPointFeatures(features);
117
+ if (points.length === 0) {
118
+ return null;
119
+ }
120
+ const radius = getStyleAccessor(
121
+ styleLayer,
122
+ ['circle-radius', 'circle-stroke-width'],
123
+ zoom,
124
+ ([value, width]) =>
125
+ Math.max(0, Number(value ?? DEFAULT_RADIUS)) +
126
+ Math.max(0, Number(width ?? DEFAULT_STROKE_WIDTH)) / 2
127
+ );
128
+ const lineWidth = getStyleAccessor(styleLayer, ['circle-stroke-width'], zoom, ([width]) =>
129
+ Math.max(0, Number(width ?? DEFAULT_STROKE_WIDTH))
130
+ );
131
+ const fillColor = getStyleAccessor(
132
+ styleLayer,
133
+ ['circle-color', 'circle-opacity'],
134
+ zoom,
135
+ ([color, opacity]) => withOpacity(color ?? DEFAULT_COLOR, opacity ?? DEFAULT_OPACITY)
136
+ );
137
+ const lineColor = getStyleAccessor(
138
+ styleLayer,
139
+ ['circle-stroke-color', 'circle-stroke-opacity'],
140
+ zoom,
141
+ ([color, opacity]) =>
142
+ withOpacity(color ?? DEFAULT_STROKE_COLOR, opacity ?? DEFAULT_STROKE_OPACITY)
143
+ );
144
+ const blur = getStyleAccessor(styleLayer, ['circle-blur'], zoom, ([value]) =>
145
+ Math.min(1, Math.max(0, Number(value ?? DEFAULT_BLUR) || 0))
146
+ );
147
+ const pixelOffset = getStyleAccessor<[number, number]>(
148
+ styleLayer,
149
+ ['circle-translate'],
150
+ zoom,
151
+ ([value]) => {
152
+ const [x, y] = value ?? [0, 0];
153
+ // Scatterplot adds offsets in clip space (billboard) or common space (map), both y-up.
154
+ return [x, y === 0 ? 0 : -y];
155
+ }
156
+ );
157
+ const alignment = getStyleAccessor(styleLayer, ['circle-pitch-alignment'], zoom, ([v]) => v);
158
+
159
+ const hasBlur = styleLayer.paint?.['circle-blur'] !== undefined;
160
+
161
+ return new ScatterplotLayer({
162
+ ...baseProps,
163
+ id,
164
+ data: sortFeaturesByKey(points, styleLayer, 'circle-sort-key', zoom),
165
+ getPosition: d => d.position ?? d.geometry.coordinates,
166
+ getRadius: radius.value,
167
+ getLineWidth: lineWidth.value,
168
+ getFillColor: fillColor.value,
169
+ getLineColor: lineColor.value,
170
+ extensions: [...(baseProps.extensions || []), ...(hasBlur ? [CIRCLE_BLUR_EXTENSION] : [])],
171
+ ...(hasBlur ? {getBlur: blur.value} : {}),
172
+ getPixelOffset: pixelOffset.value,
173
+ billboard: globe || alignment.value !== 'map',
174
+ stroked: styleLayer.paint?.['circle-stroke-width'] !== undefined && lineWidth.value !== 0,
175
+ updateTriggers: {
176
+ getRadius: radius.updateTrigger,
177
+ getLineWidth: lineWidth.updateTrigger,
178
+ getFillColor: fillColor.updateTrigger,
179
+ getLineColor: lineColor.updateTrigger,
180
+ getBlur: hasBlur ? blur.updateTrigger : undefined,
181
+ getPixelOffset: pixelOffset.updateTrigger
182
+ },
183
+ radiusUnits: 'pixels',
184
+ lineWidthUnits: 'pixels',
185
+ radiusMinPixels: 0,
186
+ lineWidthMinPixels: 0,
187
+ filled: true,
188
+ antialiasing: true,
189
+ parameters
190
+ });
191
+ }
@@ -0,0 +1,206 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ import {LayerExtension} from '@deck.gl/core';
10
+ import type {Layer} from '@deck.gl/core';
11
+ import type {ShaderModule} from '@luma.gl/shadertools';
12
+
13
+ /** A style's `light`, evaluated at one zoom. */
14
+ export type FillExtrusionLight = {
15
+ /** Whether the light turns with the map (`map`) or stays fixed on the screen (`viewport`). */
16
+ anchor: 'map' | 'viewport';
17
+ /** Radial coordinate, azimuthal angle and polar angle, in degrees, as in the style spec. */
18
+ position: [number, number, number];
19
+ /** RGB in 0-1. */
20
+ color: [number, number, number];
21
+ /** 0-1. */
22
+ intensity: number;
23
+ };
24
+
25
+ /** The style spec's default `light`. */
26
+ export const DEFAULT_FILL_EXTRUSION_LIGHT: FillExtrusionLight = Object.freeze({
27
+ anchor: 'viewport',
28
+ position: Object.freeze([1.15, 210, 30]),
29
+ color: Object.freeze([1, 1, 1]),
30
+ intensity: 0.5
31
+ }) as FillExtrusionLight;
32
+
33
+ type FillExtrusionLightingProps = {
34
+ /** The light's direction in deck.gl's common space (x east, y north, z up), and intensity. */
35
+ lightDirectionIntensity: [number, number, number, number];
36
+ /** The light's color, and 1 when the walls darken toward the ground, else 0. */
37
+ lightColorGradient: [number, number, number, number];
38
+ };
39
+
40
+ /**
41
+ * Returns the light vector that deck.gl's normals, in common space (x east, y north, z up), are
42
+ * shaded against, as MapLibre GL JS shades its own. MapLibre takes the light's spherical position
43
+ * in tile coordinates (y south) and turns it by the map's bearing when the light is anchored to
44
+ * the viewport. It keeps the light's radial length, which it does not normalize. Its wall normals
45
+ * point into the buildings: `_generateSideFaces` takes the perpendicular `(-y, x)` of each edge
46
+ * of a ring that is clockwise in tile coordinates. deck.gl's point out, so the horizontal part of
47
+ * the light is reversed for them: x is negated, and y, negated from tile to common space, is
48
+ * negated back.
49
+ */
50
+ export function getFillExtrusionLightDirection(
51
+ light: FillExtrusionLight,
52
+ bearing: number
53
+ ): [number, number, number] {
54
+ const [radius, azimuthalDegrees, polarDegrees] = light.position;
55
+ const azimuthal = ((azimuthalDegrees + 90) * Math.PI) / 180;
56
+ const polar = (polarDegrees * Math.PI) / 180;
57
+ let x = radius * Math.cos(azimuthal) * Math.sin(polar);
58
+ let y = radius * Math.sin(azimuthal) * Math.sin(polar);
59
+ const z = radius * Math.cos(polar);
60
+ if (light.anchor === 'viewport') {
61
+ const angle = (bearing * Math.PI) / 180;
62
+ const cos = Math.cos(angle);
63
+ const sin = Math.sin(angle);
64
+ [x, y] = [cos * x - sin * y, sin * x + cos * y];
65
+ }
66
+ return [-x, y, z];
67
+ }
68
+
69
+ const UNIFORMS = /* glsl */ `\
70
+ layout(std140) uniform fillExtrusionLightingUniforms {
71
+ vec4 lightDirectionIntensity;
72
+ vec4 lightColorGradient;
73
+ } fillExtrusionLighting;
74
+ `;
75
+
76
+ /**
77
+ * MapLibre GL JS's `fill-extrusion` shading (`fill_extrusion.vertex.glsl`): a directional light
78
+ * whose strength rises with the color's darkness, a small ambient term, and walls that darken
79
+ * toward the ground when `fill-extrusion-vertical-gradient` is on. `t` is 1 at the top of a wall
80
+ * and 0 at its bottom; `base` and `height` are the extrusion's, in meters.
81
+ */
82
+ const SHADE = /* glsl */ `\
83
+ vec3 fillExtrusionLighting_shade(vec3 color, vec3 normal, bool isWall, float t, float base, float height) {
84
+ vec3 lightDirection = fillExtrusionLighting.lightDirectionIntensity.xyz;
85
+ float intensity = fillExtrusionLighting.lightDirectionIntensity.w;
86
+ vec3 lightColor = fillExtrusionLighting.lightColorGradient.rgb;
87
+ float verticalGradient = fillExtrusionLighting.lightColorGradient.a;
88
+
89
+ float colorValue = color.r * 0.2126 + color.g * 0.7152 + color.b * 0.0722;
90
+ color += vec3(0.03);
91
+ float directional = clamp(dot(normal, lightDirection), 0.0, 1.0);
92
+ directional = mix(1.0 - intensity, max(1.0 - colorValue + intensity, 1.0), directional);
93
+ if (isWall) {
94
+ float gradient = clamp(
95
+ (t + max(0.0, base)) * pow(max(0.0, height) / 150.0, 0.5),
96
+ mix(0.7, 0.98, 1.0 - intensity),
97
+ 1.0
98
+ );
99
+ directional *= (1.0 - verticalGradient) + verticalGradient * gradient;
100
+ }
101
+ return clamp(color * directional * lightColor, mix(vec3(0.0), vec3(0.3), 1.0 - lightColor), vec3(1.0));
102
+ }
103
+ `;
104
+
105
+ const fillExtrusionLighting: ShaderModule<FillExtrusionLightingProps> = {
106
+ name: 'fillExtrusionLighting',
107
+ inject: {
108
+ 'vs:#decl': `${UNIFORMS}${SHADE}
109
+ bool fillExtrusionLighting_isWall;
110
+ float fillExtrusionLighting_t;
111
+ float fillExtrusionLighting_base;
112
+ float fillExtrusionLighting_height;
113
+ `,
114
+ // The attributes are declared in the layer's own shaders, which only `main` follows.
115
+ 'vs:#main-start': /* glsl */ `
116
+ #ifdef IS_SIDE_VERTEX
117
+ fillExtrusionLighting_isWall = true;
118
+ fillExtrusionLighting_t = positions.y;
119
+ #else
120
+ fillExtrusionLighting_isWall = false;
121
+ fillExtrusionLighting_t = 1.0;
122
+ #endif
123
+ // Polygons are raised to fill-extrusion-base, and extruded from there by the rest of the height.
124
+ fillExtrusionLighting_base = vertexPositions.z;
125
+ fillExtrusionLighting_height = vertexPositions.z + elevations;
126
+ `,
127
+ // The layer is drawn unlit (see `draw`), so color is the extrusion's own.
128
+ 'vs:DECKGL_FILTER_COLOR': /* glsl */ `
129
+ // The light is placed relative to the ground under the building: east, north and up. On a
130
+ // globe, deck.gl turns a roof's normal into globe space (project_normal), but leaves the walls
131
+ // of longitude/latitude polygons in east and north (project_offset_normal). A roof faces up.
132
+ vec3 fillExtrusionLighting_normal = geometry.normal;
133
+ if (project.projectionMode == PROJECTION_MODE_GLOBE && !fillExtrusionLighting_isWall) {
134
+ fillExtrusionLighting_normal = vec3(0.0, 0.0, 1.0);
135
+ }
136
+ color.rgb = fillExtrusionLighting_shade(
137
+ color.rgb,
138
+ fillExtrusionLighting_normal,
139
+ fillExtrusionLighting_isWall,
140
+ fillExtrusionLighting_t,
141
+ fillExtrusionLighting_base,
142
+ fillExtrusionLighting_height
143
+ );
144
+ `
145
+ },
146
+ uniformTypes: {lightDirectionIntensity: 'vec4<f32>', lightColorGradient: 'vec4<f32>'}
147
+ };
148
+
149
+ /**
150
+ * The material the extension draws with: deck.gl's lighting leaves the color as it is, for the
151
+ * extension to shade.
152
+ */
153
+ const UNLIT_MATERIAL = {unlit: true};
154
+
155
+ function isExtrudedPolygonLayer(layer: Layer): boolean {
156
+ const state = layer.state as Record<string, unknown> | undefined;
157
+ return (
158
+ layer.context.device.type !== 'webgpu' &&
159
+ Boolean(state && 'polygonTesselator' in state && (layer.props as any).extruded)
160
+ );
161
+ }
162
+
163
+ /**
164
+ * Shades the extruded `SolidPolygonLayer`s of a layer as MapLibre GL JS shades `fill-extrusion`,
165
+ * from the style's `light` (`fillExtrusionLight`) and `fill-extrusion-vertical-gradient`
166
+ * (`fillExtrusionVerticalGradient`), instead of with deck.gl's lights, which it turns off for the
167
+ * layer at every draw. A light anchored to the viewport turns with the viewport's bearing, which is
168
+ * read at every draw. On a globe, the light is placed relative to
169
+ * the ground under each building. Polygons must lie at `fill-extrusion-base` and be extruded by
170
+ * the rest of the height. WebGL only: on WebGPU the extension adds no shader code, and buildings
171
+ * keep deck.gl's lights.
172
+ */
173
+ export class FillExtrusionLightingExtension extends LayerExtension {
174
+ static extensionName = 'FillExtrusionLightingExtension';
175
+ static defaultProps = {
176
+ fillExtrusionLight: {type: 'object', value: DEFAULT_FILL_EXTRUSION_LIGHT},
177
+ fillExtrusionVerticalGradient: true
178
+ };
179
+
180
+ isEnabled(layer: Layer): boolean {
181
+ return isExtrudedPolygonLayer(layer);
182
+ }
183
+
184
+ getShaders(this: Layer, extension: this): any {
185
+ return extension.isEnabled(this) ? {modules: [fillExtrusionLighting]} : null;
186
+ }
187
+
188
+ draw(this: Layer<any>, params: any, extension: this) {
189
+ if (!extension.isEnabled(this)) {
190
+ return;
191
+ }
192
+ const light: FillExtrusionLight = this.props.fillExtrusionLight || DEFAULT_FILL_EXTRUSION_LIGHT;
193
+ const bearing = (this.context.viewport as {bearing?: number}).bearing || 0;
194
+ const [x, y, z] = getFillExtrusionLightDirection(light, bearing);
195
+ const fillExtrusionLightingProps: FillExtrusionLightingProps = {
196
+ lightDirectionIntensity: [x, y, z, light.intensity],
197
+ lightColorGradient: [...light.color, this.props.fillExtrusionVerticalGradient ? 1 : 0]
198
+ };
199
+ // After the lighting effect has set the layer's material (see `LayersPass`).
200
+ this.setShaderModuleProps({
201
+ fillExtrusionLighting: fillExtrusionLightingProps,
202
+ phongMaterial: UNLIT_MATERIAL,
203
+ gouraudMaterial: UNLIT_MATERIAL
204
+ } as any);
205
+ }
206
+ }
@@ -0,0 +1,70 @@
1
+ // tangram-layers
2
+ // SPDX-License-Identifier: MIT
3
+ // Copyright (c) vis.gl contributors
4
+
5
+ // deck.gl-community
6
+ // SPDX-License-Identifier: MIT
7
+ // Copyright (c) vis.gl contributors
8
+
9
+ import {LayerExtension} from '@deck.gl/core';
10
+ import type {Layer} from '@deck.gl/core';
11
+ import type {ShaderModule} from '@luma.gl/shadertools';
12
+
13
+ /**
14
+ * Draws the `PathLayer`s of a fill's outline (`fill-outline-color`) as MapLibre GL JS draws it: as
15
+ * `gl.LINES`, which draw each segment on its own, one pixel across the axis it crosses most
16
+ * steeply. deck.gl's 1 pixel stroke covers more pixels than that on diagonals, and its joins draw
17
+ * past the corners of short segments, so outlines came out darker and wider than MapLibre's. The
18
+ * stroke, drawn without antialiasing, is cut to the pixels a line draws: those within half a pixel
19
+ * of the segment along that axis, and not in the joins. MapLibre's shader also fades each pixel
20
+ * with its distance from the line (`fill_outline.fragment.glsl`); its outlines in Chrome have the
21
+ * colors of unfaded pixels, and so do these. Other sublayers are left unchanged. WebGL only; on
22
+ * WebGPU the outline draws as a stroke.
23
+ */
24
+ export class FillOutlineExtension extends LayerExtension {
25
+ static extensionName = 'FillOutlineExtension';
26
+
27
+ getShaders(this: Layer) {
28
+ if (!isOutlinePathLayer(this)) {
29
+ return {};
30
+ }
31
+ // `vPathPosition.x` runs from -1 to 1 across the stroke. Its screen gradient gives the
32
+ // distance to the path in device pixels, and the direction across the line; the gradient is
33
+ // taken before any fragment of the quad is discarded. Picking draws the same pixels.
34
+ const fillOutline: ShaderModule = {
35
+ name: 'fillOutline',
36
+ inject: {
37
+ // Drawn 1.5 device pixels to each side of the path, so that with multisampling every pixel
38
+ // the line keeps, whose center is within half a pixel of the path, is covered whole.
39
+ 'vs:DECKGL_FILTER_SIZE': `
40
+ if (size.x > 0.0) {
41
+ vec2 fillOutline_size = vec2(1.5 / project.devicePixelRatio);
42
+ size.xy = path.billboard ? fillOutline_size : project_pixel_size(fillOutline_size);
43
+ }
44
+ `,
45
+ 'fs:#main-start': `
46
+ vec2 fillOutline_gradient = vec2(dFdx(vPathPosition.x), dFdy(vPathPosition.x));
47
+ `,
48
+ 'fs:#main-end': `
49
+ // Each segment draws on its own, as a line does: not in the joins between segments, and not at
50
+ // its end, where the next segment starts.
51
+ if (vPathPosition.y < 0.0 || vPathPosition.y >= vPathLength) {
52
+ discard;
53
+ }
54
+ float fillOutline_gradientLength = max(length(fillOutline_gradient), 1e-6);
55
+ float fillOutline_distance = abs(vPathPosition.x) / fillOutline_gradientLength;
56
+ vec2 fillOutline_normal = abs(fillOutline_gradient) / fillOutline_gradientLength;
57
+ // The distance along the axis the line crosses most steeply: a line draws one pixel there.
58
+ if (fillOutline_distance / max(fillOutline_normal.x, fillOutline_normal.y) > 0.5) {
59
+ discard;
60
+ }
61
+ `
62
+ }
63
+ };
64
+ return {modules: [fillOutline]};
65
+ }
66
+ }
67
+
68
+ function isOutlinePathLayer(layer: Layer): boolean {
69
+ return layer.context.device.type !== 'webgpu' && 'pathTesselator' in (layer.state || {});
70
+ }