@navaramap/three 0.0.1 → 0.0.3
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-wz61edjR.js +2 -0
- package/dist/assets/fontWorker-wz61edjR.js.map +1 -0
- package/dist/assets/index-ywHSGImj.js +25 -0
- package/dist/assets/index-ywHSGImj.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-Bnck8OGf.wasm → navara_wasm_font_worker_bg-Du9nb2Pn.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-DG9ORAmj.wasm → navara_wasm_worker_bg-Xadu67fw.wasm} +0 -0
- package/dist/draco_decoder.wasm +0 -0
- package/dist/draco_decoder2.wasm +0 -0
- package/dist/index.d.ts +578 -18
- package/dist/index.js +5298 -4843
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +266 -234
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +34 -33
- package/src/atmosphere.ts +43 -1
- 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/feature.ts +43 -13
- package/src/event/features/modelGpuBytes.ts +1 -3
- package/src/event/features/polygon.ts +2 -1
- 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/input.ts +48 -15
- package/src/material/enhancer/ShaderReplacer.ts +1 -2
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +1 -0
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/types.ts +2 -0
- package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +5 -1
- package/src/material/enhancer/polygon/polygonBaseEnhancer/types.ts +2 -0
- package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +2 -0
- package/src/material/enhancer/polyline/polylineBaseEnhancer/types.ts +2 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +8 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +13 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +13 -3
- package/src/material/overrideMaterialsForMRT.ts +7 -2
- package/src/mesh/batchTexture.ts +2 -4
- package/src/mesh/batchedSdfText.ts +294 -70
- package/src/mesh/instanced.ts +1 -1
- package/src/mesh/polygon.ts +11 -4
- package/src/mesh/polyline.ts +10 -2
- package/src/mesh/sdfText.ts +176 -6
- 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/solar.test.ts +33 -0
- package/src/solar.ts +31 -0
- 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
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
|
|
package/src/input.ts
CHANGED
|
@@ -15,6 +15,21 @@ export function registerInputEvents(
|
|
|
15
15
|
|
|
16
16
|
let lastTerrainDistance: number | undefined;
|
|
17
17
|
|
|
18
|
+
// Mousedown and mousemove must share the same coordinate space: the engine
|
|
19
|
+
// re-anchors its cursor position from the mousedown coordinates so the first
|
|
20
|
+
// drag delta is never computed against a stale position (e.g. after the
|
|
21
|
+
// window lost focus while the OS cursor kept moving).
|
|
22
|
+
const normalizePosition = (event: MouseEvent) => {
|
|
23
|
+
const rect = element.getBoundingClientRect();
|
|
24
|
+
const width = element.clientWidth;
|
|
25
|
+
const height = element.clientHeight;
|
|
26
|
+
const aspectRatio = width / height;
|
|
27
|
+
return {
|
|
28
|
+
x: ((event.clientX - rect.left) / width) * aspectRatio,
|
|
29
|
+
y: (event.clientY - rect.top) / height,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
18
33
|
const mousedown = (event: MouseEvent) => {
|
|
19
34
|
mouseEvent = {
|
|
20
35
|
type: "mousedown",
|
|
@@ -22,7 +37,11 @@ export function registerInputEvents(
|
|
|
22
37
|
};
|
|
23
38
|
const terrainDistance = getTerrainDistance?.() ?? undefined;
|
|
24
39
|
lastTerrainDistance = terrainDistance;
|
|
25
|
-
core.input({
|
|
40
|
+
core.input({
|
|
41
|
+
...mouseEvent,
|
|
42
|
+
...normalizePosition(event),
|
|
43
|
+
terrain_distance: terrainDistance,
|
|
44
|
+
});
|
|
26
45
|
};
|
|
27
46
|
const mouseup = () => {
|
|
28
47
|
core.input({
|
|
@@ -33,13 +52,9 @@ export function registerInputEvents(
|
|
|
33
52
|
};
|
|
34
53
|
|
|
35
54
|
const mousemove = (event: MouseEvent) => {
|
|
36
|
-
const width = element.clientWidth;
|
|
37
|
-
const height = element.clientHeight;
|
|
38
|
-
const aspectRatio = width / height;
|
|
39
55
|
core.input({
|
|
40
56
|
type: "mousemove",
|
|
41
|
-
|
|
42
|
-
y: event.clientY / height,
|
|
57
|
+
...normalizePosition(event),
|
|
43
58
|
});
|
|
44
59
|
};
|
|
45
60
|
|
|
@@ -109,36 +124,54 @@ export function registerInputEvents(
|
|
|
109
124
|
key: event?.key,
|
|
110
125
|
});
|
|
111
126
|
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
127
|
+
|
|
128
|
+
// Releases every pressed button and modifier key. When the window loses
|
|
129
|
+
// focus or visibility the matching mouseup/keyup fires in another window, so
|
|
130
|
+
// without this the engine would keep acting on stale pressed state after the
|
|
131
|
+
// user comes back.
|
|
132
|
+
const releaseInputs = () => {
|
|
133
|
+
for (const button of [0, 1, 2]) {
|
|
134
|
+
core.input({ type: "mouseup", button });
|
|
135
|
+
}
|
|
136
|
+
mouseEvent = undefined;
|
|
137
|
+
core.input({ type: "keyup", key_code: "ControlLeft", key: "Control" });
|
|
138
|
+
core.input({ type: "keyup", key_code: "ControlRight", key: "Control" });
|
|
139
|
+
};
|
|
140
|
+
const visibilitychange = () => {
|
|
141
|
+
if (document.visibilityState === "hidden") {
|
|
142
|
+
releaseInputs();
|
|
143
|
+
}
|
|
117
144
|
};
|
|
118
145
|
|
|
119
146
|
element.addEventListener("mousedown", mousedown);
|
|
120
147
|
element.addEventListener("mouseup", mouseup);
|
|
121
|
-
element.addEventListener("mouseleave",
|
|
122
|
-
element.addEventListener("mouseleave", keyupEmpty);
|
|
148
|
+
element.addEventListener("mouseleave", releaseInputs);
|
|
123
149
|
element.addEventListener("mousemove", mousemove);
|
|
124
150
|
element.addEventListener("touchstart", touchstart);
|
|
125
151
|
element.addEventListener("touchend", touchend);
|
|
152
|
+
// The browser cancels active touches on app switches and system gestures;
|
|
153
|
+
// treat them as ended so no stale touch stays in the engine.
|
|
154
|
+
element.addEventListener("touchcancel", touchend);
|
|
126
155
|
element.addEventListener("touchmove", touchmove);
|
|
127
156
|
element.addEventListener("wheel", wheel);
|
|
128
157
|
document.addEventListener("keydown", keydown);
|
|
129
158
|
document.addEventListener("keyup", keyup);
|
|
159
|
+
window.addEventListener("blur", releaseInputs);
|
|
160
|
+
document.addEventListener("visibilitychange", visibilitychange);
|
|
130
161
|
|
|
131
162
|
return () => {
|
|
132
163
|
element.removeEventListener("mousedown", mousedown);
|
|
133
164
|
element.removeEventListener("mouseup", mouseup);
|
|
134
|
-
element.removeEventListener("mouseleave",
|
|
135
|
-
element.removeEventListener("mouseleave", keyupEmpty);
|
|
165
|
+
element.removeEventListener("mouseleave", releaseInputs);
|
|
136
166
|
element.removeEventListener("mousemove", mousemove);
|
|
137
167
|
element.removeEventListener("touchstart", touchstart);
|
|
138
168
|
element.removeEventListener("touchend", touchend);
|
|
169
|
+
element.removeEventListener("touchcancel", touchend);
|
|
139
170
|
element.removeEventListener("touchmove", touchmove);
|
|
140
171
|
element.removeEventListener("wheel", wheel);
|
|
141
172
|
document.removeEventListener("keydown", keydown);
|
|
142
173
|
document.removeEventListener("keyup", keyup);
|
|
174
|
+
window.removeEventListener("blur", releaseInputs);
|
|
175
|
+
document.removeEventListener("visibilitychange", visibilitychange);
|
|
143
176
|
};
|
|
144
177
|
}
|
|
@@ -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.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { encodePosition } from "@navaramap/engine-api";
|
|
2
|
+
import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
|
|
2
3
|
import {
|
|
3
4
|
type Matrix4,
|
|
4
5
|
type Texture,
|
|
@@ -82,6 +83,7 @@ export const createBaseMutates = (
|
|
|
82
83
|
uniforms.uRTCCenterView = refs.uRTCCenterView;
|
|
83
84
|
uniforms.uEyeRTELow = refs.uEyeRTELow;
|
|
84
85
|
uniforms.uEyeRTEHigh = refs.uEyeRTEHigh;
|
|
86
|
+
uniforms.u_rteOne = RTE_ONE_UNIFORM;
|
|
85
87
|
uniforms.uScale = refs.uScale;
|
|
86
88
|
uniforms.uCenter = refs.uCenter;
|
|
87
89
|
uniforms.uSizeInMeters = refs.uSizeInMeters;
|
|
@@ -77,6 +77,8 @@ export type InstancedSpriteBaseRefs = {
|
|
|
77
77
|
uRTCCenterView: UniformValue<Vector3>;
|
|
78
78
|
uEyeRTELow: UniformValue<Vector3>;
|
|
79
79
|
uEyeRTEHigh: UniformValue<Vector3>;
|
|
80
|
+
/** Always 1.0 — blocks fast-math reassociation of the RTE recombination. */
|
|
81
|
+
u_rteOne?: UniformValue<number>;
|
|
80
82
|
uScale: UniformValue<number>;
|
|
81
83
|
uCenter: UniformValue<Vector2>;
|
|
82
84
|
uSizeInMeters: UniformValue<boolean>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
|
|
1
2
|
import type { Matrix4, Texture, Vector3 } from "three";
|
|
2
3
|
import { Matrix4 as ThreeMatrix4, Vector3 as ThreeVector3 } from "three";
|
|
3
4
|
|
|
@@ -78,6 +79,7 @@ export const createBaseMutates = (useRTE: boolean): PolygonBaseMutates => {
|
|
|
78
79
|
uniforms.u_cameraPositionHigh = refs.u_cameraPositionHigh;
|
|
79
80
|
uniforms.u_cameraPositionLow = refs.u_cameraPositionLow;
|
|
80
81
|
uniforms.modelViewMatrixRTE = refs.modelViewMatrixRTE;
|
|
82
|
+
uniforms.u_rteOne = RTE_ONE_UNIFORM;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
// Height uniforms
|
|
@@ -115,7 +115,11 @@ ${ShadowMapDepthVertex}
|
|
|
115
115
|
#include <envmap_vertex>
|
|
116
116
|
|
|
117
117
|
vPosition = absTransformed.xyz;
|
|
118
|
-
|
|
118
|
+
// mvPosition comes from the RTE projection (modelViewMatrixRTE * camera-relative
|
|
119
|
+
// position), so it is precise at any distance. Deriving vViewPosition from
|
|
120
|
+
// modelViewMatrix * absTransformed instead would suffer f32 cancellation at
|
|
121
|
+
// ECEF magnitude and make view-dependent shading wobble as the camera moves.
|
|
122
|
+
vViewPosition = -mvPosition.xyz;
|
|
119
123
|
`,
|
|
120
124
|
useRTE,
|
|
121
125
|
)
|
|
@@ -108,6 +108,8 @@ export type PolygonBaseRefs = {
|
|
|
108
108
|
modelViewMatrixRTE?: UniformValue<Matrix4>;
|
|
109
109
|
u_cameraPositionHigh?: UniformValue<Vector3>;
|
|
110
110
|
u_cameraPositionLow?: UniformValue<Vector3>;
|
|
111
|
+
/** Always 1.0 — blocks fast-math reassociation of the RTE recombination. */
|
|
112
|
+
u_rteOne?: UniformValue<number>;
|
|
111
113
|
};
|
|
112
114
|
|
|
113
115
|
export type PolygonBaseUniforms = Partial<PolygonBaseRefs>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
|
|
1
2
|
import {
|
|
2
3
|
Color as ThreeColor,
|
|
3
4
|
Matrix4 as ThreeMatrix4,
|
|
@@ -101,6 +102,7 @@ export const createBaseMutates = (useRTE: boolean): PolylineBaseMutates => {
|
|
|
101
102
|
uniforms.u_cameraPositionHigh = refs.u_cameraPositionHigh;
|
|
102
103
|
uniforms.u_cameraPositionLow = refs.u_cameraPositionLow;
|
|
103
104
|
uniforms.modelViewMatrixRTE = refs.modelViewMatrixRTE;
|
|
105
|
+
uniforms.u_rteOne = RTE_ONE_UNIFORM;
|
|
104
106
|
}
|
|
105
107
|
|
|
106
108
|
// Batch texture
|
|
@@ -114,6 +114,8 @@ export type PolylineBaseRefs = {
|
|
|
114
114
|
modelViewMatrixRTE?: UniformValue<Matrix4>;
|
|
115
115
|
u_cameraPositionHigh?: UniformValue<Vector3>;
|
|
116
116
|
u_cameraPositionLow?: UniformValue<Vector3>;
|
|
117
|
+
/** Always 1.0 — blocks fast-math reassociation of the RTE recombination. */
|
|
118
|
+
u_rteOne?: UniformValue<number>;
|
|
117
119
|
};
|
|
118
120
|
|
|
119
121
|
export type PolylineBaseUniforms = Partial<PolylineBaseRefs>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { encodePosition } from "@navaramap/engine-api";
|
|
2
|
+
import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
|
|
2
3
|
import { Color, DataTexture, type Matrix4, Vector2, Vector3 } from "three";
|
|
3
4
|
|
|
4
5
|
import type { UniformValue } from "../../../types";
|
|
@@ -44,6 +45,7 @@ export const createBaseMutates = (
|
|
|
44
45
|
uTextWidth: { value: 0.0 },
|
|
45
46
|
uTextHeight: { value: 0.0 },
|
|
46
47
|
uBgYBounds: { value: new Vector2(0.0, 1.0) },
|
|
48
|
+
uDeclutterHide: { value: 0.0 },
|
|
47
49
|
uRTCCenter: {
|
|
48
50
|
value: new Vector3(
|
|
49
51
|
rtcCenter?.[0] ?? 0,
|
|
@@ -113,10 +115,12 @@ export const createBaseMutates = (
|
|
|
113
115
|
uniforms.uTextWidth = refs.uTextWidth;
|
|
114
116
|
uniforms.uTextHeight = refs.uTextHeight;
|
|
115
117
|
uniforms.uBgYBounds = refs.uBgYBounds;
|
|
118
|
+
uniforms.uDeclutterHide = refs.uDeclutterHide;
|
|
116
119
|
uniforms.uRTCCenter = refs.uRTCCenter;
|
|
117
120
|
uniforms.uRTCCenterView = refs.uRTCCenterView;
|
|
118
121
|
uniforms.uEyeRTELow = refs.uEyeRTELow;
|
|
119
122
|
uniforms.uEyeRTEHigh = refs.uEyeRTEHigh;
|
|
123
|
+
uniforms.u_rteOne = RTE_ONE_UNIFORM;
|
|
120
124
|
uniforms.nvr_uBatchId = refs.nvr_uBatchId;
|
|
121
125
|
uniforms.nvr_uPickable = refs.nvr_uPickable;
|
|
122
126
|
uniforms.uAtlas = refs.uAtlas;
|
|
@@ -228,5 +232,9 @@ export const createBaseMutates = (
|
|
|
228
232
|
setBatchId: (id: number) => {
|
|
229
233
|
refs.nvr_uBatchId.value = id;
|
|
230
234
|
},
|
|
235
|
+
|
|
236
|
+
setDeclutterHide: (value: number) => {
|
|
237
|
+
refs.uDeclutterHide.value = value;
|
|
238
|
+
},
|
|
231
239
|
};
|
|
232
240
|
};
|
|
@@ -103,10 +103,16 @@ 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>;
|
|
109
113
|
uEyeRTEHigh: UniformValue<Vector3>;
|
|
114
|
+
/** Always 1.0 — blocks fast-math reassociation of the RTE recombination. */
|
|
115
|
+
u_rteOne?: UniformValue<number>;
|
|
110
116
|
nvr_uBatchId: UniformValue<number>;
|
|
111
117
|
nvr_uPickable: UniformValue<number>;
|
|
112
118
|
uAtlas: UniformValue<DataTexture | null>;
|
|
@@ -180,5 +186,12 @@ export type SdfTextBaseMutates = Mutates<
|
|
|
180
186
|
* Set the batch ID uniform (immutable after mount, but needed for batch creation).
|
|
181
187
|
*/
|
|
182
188
|
setBatchId: (batchId: number) => void;
|
|
189
|
+
/**
|
|
190
|
+
* Set the declutter hide factor (0 = shown … 1 = hidden); the fragment
|
|
191
|
+
* shader scales opacity by `1 - value` and the vertex shader hard-culls
|
|
192
|
+
* at 1. Kept separate from `show`/`opacity` so user-driven visibility and
|
|
193
|
+
* declutter results compose instead of clobbering each other.
|
|
194
|
+
*/
|
|
195
|
+
setDeclutterHide: (value: number) => void;
|
|
183
196
|
}
|
|
184
197
|
>;
|
|
@@ -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
|
+
});
|