@navaramap/three 0.0.1 → 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.
- package/README.md +70 -0
- package/dist/assets/fontWorker-8oWvyvwL.js +2 -0
- package/dist/assets/fontWorker-8oWvyvwL.js.map +1 -0
- package/dist/assets/index-D9G7kIEj.js +25 -0
- package/dist/assets/index-D9G7kIEj.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-Bnck8OGf.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-DG9ORAmj.wasm → navara_wasm_worker_bg-Q8zrkLCR.wasm} +0 -0
- package/dist/draco_decoder.wasm +0 -0
- package/dist/draco_decoder2.wasm +0 -0
- package/dist/index.d.ts +445 -16
- package/dist/index.js +6151 -5717
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +241 -222
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +34 -33
- package/src/core/EffectDesc.ts +2 -4
- package/src/core/MeshDesc.ts +1 -2
- package/src/core/MeshDescWithSelectiveEffect.ts +1 -2
- package/src/declutter/DeclutterManager.test.ts +235 -0
- package/src/declutter/DeclutterManager.ts +256 -0
- package/src/declutter/index.ts +7 -0
- package/src/declutter/kernel.ts +46 -0
- package/src/declutter/types.ts +61 -0
- package/src/declutter/wasmKernel.ts +13 -0
- package/src/device.ts +18 -4
- package/src/evaluations/FeatureEvaluator.ts +21 -0
- package/src/event/context.ts +12 -0
- package/src/event/features/modelGpuBytes.ts +1 -3
- package/src/event/hillshade.test.ts +7 -13
- package/src/event/index.ts +2 -3
- package/src/event/tileHandler.ts +54 -0
- package/src/index.ts +137 -35
- package/src/material/enhancer/ShaderReplacer.ts +1 -2
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +6 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +11 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +13 -3
- package/src/mesh/batchTexture.ts +2 -4
- package/src/mesh/batchedSdfText.ts +61 -1
- package/src/mesh/sdfText.ts +170 -5
- package/src/mesh/sprite/instancedSprite.ts +245 -3
- package/src/orchestrators/RenderPassOrchestrator.ts +25 -0
- package/src/passes/CustomRenderPass.ts +21 -5
- package/src/passes/RenderTargetCopyPass.ts +24 -2
- package/src/test-utils/engine.ts +53 -0
- package/src/tileTexture/TileTextureCompositor.test.ts +2 -0
- package/src/tileTexture/TileTextureCompositor.ts +7 -1
- package/src/utils/fixedGpuFootprint.test.ts +110 -0
- package/src/utils/fixedGpuFootprint.ts +162 -0
- package/src/worker/tasks/toCreasedNormals.ts +2 -7
- package/dist/assets/fontWorker-CCWL5pbx.js +0 -2
- package/dist/assets/fontWorker-CCWL5pbx.js.map +0 -1
- package/dist/assets/index-BdtHOOT9.js +0 -12
- package/dist/assets/index-BdtHOOT9.js.map +0 -1
|
@@ -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
|
-
*
|
|
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
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
package/src/event/context.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type { Texture } from "three";
|
|
|
22
22
|
import type { ViewEvents } from "..";
|
|
23
23
|
import type { ThreeViewCamera } from "../camera";
|
|
24
24
|
import type { ViewContext } from "../core";
|
|
25
|
+
import type { DeclutterManager } from "../declutter";
|
|
25
26
|
import type { LayersManager } from "../layersManager";
|
|
26
27
|
import type { TileMesh } from "../mesh/tile";
|
|
27
28
|
import type { Scenes, TexturizedSceneByTileCoordinates } from "../scene";
|
|
@@ -120,6 +121,12 @@ export type TileHandler = {
|
|
|
120
121
|
textureZoom: number,
|
|
121
122
|
textureWidth: number,
|
|
122
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;
|
|
123
130
|
};
|
|
124
131
|
|
|
125
132
|
/**
|
|
@@ -214,6 +221,7 @@ type EventContextArgs = {
|
|
|
214
221
|
textureFragmentIndex?: Map<string, Set<TextureSlot>>;
|
|
215
222
|
tileMeshToFragmentIds?: Map<TileMesh, Set<string>>;
|
|
216
223
|
hillshadeContext?: HillshadeContext;
|
|
224
|
+
declutter?: DeclutterManager;
|
|
217
225
|
};
|
|
218
226
|
|
|
219
227
|
/**
|
|
@@ -248,6 +256,9 @@ export class EventContext {
|
|
|
248
256
|
readonly textureFragmentIndex?: Map<string, Set<TextureSlot>>;
|
|
249
257
|
readonly tileMeshToFragmentIds?: Map<TileMesh, Set<string>>;
|
|
250
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;
|
|
251
262
|
|
|
252
263
|
updatedAt = 0;
|
|
253
264
|
|
|
@@ -280,5 +291,6 @@ export class EventContext {
|
|
|
280
291
|
this.textureFragmentIndex = args.textureFragmentIndex;
|
|
281
292
|
this.tileMeshToFragmentIds = args.tileMeshToFragmentIds;
|
|
282
293
|
this.hillshadeContext = args.hillshadeContext;
|
|
294
|
+
this.declutter = args.declutter;
|
|
283
295
|
}
|
|
284
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
|
-
|
|
498
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
package/src/event/index.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
type AnyMeshDesc,
|
|
71
71
|
} from "./core/BaseHandle";
|
|
72
72
|
import { Registries } from "./core/Registries";
|
|
73
|
+
import { DeclutterManager, wasmDeclutterKernel } from "./declutter";
|
|
73
74
|
import {
|
|
74
75
|
getCompositeAtlasSize,
|
|
75
76
|
getDefaultDynamicSse,
|
|
@@ -95,6 +96,7 @@ import {
|
|
|
95
96
|
type WorkerTaskHandler,
|
|
96
97
|
} from "./event";
|
|
97
98
|
import { TEXTURE_LOADER } from "./event/loaders";
|
|
99
|
+
import { createTileHandler } from "./event/tileHandler";
|
|
98
100
|
import { InMemoryBufferStore } from "./inMemoryBufferStore";
|
|
99
101
|
import { registerInputEvents } from "./input";
|
|
100
102
|
import { Layer, type LayerEvent } from "./layer";
|
|
@@ -222,6 +224,10 @@ export type MemoryStats = {
|
|
|
222
224
|
gpuBytesEst: number;
|
|
223
225
|
externalCpuBytes: number;
|
|
224
226
|
reservedBytes: number;
|
|
227
|
+
/** Estimated GPU bytes of fixed screen-sized allocations (the
|
|
228
|
+
* postprocessing render-target stack), reported via
|
|
229
|
+
* `Core.setFixedGpuBytes` on init, resize, and pass-list changes. */
|
|
230
|
+
fixedGpuBytes: number;
|
|
225
231
|
budgetBytes: number | undefined;
|
|
226
232
|
evictedCount: number;
|
|
227
233
|
sseMultiplier: number;
|
|
@@ -252,7 +258,10 @@ export type Options = {
|
|
|
252
258
|
container?: HTMLElement;
|
|
253
259
|
/** Canvas element for rendering. If not provided, a new canvas is created. */
|
|
254
260
|
canvas?: HTMLCanvasElement | OffscreenCanvas;
|
|
255
|
-
/** Device pixel ratio override.
|
|
261
|
+
/** Device pixel ratio override. When not specified, the device value is
|
|
262
|
+
* used, capped at 2 on mobile (1 with `mobileOptimization`) — screen-sized
|
|
263
|
+
* render targets grow quadratically with pixel ratio and dominate mobile
|
|
264
|
+
* GPU memory. An explicit value bypasses the caps. */
|
|
256
265
|
pixelRatio?: number;
|
|
257
266
|
/** Disables automatic resize handling on window resize events. */
|
|
258
267
|
disableAutoResize?: boolean;
|
|
@@ -442,6 +451,14 @@ export default class ThreeView<
|
|
|
442
451
|
forceUpdate: false,
|
|
443
452
|
animation: false,
|
|
444
453
|
};
|
|
454
|
+
/** Shared screen-space label declutterer for text/sprite features. */
|
|
455
|
+
private _declutter = new DeclutterManager(wasmDeclutterKernel);
|
|
456
|
+
/** Reusable Vector2 for the declutter pass's per-frame viewport query. */
|
|
457
|
+
private _declutterSize = new Vector2();
|
|
458
|
+
/** Pending follow-up frame for a throttled declutter pass or an active
|
|
459
|
+
* fade, with the delay it was scheduled at (so a shorter need preempts). */
|
|
460
|
+
private _declutterRetry: ReturnType<typeof setTimeout> | undefined;
|
|
461
|
+
private _declutterRetryDelay = 0;
|
|
445
462
|
private _isIdle = false;
|
|
446
463
|
private _uniforms: CommonUniforms;
|
|
447
464
|
|
|
@@ -634,39 +651,11 @@ export default class ThreeView<
|
|
|
634
651
|
private _vectorRevision = 0;
|
|
635
652
|
// Per-frame snapshot of the WASM raster drape resolution revision, refreshed in `_render`.
|
|
636
653
|
private _rasterRevision = 0;
|
|
637
|
-
private _tileHandler: TileHandler = {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
return this._core?.getTile(handle);
|
|
643
|
-
},
|
|
644
|
-
getParentTile: (handle) => {
|
|
645
|
-
return this._core?.getParentTile(handle);
|
|
646
|
-
},
|
|
647
|
-
getTileElevationDecoder: (handle) => {
|
|
648
|
-
return this._core?.getTileElevationDecoder(handle);
|
|
649
|
-
},
|
|
650
|
-
getVectorTileStates: (handle) => {
|
|
651
|
-
return this._core?.getVectorTileStates(handle);
|
|
652
|
-
},
|
|
653
|
-
// Returns the per-frame cached revision (read once in `_render`), so per-tile
|
|
654
|
-
// `onBeforeRender` gating costs no WASM-boundary call.
|
|
655
|
-
vectorRevision: () => this._vectorRevision,
|
|
656
|
-
getRasterTileStates: (handle) => {
|
|
657
|
-
return this._core?.getRasterTileStates(handle);
|
|
658
|
-
},
|
|
659
|
-
rasterRevision: () => this._rasterRevision,
|
|
660
|
-
reportDrapeGpuBytes: (handle, bytes) => {
|
|
661
|
-
this._core?.reportTerrainDrapeGpuBytes(handle, bytes);
|
|
662
|
-
},
|
|
663
|
-
calcMetersPerTexel: (tileHandle, textureZoom, textureWidth) => {
|
|
664
|
-
return (
|
|
665
|
-
this._core?.calcMetersPerTexel(tileHandle, textureZoom, textureWidth) ??
|
|
666
|
-
1.0
|
|
667
|
-
);
|
|
668
|
-
},
|
|
669
|
-
};
|
|
654
|
+
private _tileHandler: TileHandler = createTileHandler({
|
|
655
|
+
getCore: () => this._core,
|
|
656
|
+
getVectorRevision: () => this._vectorRevision,
|
|
657
|
+
getRasterRevision: () => this._rasterRevision,
|
|
658
|
+
});
|
|
670
659
|
private _globeHandler: GlobeHandler = {
|
|
671
660
|
getTransparent: () => {
|
|
672
661
|
return this._core?.getGlobeTransparent();
|
|
@@ -805,6 +794,11 @@ export default class ThreeView<
|
|
|
805
794
|
|
|
806
795
|
private pixelRatioMatchedMedia?: MediaQueryList;
|
|
807
796
|
|
|
797
|
+
private fixedGpuBytesReportScheduled = false;
|
|
798
|
+
private fixedGpuBytesResyncPending = false;
|
|
799
|
+
private framesSinceFixedGpuBytesReport = 0;
|
|
800
|
+
private lastReportedFixedGpuBytes = -1;
|
|
801
|
+
|
|
808
802
|
/**
|
|
809
803
|
* The built-in attribution (credit) UI. `undefined` when disabled via
|
|
810
804
|
* `defaultAttribution: false`, or in a worker / no-DOM environment. Feed it
|
|
@@ -907,6 +901,7 @@ export default class ThreeView<
|
|
|
907
901
|
this._tileTextureCompositor = new TileTextureCompositor({
|
|
908
902
|
renderer: this._renderer,
|
|
909
903
|
texturizedSceneByTileCoordinates: this._texturizedSceneByTileCoordinates,
|
|
904
|
+
mercatorY: this._tileHandler.mercatorY,
|
|
910
905
|
size: getCompositeAtlasSize(this.isMobileOptimized()),
|
|
911
906
|
});
|
|
912
907
|
|
|
@@ -929,6 +924,8 @@ export default class ThreeView<
|
|
|
929
924
|
});
|
|
930
925
|
|
|
931
926
|
this.renderPassOrchestrator.setSize(width, height);
|
|
927
|
+
this.renderPassOrchestrator.onPassesChanged = () =>
|
|
928
|
+
this._scheduleFixedGpuBytesReport();
|
|
932
929
|
|
|
933
930
|
// Background color
|
|
934
931
|
const bgColor = options.backgroundColor
|
|
@@ -1245,6 +1242,7 @@ export default class ThreeView<
|
|
|
1245
1242
|
gpuBytesEst: stats.gpuBytesEst,
|
|
1246
1243
|
externalCpuBytes: stats.externalCpuBytes,
|
|
1247
1244
|
reservedBytes: stats.reservedBytes,
|
|
1245
|
+
fixedGpuBytes: stats.fixedGpuBytes,
|
|
1248
1246
|
budgetBytes: stats.budgetBytes,
|
|
1249
1247
|
evictedCount: stats.evictedCount,
|
|
1250
1248
|
sseMultiplier: stats.sseMultiplier,
|
|
@@ -1370,6 +1368,10 @@ export default class ThreeView<
|
|
|
1370
1368
|
costHints.rasterTileBytes,
|
|
1371
1369
|
);
|
|
1372
1370
|
|
|
1371
|
+
// Passes added before `_core` existed could not be reported; sync the
|
|
1372
|
+
// fixed render-target footprint now that the ledger is reachable.
|
|
1373
|
+
this._scheduleFixedGpuBytesReport();
|
|
1374
|
+
|
|
1373
1375
|
// LOD fog: stronger distance-based degrade on low-memory devices.
|
|
1374
1376
|
const lodFog = {
|
|
1375
1377
|
...getDefaultLodFog({ isMobile: mobileOptimized }),
|
|
@@ -1440,6 +1442,7 @@ export default class ThreeView<
|
|
|
1440
1442
|
textureFragmentIndex: this._textureFragmentIndex,
|
|
1441
1443
|
tileMeshToFragmentIds: this._tileMeshToFragmentIds,
|
|
1442
1444
|
hillshadeContext: this._hillshadeContext,
|
|
1445
|
+
declutter: this._declutter,
|
|
1443
1446
|
});
|
|
1444
1447
|
|
|
1445
1448
|
// Register built-in descriptors
|
|
@@ -1544,6 +1547,10 @@ export default class ThreeView<
|
|
|
1544
1547
|
dispose() {
|
|
1545
1548
|
this._disposed = true;
|
|
1546
1549
|
this._initialized = false;
|
|
1550
|
+
if (this._declutterRetry !== undefined) {
|
|
1551
|
+
clearTimeout(this._declutterRetry);
|
|
1552
|
+
this._declutterRetry = undefined;
|
|
1553
|
+
}
|
|
1547
1554
|
// Dispose the view-owned attribution plugin (other plugins are the caller's).
|
|
1548
1555
|
this._attribution?.dispose();
|
|
1549
1556
|
this._attribution = undefined;
|
|
@@ -1593,6 +1600,10 @@ export default class ThreeView<
|
|
|
1593
1600
|
// Cleanup hillshade context (temp DEMs, generator, etc.)
|
|
1594
1601
|
this._hillshadeContext.dispose();
|
|
1595
1602
|
|
|
1603
|
+
// Drop declutter participants so the manager doesn't retain removed
|
|
1604
|
+
// meshes' GPU/font resources past this view's lifetime.
|
|
1605
|
+
this._declutter.dispose();
|
|
1606
|
+
|
|
1596
1607
|
// Clear caches and maps
|
|
1597
1608
|
this._meshes.clear();
|
|
1598
1609
|
this._inMemoryBufferStore.clear();
|
|
@@ -1661,10 +1672,41 @@ export default class ThreeView<
|
|
|
1661
1672
|
this._pickHelper?.setSize(drawingBufferSize.x, drawingBufferSize.y);
|
|
1662
1673
|
|
|
1663
1674
|
this._core?.resize(w, h, pixelRatio ?? 1);
|
|
1675
|
+
this._scheduleFixedGpuBytesReport();
|
|
1664
1676
|
|
|
1665
1677
|
this.emit("resize", w, h);
|
|
1666
1678
|
};
|
|
1667
1679
|
|
|
1680
|
+
/**
|
|
1681
|
+
* Walks the fixed render-target stack and pushes the estimate into the
|
|
1682
|
+
* memory ledger; the WASM call is skipped while the value is unchanged.
|
|
1683
|
+
*/
|
|
1684
|
+
private _reportFixedGpuBytes(): void {
|
|
1685
|
+
if (this._disposed || !this._core) return;
|
|
1686
|
+
const bytes = this.renderPassOrchestrator.estimateFixedGpuBytes();
|
|
1687
|
+
this.framesSinceFixedGpuBytesReport = 0;
|
|
1688
|
+
if (bytes === this.lastReportedFixedGpuBytes) return;
|
|
1689
|
+
this.lastReportedFixedGpuBytes = bytes;
|
|
1690
|
+
this._core.setFixedGpuBytes(bytes);
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
/**
|
|
1694
|
+
* Re-reports the fixed GPU footprint for event-triggered changes (init,
|
|
1695
|
+
* resize, pass add/remove). Scheduled as a microtask so bursts — e.g.
|
|
1696
|
+
* effect setup during init — coalesce into a single walk, and marks a
|
|
1697
|
+
* one-shot post-render resync because these events run before the GPU has
|
|
1698
|
+
* lazily allocated the affected targets.
|
|
1699
|
+
*/
|
|
1700
|
+
private _scheduleFixedGpuBytesReport(): void {
|
|
1701
|
+
this.fixedGpuBytesResyncPending = true;
|
|
1702
|
+
if (this.fixedGpuBytesReportScheduled) return;
|
|
1703
|
+
this.fixedGpuBytesReportScheduled = true;
|
|
1704
|
+
queueMicrotask(() => {
|
|
1705
|
+
this.fixedGpuBytesReportScheduled = false;
|
|
1706
|
+
this._reportFixedGpuBytes();
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1668
1710
|
private _updateUniforms() {
|
|
1669
1711
|
const viewport = this._getCanvasSize();
|
|
1670
1712
|
const pixelRatio = this._renderer.getPixelRatio();
|
|
@@ -1744,6 +1786,25 @@ export default class ThreeView<
|
|
|
1744
1786
|
}
|
|
1745
1787
|
}
|
|
1746
1788
|
|
|
1789
|
+
/**
|
|
1790
|
+
* Ask for a future frame on behalf of the declutter pass. Runs through a
|
|
1791
|
+
* timer because the main loop clears `forceUpdate` right after `_render` —
|
|
1792
|
+
* a flag set synchronously here would be wiped before the next tick reads
|
|
1793
|
+
* it. A pending longer timer is preempted when a shorter delay is needed
|
|
1794
|
+
* (a fade must not wait out a throttle window).
|
|
1795
|
+
*/
|
|
1796
|
+
private _scheduleDeclutterFrame(delayMs: number) {
|
|
1797
|
+
if (this._declutterRetry !== undefined) {
|
|
1798
|
+
if (delayMs >= this._declutterRetryDelay) return;
|
|
1799
|
+
clearTimeout(this._declutterRetry);
|
|
1800
|
+
}
|
|
1801
|
+
this._declutterRetryDelay = delayMs;
|
|
1802
|
+
this._declutterRetry = setTimeout(() => {
|
|
1803
|
+
this._declutterRetry = undefined;
|
|
1804
|
+
this._renderFlag.forceUpdate = true;
|
|
1805
|
+
}, delayMs);
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1747
1808
|
private _render(updatedAt: number) {
|
|
1748
1809
|
// Read the vector/raster resolution revisions once per frame so each terrain
|
|
1749
1810
|
// tile's `onBeforeRender` can gate its slot re-fetch against them without its
|
|
@@ -1755,6 +1816,29 @@ export default class ThreeView<
|
|
|
1755
1816
|
|
|
1756
1817
|
this._atmosphere._update();
|
|
1757
1818
|
|
|
1819
|
+
// Screen-space label decluttering runs before the render passes so
|
|
1820
|
+
// placement changes land in this frame. Throttled passes and active
|
|
1821
|
+
// fades both need future frames, requested via _scheduleDeclutterFrame.
|
|
1822
|
+
{
|
|
1823
|
+
const size = this._renderer.getDrawingBufferSize(this._declutterSize);
|
|
1824
|
+
const pixelRatio = this._renderer.getPixelRatio();
|
|
1825
|
+
const result = this._declutter.update(
|
|
1826
|
+
this._camera.raw,
|
|
1827
|
+
size.x / pixelRatio,
|
|
1828
|
+
size.y / pixelRatio,
|
|
1829
|
+
updatedAt,
|
|
1830
|
+
);
|
|
1831
|
+
if (result === "throttled") {
|
|
1832
|
+
this._scheduleDeclutterFrame(
|
|
1833
|
+
this._declutter.remainingThrottleMs(updatedAt),
|
|
1834
|
+
);
|
|
1835
|
+
} else if (result === "animating") {
|
|
1836
|
+
// Keep frames coming at fade cadence until every label reaches its
|
|
1837
|
+
// placement target.
|
|
1838
|
+
this._scheduleDeclutterFrame(16);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1758
1842
|
this.emit("preRender", updatedAt);
|
|
1759
1843
|
|
|
1760
1844
|
this.renderPassOrchestrator.render();
|
|
@@ -1763,6 +1847,19 @@ export default class ThreeView<
|
|
|
1763
1847
|
this.shadowMapViewers.render(this._renderer);
|
|
1764
1848
|
|
|
1765
1849
|
this.emit("postRender", updatedAt);
|
|
1850
|
+
|
|
1851
|
+
// Render targets are GL-allocated lazily during rendering (and freed on
|
|
1852
|
+
// resize), so a footprint taken at init/resize/pass-change time goes
|
|
1853
|
+
// stale: re-estimate once on the first frame after such an event, and
|
|
1854
|
+
// otherwise at a low cadence. The report skips the WASM call when the
|
|
1855
|
+
// value is unchanged.
|
|
1856
|
+
if (this.fixedGpuBytesResyncPending) {
|
|
1857
|
+
this.fixedGpuBytesResyncPending = false;
|
|
1858
|
+
this._reportFixedGpuBytes();
|
|
1859
|
+
} else if (++this.framesSinceFixedGpuBytesReport >= 120) {
|
|
1860
|
+
this.framesSinceFixedGpuBytesReport = 0;
|
|
1861
|
+
this._reportFixedGpuBytes();
|
|
1862
|
+
}
|
|
1766
1863
|
}
|
|
1767
1864
|
|
|
1768
1865
|
/**
|
|
@@ -2448,9 +2545,14 @@ export default class ThreeView<
|
|
|
2448
2545
|
const { width, height } = this._getCanvasSize() ?? {};
|
|
2449
2546
|
if (!width || !height) return;
|
|
2450
2547
|
|
|
2548
|
+
// Route through getDevicePixelRatio so a devicePixelRatio change (zoom,
|
|
2549
|
+
// monitor move) cannot bypass the mobile caps applied at init.
|
|
2451
2550
|
const pixelRatio = isWorker()
|
|
2452
2551
|
? (this._options.pixelRatio ?? 1)
|
|
2453
|
-
:
|
|
2552
|
+
: getDevicePixelRatio({
|
|
2553
|
+
override: this._options.pixelRatio,
|
|
2554
|
+
mobileOptimization: this._options.mobileOptimization,
|
|
2555
|
+
});
|
|
2454
2556
|
this.resize(width, height, pixelRatio);
|
|
2455
2557
|
};
|
|
2456
2558
|
|
|
@@ -13,8 +13,7 @@ type PickMarker<Markers extends ShaderMarkers, Key extends keyof Markers> = {
|
|
|
13
13
|
}[keyof Markers[Key]];
|
|
14
14
|
|
|
15
15
|
type PickMarkers<Markers extends ShaderMarkers> =
|
|
16
|
-
| PickMarker<Markers, "
|
|
17
|
-
| PickMarker<Markers, "fragment">;
|
|
16
|
+
PickMarker<Markers, "vertex"> | PickMarker<Markers, "fragment">;
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
19
|
* A type-safe shader string replacer that restricts operations to declared markers.
|