@navaramap/three 0.0.0 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +70 -0
  2. package/dist/assets/fontWorker-8oWvyvwL.js +2 -0
  3. package/dist/assets/fontWorker-8oWvyvwL.js.map +1 -0
  4. package/dist/assets/index-D9G7kIEj.js +25 -0
  5. package/dist/assets/index-D9G7kIEj.js.map +1 -0
  6. package/dist/assets/{navara_wasm_font_worker_bg-665OLxjy.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
  7. package/dist/assets/{navara_wasm_worker_bg-BXhKC5bE.wasm → navara_wasm_worker_bg-Q8zrkLCR.wasm} +0 -0
  8. package/dist/draco_decoder.wasm +0 -0
  9. package/dist/draco_decoder2.wasm +0 -0
  10. package/dist/index.d.ts +587 -67
  11. package/dist/index.js +5640 -4944
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.umd.cjs +238 -211
  14. package/dist/index.umd.cjs.map +1 -1
  15. package/package.json +35 -34
  16. package/src/core/EffectDesc.ts +2 -4
  17. package/src/core/MeshDesc.ts +1 -2
  18. package/src/core/MeshDescWithSelectiveEffect.ts +1 -2
  19. package/src/declutter/DeclutterManager.test.ts +235 -0
  20. package/src/declutter/DeclutterManager.ts +256 -0
  21. package/src/declutter/index.ts +7 -0
  22. package/src/declutter/kernel.ts +46 -0
  23. package/src/declutter/types.ts +61 -0
  24. package/src/declutter/wasmKernel.ts +13 -0
  25. package/src/device.ts +18 -4
  26. package/src/evaluations/FeatureEvaluator.ts +21 -0
  27. package/src/event/context.ts +17 -0
  28. package/src/event/features/modelGpuBytes.ts +1 -3
  29. package/src/event/hillshade.test.ts +7 -13
  30. package/src/event/index.ts +2 -3
  31. package/src/event/tileHandler.ts +54 -0
  32. package/src/index.ts +143 -33
  33. package/src/material/enhancer/ShaderReplacer.ts +1 -2
  34. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +6 -0
  35. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +11 -0
  36. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
  37. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +19 -4
  38. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +9 -2
  39. package/src/mesh/batchTexture.ts +2 -4
  40. package/src/mesh/batchedSdfText.ts +61 -1
  41. package/src/mesh/sdfText.ts +170 -5
  42. package/src/mesh/sprite/instancedSprite.ts +245 -3
  43. package/src/mesh/tile/drapeResolver.ts +76 -0
  44. package/src/mesh/{tile.test.ts → tile/index.test.ts} +16 -6
  45. package/src/mesh/{tile.ts → tile/index.ts} +124 -392
  46. package/src/mesh/tile/rasterDrapeResolver.ts +423 -0
  47. package/src/mesh/tile/vectorDrapeResolver.ts +404 -0
  48. package/src/orchestrators/RenderPassOrchestrator.ts +25 -0
  49. package/src/passes/CustomRenderPass.ts +21 -5
  50. package/src/passes/RenderTargetCopyPass.ts +24 -2
  51. package/src/test-utils/engine.ts +53 -0
  52. package/src/tileTexture/TileTextureCache.test.ts +2 -0
  53. package/src/tileTexture/TileTextureCache.ts +1 -0
  54. package/src/tileTexture/TileTextureCompositor.test.ts +107 -0
  55. package/src/tileTexture/TileTextureCompositor.ts +248 -32
  56. package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +36 -16
  57. package/src/tileTexture/types.ts +7 -0
  58. package/src/utils/demNoDataColor.test.ts +47 -0
  59. package/src/utils/demNoDataColor.ts +48 -0
  60. package/src/utils/fixedGpuFootprint.test.ts +110 -0
  61. package/src/utils/fixedGpuFootprint.ts +162 -0
  62. package/src/worker/tasks/toCreasedNormals.ts +2 -7
  63. package/dist/assets/fontWorker-CEJEfRpb.js +0 -2
  64. package/dist/assets/fontWorker-CEJEfRpb.js.map +0 -1
  65. package/dist/assets/index-BUB-ijMO.js +0 -12
  66. package/dist/assets/index-BUB-ijMO.js.map +0 -1
@@ -0,0 +1,423 @@
1
+ import { generate_id_from_ind_gen, type TileHandle } from "@navaramap/core";
2
+ import {
3
+ NearestFilter,
4
+ NoColorSpace,
5
+ RGBAFormat,
6
+ SRGBColorSpace,
7
+ type Texture,
8
+ WebGLRenderTarget,
9
+ } from "three";
10
+
11
+ import type { TileHandler } from "../../event/context";
12
+ import type { TextureOptions } from "../../textures";
13
+ import type {
14
+ RasterBakeSlot,
15
+ TileTextureCompositor,
16
+ } from "../../tileTexture/TileTextureCompositor";
17
+ import { demNoDataColorBytes } from "../../utils/demNoDataColor";
18
+
19
+ import {
20
+ configureTexturizedTexture,
21
+ type DrapeResolver,
22
+ } from "./drapeResolver";
23
+
24
+ import type { TileMaterial } from ".";
25
+
26
+ /**
27
+ * One WebMercator raster tile baked into a baked layer's render target, resolved
28
+ * by Rust (`getRasterTileStates`). `fragmentId` keys the loaded texture in
29
+ * `loadedTexs`; `uvOffset`/`uvScale` are the **mercator** affine framing the
30
+ * terrain tile's sub-region of that (possibly ancestor-resolved) raster tile,
31
+ * driving the bake camera.
32
+ */
33
+ type RasterSource = {
34
+ fragmentId: string;
35
+ uvOffset: [number, number];
36
+ uvScale: [number, number];
37
+ };
38
+
39
+ /**
40
+ * One baked raster layer draped on this terrain tile. Rust emits one
41
+ * fragment-less composite slot per baked layer; the k-th such slot pairs with
42
+ * `ordinal == k` here, and every overlapping WM tile is baked into the slot's
43
+ * one render target. `reproject` carries the terrain `[south, north]` band for
44
+ * the composite's Mercator latitude remap.
45
+ */
46
+ type RasterSlot = {
47
+ ordinal: number;
48
+ sources: RasterSource[];
49
+ reproject?: [number, number];
50
+ };
51
+
52
+ /**
53
+ * The raster half of the {@link DrapeResolver} family: how a terrain tile's
54
+ * raster (non-hillshade) layers get their textures onto its composite material
55
+ * slots. Two implementations express the WebMercator vs Geographic
56
+ * (quantized-mesh terrain) rendering difference; the resolver is chosen once at
57
+ * tile-mesh creation from `Globe.isGeographicTiling` — a runtime scheme flip
58
+ * rebuilds every tile mesh (`init_globe_tiling` drains the tiling), so it
59
+ * never changes mid-life.
60
+ *
61
+ * WebMercator terrain: raster tiles and terrain tiles share the tiling scheme,
62
+ * so each layer drapes as a single identity (or ancestor-fallback) texture
63
+ * through the regular per-slot material path — there is nothing to resolve,
64
+ * bake, or reproject on this side.
65
+ */
66
+ export class DirectRasterDrapeResolver implements DrapeResolver {
67
+ update(): void {}
68
+ syncMaterialSlots(): void {}
69
+ slotReproject(): undefined {
70
+ return undefined;
71
+ }
72
+ invalidate(): void {}
73
+ liveRenderTargetCount(): number {
74
+ return 0;
75
+ }
76
+ dispose(): void {}
77
+ }
78
+
79
+ /** The slice of TileMesh the baked resolver works against. `material` is an
80
+ * accessor because the tile's material instance can be swapped over its life
81
+ * (Basic ↔ Lambert). */
82
+ export type RasterDrapeHost = {
83
+ handle: TileHandle;
84
+ tileHandler: TileHandler;
85
+ compositor: TileTextureCompositor;
86
+ /** Decoded source textures keyed by fragment id (shared event-context map). */
87
+ loadedTexs: Map<string, Texture>;
88
+ textureOptions: TextureOptions;
89
+ /** Drape render target side length in texels. */
90
+ drapeRtSize: number;
91
+ /** End of the raster slot region (`[0, texturizedSceneIndexFrom)`). */
92
+ texturizedSceneIndexFrom: number;
93
+ material: () => TileMaterial;
94
+ /** Re-report the tile's drape GPU footprint after the RT pool changed. */
95
+ reportDrapeGpuBytes: () => void;
96
+ };
97
+
98
+ /**
99
+ * Geographic (quantized-mesh) terrain: WebMercator raster draped cross-scheme.
100
+ * Owns the baked-slot pairing, the per-layer drape render-target pool and the
101
+ * revision/signature gates, mirroring the texturized-vector drape path.
102
+ * The material's k-th fragment-less non-hillshade slot (`bakedSlotIndices[k]`)
103
+ * is backed by `rasterSlots[k]` baked into `renderTargets[k]`.
104
+ */
105
+ export class BakedRasterDrapeResolver implements DrapeResolver {
106
+ // Per-ordinal slots resolved by Rust and their render-target pool.
107
+ private rasterSlots: (RasterSlot | undefined)[] = [];
108
+ private renderTargets: (WebGLRenderTarget | undefined)[] = [];
109
+ // The kind each pooled render target was configured for (heatmap = nearest /
110
+ // NoColorSpace, color = linear / sRGB). Color space is fixed at GL allocation,
111
+ // so a layer flipping kinds must get a fresh target, not a reconfigured one.
112
+ private renderTargetIsHeatmap: (boolean | undefined)[] = [];
113
+ // Absolute composite slot index per baked ordinal, derived from the material
114
+ // in `syncMaterialSlots`.
115
+ private bakedSlotIndices: number[] = [];
116
+ private bakedSlotIsHeatmap: boolean[] = [];
117
+ private prevSignature = "";
118
+ private lastRevision = -1;
119
+ // True once every resolved source's texture was present at bake time: the
120
+ // signature can then only change via a slot refresh (revision bump / material
121
+ // slot change), so the per-frame signature hashing is skipped until one.
122
+ private signatureSettled = false;
123
+
124
+ constructor(private readonly host: RasterDrapeHost) {}
125
+
126
+ update(): void {
127
+ if (this.bakedSlotIndices.length === 0) return;
128
+
129
+ // Gated exactly like the vector drape: refresh on the raster resolve
130
+ // revision, re-bake on a slot/texture signature change.
131
+ const revision = this.host.tileHandler.rasterRevision();
132
+ if (revision !== this.lastRevision) {
133
+ this.lastRevision = revision;
134
+ this.refreshSlots();
135
+ this.signatureSettled = false;
136
+ }
137
+ if (!this.signatureSettled) {
138
+ const signature = this.signature();
139
+ if (signature !== this.prevSignature) {
140
+ this.prevSignature = signature;
141
+ this.bakeSlots();
142
+ }
143
+ // Once every resolved source's texture is present (and thus baked), the
144
+ // signature is stable until the next refresh — skip hashing per frame.
145
+ // A vanished texture always comes with a Rust-side destroy bump, which
146
+ // refreshes and un-settles.
147
+ this.signatureSettled = this.allTexturesLoaded();
148
+ }
149
+ }
150
+
151
+ syncMaterialSlots(
152
+ textureFragments: readonly (string | null | undefined)[] | undefined,
153
+ ): void {
154
+ // A non-hillshade raster slot with no fragment entity is backed by a
155
+ // per-layer baked render target instead of a direct texture. The k-th such
156
+ // slot pairs with `layer_ordinal == k` from `getRasterTileStates` — both
157
+ // sides derive k from the same sorted layer list.
158
+ const ud = this.host.material().userData;
159
+ const prevIndices = this.bakedSlotIndices;
160
+ const prevIsHeatmap = this.bakedSlotIsHeatmap;
161
+ this.bakedSlotIndices = [];
162
+ this.bakedSlotIsHeatmap = [];
163
+ const isHillshades: boolean[] = ud.isHillshades?.value ?? [];
164
+ const isHeatmaps: boolean[] = ud.isElevationHeatmaps?.value ?? [];
165
+ const len = Math.min(
166
+ textureFragments?.length ?? 0,
167
+ this.host.texturizedSceneIndexFrom,
168
+ );
169
+ for (let i = 0; i < len; i++) {
170
+ if (!textureFragments?.[i] && !isHillshades[i]) {
171
+ this.bakedSlotIndices.push(i);
172
+ this.bakedSlotIsHeatmap.push(!!isHeatmaps[i]);
173
+ }
174
+ }
175
+ // The RT pool follows the baked slot count (disposes leftovers when layers
176
+ // vanish).
177
+ this.syncRenderTargets();
178
+ const slotsChanged =
179
+ prevIndices.length !== this.bakedSlotIndices.length ||
180
+ this.bakedSlotIndices.some((v, i) => v !== prevIndices[i]) ||
181
+ this.bakedSlotIsHeatmap.some((v, i) => v !== prevIsHeatmap[i]);
182
+ if (slotsChanged) {
183
+ // Slot pairing changed (layers added/removed/reordered): re-resolve + re-bake.
184
+ this.prevSignature = "";
185
+ this.signatureSettled = false;
186
+ } else {
187
+ // Same slots, and the render targets still hold the last bake — but the
188
+ // material texture reset nulled the bindings, so rebind them without
189
+ // paying a re-bake. Material updates fire on every tile activation during
190
+ // camera motion; an unconditional signature reset here re-baked every such
191
+ // tile every time (an FPS killer).
192
+ this.bindSlots();
193
+ }
194
+ }
195
+
196
+ slotReproject(absSlot: number): [number, number] | undefined {
197
+ // Stashed by `setupTextures` from the Rust material: the per-slot flag plus
198
+ // the tile-wide terrain `[south, north]` band. Baked slots have identity
199
+ // UV, so the band recovered from the affine is exactly the terrain band.
200
+ const ud = this.host.material().userData;
201
+ const layerReproject: number[] = ud.layerReproject ?? [];
202
+ const terrainLatRange: number[] = ud.terrainLatRange ?? [];
203
+ return layerReproject[absSlot] === 1 && terrainLatRange.length === 2
204
+ ? [terrainLatRange[0], terrainLatRange[1]]
205
+ : undefined;
206
+ }
207
+
208
+ invalidate(): void {
209
+ this.prevSignature = "";
210
+ this.signatureSettled = false;
211
+ }
212
+
213
+ liveRenderTargetCount(): number {
214
+ let count = 0;
215
+ for (const rt of this.renderTargets) {
216
+ if (rt) count++;
217
+ }
218
+ return count;
219
+ }
220
+
221
+ dispose(): void {
222
+ for (const rt of this.renderTargets) {
223
+ rt?.dispose();
224
+ }
225
+ this.renderTargets.length = 0;
226
+ this.renderTargetIsHeatmap.length = 0;
227
+ }
228
+
229
+ /**
230
+ * Pull the Rust-resolved baked-raster states for this terrain tile into
231
+ * {@link rasterSlots}: one slot per baked (non-hillshade) raster layer,
232
+ * grouped by `layer_ordinal` — the raster twin of the vector-slot refresh.
233
+ * An unloaded layer leaves a hole (its ordinal stays paired with its material
234
+ * slot; the slot just stays transparent).
235
+ */
236
+ private refreshSlots() {
237
+ const states =
238
+ this.host.tileHandler.getRasterTileStates(this.host.handle) ?? [];
239
+
240
+ const byOrdinal: (RasterSlot | undefined)[] = [];
241
+ for (const state of states) {
242
+ const ordinal = state.layer_ordinal;
243
+ let slot = byOrdinal[ordinal];
244
+ if (!slot) {
245
+ const reproject = state.reproject_terrain_lat;
246
+ slot = {
247
+ ordinal,
248
+ sources: [],
249
+ reproject:
250
+ reproject.length >= 2 ? [reproject[0], reproject[1]] : undefined,
251
+ };
252
+ byOrdinal[ordinal] = slot;
253
+ }
254
+ // Read each wasm getter once — every access crosses the boundary and the
255
+ // uv getters allocate a fresh Float32Array.
256
+ const uvOffset = state.uv_offset;
257
+ const uvScale = state.uv_scale;
258
+ slot.sources.push({
259
+ fragmentId: generate_id_from_ind_gen(
260
+ state.fragment_ind,
261
+ state.fragment_gen,
262
+ ),
263
+ uvOffset: [uvOffset[0] ?? 0, uvOffset[1] ?? 0],
264
+ uvScale: [uvScale[0] ?? 1, uvScale[1] ?? 1],
265
+ });
266
+ // wasm-bindgen objects hold Rust-heap memory; release it deterministically
267
+ // instead of waiting for the GC's finalizer (see guide/WASM_API_POLICY.md).
268
+ state.free();
269
+ }
270
+
271
+ this.rasterSlots = byOrdinal;
272
+ this.syncRenderTargets();
273
+ }
274
+
275
+ /**
276
+ * Size the render-target pool to the live baked slots: allocate lazily per
277
+ * ordinal with sources, dispose when a layer vanishes. Heatmap layers get a
278
+ * value-preserving target (nearest, `NoColorSpace`, no mips) so the encoded
279
+ * DEM survives the bake; color layers mirror the vector drape targets
280
+ * (`configureTexturizedTexture`) plus an sRGB declaration so the composite
281
+ * decodes them exactly like the direct (non-baked) textures.
282
+ */
283
+ private syncRenderTargets() {
284
+ const rts = this.renderTargets;
285
+ const kinds = this.renderTargetIsHeatmap;
286
+ const count = this.bakedSlotIndices.length;
287
+ for (let ordinal = 0; ordinal < count; ordinal++) {
288
+ const active = !!this.rasterSlots[ordinal]?.sources.length;
289
+ const isHeatmap = this.bakedSlotIsHeatmap[ordinal];
290
+ // A layer that flipped kinds (color ↔ heatmap via updateLayer) must not
291
+ // reuse the old target: its color space is baked into the GL allocation,
292
+ // and a heatmap baked into an sRGB/linear-filtered target decodes to
293
+ // garbage elevations. Dispose and let the branch below re-allocate.
294
+ if (rts[ordinal] && kinds[ordinal] !== isHeatmap) {
295
+ rts[ordinal]?.dispose();
296
+ rts[ordinal] = undefined;
297
+ }
298
+ if (active && !rts[ordinal]) {
299
+ const rt = new WebGLRenderTarget(
300
+ this.host.drapeRtSize,
301
+ this.host.drapeRtSize,
302
+ {
303
+ format: RGBAFormat,
304
+ depthBuffer: false,
305
+ stencilBuffer: false,
306
+ },
307
+ );
308
+ if (isHeatmap) {
309
+ rt.texture.minFilter = NearestFilter;
310
+ rt.texture.magFilter = NearestFilter;
311
+ rt.texture.generateMipmaps = false;
312
+ rt.texture.colorSpace = NoColorSpace;
313
+ } else {
314
+ configureTexturizedTexture(rt.texture, this.host.textureOptions);
315
+ rt.texture.colorSpace = SRGBColorSpace;
316
+ }
317
+ rts[ordinal] = rt;
318
+ kinds[ordinal] = isHeatmap;
319
+ } else if (!active && rts[ordinal]) {
320
+ rts[ordinal]?.dispose();
321
+ rts[ordinal] = undefined;
322
+ }
323
+ }
324
+ // Layers removed: drop targets past the live baked-slot count.
325
+ for (let ordinal = count; ordinal < rts.length; ordinal++) {
326
+ rts[ordinal]?.dispose();
327
+ }
328
+ rts.length = count;
329
+ kinds.length = count;
330
+
331
+ this.host.reportDrapeGpuBytes();
332
+ }
333
+
334
+ /**
335
+ * Identity of the baked slot set + each source texture's availability. A change
336
+ * means the sources must be re-baked and re-bound — including a texture arriving
337
+ * in `loadedTexs` after the resolve (decode is async).
338
+ */
339
+ private signature(): string {
340
+ const { loadedTexs } = this.host;
341
+ return this.bakedSlotIndices
342
+ .map((absSlot, ordinal) => {
343
+ const sourceSig = this.rasterSlots[ordinal]?.sources
344
+ .map(
345
+ (s) =>
346
+ `${s.fragmentId}@${s.uvOffset[0]},${s.uvOffset[1]},${s.uvScale[0]},${s.uvScale[1]}#${loadedTexs.has(s.fragmentId) ? 1 : 0}`,
347
+ )
348
+ .join(",");
349
+ return `${absSlot}:${this.bakedSlotIsHeatmap[ordinal] ? 1 : 0}[${sourceSig ?? ""}]`;
350
+ })
351
+ .join("|");
352
+ }
353
+
354
+ /** Whether every resolved source's texture is present in `loadedTexs`. */
355
+ private allTexturesLoaded(): boolean {
356
+ const { loadedTexs } = this.host;
357
+ for (const slot of this.rasterSlots) {
358
+ if (!slot) continue;
359
+ for (const source of slot.sources) {
360
+ if (!loadedTexs.has(source.fragmentId)) return false;
361
+ }
362
+ }
363
+ return true;
364
+ }
365
+
366
+ /**
367
+ * Bake each baked layer's loaded source textures into its render target and
368
+ * bind the targets to the material slots — the raster twin of the
369
+ * `renderVectorScenes` + `bindVectorSlots` pair.
370
+ */
371
+ private bakeSlots() {
372
+ const { loadedTexs } = this.host;
373
+ const ud = this.host.material().userData;
374
+ // The decoder's no-data color: painted under a baked heatmap's sources so
375
+ // uncovered regions decode as "no elevation" (rendered transparent by the
376
+ // composite) — the DEM alpha channel is never used for coverage.
377
+ const noDataColor =
378
+ demNoDataColorBytes(
379
+ ud.elevationRGBScaler?.value ?? { x: 0, y: 0, z: 0 },
380
+ ud.elevationMinMaxHeightAndBoundary?.value.z ?? 0,
381
+ ) ?? undefined;
382
+ const bakeSlots: (RasterBakeSlot | undefined)[] = this.bakedSlotIndices.map(
383
+ (_, ordinal) => {
384
+ const slot = this.rasterSlots[ordinal];
385
+ // No resolved sources → no live render target for this ordinal.
386
+ if (!slot?.sources.length) return undefined;
387
+ const isElevationHeatmap = this.bakedSlotIsHeatmap[ordinal];
388
+ // Loaded textures only; a slot whose textures are all still decoding
389
+ // keeps its (cleared + no-data-painted) target until they land.
390
+ const sources = slot.sources.flatMap((s) => {
391
+ const texture = loadedTexs.get(s.fragmentId);
392
+ return texture
393
+ ? [{ texture, uvOffset: s.uvOffset, uvScale: s.uvScale }]
394
+ : [];
395
+ });
396
+ return {
397
+ isElevationHeatmap,
398
+ noDataColor: isElevationHeatmap ? noDataColor : undefined,
399
+ sources,
400
+ };
401
+ },
402
+ );
403
+ this.host.compositor.renderRasterTiles(bakeSlots, this.renderTargets);
404
+ this.bindSlots();
405
+ this.host.compositor.markDirty(this.host.handle, "raster-revision");
406
+ }
407
+
408
+ /**
409
+ * Bind each baked layer's render target to its material slot. Shows, colors
410
+ * and opacities already cross via the material's per-slot arrays (the baked
411
+ * slot is a regular material slot, just fragment-less); only the texture is
412
+ * bound here. A slot with no target yet stays `null`, which the composite
413
+ * skips — transparent until the first source texture arrives.
414
+ */
415
+ private bindSlots() {
416
+ const textures = this.host.material()?.userData?.textures?.value;
417
+ if (!textures) return;
418
+ for (let ordinal = 0; ordinal < this.bakedSlotIndices.length; ordinal++) {
419
+ const absSlot = this.bakedSlotIndices[ordinal];
420
+ textures[absSlot] = this.renderTargets[ordinal]?.texture ?? null;
421
+ }
422
+ }
423
+ }