@navaramap/three 0.0.8 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +20 -8
  3. package/dist/assets/fontWorker-C5cHhkWQ.js +2 -0
  4. package/dist/assets/fontWorker-C5cHhkWQ.js.map +1 -0
  5. package/dist/assets/{index-DaejBdUz.js → index-VfUncKuS.js} +2 -2
  6. package/dist/assets/index-VfUncKuS.js.map +1 -0
  7. package/dist/assets/{navara_wasm_font_worker_bg-CSmoDs_Q.wasm → navara_wasm_font_worker_bg-CKqHBoCR.wasm} +0 -0
  8. package/dist/assets/{navara_wasm_worker_bg-C1dUd_Nk.wasm → navara_wasm_worker_bg-CT26EH41.wasm} +0 -0
  9. package/dist/index.d.ts +298 -54
  10. package/dist/index.js +1569 -1396
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.umd.cjs +90 -69
  13. package/dist/index.umd.cjs.map +1 -1
  14. package/package.json +12 -12
  15. package/src/core/BaseDesc.ts +25 -1
  16. package/src/core/MeshDesc.test.ts +6 -8
  17. package/src/core/MeshDesc.ts +4 -6
  18. package/src/evaluations/FeatureEvaluator.ts +17 -10
  19. package/src/event/feature.ts +15 -2
  20. package/src/event/features/billboard.test.ts +62 -0
  21. package/src/event/features/billboard.ts +4 -2
  22. package/src/index.ts +266 -98
  23. package/src/material/enhancer/model/modelBaseEnhancer/shader.ts +2 -2
  24. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +2 -2
  25. package/src/material/macro/tileShader.ts +12 -2
  26. package/src/mesh/polyline.ts +32 -4
  27. package/src/mesh/sdfText/batchedSdfText.test.ts +65 -0
  28. package/src/mesh/sdfText/batchedSdfText.ts +121 -43
  29. package/src/mesh/sprite/instancedSprite.ts +173 -65
  30. package/src/pick/pickHelper.test.ts +389 -4
  31. package/src/pick/pickHelper.ts +212 -37
  32. package/src/plugins/AttributionPlugin/index.ts +1 -1
  33. package/src/renderOrder.ts +10 -0
  34. package/src/terrain/sampleTerrainMostDetailed.test.ts +6 -6
  35. package/src/terrain/sampleTerrainMostDetailed.ts +7 -5
  36. package/src/type/index.ts +22 -1
  37. package/dist/assets/fontWorker-CkLGtrXf.js +0 -2
  38. package/dist/assets/fontWorker-CkLGtrXf.js.map +0 -1
  39. package/dist/assets/index-DaejBdUz.js.map +0 -1
@@ -19,22 +19,40 @@ export type PickHelperOptions = {
19
19
  };
20
20
 
21
21
  /**
22
- * Travel tolerance in CSS pixels between mousedown and mouseup to count as a click.
22
+ * Travel tolerance in CSS pixels between pointerdown and pointerup to count as a click.
23
23
  * Follows Cesium's `_clickPixelTolerance` (MapLibre uses 3px).
24
24
  * ref: https://github.com/CesiumGS/cesium/blob/9fda7ab97a762e40c74b1d0e1814c98a2de43337/packages/engine/Source/Core/ScreenSpaceEventHandler.js#L1012
25
25
  * ref: https://github.com/maplibre/maplibre-gl-js/blob/3a0a4f795fef5b2a29034d71833475589c344eaf/src/ui/map.ts#L542
26
26
  */
27
- const CLICK_PIXEL_TOLERANCE = 5;
27
+ export const CLICK_PIXEL_TOLERANCE = 5;
28
+
29
+ /**
30
+ * Travel tolerance for touch taps. Finger contact jitters far more than a
31
+ * mouse, so follows MapLibre's tap `MAX_DIST` instead.
32
+ * ref: https://github.com/maplibre/maplibre-gl-js/blob/3a0a4f795fef5b2a29034d71833475589c344eaf/src/ui/handler/tap_recognizer.ts
33
+ */
34
+ export const TAP_PIXEL_TOLERANCE = 30;
28
35
 
29
36
  export function isClickGesture(
30
37
  downPosition: Vector2,
31
38
  upPosition: Vector2,
39
+ tolerance: number = CLICK_PIXEL_TOLERANCE,
32
40
  ): boolean {
33
- return downPosition.distanceTo(upPosition) < CLICK_PIXEL_TOLERANCE;
41
+ return downPosition.distanceTo(upPosition) < tolerance;
34
42
  }
35
43
 
36
44
  /**
37
- * GPU picking using a dedicated single-pixel render pass.
45
+ * Pick search radius in CSS pixels around the pointer. The pick reads a
46
+ * (2r+1)² window and takes the non-zero batch id closest to its center, so
47
+ * small features (points, thin lines) don't demand pixel-perfect aim.
48
+ */
49
+ export const PICK_RADIUS = 3;
50
+
51
+ /** Pick search radius for touch: fingertips land less precisely than a cursor. */
52
+ export const TOUCH_PICK_RADIUS = 10;
53
+
54
+ /**
55
+ * GPU picking using a dedicated render pass over a small search window.
38
56
  *
39
57
  * The pick pass does NOT reuse the main render pipeline (globe / MRT /
40
58
  * draped / copy passes). Instead, every pickable raw — regardless of
@@ -47,6 +65,15 @@ export function isClickGesture(
47
65
  * Re-parenting is safe because pass scenes all have identity world
48
66
  * transforms, so world matrices of the re-parented objects are
49
67
  * unaffected.
68
+ *
69
+ * Two gestures trigger a pick: a click or touch tap (pointerdown/pointerup
70
+ * within {@link CLICK_PIXEL_TOLERANCE} / {@link TAP_PIXEL_TOLERANCE}) and
71
+ * hovering, throttled to at most one pick per animation frame. Both are
72
+ * lazy: a pick runs only while the matching `shouldClickPick` /
73
+ * `shouldHoverPick` gate returns true (i.e. someone is listening for the
74
+ * result). The gestures are detected from pointer events, which fire for
75
+ * touch as well. The camera-input handlers call `preventDefault()` on
76
+ * touch events, so the compatibility mouse events never do.
50
77
  */
51
78
  export class PickHelper {
52
79
  private element: HTMLElement;
@@ -64,9 +91,21 @@ export class PickHelper {
64
91
  /** Full-size render target used for click picking with scissor restriction. */
65
92
  private pickRenderTarget: WebGLRenderTarget;
66
93
 
67
- private mouseDownPosition?: Vector2;
68
- private mouseDownHandler: (event: MouseEvent) => void;
69
- private mouseUpHandler: (event: MouseEvent) => void;
94
+ private onHoverCallback: (pickArr: number[]) => void;
95
+ /** Click picks run only while this returns true (e.g. featureClick listeners exist). */
96
+ private shouldClickPick: () => boolean;
97
+ /** Hover picks run only while this returns true (e.g. hover listeners exist). */
98
+ private shouldHoverPick: () => boolean;
99
+
100
+ private pointerDownPosition?: Vector2;
101
+ private pointerDownHandler: (event: PointerEvent) => void;
102
+ private pointerUpHandler: (event: PointerEvent) => void;
103
+ private pointerCancelHandler: () => void;
104
+ private hoverMoveHandler: (event: PointerEvent) => void;
105
+ private hoverLeaveHandler: () => void;
106
+ /** Latest pointermove position awaiting a hover pick, in client coords. */
107
+ private hoverPosition?: Vector2;
108
+ private hoverRafId?: number;
70
109
 
71
110
  constructor(
72
111
  element: HTMLElement,
@@ -74,6 +113,9 @@ export class PickHelper {
74
113
  camera: PerspectiveCamera,
75
114
  meshes: MeshCache,
76
115
  onPickCallback: (pickArr: number[]) => void,
116
+ onHoverCallback: (pickArr: number[]) => void,
117
+ shouldClickPick: () => boolean,
118
+ shouldHoverPick: () => boolean,
77
119
  options?: PickHelperOptions,
78
120
  ) {
79
121
  this.element = element;
@@ -82,9 +124,17 @@ export class PickHelper {
82
124
  this._camera = camera;
83
125
  this._meshes = meshes;
84
126
  this.onPickCallback = onPickCallback;
85
-
86
- this.mouseDownHandler = (event: MouseEvent) => this.onMouseDown(event);
87
- this.mouseUpHandler = (event: MouseEvent) => this.onMouseUp(event);
127
+ this.onHoverCallback = onHoverCallback;
128
+ this.shouldClickPick = shouldClickPick;
129
+ this.shouldHoverPick = shouldHoverPick;
130
+
131
+ this.pointerDownHandler = (event: PointerEvent) =>
132
+ this.onPointerDown(event);
133
+ this.pointerUpHandler = (event: PointerEvent) => this.onPointerUp(event);
134
+ this.pointerCancelHandler = () => this.onPointerCancel();
135
+ this.hoverMoveHandler = (event: PointerEvent) =>
136
+ this.onHoverPointerMove(event);
137
+ this.hoverLeaveHandler = () => this.onHoverPointerLeave();
88
138
 
89
139
  const width = this._renderer.getContext().drawingBufferWidth;
90
140
  const height = this._renderer.getContext().drawingBufferHeight;
@@ -105,32 +155,113 @@ export class PickHelper {
105
155
  }
106
156
  }
107
157
 
108
- private onMouseDown(event: MouseEvent) {
109
- this.mouseDownPosition = new Vector2(event.clientX, event.clientY);
158
+ private onPointerDown(event: PointerEvent) {
159
+ // A second concurrent pointer (e.g. the pinch-zoom finger) turns the
160
+ // gesture into something other than a tap; abandon the pending click.
161
+ if (!event.isPrimary) {
162
+ this.pointerDownPosition = undefined;
163
+ return;
164
+ }
165
+ this.pointerDownPosition = new Vector2(event.clientX, event.clientY);
110
166
  }
111
167
 
112
- private onMouseUp(event: MouseEvent) {
113
- const downPosition = this.mouseDownPosition;
114
- this.mouseDownPosition = undefined;
168
+ private onPointerUp(event: PointerEvent) {
169
+ if (!event.isPrimary) return;
170
+
171
+ const downPosition = this.pointerDownPosition;
172
+ this.pointerDownPosition = undefined;
115
173
  if (!downPosition) return;
116
174
 
175
+ // Skip the GPU pick entirely when nobody is listening for the result.
176
+ if (!this.shouldClickPick()) return;
177
+
178
+ const tolerance =
179
+ event.pointerType === "touch"
180
+ ? TAP_PIXEL_TOLERANCE
181
+ : CLICK_PIXEL_TOLERANCE;
117
182
  if (
118
- isClickGesture(downPosition, new Vector2(event.clientX, event.clientY))
183
+ isClickGesture(
184
+ downPosition,
185
+ new Vector2(event.clientX, event.clientY),
186
+ tolerance,
187
+ )
119
188
  ) {
120
- this.onMouseClick(event);
189
+ this.onClickPick(event);
121
190
  }
122
191
  }
123
192
 
193
+ /** The browser took over the gesture (scroll, app switch): not a click. */
194
+ private onPointerCancel() {
195
+ this.pointerDownPosition = undefined;
196
+ }
197
+
124
198
  enablePick(bPick: boolean) {
125
199
  if (bPick) {
126
- this.element.addEventListener("mousedown", this.mouseDownHandler);
127
- this.element.addEventListener("mouseup", this.mouseUpHandler);
200
+ this.element.addEventListener("pointerdown", this.pointerDownHandler);
201
+ this.element.addEventListener("pointerup", this.pointerUpHandler);
202
+ this.element.addEventListener("pointercancel", this.pointerCancelHandler);
203
+ this.element.addEventListener("pointermove", this.hoverMoveHandler);
204
+ this.element.addEventListener("pointerleave", this.hoverLeaveHandler);
128
205
  } else {
129
- this.element.removeEventListener("mousedown", this.mouseDownHandler);
130
- this.element.removeEventListener("mouseup", this.mouseUpHandler);
206
+ this.element.removeEventListener("pointerdown", this.pointerDownHandler);
207
+ this.element.removeEventListener("pointerup", this.pointerUpHandler);
208
+ this.element.removeEventListener(
209
+ "pointercancel",
210
+ this.pointerCancelHandler,
211
+ );
212
+ this.element.removeEventListener("pointermove", this.hoverMoveHandler);
213
+ this.element.removeEventListener("pointerleave", this.hoverLeaveHandler);
214
+ this.cancelPendingHoverPick();
131
215
  }
132
216
  }
133
217
 
218
+ /**
219
+ * Hover picking: schedules at most one GPU pick per animation frame from
220
+ * the latest pointermove position. Suppressed while any button is pressed
221
+ * (Camera drags would otherwise trigger a pick per frame. For touch,
222
+ * contact itself sets `buttons`, so touch never hover-picks) and while
223
+ * `shouldHoverPick` returns false, so pages without hover listeners pay
224
+ * nothing.
225
+ */
226
+ private onHoverPointerMove(event: PointerEvent) {
227
+ if (event.buttons !== 0 || !this.shouldHoverPick()) return;
228
+
229
+ if (!this.hoverPosition) this.hoverPosition = new Vector2();
230
+ this.hoverPosition.set(event.clientX, event.clientY);
231
+
232
+ if (this.hoverRafId !== undefined) return;
233
+ this.hoverRafId = requestAnimationFrame(() => {
234
+ this.hoverRafId = undefined;
235
+ const position = this.hoverPosition;
236
+ if (!position) return;
237
+ this.hoverPosition = undefined;
238
+
239
+ // Listeners may have been removed between the pointermove that
240
+ // scheduled this frame and now. Re-check so no orphan pick runs.
241
+ if (!this.shouldHoverPick()) return;
242
+
243
+ const batchId = this.pickBatchIdAt(position.x, position.y);
244
+ this.onHoverCallback(batchId > 0 ? [batchId] : []);
245
+ });
246
+ }
247
+
248
+ private onHoverPointerLeave() {
249
+ this.cancelPendingHoverPick();
250
+ // Report "nothing hovered" so diff-based enter/leave synthesis can
251
+ // close out the current hover when the cursor exits the canvas.
252
+ if (this.shouldHoverPick()) {
253
+ this.onHoverCallback([]);
254
+ }
255
+ }
256
+
257
+ private cancelPendingHoverPick() {
258
+ if (this.hoverRafId !== undefined) {
259
+ cancelAnimationFrame(this.hoverRafId);
260
+ this.hoverRafId = undefined;
261
+ }
262
+ this.hoverPosition = undefined;
263
+ }
264
+
134
265
  /**
135
266
  * Moves every pickable raw (visible, currently parented) into the
136
267
  * dedicated `pickScene` and activates the picking uniforms on its
@@ -224,10 +355,27 @@ export class PickHelper {
224
355
  this.debugBufferView.render(this._renderer, this.debugRenderTarget);
225
356
  }
226
357
 
227
- private onMouseClick(event: MouseEvent) {
358
+ private onClickPick(event: PointerEvent) {
359
+ const radius =
360
+ event.pointerType === "touch" ? TOUCH_PICK_RADIUS : PICK_RADIUS;
361
+ const batchId = this.pickBatchIdAt(event.clientX, event.clientY, radius);
362
+ const pickArr = batchId > 0 ? [batchId] : [];
363
+ this.onPickCallback(pickArr);
364
+ }
365
+
366
+ /**
367
+ * Runs the pick render in a search window around the given client
368
+ * coordinates and returns the non-zero batch ID closest to its center
369
+ * (0 when nothing pickable is inside the window).
370
+ */
371
+ private pickBatchIdAt(
372
+ clientX: number,
373
+ clientY: number,
374
+ radiusCss: number = PICK_RADIUS,
375
+ ): number {
228
376
  const rect = this.element.getBoundingClientRect();
229
- const x = event.clientX - rect.left;
230
- const y = event.clientY - rect.top;
377
+ const x = clientX - rect.left;
378
+ const y = clientY - rect.top;
231
379
 
232
380
  const pixelRatio = this._renderer.getPixelRatio();
233
381
  const fullWidth = this._renderer.getContext().drawingBufferWidth;
@@ -249,31 +397,58 @@ export class PickHelper {
249
397
  );
250
398
  const readY = fullHeight - 1 - pixelY;
251
399
 
400
+ // The search window in device pixels, clamped to the viewport.
401
+ const radius = Math.max(0, Math.round(radiusCss * pixelRatio));
402
+ const minX = Math.max(0, pixelX - radius);
403
+ const maxX = Math.min(fullWidth - 1, pixelX + radius);
404
+ const minY = Math.max(0, readY - radius);
405
+ const maxY = Math.min(fullHeight - 1, readY + radius);
406
+ const width = maxX - minX + 1;
407
+ const height = maxY - minY + 1;
408
+
252
409
  // Keep the camera projection unchanged for wide lines / screen-space
253
- // expanded shaders, and limit fragment work to one pixel via scissor.
254
- this._renderer.setScissor(pixelX, readY, 1, 1);
410
+ // expanded shaders, and limit fragment work to the window via scissor.
411
+ this._renderer.setScissor(minX, minY, width, height);
255
412
  this._renderer.setScissorTest(true);
256
413
 
257
414
  this.processRender(this.pickRenderTarget, pickingCoord);
258
415
 
416
+ if (this.pixelBuffer.length < width * height * 4) {
417
+ this.pixelBuffer = new Uint8Array(width * height * 4);
418
+ }
259
419
  this._renderer.readRenderTargetPixels(
260
420
  this.pickRenderTarget,
261
- pixelX,
262
- readY,
263
- 1,
264
- 1,
421
+ minX,
422
+ minY,
423
+ width,
424
+ height,
265
425
  this.pixelBuffer,
266
426
  );
267
427
 
268
428
  this._renderer.setScissorTest(false);
269
429
 
270
- const batchId =
271
- (this.pixelBuffer[0] << 16) +
272
- (this.pixelBuffer[1] << 8) +
273
- this.pixelBuffer[2];
274
-
275
- const pickArr = batchId > 0 ? [batchId] : [];
276
- this.onPickCallback(pickArr);
430
+ // The non-zero batch id nearest to the pointer wins, so a small feature
431
+ // beside a large one is still pickable by aiming at it.
432
+ let bestId = 0;
433
+ let bestDistSq = Infinity;
434
+ for (let row = 0; row < height; row++) {
435
+ for (let col = 0; col < width; col++) {
436
+ const i = (row * width + col) * 4;
437
+ const id =
438
+ (this.pixelBuffer[i] << 16) +
439
+ (this.pixelBuffer[i + 1] << 8) +
440
+ this.pixelBuffer[i + 2];
441
+ if (id === 0) continue;
442
+ const dx = minX + col - pixelX;
443
+ const dy = minY + row - readY;
444
+ const distSq = dx * dx + dy * dy;
445
+ if (distSq < bestDistSq) {
446
+ bestDistSq = distSq;
447
+ bestId = id;
448
+ }
449
+ }
450
+ }
451
+ return bestId;
277
452
  }
278
453
 
279
454
  public dispose() {
@@ -125,7 +125,7 @@ let styleRefCount = 0;
125
125
  */
126
126
  const NAVARA_CREDIT: AttributionItem = {
127
127
  attribution: "Navara",
128
- attributionUrl: "https://navara-docs.reearth.workers.dev//",
128
+ attributionUrl: "https://navara.world/",
129
129
  };
130
130
 
131
131
  /**
@@ -1,3 +1,13 @@
1
1
  export const FEATURE_RENDER_ORDER = 1;
2
+ /**
3
+ * Polylines (strokes) must draw after polygon fills. In the per-tile bake
4
+ * scene, sibling fill/stroke meshes are created asynchronously and land in
5
+ * the scene in arbitrary order, so a fill baked later paints over the stroke.
6
+ * In the MRT scene, polyline materials skip the depth test but still write
7
+ * depth, so a coplanar fill drawn after a stroke z-fights against the
8
+ * stroke's depth per fragment (mottled boundaries). Encoding the stacking in
9
+ * renderOrder makes both cases deterministic.
10
+ */
11
+ export const STROKE_RENDER_ORDER = FEATURE_RENDER_ORDER + 1;
2
12
  export const SKY_RENDER_ORDER = 100;
3
13
  export const STARS_RENDER_ORDER = SKY_RENDER_ORDER + 1;
@@ -37,6 +37,7 @@ vi.mock("@navaramap/engine-api", () => {
37
37
  };
38
38
  return {
39
39
  default: vi.fn().mockResolvedValue(undefined),
40
+ angleToRadian: (deg: number) => (deg * Math.PI) / 180,
40
41
  terrainPositionsToTiles: vi.fn(
41
42
  (geographic: boolean, level: number, positions: Float64Array) => {
42
43
  const result = new Uint32Array(positions.length);
@@ -87,12 +88,11 @@ vi.mock("@navaramap/engine-api", () => {
87
88
  };
88
89
  });
89
90
 
90
- const KARIYA = { lat: (35.0 * Math.PI) / 180, lng: (137.0 * Math.PI) / 180 };
91
- const NEARBY = {
92
- lat: (35.001 * Math.PI) / 180,
93
- lng: (137.001 * Math.PI) / 180,
94
- };
95
- const TOKYO = { lat: (35.68 * Math.PI) / 180, lng: (139.76 * Math.PI) / 180 };
91
+ // Public inputs are degrees; the mocked engine functions still receive the
92
+ // radians the implementation converts to.
93
+ const KARIYA = { lat: 35.0, lng: 137.0 };
94
+ const NEARBY = { lat: 35.001, lng: 137.001 };
95
+ const TOKYO = { lat: 35.68, lng: 139.76 };
96
96
 
97
97
  const SOURCE: QuantizedMeshSource = {
98
98
  type: "quantized-mesh",
@@ -1,5 +1,6 @@
1
1
  import type { LatLng } from "@navaramap/core";
2
2
  import {
3
+ angleToRadian,
3
4
  sampleQuantizedMeshHeights,
4
5
  sampleRasterDemHeights,
5
6
  terrainPositionsToTiles,
@@ -29,9 +30,9 @@ export type SampleTerrainOptions = {
29
30
  };
30
31
 
31
32
  export type SampledTerrainPosition = {
32
- /** Latitude in radians, echoed from the input. */
33
+ /** Latitude in degrees, echoed from the input. */
33
34
  lat: number;
34
- /** Longitude in radians, echoed from the input. */
35
+ /** Longitude in degrees, echoed from the input. */
35
36
  lng: number;
36
37
  /**
37
38
  * Terrain height in meters, or `undefined` when no tile covering the
@@ -104,7 +105,7 @@ type SourceConfig = {
104
105
  *
105
106
  * @param source - A `quantized-mesh` or `raster-dem` source description (the
106
107
  * same shape `addSource` accepts)
107
- * @param positions - Geodetic positions (lat/lng in radians)
108
+ * @param positions - Geodetic positions (lat/lng in degrees)
108
109
  * @param options - Optional abort signal and fixed-level sampling
109
110
  * @returns One result per input position, in the same order (the input
110
111
  * objects are not mutated)
@@ -123,10 +124,11 @@ export async function sampleTerrainMostDetailed(
123
124
  // Fixed-level sampling never falls back to parents.
124
125
  const floorLevel = options?.level != null ? startLevel : config.minZoom;
125
126
 
127
+ // The engine samplers work in radians; inputs and echoed outputs are degrees.
126
128
  const flat = new Float64Array(positions.length * 2);
127
129
  positions.forEach((p, i) => {
128
- flat[i * 2] = p.lng;
129
- flat[i * 2 + 1] = p.lat;
130
+ flat[i * 2] = angleToRadian(p.lng);
131
+ flat[i * 2 + 1] = angleToRadian(p.lat);
130
132
  });
131
133
 
132
134
  // Group positions by their containing tile at the starting level; each
package/src/type/index.ts CHANGED
@@ -75,6 +75,27 @@ type ConvertColorFields<T> = {
75
75
  */
76
76
  type WithColorSupport<T> = ConvertColorFields<T>;
77
77
 
78
+ /**
79
+ * Source geometry categories a vector appearance can consume via its
80
+ * `geometryTypes` option. Each appearance defaults to its native geometry only
81
+ * (`"point"` for point/billboard/text, `"line"` for polyline); adding entries
82
+ * derives extra representations, e.g. `polyline: { geometryTypes: ["line",
83
+ * "polygon"] }` also renders polygon boundary rings as polylines.
84
+ */
85
+ export type SourceGeometryType = "point" | "line" | "polygon";
86
+
87
+ /**
88
+ * Helper type to narrow the generated `geometryTypes?: string[]` material
89
+ * fields to the {@link SourceGeometryType} literal union.
90
+ */
91
+ type ConvertGeometryTypeFields<T> = {
92
+ [K in keyof T]: K extends "geometryTypes"
93
+ ? SourceGeometryType[] | Extract<T[K], undefined>
94
+ : T[K] extends object | undefined
95
+ ? ConvertGeometryTypeFields<T[K]> | Extract<T[K], undefined>
96
+ : T[K];
97
+ };
98
+
78
99
  /**
79
100
  * A reference to a {@link Source}: either the source handle returned by
80
101
  * `addSource`, or its `id` string.
@@ -87,7 +108,7 @@ export type SourceLayerBase<Layer extends { source?: string | undefined }> =
87
108
  };
88
109
 
89
110
  type VectorLayerBase = WithColorSupport<
90
- Layer<VectorLayerDescription & { type: "vector" }>
111
+ ConvertGeometryTypeFields<Layer<VectorLayerDescription & { type: "vector" }>>
91
112
  >;
92
113
 
93
114
  /**