@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.
- package/README.md +34 -2
- package/THIRD-PARTY-NOTICES.md +205 -0
- package/dist/geojson-worker.js +2160 -0
- package/dist/geojson-worker.js.map +6 -0
- package/dist/index.cjs +11388 -0
- package/dist/index.cjs.map +6 -0
- package/dist/index.js +11369 -0
- package/dist/index.js.map +6 -0
- package/dist/map-style.cjs +445 -0
- package/dist/map-style.cjs.map +6 -0
- package/dist/map-style.js +431 -0
- package/dist/map-style.js.map +6 -0
- package/dist/types/arabic-shaping.d.ts +7 -0
- package/dist/types/atmosphere-layer.d.ts +10 -0
- package/dist/types/basemap-layer.d.ts +150 -0
- package/dist/types/bidi.d.ts +8 -0
- package/dist/types/circle-layer.d.ts +49 -0
- package/dist/types/fill-extrusion-lighting-extension.d.ts +49 -0
- package/dist/types/fill-outline-extension.d.ts +22 -0
- package/dist/types/fill-pattern.d.ts +99 -0
- package/dist/types/fill-style-extensions.d.ts +100 -0
- package/dist/types/geojson-source.d.ts +29 -0
- package/dist/types/geojson-tiler.d.ts +48 -0
- package/dist/types/geojson-worker.d.ts +40 -0
- package/dist/types/globe-horizon-extension.d.ts +61 -0
- package/dist/types/globe-layers.d.ts +239 -0
- package/dist/types/globe-line-angle-extension.d.ts +37 -0
- package/dist/types/glyph-font-renderer.d.ts +37 -0
- package/dist/types/glyph-pbf.d.ts +37 -0
- package/dist/types/glyph-store.d.ts +103 -0
- package/dist/types/grid-index.d.ts +47 -0
- package/dist/types/hillshade-layer.d.ts +154 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/is-globe-viewport.d.ts +2 -0
- package/dist/types/label-font-renderer.d.ts +18 -0
- package/dist/types/label-placement.d.ts +247 -0
- package/dist/types/label-text-layer.d.ts +231 -0
- package/dist/types/line-blur-extension.d.ts +73 -0
- package/dist/types/line-glyphs.d.ts +60 -0
- package/dist/types/line-placement.d.ts +93 -0
- package/dist/types/map-style-loader.d.ts +48 -0
- package/dist/types/map-style-schema.d.ts +72 -0
- package/dist/types/map-style.d.ts +6 -0
- package/dist/types/mapbox-style.d.ts +40 -0
- package/dist/types/mvt-label-layer.d.ts +923 -0
- package/dist/types/polylabel.d.ts +17 -0
- package/dist/types/raster-color-extension.d.ts +146 -0
- package/dist/types/shared-texture.d.ts +34 -0
- package/dist/types/sort-key.d.ts +10 -0
- package/dist/types/sprite.d.ts +82 -0
- package/dist/types/style-accessor.d.ts +45 -0
- package/dist/types/style-expression.d.ts +83 -0
- package/dist/types/style-resolver.d.ts +94 -0
- package/dist/types/text-font.d.ts +36 -0
- package/dist/types/text-halo-blur-extension.d.ts +55 -0
- package/dist/types/text-layout.d.ts +71 -0
- package/dist/types/util.d.ts +22 -0
- package/dist/types/zoom-paint-extension.d.ts +72 -0
- package/package.json +80 -3
- package/src/arabic-shaping.ts +366 -0
- package/src/atmosphere-layer.ts +19 -0
- package/src/basemap-layer.ts +533 -0
- package/src/bidi.ts +199 -0
- package/src/circle-layer.ts +191 -0
- package/src/fill-extrusion-lighting-extension.ts +206 -0
- package/src/fill-outline-extension.ts +70 -0
- package/src/fill-pattern.ts +400 -0
- package/src/fill-style-extensions.ts +177 -0
- package/src/geojson-source.ts +318 -0
- package/src/geojson-tiler.ts +185 -0
- package/src/geojson-worker.ts +98 -0
- package/src/globe-horizon-extension.ts +64 -0
- package/src/globe-layers.ts +3487 -0
- package/src/globe-line-angle-extension.ts +131 -0
- package/src/glyph-font-renderer.ts +173 -0
- package/src/glyph-pbf.ts +164 -0
- package/src/glyph-store.ts +326 -0
- package/src/grid-index.ts +177 -0
- package/src/hillshade-layer.ts +775 -0
- package/src/index.ts +18 -0
- package/src/is-globe-viewport.ts +24 -0
- package/src/label-font-renderer.ts +159 -0
- package/src/label-placement.ts +727 -0
- package/src/label-text-layer.ts +913 -0
- package/src/line-blur-extension.ts +237 -0
- package/src/line-glyphs.ts +364 -0
- package/src/line-placement.ts +446 -0
- package/src/map-style-loader.ts +56 -0
- package/src/map-style-schema.ts +83 -0
- package/src/map-style.ts +23 -0
- package/src/mapbox-style.ts +168 -0
- package/src/mvt-label-layer.ts +3744 -0
- package/src/polylabel.ts +312 -0
- package/src/raster-color-extension.ts +245 -0
- package/src/shared-texture.ts +153 -0
- package/src/sort-key.ts +63 -0
- package/src/sprite.ts +244 -0
- package/src/style-accessor.ts +95 -0
- package/src/style-expression.ts +327 -0
- package/src/style-resolver.ts +301 -0
- package/src/text-font.ts +150 -0
- package/src/text-halo-blur-extension.ts +115 -0
- package/src/text-layout.ts +441 -0
- package/src/util.ts +47 -0
- 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
|
+
}
|