@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.
- 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-665OLxjy.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-BXhKC5bE.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 +587 -67
- package/dist/index.js +5640 -4944
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +238 -211
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +35 -34
- 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 +17 -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 +143 -33
- 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 +19 -4
- package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +9 -2
- 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/mesh/tile/drapeResolver.ts +76 -0
- package/src/mesh/{tile.test.ts → tile/index.test.ts} +16 -6
- package/src/mesh/{tile.ts → tile/index.ts} +124 -392
- package/src/mesh/tile/rasterDrapeResolver.ts +423 -0
- package/src/mesh/tile/vectorDrapeResolver.ts +404 -0
- 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/TileTextureCache.test.ts +2 -0
- package/src/tileTexture/TileTextureCache.ts +1 -0
- package/src/tileTexture/TileTextureCompositor.test.ts +107 -0
- package/src/tileTexture/TileTextureCompositor.ts +248 -32
- package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +36 -16
- package/src/tileTexture/types.ts +7 -0
- package/src/utils/demNoDataColor.test.ts +47 -0
- package/src/utils/demNoDataColor.ts +48 -0
- 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-CEJEfRpb.js +0 -2
- package/dist/assets/fontWorker-CEJEfRpb.js.map +0 -1
- package/dist/assets/index-BUB-ijMO.js +0 -12
- package/dist/assets/index-BUB-ijMO.js.map +0 -1
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
|
|
|
@@ -632,35 +649,13 @@ export default class ThreeView<
|
|
|
632
649
|
};
|
|
633
650
|
// Per-frame snapshot of the WASM vector resolution revision, refreshed in `_render`.
|
|
634
651
|
private _vectorRevision = 0;
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
getParentTile: (handle) => {
|
|
643
|
-
return this._core?.getParentTile(handle);
|
|
644
|
-
},
|
|
645
|
-
getTileElevationDecoder: (handle) => {
|
|
646
|
-
return this._core?.getTileElevationDecoder(handle);
|
|
647
|
-
},
|
|
648
|
-
getVectorTileStates: (handle) => {
|
|
649
|
-
return this._core?.getVectorTileStates(handle);
|
|
650
|
-
},
|
|
651
|
-
// Returns the per-frame cached revision (read once in `_render`), so per-tile
|
|
652
|
-
// `onBeforeRender` gating costs no WASM-boundary call.
|
|
653
|
-
vectorRevision: () => this._vectorRevision,
|
|
654
|
-
reportDrapeGpuBytes: (handle, bytes) => {
|
|
655
|
-
this._core?.reportTerrainDrapeGpuBytes(handle, bytes);
|
|
656
|
-
},
|
|
657
|
-
calcMetersPerTexel: (tileHandle, textureZoom, textureWidth) => {
|
|
658
|
-
return (
|
|
659
|
-
this._core?.calcMetersPerTexel(tileHandle, textureZoom, textureWidth) ??
|
|
660
|
-
1.0
|
|
661
|
-
);
|
|
662
|
-
},
|
|
663
|
-
};
|
|
652
|
+
// Per-frame snapshot of the WASM raster drape resolution revision, refreshed in `_render`.
|
|
653
|
+
private _rasterRevision = 0;
|
|
654
|
+
private _tileHandler: TileHandler = createTileHandler({
|
|
655
|
+
getCore: () => this._core,
|
|
656
|
+
getVectorRevision: () => this._vectorRevision,
|
|
657
|
+
getRasterRevision: () => this._rasterRevision,
|
|
658
|
+
});
|
|
664
659
|
private _globeHandler: GlobeHandler = {
|
|
665
660
|
getTransparent: () => {
|
|
666
661
|
return this._core?.getGlobeTransparent();
|
|
@@ -799,6 +794,11 @@ export default class ThreeView<
|
|
|
799
794
|
|
|
800
795
|
private pixelRatioMatchedMedia?: MediaQueryList;
|
|
801
796
|
|
|
797
|
+
private fixedGpuBytesReportScheduled = false;
|
|
798
|
+
private fixedGpuBytesResyncPending = false;
|
|
799
|
+
private framesSinceFixedGpuBytesReport = 0;
|
|
800
|
+
private lastReportedFixedGpuBytes = -1;
|
|
801
|
+
|
|
802
802
|
/**
|
|
803
803
|
* The built-in attribution (credit) UI. `undefined` when disabled via
|
|
804
804
|
* `defaultAttribution: false`, or in a worker / no-DOM environment. Feed it
|
|
@@ -901,6 +901,7 @@ export default class ThreeView<
|
|
|
901
901
|
this._tileTextureCompositor = new TileTextureCompositor({
|
|
902
902
|
renderer: this._renderer,
|
|
903
903
|
texturizedSceneByTileCoordinates: this._texturizedSceneByTileCoordinates,
|
|
904
|
+
mercatorY: this._tileHandler.mercatorY,
|
|
904
905
|
size: getCompositeAtlasSize(this.isMobileOptimized()),
|
|
905
906
|
});
|
|
906
907
|
|
|
@@ -923,6 +924,8 @@ export default class ThreeView<
|
|
|
923
924
|
});
|
|
924
925
|
|
|
925
926
|
this.renderPassOrchestrator.setSize(width, height);
|
|
927
|
+
this.renderPassOrchestrator.onPassesChanged = () =>
|
|
928
|
+
this._scheduleFixedGpuBytesReport();
|
|
926
929
|
|
|
927
930
|
// Background color
|
|
928
931
|
const bgColor = options.backgroundColor
|
|
@@ -1239,6 +1242,7 @@ export default class ThreeView<
|
|
|
1239
1242
|
gpuBytesEst: stats.gpuBytesEst,
|
|
1240
1243
|
externalCpuBytes: stats.externalCpuBytes,
|
|
1241
1244
|
reservedBytes: stats.reservedBytes,
|
|
1245
|
+
fixedGpuBytes: stats.fixedGpuBytes,
|
|
1242
1246
|
budgetBytes: stats.budgetBytes,
|
|
1243
1247
|
evictedCount: stats.evictedCount,
|
|
1244
1248
|
sseMultiplier: stats.sseMultiplier,
|
|
@@ -1364,6 +1368,10 @@ export default class ThreeView<
|
|
|
1364
1368
|
costHints.rasterTileBytes,
|
|
1365
1369
|
);
|
|
1366
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
|
+
|
|
1367
1375
|
// LOD fog: stronger distance-based degrade on low-memory devices.
|
|
1368
1376
|
const lodFog = {
|
|
1369
1377
|
...getDefaultLodFog({ isMobile: mobileOptimized }),
|
|
@@ -1434,6 +1442,7 @@ export default class ThreeView<
|
|
|
1434
1442
|
textureFragmentIndex: this._textureFragmentIndex,
|
|
1435
1443
|
tileMeshToFragmentIds: this._tileMeshToFragmentIds,
|
|
1436
1444
|
hillshadeContext: this._hillshadeContext,
|
|
1445
|
+
declutter: this._declutter,
|
|
1437
1446
|
});
|
|
1438
1447
|
|
|
1439
1448
|
// Register built-in descriptors
|
|
@@ -1538,6 +1547,10 @@ export default class ThreeView<
|
|
|
1538
1547
|
dispose() {
|
|
1539
1548
|
this._disposed = true;
|
|
1540
1549
|
this._initialized = false;
|
|
1550
|
+
if (this._declutterRetry !== undefined) {
|
|
1551
|
+
clearTimeout(this._declutterRetry);
|
|
1552
|
+
this._declutterRetry = undefined;
|
|
1553
|
+
}
|
|
1541
1554
|
// Dispose the view-owned attribution plugin (other plugins are the caller's).
|
|
1542
1555
|
this._attribution?.dispose();
|
|
1543
1556
|
this._attribution = undefined;
|
|
@@ -1587,6 +1600,10 @@ export default class ThreeView<
|
|
|
1587
1600
|
// Cleanup hillshade context (temp DEMs, generator, etc.)
|
|
1588
1601
|
this._hillshadeContext.dispose();
|
|
1589
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
|
+
|
|
1590
1607
|
// Clear caches and maps
|
|
1591
1608
|
this._meshes.clear();
|
|
1592
1609
|
this._inMemoryBufferStore.clear();
|
|
@@ -1655,10 +1672,41 @@ export default class ThreeView<
|
|
|
1655
1672
|
this._pickHelper?.setSize(drawingBufferSize.x, drawingBufferSize.y);
|
|
1656
1673
|
|
|
1657
1674
|
this._core?.resize(w, h, pixelRatio ?? 1);
|
|
1675
|
+
this._scheduleFixedGpuBytesReport();
|
|
1658
1676
|
|
|
1659
1677
|
this.emit("resize", w, h);
|
|
1660
1678
|
};
|
|
1661
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
|
+
|
|
1662
1710
|
private _updateUniforms() {
|
|
1663
1711
|
const viewport = this._getCanvasSize();
|
|
1664
1712
|
const pixelRatio = this._renderer.getPixelRatio();
|
|
@@ -1738,15 +1786,59 @@ export default class ThreeView<
|
|
|
1738
1786
|
}
|
|
1739
1787
|
}
|
|
1740
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
|
+
|
|
1741
1808
|
private _render(updatedAt: number) {
|
|
1742
|
-
// Read the vector resolution
|
|
1743
|
-
// `onBeforeRender` can gate its slot re-fetch against
|
|
1809
|
+
// Read the vector/raster resolution revisions once per frame so each terrain
|
|
1810
|
+
// tile's `onBeforeRender` can gate its slot re-fetch against them without its
|
|
1811
|
+
// own FFI calls.
|
|
1744
1812
|
this._vectorRevision = this._core?.vectorRevision() ?? this._vectorRevision;
|
|
1813
|
+
this._rasterRevision = this._core?.rasterRevision() ?? this._rasterRevision;
|
|
1745
1814
|
|
|
1746
1815
|
this._uniforms.time.value = updatedAt;
|
|
1747
1816
|
|
|
1748
1817
|
this._atmosphere._update();
|
|
1749
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
|
+
|
|
1750
1842
|
this.emit("preRender", updatedAt);
|
|
1751
1843
|
|
|
1752
1844
|
this.renderPassOrchestrator.render();
|
|
@@ -1755,6 +1847,19 @@ export default class ThreeView<
|
|
|
1755
1847
|
this.shadowMapViewers.render(this._renderer);
|
|
1756
1848
|
|
|
1757
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
|
+
}
|
|
1758
1863
|
}
|
|
1759
1864
|
|
|
1760
1865
|
/**
|
|
@@ -2440,9 +2545,14 @@ export default class ThreeView<
|
|
|
2440
2545
|
const { width, height } = this._getCanvasSize() ?? {};
|
|
2441
2546
|
if (!width || !height) return;
|
|
2442
2547
|
|
|
2548
|
+
// Route through getDevicePixelRatio so a devicePixelRatio change (zoom,
|
|
2549
|
+
// monitor move) cannot bypass the mobile caps applied at init.
|
|
2443
2550
|
const pixelRatio = isWorker()
|
|
2444
2551
|
? (this._options.pixelRatio ?? 1)
|
|
2445
|
-
:
|
|
2552
|
+
: getDevicePixelRatio({
|
|
2553
|
+
override: this._options.pixelRatio,
|
|
2554
|
+
mobileOptimization: this._options.mobileOptimization,
|
|
2555
|
+
});
|
|
2446
2556
|
this.resize(width, height, pixelRatio);
|
|
2447
2557
|
};
|
|
2448
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.
|
|
@@ -44,6 +44,7 @@ export const createBaseMutates = (
|
|
|
44
44
|
uTextWidth: { value: 0.0 },
|
|
45
45
|
uTextHeight: { value: 0.0 },
|
|
46
46
|
uBgYBounds: { value: new Vector2(0.0, 1.0) },
|
|
47
|
+
uDeclutterHide: { value: 0.0 },
|
|
47
48
|
uRTCCenter: {
|
|
48
49
|
value: new Vector3(
|
|
49
50
|
rtcCenter?.[0] ?? 0,
|
|
@@ -113,6 +114,7 @@ export const createBaseMutates = (
|
|
|
113
114
|
uniforms.uTextWidth = refs.uTextWidth;
|
|
114
115
|
uniforms.uTextHeight = refs.uTextHeight;
|
|
115
116
|
uniforms.uBgYBounds = refs.uBgYBounds;
|
|
117
|
+
uniforms.uDeclutterHide = refs.uDeclutterHide;
|
|
116
118
|
uniforms.uRTCCenter = refs.uRTCCenter;
|
|
117
119
|
uniforms.uRTCCenterView = refs.uRTCCenterView;
|
|
118
120
|
uniforms.uEyeRTELow = refs.uEyeRTELow;
|
|
@@ -228,5 +230,9 @@ export const createBaseMutates = (
|
|
|
228
230
|
setBatchId: (id: number) => {
|
|
229
231
|
refs.nvr_uBatchId.value = id;
|
|
230
232
|
},
|
|
233
|
+
|
|
234
|
+
setDeclutterHide: (value: number) => {
|
|
235
|
+
refs.uDeclutterHide.value = value;
|
|
236
|
+
},
|
|
231
237
|
};
|
|
232
238
|
};
|
|
@@ -103,6 +103,10 @@ export type SdfTextBaseRefs = {
|
|
|
103
103
|
uTextWidth: UniformValue<number>;
|
|
104
104
|
uTextHeight: UniformValue<number>;
|
|
105
105
|
uBgYBounds: UniformValue<Vector2>;
|
|
106
|
+
/** Screen-space declutter hide factor (0 = shown … 1 = hidden), animated
|
|
107
|
+
* by the declutter fade. Driven by the declutter pass, not by material
|
|
108
|
+
* style updates. */
|
|
109
|
+
uDeclutterHide: UniformValue<number>;
|
|
106
110
|
uRTCCenter: UniformValue<Vector3>;
|
|
107
111
|
uRTCCenterView: UniformValue<Vector3>;
|
|
108
112
|
uEyeRTELow: UniformValue<Vector3>;
|
|
@@ -180,5 +184,12 @@ export type SdfTextBaseMutates = Mutates<
|
|
|
180
184
|
* Set the batch ID uniform (immutable after mount, but needed for batch creation).
|
|
181
185
|
*/
|
|
182
186
|
setBatchId: (batchId: number) => void;
|
|
187
|
+
/**
|
|
188
|
+
* Set the declutter hide factor (0 = shown … 1 = hidden); the fragment
|
|
189
|
+
* shader scales opacity by `1 - value` and the vertex shader hard-culls
|
|
190
|
+
* at 1. Kept separate from `show`/`opacity` so user-driven visibility and
|
|
191
|
+
* declutter results compose instead of clobbering each other.
|
|
192
|
+
*/
|
|
193
|
+
setDeclutterHide: (value: number) => void;
|
|
183
194
|
}
|
|
184
195
|
>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Color, Texture, Vector2 } from "three";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { createTestTileHandler } from "../../../../test-utils/engine";
|
|
5
|
+
import type { RasterCompositeLayer } from "../../../../tileTexture/types";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createCoreUniformMutates,
|
|
9
|
+
type CompositeUniformTarget,
|
|
10
|
+
} from "./mutates";
|
|
11
|
+
|
|
12
|
+
// The real Rust `mercator_y` (clamped to the WM band), reached through the
|
|
13
|
+
// same context handler the app injects — the clamp regression below exercises
|
|
14
|
+
// the production implementation, not a reimplementation.
|
|
15
|
+
const { mercatorY } = createTestTileHandler();
|
|
16
|
+
|
|
17
|
+
const rasterLayer = (
|
|
18
|
+
reproject: [number, number] | undefined,
|
|
19
|
+
): RasterCompositeLayer => ({
|
|
20
|
+
absSlot: 0,
|
|
21
|
+
region: "raster",
|
|
22
|
+
texture: null,
|
|
23
|
+
uvOffset: new Vector2(0, 0),
|
|
24
|
+
uvScale: new Vector2(1, 1),
|
|
25
|
+
kind: "raster",
|
|
26
|
+
color: new Color(1, 1, 1),
|
|
27
|
+
opacity: 1,
|
|
28
|
+
water: false,
|
|
29
|
+
reproject,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const bind = (reproject: [number, number] | undefined) => {
|
|
33
|
+
const mutates = createCoreUniformMutates(mercatorY);
|
|
34
|
+
const target: CompositeUniformTarget = {};
|
|
35
|
+
mutates.attachUniforms(target, 1, new Texture());
|
|
36
|
+
mutates.bindSlot(0, rasterLayer(reproject));
|
|
37
|
+
return target;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe("bindSlot Mercator reprojection constants", () => {
|
|
41
|
+
it("clamps a polar latitude band to the WebMercator limit instead of NaN", () => {
|
|
42
|
+
// A z0 Geographic terrain tile's band is ±90°, and Rust hands it over as
|
|
43
|
+
// f32 — slightly PAST ±π/2, where an unclamped log(tan(...)) is NaN. A
|
|
44
|
+
// NaN span fails the shader's `abs(mDen) > 1e-3` guard, silently skipping
|
|
45
|
+
// reprojection for the whole tile (imagery squashes toward the equator).
|
|
46
|
+
const f32HalfPi = Math.fround(Math.PI / 2);
|
|
47
|
+
const target = bind([-f32HalfPi, f32HalfPi]);
|
|
48
|
+
|
|
49
|
+
const merc = target.uReprojectMerc.value[0];
|
|
50
|
+
// Band start/span from the clamped WM limit: mRs = -π, mDen = 2π.
|
|
51
|
+
expect(merc.x).toBeCloseTo(-Math.PI, 5);
|
|
52
|
+
expect(merc.y).toBeCloseTo(2 * Math.PI, 5);
|
|
53
|
+
// Both edges sit on the WM limit → both polar-cap clamp flags set.
|
|
54
|
+
expect(merc.z).toBe(1);
|
|
55
|
+
expect(merc.w).toBe(1);
|
|
56
|
+
expect(target.uReproject.value[0]).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("keeps an interior band un-clamped and un-flagged", () => {
|
|
60
|
+
const south = 0.2;
|
|
61
|
+
const north = 0.7;
|
|
62
|
+
const target = bind([south, north]);
|
|
63
|
+
|
|
64
|
+
const merc = target.uReprojectMerc.value[0];
|
|
65
|
+
expect(merc.x).toBeCloseTo(mercatorY(south), 9);
|
|
66
|
+
expect(merc.y).toBeCloseTo(mercatorY(north) - mercatorY(south), 9);
|
|
67
|
+
expect(merc.z).toBe(0);
|
|
68
|
+
expect(merc.w).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("flags but does not NaN a band touching one WM edge", () => {
|
|
72
|
+
// z1 north Geographic tile: [0, 90°]. South edge is fine; the north edge
|
|
73
|
+
// must clamp to the WM limit and set only the top polar-cap flag.
|
|
74
|
+
const target = bind([0, Math.fround(Math.PI / 2)]);
|
|
75
|
+
|
|
76
|
+
const merc = target.uReprojectMerc.value[0];
|
|
77
|
+
expect(merc.x).toBeCloseTo(0, 9);
|
|
78
|
+
expect(merc.y).toBeCloseTo(Math.PI, 5);
|
|
79
|
+
expect(merc.z).toBe(1);
|
|
80
|
+
expect(merc.w).toBe(0);
|
|
81
|
+
expect(Number.isFinite(merc.y)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -26,7 +26,11 @@ export type CoreUniformMutates = {
|
|
|
26
26
|
bindSlot: (compactSlot: number, layer: CompositeLayer | undefined) => void;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/** `mercatorY` is Rust's clamped WebMercator northing, injected through the
|
|
30
|
+
* event context (`TileHandler.mercatorY`) like every other engine call. */
|
|
31
|
+
export function createCoreUniformMutates(
|
|
32
|
+
mercatorY: (lat: number) => number,
|
|
33
|
+
): CoreUniformMutates {
|
|
30
34
|
const refs = {
|
|
31
35
|
uShows: new Uniform<number[]>([]),
|
|
32
36
|
uOpacities: new Uniform<number[]>([]),
|
|
@@ -104,8 +108,13 @@ export function createCoreUniformMutates(): CoreUniformMutates {
|
|
|
104
108
|
refs.uLayerUvScale.value[k].set(1, 1);
|
|
105
109
|
}
|
|
106
110
|
|
|
111
|
+
// Reproject applies to color rasters and elevation heatmaps alike — the
|
|
112
|
+
// shader warps the per-slot UV before either sampler runs; hillshade is
|
|
113
|
+
// terrain-side and never reprojects.
|
|
107
114
|
const reproject =
|
|
108
|
-
layer && layer.kind === "raster"
|
|
115
|
+
layer && (layer.kind === "raster" || layer.kind === "elevationHeatmap")
|
|
116
|
+
? layer.reproject
|
|
117
|
+
: undefined;
|
|
109
118
|
if (reproject && layer) {
|
|
110
119
|
const [Ts, Tn] = reproject;
|
|
111
120
|
// Recover the source tile's latitude band [Rs, Rn] from the affine y
|
|
@@ -115,8 +124,14 @@ export function createCoreUniformMutates(): CoreUniformMutates {
|
|
|
115
124
|
const span = (Tn - Ts) / layer.uvScale.y;
|
|
116
125
|
const Rs = Ts - layer.uvOffset.y * span;
|
|
117
126
|
const Rn = Rs + span;
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
// Rust's clamped Mercator northing — the same function the bake affine
|
|
128
|
+
// uses (`uv_rect_from_extents_mercator`), so the pasted band stays
|
|
129
|
+
// aligned with the baked render target. The clamp matters: a polar
|
|
130
|
+
// tile's band reaches ±90° (as f32, slightly past it), where unclamped
|
|
131
|
+
// log(tan(...)) returns NaN — and a NaN span fails the shader's
|
|
132
|
+
// `abs(mDen)` guard, silently disabling reprojection for the tile.
|
|
133
|
+
const mRs = mercatorY(Rs);
|
|
134
|
+
const mRn = mercatorY(Rn);
|
|
120
135
|
// Polar cap: clamp the overshoot onto the band-edge texel when this tile's
|
|
121
136
|
// north/south edge sits on the WebMercator latitude limit.
|
|
122
137
|
const clampTop = Rn >= WM_MAX_LAT - 1e-4 ? 1 : 0;
|
|
@@ -10,6 +10,12 @@ export const elevationIncludes = (): string => ElevationParsFragment;
|
|
|
10
10
|
/**
|
|
11
11
|
* Replaces the per-slot sampler with a DEM-decode + colormap lookup; non-heatmap
|
|
12
12
|
* slots fall back to the straight raster fetch in the else branch.
|
|
13
|
+
*
|
|
14
|
+
* The heatmap alpha derives from the decode's validity: a no-data (boundary)
|
|
15
|
+
* texel — including a baked drape render target's uncovered regions, which the
|
|
16
|
+
* bake paints with the boundary color — renders transparent instead of being
|
|
17
|
+
* colormapped as height 0. The DEM texel's alpha channel is never read: it is
|
|
18
|
+
* not part of any elevation encoding and stays reserved for future RGBA ones.
|
|
13
19
|
*/
|
|
14
20
|
export const elevationSampleProducer = ({
|
|
15
21
|
k,
|
|
@@ -17,8 +23,9 @@ export const elevationSampleProducer = ({
|
|
|
17
23
|
vec4 texColor${k};
|
|
18
24
|
if (uIsElevationHeatmaps[${k}]) {
|
|
19
25
|
ivec2 demTexSize = textureSize(uTextures[${k}], 0);
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
bool demValid${k};
|
|
27
|
+
float normalized_h = sampleElevationBilinear(uTextures[${k}], texUv${k}, demTexSize, demValid${k});
|
|
28
|
+
texColor${k} = vec4(texture2D(uColorMapTexture, vec2(normalized_h, 0.5)).rgb, demValid${k} ? 1.0 : 0.0);
|
|
22
29
|
} else {
|
|
23
30
|
texColor${k} = texture2D(uTextures[${k}], texUv${k}) * vec4(uColors[${k}], 1.0);
|
|
24
31
|
}`;
|
package/src/mesh/batchTexture.ts
CHANGED
|
@@ -183,8 +183,7 @@ export function getRowIndex(
|
|
|
183
183
|
row: BatchTextureRowKey,
|
|
184
184
|
): number {
|
|
185
185
|
const config = material.userData.batchTextureConfig as
|
|
186
|
-
|
|
|
187
|
-
| undefined;
|
|
186
|
+
BatchTextureConfig | undefined;
|
|
188
187
|
invariant(config);
|
|
189
188
|
|
|
190
189
|
const rowIndex = config.rows.indexOf(row);
|
|
@@ -238,8 +237,7 @@ export function updateBatchAttribute(
|
|
|
238
237
|
const data = texture.image.data as Float32Array;
|
|
239
238
|
const texWidth = texture.image.width;
|
|
240
239
|
const config = material.userData.batchTextureConfig as
|
|
241
|
-
|
|
|
242
|
-
| undefined;
|
|
240
|
+
BatchTextureConfig | undefined;
|
|
243
241
|
const rowCount = config?.rows.length ?? BATCH_TEXTURE_ROW.length;
|
|
244
242
|
|
|
245
243
|
switch (attribute) {
|
|
@@ -6,6 +6,10 @@ import type { FontManager } from "@navaramap/font";
|
|
|
6
6
|
import { type Color } from "three";
|
|
7
7
|
import invariant from "tiny-invariant";
|
|
8
8
|
|
|
9
|
+
import type {
|
|
10
|
+
DeclutterCandidate,
|
|
11
|
+
DeclutterParticipant,
|
|
12
|
+
} from "../declutter/types";
|
|
9
13
|
import type { EventContext } from "../event/context";
|
|
10
14
|
|
|
11
15
|
import { InstancedMesh, type InstancedMeshOptions } from "./instanced";
|
|
@@ -36,7 +40,7 @@ type PositionsInfo = PositionsInfoBase &
|
|
|
36
40
|
|
|
37
41
|
export class BatchedSdfTextMesh
|
|
38
42
|
extends InstancedMesh<SDFTextMesh>
|
|
39
|
-
implements PickableMesh
|
|
43
|
+
implements PickableMesh, DeclutterParticipant
|
|
40
44
|
{
|
|
41
45
|
readonly ctx: EventContext;
|
|
42
46
|
/** The font identifier from material — may be a family name or a URL. */
|
|
@@ -79,6 +83,41 @@ export class BatchedSdfTextMesh
|
|
|
79
83
|
this._highQuality,
|
|
80
84
|
() => this._revalidateStaleMeshes(),
|
|
81
85
|
);
|
|
86
|
+
ctx.declutter?.register(this);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- DeclutterParticipant ---
|
|
90
|
+
|
|
91
|
+
collectDeclutterCandidates(out: DeclutterCandidate[]): void {
|
|
92
|
+
if (!this.visible) return;
|
|
93
|
+
const meshes = this.meshes();
|
|
94
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
95
|
+
const candidate = meshes[i].getDeclutterCandidate(this, i);
|
|
96
|
+
if (candidate) out.push(candidate);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
applyDeclutter(handle: number, hidden: boolean): void {
|
|
101
|
+
this.meshes()[handle]?.setDeclutterHidden(hidden);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
stepDeclutterFade(deltaMs: number): boolean {
|
|
105
|
+
let animating = false;
|
|
106
|
+
for (const mesh of this.meshes()) {
|
|
107
|
+
animating = mesh.stepDeclutterFade(deltaMs) || animating;
|
|
108
|
+
}
|
|
109
|
+
return animating;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Candidates changed (text, style, position, visibility): ask the shared
|
|
113
|
+
* declutter pass to re-place on its next update. */
|
|
114
|
+
private _markDeclutterDirty(): void {
|
|
115
|
+
this.ctx.declutter?.markDirty();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
override setActive(active: boolean) {
|
|
119
|
+
super.setActive(active);
|
|
120
|
+
this._markDeclutterDirty();
|
|
82
121
|
}
|
|
83
122
|
|
|
84
123
|
/**
|
|
@@ -111,6 +150,7 @@ export class BatchedSdfTextMesh
|
|
|
111
150
|
// Force a rebuild even though the text string is unchanged: the baked
|
|
112
151
|
// atlas rects and glyph retains must refresh against the new metrics.
|
|
113
152
|
mesh.setText(text, true);
|
|
153
|
+
this._markDeclutterDirty();
|
|
114
154
|
this._needRender?.();
|
|
115
155
|
})
|
|
116
156
|
.catch((err: unknown) => {
|
|
@@ -222,6 +262,8 @@ export class BatchedSdfTextMesh
|
|
|
222
262
|
: position.subarray(posIdx, posIdx + positionSize);
|
|
223
263
|
this.meshes()[i].setPosition(pos, RTE, transform);
|
|
224
264
|
}
|
|
265
|
+
// Anchors moved (e.g. terrain height resolution) — re-place labels.
|
|
266
|
+
this._markDeclutterDirty();
|
|
225
267
|
}
|
|
226
268
|
|
|
227
269
|
const fontIdentifier = m.material.font ?? this._fontIdentifier;
|
|
@@ -292,6 +334,7 @@ export class BatchedSdfTextMesh
|
|
|
292
334
|
mesh.update(material, forceUpdate);
|
|
293
335
|
this.markVisibility(mesh);
|
|
294
336
|
}
|
|
337
|
+
this._markDeclutterDirty();
|
|
295
338
|
|
|
296
339
|
if (needRender) needRender();
|
|
297
340
|
}
|
|
@@ -412,6 +455,7 @@ export class BatchedSdfTextMesh
|
|
|
412
455
|
// Restore intended visibility now that text content is available
|
|
413
456
|
mesh.visible = intendedVisible;
|
|
414
457
|
this.markVisibility(mesh);
|
|
458
|
+
this._markDeclutterDirty();
|
|
415
459
|
this._needRender?.();
|
|
416
460
|
})
|
|
417
461
|
.catch((err: unknown) => {
|
|
@@ -422,6 +466,7 @@ export class BatchedSdfTextMesh
|
|
|
422
466
|
}
|
|
423
467
|
mesh.setText(text);
|
|
424
468
|
this.markVisibility(mesh);
|
|
469
|
+
this._markDeclutterDirty();
|
|
425
470
|
this._needRender?.();
|
|
426
471
|
}
|
|
427
472
|
}
|
|
@@ -438,6 +483,7 @@ export class BatchedSdfTextMesh
|
|
|
438
483
|
if (mesh) {
|
|
439
484
|
mesh._setFeatureShow(rawVisible);
|
|
440
485
|
this.markVisibility(mesh);
|
|
486
|
+
this._markDeclutterDirty();
|
|
441
487
|
}
|
|
442
488
|
}
|
|
443
489
|
|
|
@@ -445,6 +491,7 @@ export class BatchedSdfTextMesh
|
|
|
445
491
|
const mesh = this.meshes()[batchIndex];
|
|
446
492
|
if (mesh) {
|
|
447
493
|
mesh.setHeight(height);
|
|
494
|
+
this._markDeclutterDirty();
|
|
448
495
|
}
|
|
449
496
|
}
|
|
450
497
|
|
|
@@ -452,6 +499,18 @@ export class BatchedSdfTextMesh
|
|
|
452
499
|
const mesh = this.meshes()[batchIndex];
|
|
453
500
|
if (mesh) {
|
|
454
501
|
mesh.setSize(size);
|
|
502
|
+
this._markDeclutterDirty();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
setFeatureDeclutterPriorityByBatchIndex(
|
|
507
|
+
batchIndex: number,
|
|
508
|
+
priority: number,
|
|
509
|
+
) {
|
|
510
|
+
const mesh = this.meshes()[batchIndex];
|
|
511
|
+
if (mesh) {
|
|
512
|
+
mesh.setDeclutterPriority(priority);
|
|
513
|
+
this._markDeclutterDirty();
|
|
455
514
|
}
|
|
456
515
|
}
|
|
457
516
|
|
|
@@ -466,6 +525,7 @@ export class BatchedSdfTextMesh
|
|
|
466
525
|
}
|
|
467
526
|
|
|
468
527
|
dispose() {
|
|
528
|
+
this.ctx.declutter?.unregister(this);
|
|
469
529
|
this._unsubscribeEvict?.();
|
|
470
530
|
this._unsubscribeEvict = undefined;
|
|
471
531
|
const q = this._highQuality;
|