@navaramap/three 0.0.0 → 0.0.2

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 (66) hide show
  1. package/README.md +70 -0
  2. package/dist/assets/fontWorker-8oWvyvwL.js +2 -0
  3. package/dist/assets/fontWorker-8oWvyvwL.js.map +1 -0
  4. package/dist/assets/index-D9G7kIEj.js +25 -0
  5. package/dist/assets/index-D9G7kIEj.js.map +1 -0
  6. package/dist/assets/{navara_wasm_font_worker_bg-665OLxjy.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
  7. package/dist/assets/{navara_wasm_worker_bg-BXhKC5bE.wasm → navara_wasm_worker_bg-Q8zrkLCR.wasm} +0 -0
  8. package/dist/draco_decoder.wasm +0 -0
  9. package/dist/draco_decoder2.wasm +0 -0
  10. package/dist/index.d.ts +587 -67
  11. package/dist/index.js +5640 -4944
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.umd.cjs +238 -211
  14. package/dist/index.umd.cjs.map +1 -1
  15. package/package.json +35 -34
  16. package/src/core/EffectDesc.ts +2 -4
  17. package/src/core/MeshDesc.ts +1 -2
  18. package/src/core/MeshDescWithSelectiveEffect.ts +1 -2
  19. package/src/declutter/DeclutterManager.test.ts +235 -0
  20. package/src/declutter/DeclutterManager.ts +256 -0
  21. package/src/declutter/index.ts +7 -0
  22. package/src/declutter/kernel.ts +46 -0
  23. package/src/declutter/types.ts +61 -0
  24. package/src/declutter/wasmKernel.ts +13 -0
  25. package/src/device.ts +18 -4
  26. package/src/evaluations/FeatureEvaluator.ts +21 -0
  27. package/src/event/context.ts +17 -0
  28. package/src/event/features/modelGpuBytes.ts +1 -3
  29. package/src/event/hillshade.test.ts +7 -13
  30. package/src/event/index.ts +2 -3
  31. package/src/event/tileHandler.ts +54 -0
  32. package/src/index.ts +143 -33
  33. package/src/material/enhancer/ShaderReplacer.ts +1 -2
  34. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +6 -0
  35. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +11 -0
  36. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
  37. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +19 -4
  38. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +9 -2
  39. package/src/mesh/batchTexture.ts +2 -4
  40. package/src/mesh/batchedSdfText.ts +61 -1
  41. package/src/mesh/sdfText.ts +170 -5
  42. package/src/mesh/sprite/instancedSprite.ts +245 -3
  43. package/src/mesh/tile/drapeResolver.ts +76 -0
  44. package/src/mesh/{tile.test.ts → tile/index.test.ts} +16 -6
  45. package/src/mesh/{tile.ts → tile/index.ts} +124 -392
  46. package/src/mesh/tile/rasterDrapeResolver.ts +423 -0
  47. package/src/mesh/tile/vectorDrapeResolver.ts +404 -0
  48. package/src/orchestrators/RenderPassOrchestrator.ts +25 -0
  49. package/src/passes/CustomRenderPass.ts +21 -5
  50. package/src/passes/RenderTargetCopyPass.ts +24 -2
  51. package/src/test-utils/engine.ts +53 -0
  52. package/src/tileTexture/TileTextureCache.test.ts +2 -0
  53. package/src/tileTexture/TileTextureCache.ts +1 -0
  54. package/src/tileTexture/TileTextureCompositor.test.ts +107 -0
  55. package/src/tileTexture/TileTextureCompositor.ts +248 -32
  56. package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +36 -16
  57. package/src/tileTexture/types.ts +7 -0
  58. package/src/utils/demNoDataColor.test.ts +47 -0
  59. package/src/utils/demNoDataColor.ts +48 -0
  60. package/src/utils/fixedGpuFootprint.test.ts +110 -0
  61. package/src/utils/fixedGpuFootprint.ts +162 -0
  62. package/src/worker/tasks/toCreasedNormals.ts +2 -7
  63. package/dist/assets/fontWorker-CEJEfRpb.js +0 -2
  64. package/dist/assets/fontWorker-CEJEfRpb.js.map +0 -1
  65. package/dist/assets/index-BUB-ijMO.js +0 -12
  66. package/dist/assets/index-BUB-ijMO.js.map +0 -1
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The pure numeric placement kernel used by {@link DeclutterManager}.
3
+ *
4
+ * Given a flat list of candidates and the camera, it projects each anchor to a
5
+ * screen-pixel box, sorts by priority (with placement hysteresis), and greedily
6
+ * claims space in a uniform screen-space grid — returning one `hidden` flag
7
+ * (`0` = shown, `1` = hidden) per candidate, in input order.
8
+ *
9
+ * The production implementation is the Rust `declutterPlace` in
10
+ * `navara_wasm_api` (see {@link wasmDeclutterKernel}); the projection, collision
11
+ * grid, and greedy placement all live there. Injecting it keeps the manager free
12
+ * of an import-time WASM dependency, so its orchestration is unit-tested with a
13
+ * stub. Placement correctness itself is covered by the crate's Rust tests.
14
+ */
15
+ export type DeclutterKernel = {
16
+ /**
17
+ * @param candidates Packed `f64` data, {@link CANDIDATE_STRIDE} values per
18
+ * candidate; pass a subarray of exactly `n * CANDIDATE_STRIDE`.
19
+ * @param view Column-major view matrix (inverse of `camera.matrixWorld`), 16.
20
+ * @param proj Column-major projection matrix, 16.
21
+ * @returns One `hidden` flag (`0`/`1`) per candidate, in input order.
22
+ */
23
+ place(
24
+ candidates: Float64Array,
25
+ view: Float64Array,
26
+ proj: Float64Array,
27
+ cameraX: number,
28
+ cameraY: number,
29
+ cameraZ: number,
30
+ near: number,
31
+ widthPx: number,
32
+ heightPx: number,
33
+ fovRad: number,
34
+ paddingPx: number,
35
+ hysteresisPx: number,
36
+ ): Uint8Array;
37
+ };
38
+
39
+ /**
40
+ * Number of `f64` values per candidate in the packed kernel input. Must stay in
41
+ * sync with `CANDIDATE_STRIDE` in `crates/navara_wasm_api/src/declutter.rs`.
42
+ *
43
+ * Layout: `[anchorX, anchorY, anchorZ, addHeight, minX, maxX, minY, maxY,
44
+ * sizeInMeters, priority, isShown]`.
45
+ */
46
+ export const CANDIDATE_STRIDE = 11;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * One label/sprite competing for screen space in a declutter pass.
3
+ *
4
+ * The local box is expressed around the projected anchor in the shader's
5
+ * billboard-local units — CSS pixels when `sizeInMeters` is false, meters
6
+ * otherwise — with +X right and +Y up (view-space convention). The manager
7
+ * converts it to a screen-pixel AABB using the same `nvr_pxToWorld` math the
8
+ * vertex shaders use, so the collision box matches what is actually drawn.
9
+ */
10
+ export type DeclutterCandidate = {
11
+ /** World-space anchor in ECEF meters (f64), before the height offset. */
12
+ anchorX: number;
13
+ anchorY: number;
14
+ anchorZ: number;
15
+ /** Extra offset in meters along the spherical surface normal — CPU mirror
16
+ * of `mvr_getMvHeightOffset` (sprite_height_pars_vertex.glsl). */
17
+ addHeight: number;
18
+ /** Local box around the anchor (px or meters, see `sizeInMeters`). */
19
+ minX: number;
20
+ maxX: number;
21
+ minY: number;
22
+ maxY: number;
23
+ /** Unit of the local box, mirroring the material's `sizeInMeters`. */
24
+ sizeInMeters: boolean;
25
+ /** Placement priority: higher wins an overlap. */
26
+ priority: number;
27
+ /** Whether the label is currently shown (its fade target is "visible").
28
+ * Feeds placement hysteresis: shown labels win equal-priority ties and
29
+ * tolerate marginal overlaps, so slow camera drift doesn't make near-tied
30
+ * labels flicker in and out. */
31
+ isShown: boolean;
32
+ /** Participant that owns this candidate; receives the placement result. */
33
+ owner: DeclutterParticipant;
34
+ /** Participant-defined id (mesh index for text batches, instance index for
35
+ * sprites) passed back to `applyDeclutter`. */
36
+ handle: number;
37
+ };
38
+
39
+ /** Duration of the show/hide fade applied to placement results (ms). */
40
+ export const DECLUTTER_FADE_MS = 300;
41
+
42
+ /** A mesh that contributes label candidates to the shared declutter pass. */
43
+ export type DeclutterParticipant = {
44
+ /**
45
+ * Append the currently-visible, declutter-enabled candidates to `out`.
46
+ * Called once per placement pass; must not retain `out`.
47
+ */
48
+ collectDeclutterCandidates: (out: DeclutterCandidate[]) => void;
49
+ /**
50
+ * Apply a placement result as the label's fade *target*. Must be cheap and
51
+ * idempotent — the pass calls it for every candidate on every run, most
52
+ * often with an unchanged value.
53
+ */
54
+ applyDeclutter: (handle: number, hidden: boolean) => void;
55
+ /**
56
+ * Advance this participant's hide factors toward their targets by
57
+ * `deltaMs / DECLUTTER_FADE_MS`. Called by the manager every update while
58
+ * anything is fading; returns true while any label is still mid-fade.
59
+ */
60
+ stepDeclutterFade: (deltaMs: number) => boolean;
61
+ };
@@ -0,0 +1,13 @@
1
+ import { declutterPlace } from "@navaramap/engine-api";
2
+
3
+ import type { DeclutterKernel } from "./kernel";
4
+
5
+ /**
6
+ * @internal
7
+ * {@link DeclutterKernel} backed by the Rust `declutterPlace` in
8
+ * `navara_wasm_api`. The WASM module must be initialized (via `initNavaraApi()`)
9
+ * before the first placement pass runs; `ThreeView` awaits that during setup.
10
+ */
11
+ export const wasmDeclutterKernel: DeclutterKernel = {
12
+ place: declutterPlace,
13
+ };
package/src/device.ts CHANGED
@@ -50,6 +50,16 @@ export function isMobileDevice(): boolean {
50
50
  /** Maximum pixel ratio for mobile devices to balance quality vs performance */
51
51
  const MOBILE_MAX_PIXEL_RATIO = 1.0;
52
52
 
53
+ /**
54
+ * Default pixel-ratio cap applied on every mobile device (without opting into
55
+ * `mobileOptimization`). The fixed screen-sized render-target stack (composer
56
+ * buffers, gbuffer MRT, depth textures — ~100 B/px, counted by the ledger as
57
+ * `fixed_gpu_bytes`) grows quadratically with the drawing-buffer size: at
58
+ * DPR 3 it alone can exceed a mobile tile-cache budget and permanently close
59
+ * the load gate. Above DPR 2 the sharpness gain is marginal on phone screens.
60
+ */
61
+ const MOBILE_DEFAULT_MAX_PIXEL_RATIO = 2.0;
62
+
53
63
  export type DevicePixelRatioOptions = {
54
64
  /** User-specified pixel ratio override (takes precedence over all other settings) */
55
65
  override?: number;
@@ -389,7 +399,9 @@ export function getDefaultDynamicSse(): DynamicSseSettings {
389
399
 
390
400
  /**
391
401
  * Gets an appropriate pixel ratio for the current device.
392
- * Caps the ratio on mobile devices only when mobileOptimization is enabled.
402
+ * Mobile devices are capped at 2 by default (see
403
+ * {@link MOBILE_DEFAULT_MAX_PIXEL_RATIO}), and at 1 when mobileOptimization
404
+ * is enabled; an explicit `override` (the `pixelRatio` option) bypasses both.
393
405
  *
394
406
  * @param options - Configuration options for pixel ratio
395
407
  * @returns Pixel ratio appropriate for the device
@@ -405,9 +417,11 @@ export function getDevicePixelRatio(options?: DevicePixelRatioOptions): number {
405
417
 
406
418
  const deviceRatio = window.devicePixelRatio ?? 1;
407
419
 
408
- // Only cap pixel ratio on mobile when mobileOptimization is enabled
409
- if (options?.mobileOptimization && isMobileDevice()) {
410
- return Math.min(deviceRatio, MOBILE_MAX_PIXEL_RATIO);
420
+ if (isMobileDevice()) {
421
+ const cap = options?.mobileOptimization
422
+ ? MOBILE_MAX_PIXEL_RATIO
423
+ : MOBILE_DEFAULT_MAX_PIXEL_RATIO;
424
+ return Math.min(deviceRatio, cap);
411
425
  }
412
426
 
413
427
  return deviceRatio;
@@ -55,6 +55,9 @@ export type EvaluatableMaterialProperty = {
55
55
  size: AvailableMaterialProperty["size"];
56
56
  /** Opacity expression from layer configuration (for polygons/points/text where supported). */
57
57
  opacity: AvailableMaterialProperty["opacity"];
58
+ /** Per-feature declutter placement priority (for points/billboards/text);
59
+ * higher wins an overlap. Overrides the layer's `declutterPriority`. */
60
+ declutterPriority: AvailableMaterialProperty["declutterPriority"];
58
61
  /** Image URL for billboards; packed into a shared per-mesh texture atlas. */
59
62
  image: string;
60
63
  };
@@ -70,6 +73,7 @@ type EvaluatedMaterialProperty = {
70
73
  width: number;
71
74
  size: number;
72
75
  opacity: number;
76
+ declutterPriority: number;
73
77
  image: string | null;
74
78
  };
75
79
 
@@ -308,6 +312,8 @@ export class FeatureEvaluator {
308
312
  * - `width` - Line width in pixels (for polylines)
309
313
  * - `size` - Feature size in pixels (for points/text)
310
314
  * - `opacity` - Feature opacity 0-1
315
+ * - `declutterPriority` - Placement priority for decluttering; higher wins
316
+ * an overlap (for points/billboards/text with `declutter` enabled)
311
317
  * - `image` - Image URL (for billboards); loaded once per distinct URL and
312
318
  * packed into the layer's texture atlas. Return `null` to clear a
313
319
  * previous per-feature image and revert to the material's default `url`;
@@ -387,6 +393,12 @@ export class FeatureEvaluator {
387
393
  if (evaluated.opacity != null) {
388
394
  obj.setFeatureOpacityByBatchId(batchId, evaluated.opacity);
389
395
  }
396
+ if (evaluated.declutterPriority != null) {
397
+ obj.setFeatureDeclutterPriorityByBatchId(
398
+ batchId,
399
+ evaluated.declutterPriority,
400
+ );
401
+ }
390
402
  if ("image" in evaluated) {
391
403
  // Async by nature (the image may need fetching); the atlas dedupes
392
404
  // loads by URL so evaluating many features costs one fetch per image.
@@ -415,6 +427,15 @@ export class FeatureEvaluator {
415
427
  if (evaluated.opacity != null && obj instanceof BatchedSdfTextMesh) {
416
428
  obj.setFeatureOpacityByBatchIndex(batchIndex, evaluated.opacity);
417
429
  }
430
+ if (
431
+ evaluated.declutterPriority != null &&
432
+ obj instanceof BatchedSdfTextMesh
433
+ ) {
434
+ obj.setFeatureDeclutterPriorityByBatchIndex(
435
+ batchIndex,
436
+ evaluated.declutterPriority,
437
+ );
438
+ }
418
439
  return;
419
440
  }
420
441
 
@@ -7,6 +7,7 @@ import type {
7
7
  import type {
8
8
  DelegatedWorkerTasksResult,
9
9
  ElevationDecoder,
10
+ RasterTileState,
10
11
  ReconstructableEntity,
11
12
  ReturnedTransferablePolygonBatchedFeature,
12
13
  ReturnedTransferablePolylineBatchedFeature,
@@ -21,6 +22,7 @@ import type { Texture } from "three";
21
22
  import type { ViewEvents } from "..";
22
23
  import type { ThreeViewCamera } from "../camera";
23
24
  import type { ViewContext } from "../core";
25
+ import type { DeclutterManager } from "../declutter";
24
26
  import type { LayersManager } from "../layersManager";
25
27
  import type { TileMesh } from "../mesh/tile";
26
28
  import type { Scenes, TexturizedSceneByTileCoordinates } from "../scene";
@@ -106,6 +108,10 @@ export type TileHandler = {
106
108
  getTileElevationDecoder: (handle: bigint) => ElevationDecoder | undefined;
107
109
  getVectorTileStates: (handle: bigint) => VectorTileState[] | undefined;
108
110
  vectorRevision: () => number;
111
+ /** The WM raster tiles to bake into per-layer drape render targets for a
112
+ * terrain tile (Geographic terrain only; empty on WebMercator terrain). */
113
+ getRasterTileStates: (handle: bigint) => RasterTileState[] | undefined;
114
+ rasterRevision: () => number;
109
115
  /** Reports a terrain tile's drape render-target GPU footprint (bytes) so the
110
116
  * memory ledger tracks clamp-to-ground vector bakes that scale with terrain
111
117
  * subdivision past the vector `maxZoom`. Pass 0 to release on dispose. */
@@ -115,6 +121,12 @@ export type TileHandler = {
115
121
  textureZoom: number,
116
122
  textureWidth: number,
117
123
  ) => number;
124
+ /** Rust's clamped WebMercator northing (`navara_geometry::mercator_y`).
125
+ * The composite paste derives its per-slot reprojection constants through
126
+ * this so paste and bake (`uv_rect_from_extents_mercator`) share one
127
+ * implementation — a drifting reimplementation misaligns the pasted
128
+ * latitude band (and NaNs on the unclamped polar band). */
129
+ mercatorY: (lat: number) => number;
118
130
  };
119
131
 
120
132
  /**
@@ -209,6 +221,7 @@ type EventContextArgs = {
209
221
  textureFragmentIndex?: Map<string, Set<TextureSlot>>;
210
222
  tileMeshToFragmentIds?: Map<TileMesh, Set<string>>;
211
223
  hillshadeContext?: HillshadeContext;
224
+ declutter?: DeclutterManager;
212
225
  };
213
226
 
214
227
  /**
@@ -243,6 +256,9 @@ export class EventContext {
243
256
  readonly textureFragmentIndex?: Map<string, Set<TextureSlot>>;
244
257
  readonly tileMeshToFragmentIds?: Map<TileMesh, Set<string>>;
245
258
  readonly hillshadeContext?: HillshadeContext;
259
+ /** Shared screen-space label declutterer; text/sprite meshes register as
260
+ * participants on creation. Optional so headless contexts can skip it. */
261
+ readonly declutter?: DeclutterManager;
246
262
 
247
263
  updatedAt = 0;
248
264
 
@@ -275,5 +291,6 @@ export class EventContext {
275
291
  this.textureFragmentIndex = args.textureFragmentIndex;
276
292
  this.tileMeshToFragmentIds = args.tileMeshToFragmentIds;
277
293
  this.hillshadeContext = args.hillshadeContext;
294
+ this.declutter = args.declutter;
278
295
  }
279
296
  }
@@ -60,9 +60,7 @@ export function sumModelGpuBytes(obj: Object3D): number {
60
60
  }
61
61
  }
62
62
  const material = (node as Mesh | Points).material as
63
- | Material
64
- | Material[]
65
- | undefined;
63
+ Material | Material[] | undefined;
66
64
  const materials = Array.isArray(material)
67
65
  ? material
68
66
  : material
@@ -494,9 +494,8 @@ describe("hillshade normal map generation", () => {
494
494
  expect(loadedTexs.has(entityId)).toBe(true);
495
495
  const normalMap = loadedTexs.get(entityId);
496
496
  expect(normalMap).toBeInstanceOf(Texture);
497
- if (normalMap) {
498
- expect((normalMap.image as { width: number }).width).toBe(4); // Content size (no padding in normal map)
499
- }
497
+ // Content size (no padding in normal map)
498
+ expect((normalMap?.image as { width: number } | undefined)?.width).toBe(4);
500
499
 
501
500
  // Verify temp DEM entry was stored for edge updates
502
501
  const tempDem = hillshadeContext.getTempDem(entityId);
@@ -584,16 +583,11 @@ describe("hillshade normal map generation", () => {
584
583
  target_entity_gen: 123,
585
584
  } as HillshadeBackfilledEvent);
586
585
 
587
- if (direction < 3) {
588
- // Not all edges received yet
589
- expect(hillshadeContext.getTempDem(entityId)).toBeDefined();
590
- expect(hillshadeContext.getTempDem(entityId)?.receivedEdges.size).toBe(
591
- direction + 1,
592
- );
593
- } else {
594
- // All 4 edges received, temp DEM should be cleaned up
595
- expect(hillshadeContext.getTempDem(entityId)).toBeUndefined();
596
- }
586
+ // The temp DEM tracks received edges until all 4 arrive, then it is
587
+ // cleaned up (so `getTempDem` returns undefined on the last iteration).
588
+ expect(hillshadeContext.getTempDem(entityId)?.receivedEdges.size).toBe(
589
+ direction < 3 ? direction + 1 : undefined,
590
+ );
597
591
  }
598
592
 
599
593
  // Verify texture still exists after cleanup
@@ -23,7 +23,7 @@ import { BatchedSdfTextMesh, Layer } from "..";
23
23
  import { disposeTexture } from "../loaders";
24
24
  import { getImageDataFromBlob } from "../tasks/getImageDataFromBlob";
25
25
 
26
- import { EventContext } from "./context";
26
+ import { type EventContext } from "./context";
27
27
  import {
28
28
  checkFeatureParallel,
29
29
  processRenderableFeatureAdded,
@@ -463,12 +463,11 @@ async function processRequestedData(ctx: EventContext, req: DataRequestEvent) {
463
463
  return;
464
464
  }
465
465
 
466
- let u8a: Uint8Array | null = new Uint8Array(data);
466
+ const u8a = new Uint8Array(data);
467
467
  buf.setU8(req.handle, req.bits, u8a);
468
468
 
469
469
  // Prevent memory leak
470
470
  u8a.set([]);
471
- u8a = null;
472
471
 
473
472
  data.set([]);
474
473
  })
@@ -0,0 +1,54 @@
1
+ import { mercatorY, type Core } from "@navaramap/engine";
2
+
3
+ import type { TileHandler } from "./context";
4
+
5
+ export type TileHandlerDeps = {
6
+ /** Accessor (not a snapshot) — the Core is created asynchronously after
7
+ * WASM init and can be dropped on dispose. */
8
+ getCore: () => Core | undefined;
9
+ /** Per-frame cached revision snapshots (refreshed once per `_render`), so
10
+ * per-tile `onBeforeRender` gating costs no WASM-boundary call. */
11
+ getVectorRevision: () => number;
12
+ getRasterRevision: () => number;
13
+ };
14
+
15
+ /**
16
+ * Build the {@link TileHandler} context over a WASM `Core`. Extracted from
17
+ * `ThreeView` so tests can assemble the same handler over a shared test engine
18
+ * (`test-utils/engine.ts`) instead of stubbing the Rust-backed functions.
19
+ */
20
+ export function createTileHandler(deps: TileHandlerDeps): TileHandler {
21
+ const { getCore } = deps;
22
+ return {
23
+ getMartini: (id) => {
24
+ return getCore()?.getMartini(id);
25
+ },
26
+ getTile: (handle) => {
27
+ return getCore()?.getTile(handle);
28
+ },
29
+ getParentTile: (handle) => {
30
+ return getCore()?.getParentTile(handle);
31
+ },
32
+ getTileElevationDecoder: (handle) => {
33
+ return getCore()?.getTileElevationDecoder(handle);
34
+ },
35
+ getVectorTileStates: (handle) => {
36
+ return getCore()?.getVectorTileStates(handle);
37
+ },
38
+ vectorRevision: deps.getVectorRevision,
39
+ getRasterTileStates: (handle) => {
40
+ return getCore()?.getRasterTileStates(handle);
41
+ },
42
+ rasterRevision: deps.getRasterRevision,
43
+ reportDrapeGpuBytes: (handle, bytes) => {
44
+ getCore()?.reportTerrainDrapeGpuBytes(handle, bytes);
45
+ },
46
+ calcMetersPerTexel: (tileHandle, textureZoom, textureWidth) => {
47
+ return (
48
+ getCore()?.calcMetersPerTexel(tileHandle, textureZoom, textureWidth) ??
49
+ 1.0
50
+ );
51
+ },
52
+ mercatorY,
53
+ };
54
+ }