@navaramap/three 0.0.0 → 0.0.1

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 (33) hide show
  1. package/dist/assets/fontWorker-CCWL5pbx.js +2 -0
  2. package/dist/assets/fontWorker-CCWL5pbx.js.map +1 -0
  3. package/dist/assets/index-BdtHOOT9.js +12 -0
  4. package/dist/assets/index-BdtHOOT9.js.map +1 -0
  5. package/dist/assets/{navara_wasm_font_worker_bg-665OLxjy.wasm → navara_wasm_font_worker_bg-Bnck8OGf.wasm} +0 -0
  6. package/dist/assets/{navara_wasm_worker_bg-BXhKC5bE.wasm → navara_wasm_worker_bg-DG9ORAmj.wasm} +0 -0
  7. package/dist/index.d.ts +142 -51
  8. package/dist/index.js +4772 -4510
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.umd.cjs +226 -218
  11. package/dist/index.umd.cjs.map +1 -1
  12. package/package.json +13 -13
  13. package/src/event/context.ts +5 -0
  14. package/src/index.ts +10 -2
  15. package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +6 -1
  16. package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +9 -2
  17. package/src/mesh/tile/drapeResolver.ts +76 -0
  18. package/src/mesh/{tile.test.ts → tile/index.test.ts} +16 -6
  19. package/src/mesh/{tile.ts → tile/index.ts} +124 -392
  20. package/src/mesh/tile/rasterDrapeResolver.ts +423 -0
  21. package/src/mesh/tile/vectorDrapeResolver.ts +404 -0
  22. package/src/tileTexture/TileTextureCache.test.ts +2 -0
  23. package/src/tileTexture/TileTextureCache.ts +1 -0
  24. package/src/tileTexture/TileTextureCompositor.test.ts +105 -0
  25. package/src/tileTexture/TileTextureCompositor.ts +241 -31
  26. package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +36 -16
  27. package/src/tileTexture/types.ts +7 -0
  28. package/src/utils/demNoDataColor.test.ts +47 -0
  29. package/src/utils/demNoDataColor.ts +48 -0
  30. package/dist/assets/fontWorker-CEJEfRpb.js +0 -2
  31. package/dist/assets/fontWorker-CEJEfRpb.js.map +0 -1
  32. package/dist/assets/index-BUB-ijMO.js +0 -12
  33. package/dist/assets/index-BUB-ijMO.js.map +0 -1
@@ -20,7 +20,6 @@ import {
20
20
  Mesh,
21
21
  MeshLambertMaterial,
22
22
  RedFormat,
23
- RGBAFormat,
24
23
  SRGBColorSpace,
25
24
  Texture,
26
25
  UnsignedByteType,
@@ -29,7 +28,6 @@ import {
29
28
  Vector4,
30
29
  AddOperation,
31
30
  Object3D,
32
- WebGLRenderTarget,
33
31
  type MagnificationTextureFilter,
34
32
  type MinificationTextureFilter,
35
33
  ShaderChunk,
@@ -41,9 +39,8 @@ import {
41
39
  LinearFilter,
42
40
  } from "three";
43
41
 
44
- import { PolygonMesh, PolylineMesh } from "..";
45
- import { setTransform } from "../event";
46
- import type { EventContext, TileHandler } from "../event/context";
42
+ import { setTransform } from "../../event";
43
+ import type { EventContext, TileHandler } from "../../event/context";
47
44
  import {
48
45
  generateTileCommonInjection,
49
46
  generateTileMapFragment,
@@ -52,61 +49,37 @@ import {
52
49
  TILE_NORMAL_BUFFER_REPLACEMENT,
53
50
  TILE_PICK_FRAGMENT_OVERRIDE,
54
51
  TILE_VERTEX_INJECTIONS,
55
- } from "../material";
56
- import { deriveCompositeFeatures } from "../material/enhancer/tileComposite";
57
- import type { CustomObject3DEventMap } from "../object3DEvent";
58
- import type { TexturizedSceneByTileCoordinates } from "../scene";
59
- import type { TextureOptions } from "../textures";
52
+ } from "../../material";
53
+ import { deriveCompositeFeatures } from "../../material/enhancer/tileComposite";
54
+ import type { CustomObject3DEventMap } from "../../object3DEvent";
55
+ import type { TextureOptions } from "../../textures";
60
56
  import {
61
57
  planSlots,
62
58
  type CompositeFeatures,
63
59
  type CompositeGlobals,
64
60
  type CompositeLayer,
65
61
  type TileTextureCompositor,
66
- } from "../tileTexture";
67
- import type { TileMapByHandle } from "../type";
68
- import type { CommonUniforms } from "../uniforms";
69
- import { createReplacer } from "../utils";
62
+ } from "../../tileTexture";
63
+ import type { TileMapByHandle } from "../../type";
64
+ import type { CommonUniforms } from "../../uniforms";
65
+ import { createReplacer } from "../../utils";
70
66
  import {
71
67
  type TextureSlot,
72
68
  updateTextureFragmentIndex,
73
69
  removeTextureFragmentIndex,
74
- } from "../utils/textureFragmentIndex";
70
+ } from "../../utils/textureFragmentIndex";
71
+ import type { PickableMesh } from "../pickableMesh";
72
+ import { releaseGeometryArraysAfterUpload } from "../releaseGeometryArrays";
75
73
 
76
- import type { PickableMesh } from "./pickableMesh";
77
- import { releaseGeometryArraysAfterUpload } from "./releaseGeometryArrays";
74
+ import type { DrapeResolver } from "./drapeResolver";
75
+ import {
76
+ BakedRasterDrapeResolver,
77
+ DirectRasterDrapeResolver,
78
+ } from "./rasterDrapeResolver";
79
+ import { VectorDrapeResolver } from "./vectorDrapeResolver";
78
80
 
79
81
  export type TileMaterial = MeshBasicMaterial | MeshLambertMaterial;
80
82
 
81
- /**
82
- * One WebMercator vector source tile baked into a layer's render target.
83
- * `tileHandle` keys the cached offscreen scene; `uvOffset`/`uvScale` are the
84
- * **mercator** affine framing the terrain tile's sub-region of that (possibly
85
- * coarser, ancestor-resolved) source tile, driving the bake camera (identity for
86
- * an exact same-tile drape).
87
- */
88
- type VectorSource = {
89
- tileHandle: TileHandle;
90
- uvOffset: [number, number];
91
- uvScale: [number, number];
92
- };
93
-
94
- /**
95
- * One clamp-to-ground vector layer draped on this terrain tile, resolved by Rust.
96
- * A layer is backed by one source on WebMercator terrain, but by several when the
97
- * terrain is Geographic (N:M): every overlapping WM vector tile is baked into the
98
- * layer's single render target, each framed by its own `uvOffset`/`uvScale`, so
99
- * the RT ends up spanning the terrain tile's extent. `reproject` carries the
100
- * terrain `[south, north]` band on Geographic terrain (undefined on WM) so the
101
- * composite paste can remap the latitude axis WebMercator→Geographic, exactly
102
- * like a raster slot.
103
- */
104
- type VectorSlot = {
105
- layerId: string;
106
- sources: VectorSource[];
107
- reproject?: [number, number];
108
- };
109
-
110
83
  /** The MRT composite atlas has three RGBA attachments (color, attr, normal). */
111
84
  const ATLAS_ATTACHMENTS = 3;
112
85
  /** GPU bytes for one drape render target at side length `size`: size² RGBA. */
@@ -126,32 +99,6 @@ function compositeAtlasBytes(size: number): number {
126
99
  return size * size * 4 * ATLAS_ATTACHMENTS;
127
100
  }
128
101
 
129
- /** Shared 1×1 transparent texture bound to texturized sampler slots that have
130
- * no drape render target yet (they are allocated lazily). WebGL requires every
131
- * sampler to reference a valid texture; the shader gates sampling with `shows`,
132
- * so this placeholder's content is never visible. */
133
- const EMPTY_TEXTURIZED_TEXTURE = new DataTexture(
134
- new Uint8Array([0, 0, 0, 0]),
135
- 1,
136
- 1,
137
- RGBAFormat,
138
- );
139
- EMPTY_TEXTURIZED_TEXTURE.needsUpdate = true;
140
-
141
- /** Apply the shared texturized-slot sampler settings to a drape render target's
142
- * texture. Returns the texture for call-site chaining. */
143
- function configureTexturizedTexture(
144
- tex: Texture,
145
- textureOptions: TextureOptions,
146
- ): Texture {
147
- tex.minFilter = textureOptions.minFilter as MinificationTextureFilter;
148
- tex.magFilter = textureOptions.magFilter as MagnificationTextureFilter;
149
- tex.anisotropy = textureOptions.maxAnisotropy;
150
- tex.generateMipmaps = textureOptions.useMipmaps;
151
- tex.needsUpdate = true;
152
- return tex;
153
- }
154
-
155
102
  export class TileMesh
156
103
  extends Mesh<BufferGeometry, TileMaterial, CustomObject3DEventMap>
157
104
  implements PickableMesh
@@ -162,24 +109,19 @@ export class TileMesh
162
109
  texturizedSceneIndexFrom: number;
163
110
  numTexturizedVector: number;
164
111
 
165
- // Per-layer texturized-vector slots resolved by Rust: one slot per clamp-to-ground
166
- // layer, keyed by the WM vector tile whose offscreen scene backs it. Ancestor
167
- // fallback is already resolved in Rust, so the web side just caches + bakes.
168
- private vectorSlots: VectorSlot[] = [];
169
- // Signature of the last baked slot set; a change drives a re-bake + re-bind.
170
- private prevVectorSignature = "";
171
- // The vector resolution revision at which `vectorSlots` was last fetched. Re-fetching
172
- // is skipped while the global revision is unchanged (the resolve result can only change
173
- // when a traverse runs or a scene becomes ready). `-1` forces a fetch on the first frame.
174
- private _lastVectorRevision = -1;
112
+ // The tile's two drape resolvers (see `drapeResolver.ts`): the vector one is
113
+ // always the baked path (WebMercator is its degenerate identity case), the
114
+ // raster one is chosen at construction from the globe tiling scheme —
115
+ // direct 1:1 material slots on WebMercator terrain, baked per-layer render
116
+ // targets (with Mercator latitude reprojection) on Geographic terrain.
117
+ private readonly vectorDrape: DrapeResolver;
118
+ private readonly rasterDrape: DrapeResolver;
175
119
 
176
120
  // Separate mesh for shadow casting (uses terrain-only geometry without skirt)
177
121
  private shadowMesh?: Mesh<BufferGeometry, TileMaterial>;
178
122
 
179
- private texturizedSceneByTileCoordinates: TexturizedSceneByTileCoordinates;
180
123
  private compositor: TileTextureCompositor;
181
124
 
182
- texturizedSceneRenderTargets: WebGLRenderTarget[] = [];
183
125
  // Drape RT side length + atlas byte cost, read from the compositor (the
184
126
  // single source of truth shared with the Rust cost hints) in the constructor.
185
127
  // Per-instance rather than module constants because the compositor size is a
@@ -193,8 +135,6 @@ export class TileMesh
193
135
  // never reports. Assigned in the constructor once `atlasBytes` is known.
194
136
  private reportedDrapeGpuBytes: number;
195
137
 
196
- private warnedExceededTextures = false;
197
-
198
138
  readonly ctx: EventContext;
199
139
 
200
140
  constructor(ctx: EventContext, mesh: MeshAdded) {
@@ -212,7 +152,6 @@ export class TileMesh
212
152
  const handle = mesh.tile_handle;
213
153
  this.handle = handle;
214
154
 
215
- this.texturizedSceneByTileCoordinates = texturizedSceneByTileCoordinates;
216
155
  this.compositor = tileTextureCompositor;
217
156
 
218
157
  // Device-dependent atlas / drape RT sizing, sourced from the compositor so
@@ -248,113 +187,61 @@ export class TileMesh
248
187
 
249
188
  this.tileHandler = tileHandler;
250
189
 
251
- tileMapByHandle.set(handle, this);
252
-
253
- this.onBeforeRender = this._onBeforeRender;
254
- }
190
+ // Vector layers always resolve through the baked drape path (WebMercator
191
+ // terrain is its degenerate identity case).
192
+ this.vectorDrape = new VectorDrapeResolver({
193
+ handle,
194
+ tileHandler,
195
+ compositor: this.compositor,
196
+ texturizedScenes: texturizedSceneByTileCoordinates,
197
+ textureOptions,
198
+ drapeRtSize: this.drapeRtSize,
199
+ numTexturizedVector: this.numTexturizedVector,
200
+ texturizedSceneIndexFrom: this.texturizedSceneIndexFrom,
201
+ material: () => this.material,
202
+ reportDrapeGpuBytes: () => this.reportDrapeGpuBytesIfChanged(),
203
+ });
255
204
 
256
- /**
257
- * Pull the Rust-resolved texturized-vector slots for this terrain tile into
258
- * {@link vectorSlots}: one bake slot per clamp-to-ground layer, keyed by the
259
- * rendered WM vector tile that backs it (ancestor fallback already resolved in
260
- * Rust). Only layers with an actual draped scene take a slot — Rust also returns
261
- * non-draped layers (rendered in the MRT scene), which must not consume the budget.
262
- * Capped to the GPU slot budget; warns once when more draped layers are requested.
263
- */
264
- private refreshVectorSlots() {
265
- const states = this.tileHandler.getVectorTileStates(this.handle) ?? [];
266
-
267
- // Group the flattened per-(layer, source) states back into one slot per
268
- // layer, each carrying every overlapping WM source tile (N:M on Geographic
269
- // terrain). First-seen layer order assigns the render-target index, kept
270
- // consistent by renderVectorScenes / bindVectorSlots / buildCompositeLayers.
271
- const byLayer = new Map<string, VectorSlot>();
272
- const droppedLayers = new Set<string>();
273
- for (const state of states) {
274
- const layerId = state.layer_id;
275
- let slot = byLayer.get(layerId);
276
- if (!slot) {
277
- // Rust returns a state for every vector layer with a rendered tile, including
278
- // non-draped layers (those render in the MRT scene and never create a texturized
279
- // scene). Only a layer that actually has a draped scene may consume a bake slot;
280
- // otherwise non-draped layers would occupy the limited slot budget and crowd out
281
- // genuinely-draped layers past the cap. A draped layer whose scene isn't ready yet
282
- // simply gets no slot this pass and is picked up once its scene exists.
283
- const hasScene =
284
- this.texturizedSceneByTileCoordinates.findSceneByLayerId(
285
- state.tile_handle,
286
- layerId,
287
- ) != null;
288
- if (!hasScene) continue;
289
- // Cap the layer fan-out to the GPU slot budget; extra layers are dropped.
290
- if (byLayer.size >= this.numTexturizedVector) {
291
- droppedLayers.add(layerId);
292
- continue;
293
- }
294
- const reproject = state.reproject_terrain_lat;
295
- slot = {
296
- layerId,
297
- sources: [],
298
- reproject:
299
- reproject.length >= 2 ? [reproject[0], reproject[1]] : undefined,
300
- };
301
- byLayer.set(layerId, slot);
302
- }
303
- slot.sources.push({
304
- tileHandle: state.tile_handle,
305
- uvOffset: [state.uv_offset[0] ?? 0, state.uv_offset[1] ?? 0],
306
- uvScale: [state.uv_scale[0] ?? 1, state.uv_scale[1] ?? 1],
307
- });
308
- }
205
+ // The globe tiling scheme is fixed for this mesh's lifetime (a runtime
206
+ // scheme flip drains and rebuilds every tile), so the raster drape
207
+ // resolver is chosen once here. `mesh.globe` clones a wasm object;
208
+ // release it.
209
+ const globe = mesh.globe;
210
+ const isGeographicTiling = globe.isGeographicTiling;
211
+ globe.free();
212
+ this.rasterDrape = isGeographicTiling
213
+ ? new BakedRasterDrapeResolver({
214
+ handle,
215
+ tileHandler,
216
+ compositor: this.compositor,
217
+ loadedTexs: ctx.loadedTexs,
218
+ textureOptions,
219
+ drapeRtSize: this.drapeRtSize,
220
+ texturizedSceneIndexFrom: this.texturizedSceneIndexFrom,
221
+ material: () => this.material,
222
+ reportDrapeGpuBytes: () => this.reportDrapeGpuBytesIfChanged(),
223
+ })
224
+ : new DirectRasterDrapeResolver();
309
225
 
310
- if (droppedLayers.size > 0) {
311
- if (!this.warnedExceededTextures) {
312
- this.warnedExceededTextures = true;
313
- console.warn(
314
- `[TileMesh] Exceeded maximum MVT texture slots: ${byLayer.size + droppedLayers.size} layers requested, ` +
315
- `but only ${this.numTexturizedVector} slots available. ` +
316
- `Some MVT layers will not be rendered.`,
317
- );
318
- }
319
- } else {
320
- this.warnedExceededTextures = false;
321
- }
226
+ tileMapByHandle.set(handle, this);
322
227
 
323
- this.vectorSlots = [...byLayer.values()];
324
- this.syncDrapeRenderTargets();
228
+ this.onBeforeRender = this._onBeforeRender;
325
229
  }
326
230
 
327
231
  /**
328
- * Size the drape render-target pool to the live slot count (already capped to
329
- * `numTexturizedVector` by {@link refreshVectorSlots}): allocate the ones a
330
- * newly-draped layer needs, dispose the ones a vanished layer freed. Then
331
- * report the pool's GPU footprint to the memory ledger so it tracks drape
332
- * memory that scales with terrain subdivision past the vector maxZoom — a cost
333
- * per-vector-tile accounting cannot see. Reports only when the count changes.
232
+ * Report the tile's drape GPU footprint (composite atlas + vector and baked-raster
233
+ * drape render targets) to the memory ledger when it changed. The atlas is acquired
234
+ * in the constructor and held for the tile's lifetime regardless of draping, so it
235
+ * is always part of the report — otherwise a tile whose last draped layer vanished
236
+ * would report 0 and wipe the atlas cost Rust seeds at mesh-attach time. The render
237
+ * targets on top of it scale with terrain subdivision past the layers' maxZoom — a
238
+ * cost per-source-tile accounting cannot see.
334
239
  */
335
- private syncDrapeRenderTargets() {
336
- const target = this.vectorSlots.length;
337
- const rts = this.texturizedSceneRenderTargets;
338
- while (rts.length < target) {
339
- const rt = new WebGLRenderTarget(this.drapeRtSize, this.drapeRtSize, {
340
- format: RGBAFormat,
341
- });
342
- // Match the sampler settings setupTextures applies to eager RTs, so the
343
- // lazily-grown one binds identically when bindVectorSlots picks it up.
344
- configureTexturizedTexture(rt.texture, this.ctx.textureOptions);
345
- rts.push(rt);
346
- }
347
- while (rts.length > target) {
348
- rts.pop()?.dispose();
349
- }
350
-
351
- // The composite atlas is acquired in the constructor and held for the
352
- // tile's lifetime regardless of draping, so it is always part of the
353
- // report — otherwise a tile whose last vector layer vanished would report
354
- // 0 and wipe the atlas cost Rust seeds at mesh-attach time. The render
355
- // targets on top of it scale with terrain subdivision past the vector
356
- // maxZoom — a cost per-vector-tile accounting cannot see.
357
- const bytes = this.atlasBytes + rts.length * drapeRtBytes(this.drapeRtSize);
240
+ private reportDrapeGpuBytesIfChanged() {
241
+ const rtCount =
242
+ this.vectorDrape.liveRenderTargetCount() +
243
+ this.rasterDrape.liveRenderTargetCount();
244
+ const bytes = this.atlasBytes + rtCount * drapeRtBytes(this.drapeRtSize);
358
245
  if (bytes === this.reportedDrapeGpuBytes) return;
359
246
  // Only the mesh currently registered for this position-stable handle may
360
247
  // report. During a terrain mesh replacement the outgoing mesh briefly
@@ -369,54 +256,14 @@ export class TileMesh
369
256
  this.tileHandler.reportDrapeGpuBytes(this.handle, bytes);
370
257
  }
371
258
 
372
- /**
373
- * Identity of the current slot set + the backing scenes' content. A change means
374
- * the offscreen scenes must be re-baked and the per-slot material state re-bound.
375
- */
376
- private vectorSignature(): string {
377
- return this.vectorSlots
378
- .map((slot) => {
379
- const sourceSig = slot.sources
380
- .map((s) => {
381
- const scene =
382
- this.texturizedSceneByTileCoordinates.findSceneByLayerId(
383
- s.tileHandle,
384
- slot.layerId,
385
- );
386
- return `${s.tileHandle}@${s.uvOffset[0]},${s.uvOffset[1]},${s.uvScale[0]},${s.uvScale[1]}#${scene?.revision ?? -1}:${scene?.children.length ?? 0}`;
387
- })
388
- .join(",");
389
- return `${slot.layerId}[${sourceSig}]`;
390
- })
391
- .join("|");
392
- }
393
-
394
259
  private _onBeforeRender = () => {
395
260
  if (!this.visible) return;
396
261
 
397
- // Re-fetch the Rust-resolved slots only when the global vector resolution revision
398
- // changed; otherwise reuse the cached slots. The resolve result can only flip when a
399
- // traverse runs (new/removed rendered tiles, terrain subdivision) or a scene becomes
400
- // ready — both bump the revision. This skips a per-tile WASM-boundary resolve every
401
- // frame, the dominant steady-state cost when many terrain tiles are visible.
402
- const revision = this.tileHandler.vectorRevision();
403
- if (revision !== this._lastVectorRevision) {
404
- this._lastVectorRevision = revision;
405
- this.refreshVectorSlots();
406
- }
407
-
408
- // Re-bake the offscreen vector scenes only when the resolved slots or their
409
- // backing scenes changed; otherwise the existing render targets stay valid.
410
- const signature = this.vectorSignature();
411
- if (signature !== this.prevVectorSignature) {
412
- this.prevVectorSignature = signature;
413
- this.compositor.renderVectorScenes(
414
- this.vectorSlots,
415
- this.texturizedSceneRenderTargets,
416
- );
417
- this.bindVectorSlots();
418
- this.compositor.markDirty(this.handle, "vector-revision");
419
- }
262
+ // Drape resolvers (see `drapeResolver.ts`): each refreshes its Rust-resolved
263
+ // slots on a revision bump and re-bakes on a slot/content signature change.
264
+ // The raster one is a no-op on WebMercator terrain (direct 1:1 slots).
265
+ this.vectorDrape.update();
266
+ this.rasterDrape.update();
420
267
 
421
268
  // MRT composite pass: bakes N source textures into the per-tile atlas.
422
269
  // Skip building the layer snapshot entirely when nothing is dirty.
@@ -435,93 +282,6 @@ export class TileMesh
435
282
  );
436
283
  };
437
284
 
438
- /**
439
- * Drive each vector slot's main-shader state per frame: texture, per-slot UV
440
- * transform, visibility and the draped mesh's material attributes. One slot per
441
- * layer, sourced from the WM vector tile the Rust resolve picked. The resolve
442
- * walks up to the nearest rendered tile (readiness derived from ECS activation
443
- * / the Rust resolve, not a JS-side scene_ready flag), so the slot's scene is
444
- * always bakeable (its own render target was framed by the bake) and the UV is
445
- * identity — the ancestor LOD fallback lives entirely in Rust. Replaces the old
446
- * scene-observer `updateTexturizedSceneTextureVisibility`.
447
- */
448
- private bindVectorSlots() {
449
- const m = this.material;
450
- if (!m || !m.userData || !m.userData.textures?.value) return;
451
- const textures = m.userData.textures.value;
452
- const uvOffsets: Vector2[] = m.userData.layerUvOffset?.value ?? [];
453
- const uvScales: Vector2[] = m.userData.layerUvScale?.value ?? [];
454
-
455
- for (let i = 0; i < this.numTexturizedVector; i++) {
456
- const lastIdx = this.texturizedSceneIndexFrom + i;
457
- const ownRt = this.texturizedSceneRenderTargets[i];
458
- // Slots past the live pool (lazily sized to the draped-layer count) have
459
- // no render target: hide the slot and rebind the shared empty texture so
460
- // the shader never samples a stale / disposed texture from a layer that
461
- // is no longer draped here.
462
- if (!ownRt) {
463
- m.userData.shows.value[lastIdx] = 0;
464
- textures[lastIdx] = EMPTY_TEXTURIZED_TEXTURE;
465
- continue;
466
- }
467
-
468
- textures[lastIdx] = ownRt.texture;
469
- // The bake framed every source into this RT so it spans the terrain tile's
470
- // extent: the paste samples it 1:1 in longitude (identity UV here); the
471
- // latitude axis is reprojected WebMercator→Geographic in the composite
472
- // shader via the slot's reproject band (see buildCompositeLayers).
473
- uvOffsets[lastIdx]?.set(0, 0);
474
- uvScales[lastIdx]?.set(1, 1);
475
-
476
- const slot = this.vectorSlots[i];
477
- const mesh = slot ? this.representativeVectorMesh(slot) : undefined;
478
- m.userData.shows.value[lastIdx] = mesh ? 1 : 0;
479
- if (mesh) this.copyVectorMeshAttrs(lastIdx, mesh);
480
- }
481
- }
482
-
483
- /**
484
- * First available draped mesh backing any of a layer's sources. A clamp-to-ground
485
- * layer's material attributes (water/specular/emissive/…) are uniform across its
486
- * tiles, so any source's mesh is a faithful representative for the slot.
487
- */
488
- private representativeVectorMesh(slot: VectorSlot): Object3D | undefined {
489
- for (const source of slot.sources) {
490
- const scene = this.texturizedSceneByTileCoordinates.findSceneByLayerId(
491
- source.tileHandle,
492
- slot.layerId,
493
- );
494
- if (scene && !scene.removed && scene.children.length) {
495
- return scene.children[0];
496
- }
497
- }
498
- return undefined;
499
- }
500
-
501
- /** Copy a draped mesh's material attributes into the main shader's slot `lastIdx`. */
502
- private copyVectorMeshAttrs(lastIdx: number, mesh: Object3D) {
503
- const m = this.material;
504
- if (mesh instanceof PolygonMesh) {
505
- // Use PolygonMesh getters that expose material enhancer state
506
- m.userData.reflectivities.value[lastIdx] = mesh.reflectivity;
507
- m.userData.roughnesses.value[lastIdx] = mesh.roughness;
508
- m.userData.waters.value[lastIdx] = mesh.water;
509
- m.userData.waterScaleNormals.value[lastIdx] = mesh.waterScaleNormal;
510
- m.userData.waterSpeeds.value[lastIdx] = mesh.waterSpeed;
511
- m.userData.shininesses.value[lastIdx] = mesh.shininess;
512
- m.userData.specularStrengths.value[lastIdx] = mesh.specularStrength;
513
- m.userData.applyWaterNormals.value[lastIdx] = mesh.applyWaterNormal;
514
- m.userData.speculars.value[lastIdx] = mesh.specular;
515
- m.userData.emissiveIntensities.value[lastIdx] = mesh.emissiveIntensity;
516
- m.userData.emissiveColors.value[lastIdx].set(mesh.emissiveColor);
517
- m.userData.effectIdsMasks.value[lastIdx] = mesh.effectIdsMask;
518
- } else if (mesh instanceof PolylineMesh) {
519
- m.userData.emissiveIntensities.value[lastIdx] = mesh.emissiveIntensity;
520
- m.userData.emissiveColors.value[lastIdx].set(mesh.emissiveColor);
521
- m.userData.effectIdsMasks.value[lastIdx] = mesh.effectIdsMask;
522
- }
523
- }
524
-
525
285
  /**
526
286
  * Feature flags for the **main** TileMesh shader (atlas-sampling side): drives
527
287
  * its program cache key and which normal/specular branches compile. Sourced
@@ -569,13 +329,6 @@ export class TileMesh
569
329
  const isHillshades: boolean[] = ud.isHillshades?.value ?? [];
570
330
  const isElevationHeatmaps: boolean[] = ud.isElevationHeatmaps?.value ?? [];
571
331
  const waters: boolean[] = ud.waters?.value ?? [];
572
- // Per-slot Mercator reprojection flag + the shared terrain latitude range.
573
- const layerReproject: number[] = ud.layerReproject ?? [];
574
- const terrainLatRange: number[] = ud.terrainLatRange ?? [];
575
- const reproject: [number, number] | undefined =
576
- terrainLatRange.length === 2
577
- ? [terrainLatRange[0], terrainLatRange[1]]
578
- : undefined;
579
332
  const boundary = this.texturizedSceneIndexFrom;
580
333
 
581
334
  const layers: CompositeLayer[] = [];
@@ -599,39 +352,37 @@ export class TileMesh
599
352
  uvOffset,
600
353
  uvScale,
601
354
  });
602
- } else if (isElevationHeatmaps[absSlot]) {
603
- layers.push({
604
- kind: "elevationHeatmap",
605
- region,
606
- absSlot,
607
- texture,
608
- uvOffset,
609
- uvScale,
610
- opacity: opacities[absSlot] ?? 1,
611
- });
612
355
  } else {
613
- // WebMercator-on-Geographic latitude reprojection. Raster slots read the
614
- // shared per-tile flag/band stashed by setupTextures; baked-vector slots
615
- // (whose RT spans the terrain extent) carry the Rust-resolved terrain
616
- // `[south, north]` band directly on the slot. `undefined` on WM terrain.
617
- const slotReproject =
618
- region === "vector"
619
- ? this.vectorSlots[absSlot - boundary]?.reproject
620
- : layerReproject[absSlot] === 1
621
- ? reproject
622
- : undefined;
623
- layers.push({
624
- kind: "raster",
625
- region,
626
- absSlot,
627
- texture,
628
- uvOffset,
629
- uvScale,
630
- color: colors[absSlot] ?? new Color(),
631
- opacity: opacities[absSlot] ?? 1,
632
- water: waters[absSlot] ?? false,
633
- reproject: slotReproject,
634
- });
356
+ // WebMercator-on-Geographic latitude reprojection, resolved by the
357
+ // slot's drape resolver (always undefined on WM terrain).
358
+ const slotReproject = (
359
+ region === "vector" ? this.vectorDrape : this.rasterDrape
360
+ ).slotReproject(absSlot);
361
+ if (isElevationHeatmaps[absSlot]) {
362
+ layers.push({
363
+ kind: "elevationHeatmap",
364
+ region,
365
+ absSlot,
366
+ texture,
367
+ uvOffset,
368
+ uvScale,
369
+ opacity: opacities[absSlot] ?? 1,
370
+ reproject: slotReproject,
371
+ });
372
+ } else {
373
+ layers.push({
374
+ kind: "raster",
375
+ region,
376
+ absSlot,
377
+ texture,
378
+ uvOffset,
379
+ uvScale,
380
+ color: colors[absSlot] ?? new Color(),
381
+ opacity: opacities[absSlot] ?? 1,
382
+ water: waters[absSlot] ?? false,
383
+ reproject: slotReproject,
384
+ });
385
+ }
635
386
  }
636
387
  }
637
388
 
@@ -1148,7 +899,7 @@ ${generateTileCommonInjection(maxTextures)}
1148
899
 
1149
900
  // Material / texture rebind → force the vector slots to re-bind their
1150
901
  // per-slot uniform state on the next frame, and repaint the composite atlas.
1151
- this.prevVectorSignature = "";
902
+ this.vectorDrape.invalidate();
1152
903
  this.compositor.markDirty(this.handle, "material");
1153
904
  this.compositor.markDirty(this.handle, "texture-binding");
1154
905
  }
@@ -1516,13 +1267,12 @@ ${generateTileCommonInjection(maxTextures)}
1516
1267
  const textureFragments = m.userData.textureFragments?.value;
1517
1268
  const textureFragmentsLen = textureFragments?.length ?? 0;
1518
1269
 
1519
- const numTexturizedVector = this.numTexturizedVector;
1520
-
1521
1270
  // Raster textures fill slots [0, texturizedSceneIndexFrom); texturized vector
1522
1271
  // scenes occupy the rest. Exactly `texturizedSceneIndexFrom` raster layers fit
1523
- // (indices 0..from-1), so only a strictly larger count overflows. Rust caps the
1524
- // draped-tile fan-out (RASTER_DRAPE_SLOT_BUDGET) to keep this from triggering;
1525
- // this guard is the final safety net and the extra fragments are dropped below.
1272
+ // (indices 0..from-1), so only a strictly larger count overflows. Each layer
1273
+ // costs one slot (a baked layer's overlap is mosaicked into a single render
1274
+ // target), so this guard is the final safety net for an oversized layer
1275
+ // count and the extra fragments are dropped below.
1526
1276
  if (textureFragmentsLen > this.texturizedSceneIndexFrom) {
1527
1277
  console.error(
1528
1278
  `Exceeded maximum textures: ${textureFragmentsLen} layers are provided. Maximum the number of textures is ${this.texturizedSceneIndexFrom}.`,
@@ -1647,27 +1397,11 @@ ${generateTileCommonInjection(maxTextures)}
1647
1397
  textures[i] = t;
1648
1398
  }
1649
1399
 
1650
- for (let i = 0; i < numTexturizedVector; i++) {
1651
- // Every texturized sampler slot must reference a valid texture (GLSL/WebGL
1652
- // requires it even for a sampler never read). Drape render targets are now
1653
- // allocated lazily, so a slot without one binds the shared empty texture;
1654
- // `bindVectorSlots` swaps in the real RT texture once a layer drapes here.
1655
- const lastIndex = this.texturizedSceneIndexFrom + i;
1656
- const rt = this.texturizedSceneRenderTargets[i];
1657
- const texturizedSceneTexture = rt
1658
- ? configureTexturizedTexture(rt.texture, textureOptions)
1659
- : EMPTY_TEXTURIZED_TEXTURE;
1660
-
1661
- textures[lastIndex] = texturizedSceneTexture;
1662
-
1663
- // Per-slot visibility + material attributes are bound by bindVectorSlots once
1664
- // the Rust-resolved slots are known; default to hidden here.
1665
- m.userData.shows.value[lastIndex] = 0;
1666
- m.userData.colors.value[lastIndex] = new Color(0xffffff);
1667
- m.userData.opacities.value[lastIndex] = 1.0;
1668
- }
1669
- // Re-bind vector slots on the next frame now that render targets were rebound.
1670
- this.prevVectorSignature = "";
1400
+ // Re-derive each drape resolver's slot pairing / bindings from the rebuilt
1401
+ // material (the raster call is a no-op on WebMercator terrain, where every
1402
+ // raster slot carries its own fragment and drapes directly).
1403
+ this.rasterDrape.syncMaterialSlots(textureFragments);
1404
+ this.vectorDrape.syncMaterialSlots(textureFragments);
1671
1405
  }
1672
1406
 
1673
1407
  /**
@@ -1704,13 +1438,13 @@ ${generateTileCommonInjection(maxTextures)}
1704
1438
  this.material.userData.uPickable.value = 1;
1705
1439
 
1706
1440
  // Force the vector scenes to re-bake for the picking pass.
1707
- this.prevVectorSignature = "";
1441
+ this.vectorDrape.invalidate();
1708
1442
  this.compositor.markDirty(this.handle, "vector-revision");
1709
1443
  }
1710
1444
 
1711
1445
  onAfterPicking(): void {
1712
1446
  this.material.userData.uPickable.value = 0;
1713
- this.prevVectorSignature = "";
1447
+ this.vectorDrape.invalidate();
1714
1448
  this.compositor.markDirty(this.handle, "vector-revision");
1715
1449
  }
1716
1450
 
@@ -1755,11 +1489,9 @@ ${generateTileCommonInjection(maxTextures)}
1755
1489
  this.shadowMesh = undefined;
1756
1490
  }
1757
1491
 
1758
- // Dispose WebGLRenderTargets to free GPU memory
1759
- for (const renderTarget of this.texturizedSceneRenderTargets) {
1760
- renderTarget.dispose();
1761
- }
1762
- this.texturizedSceneRenderTargets.length = 0;
1492
+ // Dispose the drape resolvers' WebGLRenderTargets to free GPU memory
1493
+ this.vectorDrape.dispose();
1494
+ this.rasterDrape.dispose();
1763
1495
  // Do NOT report a zero drape cost here. The Rust `TerrainTileGpuCost` /
1764
1496
  // `TileCost` live on the mesh entity, whose despawn already subtracts the
1765
1497
  // full cost from the ledger via the component `on_replace` hook — so the