@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
|
@@ -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[]>([]),
|
|
@@ -120,8 +124,14 @@ export function createCoreUniformMutates(): CoreUniformMutates {
|
|
|
120
124
|
const span = (Tn - Ts) / layer.uvScale.y;
|
|
121
125
|
const Rs = Ts - layer.uvOffset.y * span;
|
|
122
126
|
const Rn = Rs + span;
|
|
123
|
-
|
|
124
|
-
|
|
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);
|
|
125
135
|
// Polar cap: clamp the overshoot onto the band-edge texel when this tile's
|
|
126
136
|
// north/south edge sits on the WebMercator latitude limit.
|
|
127
137
|
const clampTop = Rn >= WM_MAX_LAT - 1e-4 ? 1 : 0;
|
|
@@ -431,8 +431,13 @@ export function setupMaterialForMRT(
|
|
|
431
431
|
material: ShaderMaterial,
|
|
432
432
|
options?: SetupMaterialForMRTOptions,
|
|
433
433
|
): void {
|
|
434
|
-
|
|
435
|
-
|
|
434
|
+
// Detect LineMaterial by `type`, not only `instanceof`: callers may construct
|
|
435
|
+
// it from `three/examples/jsm/lines` while this module's `LineMaterial` comes
|
|
436
|
+
// from `three-stdlib`, so a bare `instanceof` misses those cross-copy
|
|
437
|
+
// instances and would wrongly route them through the ShaderMaterial path
|
|
438
|
+
// (whose normal-buffer write references an undeclared `normal`).
|
|
439
|
+
if (material instanceof LineMaterial || material.type === "LineMaterial") {
|
|
440
|
+
injectGBufferToLineMaterial(material as LineMaterial);
|
|
436
441
|
return;
|
|
437
442
|
}
|
|
438
443
|
injectGBufferToShaderMaterial(material, options?.normal);
|
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) {
|
|
@@ -3,9 +3,13 @@ import type {
|
|
|
3
3
|
TextMaterial as NavaraTextMaterial,
|
|
4
4
|
} from "@navaramap/engine";
|
|
5
5
|
import type { FontManager } from "@navaramap/font";
|
|
6
|
-
import {
|
|
6
|
+
import { 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";
|
|
@@ -34,9 +38,25 @@ type PositionsInfo = PositionsInfoBase &
|
|
|
34
38
|
}
|
|
35
39
|
);
|
|
36
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Per-feature evaluator state received before the feature's `SDFTextMesh`
|
|
43
|
+
* exists (meshes are created lazily on the first non-empty text — see
|
|
44
|
+
* `initMeshes`). Replayed onto the mesh at creation. Colors are kept as hex
|
|
45
|
+
* so a shared evaluator `Color` instance mutated later can't leak in.
|
|
46
|
+
*/
|
|
47
|
+
type PendingFeatureState = {
|
|
48
|
+
colorHex?: number;
|
|
49
|
+
show?: boolean;
|
|
50
|
+
height?: number;
|
|
51
|
+
size?: number;
|
|
52
|
+
opacity?: number;
|
|
53
|
+
declutterPriority?: number | undefined;
|
|
54
|
+
hasDeclutterPriority?: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
37
57
|
export class BatchedSdfTextMesh
|
|
38
58
|
extends InstancedMesh<SDFTextMesh>
|
|
39
|
-
implements PickableMesh
|
|
59
|
+
implements PickableMesh, DeclutterParticipant
|
|
40
60
|
{
|
|
41
61
|
readonly ctx: EventContext;
|
|
42
62
|
/** The font identifier from material — may be a family name or a URL. */
|
|
@@ -55,6 +75,15 @@ export class BatchedSdfTextMesh
|
|
|
55
75
|
* and must be balanced with unloadFont() on dispose or font change.
|
|
56
76
|
*/
|
|
57
77
|
private _loadedFaceUrls: Set<string>;
|
|
78
|
+
/** Geometry-extracted anchor data; per-index meshes slice it lazily. */
|
|
79
|
+
private _positions: PositionsInfo | null = null;
|
|
80
|
+
/** Sparse batchIndex → mesh; `allMeshes` holds only created meshes. */
|
|
81
|
+
private _meshByIndex: (SDFTextMesh | undefined)[] = [];
|
|
82
|
+
/** Evaluator state received before a feature's mesh exists. */
|
|
83
|
+
private _pending = new Map<number, PendingFeatureState>();
|
|
84
|
+
/** Material/transform used when creating meshes lazily. */
|
|
85
|
+
private _material: NavaraTextMaterial;
|
|
86
|
+
private _transform: NavaraTextMesh["transform"];
|
|
58
87
|
|
|
59
88
|
constructor(
|
|
60
89
|
ctx: EventContext,
|
|
@@ -70,7 +99,12 @@ export class BatchedSdfTextMesh
|
|
|
70
99
|
invariant(ctx.fontManager);
|
|
71
100
|
this._fontManager = ctx.fontManager;
|
|
72
101
|
this._loadedFaceUrls = loadedFaceUrls ?? new Set();
|
|
73
|
-
|
|
102
|
+
// One getter call each: wasm getters clone, so these snapshots are owned
|
|
103
|
+
// by this batch and safe to use after the event object is freed.
|
|
104
|
+
this._material = m.material;
|
|
105
|
+
this._transform = m.transform;
|
|
106
|
+
this._positions = this.extractPositions(m);
|
|
107
|
+
this.initMeshes();
|
|
74
108
|
// When the shared atlas evicts glyphs, a still-in-flight glyph this batch
|
|
75
109
|
// already baked into a visible mesh may have had its rect reused. Rebuild
|
|
76
110
|
// any such stale mesh so its UVs and retains refresh.
|
|
@@ -79,6 +113,41 @@ export class BatchedSdfTextMesh
|
|
|
79
113
|
this._highQuality,
|
|
80
114
|
() => this._revalidateStaleMeshes(),
|
|
81
115
|
);
|
|
116
|
+
ctx.declutter?.register(this);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// --- DeclutterParticipant ---
|
|
120
|
+
|
|
121
|
+
collectDeclutterCandidates(out: DeclutterCandidate[]): void {
|
|
122
|
+
if (!this.visible) return;
|
|
123
|
+
const meshes = this.meshes();
|
|
124
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
125
|
+
const candidate = meshes[i].getDeclutterCandidate(this, i);
|
|
126
|
+
if (candidate) out.push(candidate);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
applyDeclutter(handle: number, hidden: boolean): void {
|
|
131
|
+
this.meshes()[handle]?.setDeclutterHidden(hidden);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
stepDeclutterFade(deltaMs: number): boolean {
|
|
135
|
+
let animating = false;
|
|
136
|
+
for (const mesh of this.meshes()) {
|
|
137
|
+
animating = mesh.stepDeclutterFade(deltaMs) || animating;
|
|
138
|
+
}
|
|
139
|
+
return animating;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Candidates changed (text, style, position, visibility): ask the shared
|
|
143
|
+
* declutter pass to re-place on its next update. */
|
|
144
|
+
private _markDeclutterDirty(): void {
|
|
145
|
+
this.ctx.declutter?.markDirty();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
override setActive(active: boolean) {
|
|
149
|
+
super.setActive(active);
|
|
150
|
+
this._markDeclutterDirty();
|
|
82
151
|
}
|
|
83
152
|
|
|
84
153
|
/**
|
|
@@ -111,6 +180,7 @@ export class BatchedSdfTextMesh
|
|
|
111
180
|
// Force a rebuild even though the text string is unchanged: the baked
|
|
112
181
|
// atlas rects and glyph retains must refresh against the new metrics.
|
|
113
182
|
mesh.setText(text, true);
|
|
183
|
+
this._markDeclutterDirty();
|
|
114
184
|
this._needRender?.();
|
|
115
185
|
})
|
|
116
186
|
.catch((err: unknown) => {
|
|
@@ -127,73 +197,149 @@ export class BatchedSdfTextMesh
|
|
|
127
197
|
return this._highQuality;
|
|
128
198
|
}
|
|
129
199
|
|
|
130
|
-
|
|
131
|
-
|
|
200
|
+
/**
|
|
201
|
+
* A mesh per anchor is expensive (geometry + ShaderMaterial + enhancer
|
|
202
|
+
* mount), and with per-feature texts (MVT) most features never receive a
|
|
203
|
+
* non-empty text — they'd sit invisible while still costing construction and
|
|
204
|
+
* every per-frame loop. So meshes are created lazily by `_ensureMeshAt` on
|
|
205
|
+
* the first non-empty `setTextByBatchIndex`. Only a non-empty material-level
|
|
206
|
+
* default text (single-label sources) renders without the evaluator ever
|
|
207
|
+
* setting text, so only that case creates all meshes eagerly.
|
|
208
|
+
*/
|
|
209
|
+
private initMeshes() {
|
|
210
|
+
const positionInfo = this._positions;
|
|
132
211
|
if (!positionInfo) {
|
|
133
212
|
return;
|
|
134
213
|
}
|
|
135
214
|
|
|
136
|
-
|
|
137
|
-
|
|
215
|
+
if (this._material.text) {
|
|
216
|
+
// The atlas can't be reallocated within this synchronous loop, so fetch
|
|
217
|
+
// the shared textures once and hand them to every creation.
|
|
218
|
+
const shared = this._sharedTextures();
|
|
219
|
+
for (let i = 0; i < positionInfo.nPositions; i++) {
|
|
220
|
+
this._ensureMeshAt(i, shared);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
138
224
|
|
|
139
|
-
|
|
140
|
-
|
|
225
|
+
/** The font-level shared atlas textures for this batch's font+quality (the
|
|
226
|
+
* color atlas is `null` for monochrome fonts). */
|
|
227
|
+
private _sharedTextures() {
|
|
228
|
+
return {
|
|
229
|
+
tex: this._fontManager.getAtlasTexture(
|
|
230
|
+
this._fontIdentifier,
|
|
231
|
+
this._highQuality,
|
|
232
|
+
),
|
|
233
|
+
colorTex: this._fontManager.getColorAtlasTexture(
|
|
234
|
+
this._fontIdentifier,
|
|
235
|
+
this._highQuality,
|
|
236
|
+
),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
141
239
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
240
|
+
/**
|
|
241
|
+
* The feature's mesh, created on first need (see `initMeshes`). `shared`
|
|
242
|
+
* lets a batch creation pass hoist the atlas lookups; when omitted (a single
|
|
243
|
+
* lazy creation) they are fetched here, since the atlas may have been
|
|
244
|
+
* (re)allocated since this batch was constructed.
|
|
245
|
+
*/
|
|
246
|
+
private _ensureMeshAt(
|
|
247
|
+
batchIndex: number,
|
|
248
|
+
shared?: ReturnType<BatchedSdfTextMesh["_sharedTextures"]>,
|
|
249
|
+
): SDFTextMesh | undefined {
|
|
250
|
+
const existing = this._meshByIndex[batchIndex];
|
|
251
|
+
if (existing) return existing;
|
|
252
|
+
|
|
253
|
+
const info = this._positions;
|
|
254
|
+
if (!info || batchIndex < 0 || batchIndex >= info.nPositions) {
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const { position, positionSize, batchIDs, batchIDSize, RTE } = info;
|
|
259
|
+
const batchId = batchIDs ? batchIDs[batchIndex * batchIDSize] : undefined;
|
|
260
|
+
const posIdx = batchIndex * positionSize;
|
|
261
|
+
const pos = RTE
|
|
262
|
+
? {
|
|
263
|
+
high: position.high.subarray(posIdx, posIdx + positionSize),
|
|
264
|
+
low: position.low.subarray(posIdx, posIdx + positionSize),
|
|
265
|
+
}
|
|
266
|
+
: position.subarray(posIdx, posIdx + positionSize);
|
|
267
|
+
|
|
268
|
+
const mesh = new SDFTextMesh(
|
|
269
|
+
pos,
|
|
270
|
+
this._material,
|
|
271
|
+
this._transform,
|
|
272
|
+
this._fontManager,
|
|
146
273
|
this._fontIdentifier,
|
|
274
|
+
batchId,
|
|
275
|
+
RTE,
|
|
276
|
+
// Pin quality to the batch, not `this._material.highQuality`, which a
|
|
277
|
+
// later `_update` may have changed (quality is immutable per batch).
|
|
147
278
|
this._highQuality,
|
|
148
279
|
);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
280
|
+
mesh.renderOrder = this.renderOrder;
|
|
281
|
+
// Re-prepare evicted glyphs through the batch so font-family faces and
|
|
282
|
+
// refcounts stay consistent; request a render once they're rebuilt.
|
|
283
|
+
mesh.setGlyphLifecycleHandlers(
|
|
284
|
+
(text) =>
|
|
285
|
+
this._fontManager.prepareText(
|
|
286
|
+
this._fontIdentifier,
|
|
287
|
+
text,
|
|
288
|
+
this._highQuality,
|
|
289
|
+
this._loadedFaceUrls,
|
|
290
|
+
),
|
|
291
|
+
() => this._needRender?.(),
|
|
152
292
|
);
|
|
153
293
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
low: position.low.subarray(posIdx, posIdx + positionSize),
|
|
162
|
-
}
|
|
163
|
-
: position.subarray(posIdx, posIdx + positionSize);
|
|
164
|
-
|
|
165
|
-
const mesh = new SDFTextMesh(
|
|
166
|
-
pos,
|
|
167
|
-
material,
|
|
168
|
-
transform,
|
|
169
|
-
this._fontManager,
|
|
170
|
-
this._fontIdentifier,
|
|
171
|
-
batchId,
|
|
172
|
-
RTE,
|
|
173
|
-
);
|
|
174
|
-
mesh.renderOrder = this.renderOrder;
|
|
175
|
-
// Re-prepare evicted glyphs through the batch so font-family faces and
|
|
176
|
-
// refcounts stay consistent; request a render once they're rebuilt.
|
|
177
|
-
mesh.setGlyphLifecycleHandlers(
|
|
178
|
-
(text) =>
|
|
179
|
-
this._fontManager.prepareText(
|
|
180
|
-
this._fontIdentifier,
|
|
181
|
-
text,
|
|
182
|
-
this._highQuality,
|
|
183
|
-
this._loadedFaceUrls,
|
|
184
|
-
),
|
|
185
|
-
() => this._needRender?.(),
|
|
186
|
-
);
|
|
294
|
+
const { tex, colorTex } = shared ?? this._sharedTextures();
|
|
295
|
+
if (tex) {
|
|
296
|
+
mesh.setAtlasTexture(tex);
|
|
297
|
+
}
|
|
298
|
+
mesh.setColorAtlasTexture(colorTex);
|
|
299
|
+
|
|
300
|
+
mesh.update(this._material);
|
|
187
301
|
|
|
188
|
-
|
|
189
|
-
|
|
302
|
+
// Replay evaluator state that arrived before the mesh existed.
|
|
303
|
+
const pending = this._pending.get(batchIndex);
|
|
304
|
+
if (pending) {
|
|
305
|
+
this._pending.delete(batchIndex);
|
|
306
|
+
if (pending.colorHex !== undefined) {
|
|
307
|
+
mesh.setColor(new Color(pending.colorHex));
|
|
190
308
|
}
|
|
191
|
-
|
|
309
|
+
if (pending.show !== undefined) {
|
|
310
|
+
mesh._setFeatureShow(pending.show);
|
|
311
|
+
}
|
|
312
|
+
if (pending.height !== undefined) {
|
|
313
|
+
mesh.setHeight(pending.height);
|
|
314
|
+
}
|
|
315
|
+
if (pending.size !== undefined) {
|
|
316
|
+
mesh.setSize(pending.size);
|
|
317
|
+
}
|
|
318
|
+
if (pending.opacity !== undefined) {
|
|
319
|
+
mesh.setOpacity(pending.opacity);
|
|
320
|
+
}
|
|
321
|
+
if (pending.hasDeclutterPriority) {
|
|
322
|
+
mesh.setDeclutterPriority(pending.declutterPriority);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
192
325
|
|
|
193
|
-
|
|
326
|
+
this._meshByIndex[batchIndex] = mesh;
|
|
327
|
+
this.addWithBatchIndex(mesh, batchIndex);
|
|
328
|
+
return mesh;
|
|
329
|
+
}
|
|
194
330
|
|
|
195
|
-
|
|
331
|
+
/** Pending-state slot for a feature whose mesh doesn't exist yet. */
|
|
332
|
+
private _pendingAt(batchIndex: number): PendingFeatureState | undefined {
|
|
333
|
+
const info = this._positions;
|
|
334
|
+
if (!info || batchIndex < 0 || batchIndex >= info.nPositions) {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
let state = this._pending.get(batchIndex);
|
|
338
|
+
if (!state) {
|
|
339
|
+
state = {};
|
|
340
|
+
this._pending.set(batchIndex, state);
|
|
196
341
|
}
|
|
342
|
+
return state;
|
|
197
343
|
}
|
|
198
344
|
|
|
199
345
|
async _update(m: NavaraTextMesh, needRender?: () => void) {
|
|
@@ -201,29 +347,40 @@ export class BatchedSdfTextMesh
|
|
|
201
347
|
|
|
202
348
|
const material = m.material;
|
|
203
349
|
const text = material.text ?? "";
|
|
350
|
+
// Later lazy creations must see the updated material/transform too.
|
|
351
|
+
this._material = material;
|
|
352
|
+
this._transform = m.transform;
|
|
204
353
|
|
|
205
354
|
const positionInfo = this.extractPositions(m);
|
|
206
355
|
if (positionInfo) {
|
|
207
|
-
const { position, nPositions, positionSize, RTE } = positionInfo;
|
|
208
|
-
|
|
209
356
|
invariant(
|
|
210
|
-
|
|
211
|
-
|
|
357
|
+
this._positions === null ||
|
|
358
|
+
positionInfo.nPositions === this._positions.nPositions,
|
|
359
|
+
"Number of positions in the updated geometry must match the initial geometry",
|
|
212
360
|
);
|
|
361
|
+
this._positions = positionInfo;
|
|
213
362
|
|
|
363
|
+
const { position, positionSize, RTE } = positionInfo;
|
|
214
364
|
const transform = m.transform;
|
|
215
|
-
for (
|
|
216
|
-
const posIdx =
|
|
365
|
+
for (const mesh of this.meshes()) {
|
|
366
|
+
const posIdx = (mesh.userData.batchIndex as number) * positionSize;
|
|
217
367
|
const pos = RTE
|
|
218
368
|
? {
|
|
219
369
|
high: position.high.subarray(posIdx, posIdx + positionSize),
|
|
220
370
|
low: position.low.subarray(posIdx, posIdx + positionSize),
|
|
221
371
|
}
|
|
222
372
|
: position.subarray(posIdx, posIdx + positionSize);
|
|
223
|
-
|
|
373
|
+
mesh.setPosition(pos, RTE, transform);
|
|
224
374
|
}
|
|
375
|
+
// Anchors moved (e.g. terrain height resolution) — re-place labels.
|
|
376
|
+
this._markDeclutterDirty();
|
|
225
377
|
}
|
|
226
378
|
|
|
379
|
+
// A non-empty default text renders without per-feature setText calls, so
|
|
380
|
+
// any lazily-skipped meshes must exist before `_applyUpdate` (no-op for
|
|
381
|
+
// already-created ones).
|
|
382
|
+
if (text) this.initMeshes();
|
|
383
|
+
|
|
227
384
|
const fontIdentifier = m.material.font ?? this._fontIdentifier;
|
|
228
385
|
const needFontUpdate = fontIdentifier !== this._fontIdentifier;
|
|
229
386
|
|
|
@@ -292,6 +449,7 @@ export class BatchedSdfTextMesh
|
|
|
292
449
|
mesh.update(material, forceUpdate);
|
|
293
450
|
this.markVisibility(mesh);
|
|
294
451
|
}
|
|
452
|
+
this._markDeclutterDirty();
|
|
295
453
|
|
|
296
454
|
if (needRender) needRender();
|
|
297
455
|
}
|
|
@@ -368,7 +526,12 @@ export class BatchedSdfTextMesh
|
|
|
368
526
|
}
|
|
369
527
|
|
|
370
528
|
setTextByBatchIndex(batchIndex: number, text: string) {
|
|
371
|
-
|
|
529
|
+
// An empty text on a not-yet-created mesh changes nothing (a lazily
|
|
530
|
+
// skipped mesh is exactly an invisible empty-text label); only a
|
|
531
|
+
// non-empty text forces the mesh into existence.
|
|
532
|
+
const mesh = text
|
|
533
|
+
? this._ensureMeshAt(batchIndex)
|
|
534
|
+
: this.getMeshByBatchIndex(batchIndex);
|
|
372
535
|
|
|
373
536
|
if (mesh) {
|
|
374
537
|
// If the text hasn't been prepared in the worker yet, schedule async preparation
|
|
@@ -412,6 +575,7 @@ export class BatchedSdfTextMesh
|
|
|
412
575
|
// Restore intended visibility now that text content is available
|
|
413
576
|
mesh.visible = intendedVisible;
|
|
414
577
|
this.markVisibility(mesh);
|
|
578
|
+
this._markDeclutterDirty();
|
|
415
579
|
this._needRender?.();
|
|
416
580
|
})
|
|
417
581
|
.catch((err: unknown) => {
|
|
@@ -422,50 +586,110 @@ export class BatchedSdfTextMesh
|
|
|
422
586
|
}
|
|
423
587
|
mesh.setText(text);
|
|
424
588
|
this.markVisibility(mesh);
|
|
589
|
+
this._markDeclutterDirty();
|
|
425
590
|
this._needRender?.();
|
|
426
591
|
}
|
|
427
592
|
}
|
|
428
593
|
|
|
594
|
+
/**
|
|
595
|
+
* The mesh for a feature's `batchIndex`, or `undefined` if not created yet.
|
|
596
|
+
* Meshes are created lazily and pushed onto `allMeshes` in creation order, so
|
|
597
|
+
* `allMeshes[batchIndex]` (the base-class mapping) does NOT hold here; the
|
|
598
|
+
* batchIndex→mesh mapping lives in `_meshByIndex`. Overriding this one
|
|
599
|
+
* accessor keeps the base-class setters and any external caller (picking,
|
|
600
|
+
* plugins) correct without patching each call site.
|
|
601
|
+
*/
|
|
602
|
+
override getMeshByBatchIndex(batchIndex: number): SDFTextMesh | undefined {
|
|
603
|
+
return this._meshByIndex[batchIndex];
|
|
604
|
+
}
|
|
605
|
+
|
|
429
606
|
setFeatureColorByBatchIndex(batchIndex: number, color: Color) {
|
|
430
|
-
const mesh = this.
|
|
607
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
431
608
|
if (mesh) {
|
|
432
609
|
mesh.setColor(color);
|
|
610
|
+
return;
|
|
433
611
|
}
|
|
612
|
+
const pending = this._pendingAt(batchIndex);
|
|
613
|
+
if (pending) pending.colorHex = color.getHex();
|
|
434
614
|
}
|
|
435
615
|
|
|
436
616
|
setFeatureShowByBatchIndex(batchIndex: number, rawVisible: boolean) {
|
|
437
|
-
const mesh = this.
|
|
617
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
438
618
|
if (mesh) {
|
|
439
619
|
mesh._setFeatureShow(rawVisible);
|
|
440
620
|
this.markVisibility(mesh);
|
|
621
|
+
this._markDeclutterDirty();
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
// A lazily-created mesh starts hidden (empty text ⇒ `visible=false`), so a
|
|
625
|
+
// mesh-less feature is already effectively `show:false`. Only `show:true`
|
|
626
|
+
// needs buffering; buffering `show:false` would allocate a pending entry
|
|
627
|
+
// per hidden feature — the common MVT case (most features stay hidden and
|
|
628
|
+
// never get a mesh) and exactly the allocation lazy creation avoids. For
|
|
629
|
+
// `show:false` just clear any stale buffered `show`, never allocate.
|
|
630
|
+
if (rawVisible) {
|
|
631
|
+
const pending = this._pendingAt(batchIndex);
|
|
632
|
+
if (pending) pending.show = true;
|
|
633
|
+
} else {
|
|
634
|
+
const pending = this._pending.get(batchIndex);
|
|
635
|
+
if (pending) pending.show = undefined;
|
|
441
636
|
}
|
|
442
637
|
}
|
|
443
638
|
|
|
444
639
|
setFeatureHeightByBatchIndex(batchIndex: number, height: number) {
|
|
445
|
-
const mesh = this.
|
|
640
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
446
641
|
if (mesh) {
|
|
447
642
|
mesh.setHeight(height);
|
|
643
|
+
this._markDeclutterDirty();
|
|
644
|
+
return;
|
|
448
645
|
}
|
|
646
|
+
const pending = this._pendingAt(batchIndex);
|
|
647
|
+
if (pending) pending.height = height;
|
|
449
648
|
}
|
|
450
649
|
|
|
451
650
|
setFeatureSizeByBatchIndex(batchIndex: number, size: number) {
|
|
452
|
-
const mesh = this.
|
|
651
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
453
652
|
if (mesh) {
|
|
454
653
|
mesh.setSize(size);
|
|
654
|
+
this._markDeclutterDirty();
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const pending = this._pendingAt(batchIndex);
|
|
658
|
+
if (pending) pending.size = size;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
setFeatureDeclutterPriorityByBatchIndex(
|
|
662
|
+
batchIndex: number,
|
|
663
|
+
priority: number,
|
|
664
|
+
) {
|
|
665
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
666
|
+
if (mesh) {
|
|
667
|
+
mesh.setDeclutterPriority(priority);
|
|
668
|
+
this._markDeclutterDirty();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const pending = this._pendingAt(batchIndex);
|
|
672
|
+
if (pending) {
|
|
673
|
+
pending.declutterPriority = priority;
|
|
674
|
+
pending.hasDeclutterPriority = true;
|
|
455
675
|
}
|
|
456
676
|
}
|
|
457
677
|
|
|
458
678
|
setFeatureOpacityByBatchIndex(batchIndex: number, opacity: number) {
|
|
459
|
-
const
|
|
679
|
+
const clampedOpacity = Number.isFinite(opacity)
|
|
680
|
+
? Math.max(0, Math.min(1, opacity))
|
|
681
|
+
: 1.0;
|
|
682
|
+
const mesh = this.getMeshByBatchIndex(batchIndex);
|
|
460
683
|
if (mesh) {
|
|
461
|
-
const clampedOpacity = Number.isFinite(opacity)
|
|
462
|
-
? Math.max(0, Math.min(1, opacity))
|
|
463
|
-
: 1.0;
|
|
464
684
|
mesh.setOpacity(clampedOpacity);
|
|
685
|
+
return;
|
|
465
686
|
}
|
|
687
|
+
const pending = this._pendingAt(batchIndex);
|
|
688
|
+
if (pending) pending.opacity = clampedOpacity;
|
|
466
689
|
}
|
|
467
690
|
|
|
468
691
|
dispose() {
|
|
692
|
+
this.ctx.declutter?.unregister(this);
|
|
469
693
|
this._unsubscribeEvict?.();
|
|
470
694
|
this._unsubscribeEvict = undefined;
|
|
471
695
|
const q = this._highQuality;
|
package/src/mesh/instanced.ts
CHANGED
package/src/mesh/polygon.ts
CHANGED
|
@@ -282,7 +282,8 @@ export class PolygonMesh extends BatchedFeatureMesh<
|
|
|
282
282
|
this.receiveShadow = !!meshMaterial.receiveShadow;
|
|
283
283
|
|
|
284
284
|
// This mesh is texturized if it has a tile handle (terrain attachment).
|
|
285
|
-
|
|
285
|
+
// `!= null`, not truthiness: the root vector tile's handle is 0.
|
|
286
|
+
const isTexturized = tileHandle != null;
|
|
286
287
|
const material = this.material;
|
|
287
288
|
|
|
288
289
|
material.vertexColors = false;
|
|
@@ -390,9 +391,15 @@ export class PolygonMesh extends BatchedFeatureMesh<
|
|
|
390
391
|
_update(material: PolygonMaterial, active: boolean, isTexturized: boolean) {
|
|
391
392
|
const enhancer = this.getEnhancer();
|
|
392
393
|
|
|
393
|
-
// Update mesh properties (not handled by enhancer)
|
|
394
|
-
|
|
395
|
-
|
|
394
|
+
// Update mesh properties (not handled by enhancer).
|
|
395
|
+
// Draped (clamp-to-ground) polygons bake offscreen only — never the main
|
|
396
|
+
// scene — and the Rust drape resolve, not this LOD `active` flag, picks which
|
|
397
|
+
// baked tile composites per terrain region. Tying bake visibility to `active`
|
|
398
|
+
// makes the drape flicker on LOD swaps; keep a draped mesh bakeable whenever
|
|
399
|
+
// shown so every built tile stays a stable drape source (see
|
|
400
|
+
// PolylineMesh._update).
|
|
401
|
+
const shown = (material.show ?? true) && (material.surfaceShow ?? true);
|
|
402
|
+
this.visible = isTexturized ? shown : shown && active;
|
|
396
403
|
if (this._debugBoundingSphereMesh) {
|
|
397
404
|
this._debugBoundingSphereMesh.visible = this.visible;
|
|
398
405
|
}
|
package/src/mesh/polyline.ts
CHANGED
|
@@ -404,8 +404,16 @@ export class PolylineMesh extends BatchedFeatureMesh<
|
|
|
404
404
|
|
|
405
405
|
const enhancer = this.getEnhancer();
|
|
406
406
|
|
|
407
|
-
// Update mesh properties (not handled by enhancer)
|
|
408
|
-
|
|
407
|
+
// Update mesh properties (not handled by enhancer).
|
|
408
|
+
// Draped polylines render only through the offscreen bake, never the main
|
|
409
|
+
// scene, and the Rust drape resolve — not this LOD `active` flag — decides
|
|
410
|
+
// which baked tile is composited per terrain region. Tying bake visibility
|
|
411
|
+
// to `active` makes the drape flicker on every LOD swap (the tile goes
|
|
412
|
+
// invisible → bakes empty before its replacement is ready). Keep a draped
|
|
413
|
+
// mesh bakeable whenever its material is shown, so every built tile stays a
|
|
414
|
+
// stable drape source — the vector twin of a loaded raster texture.
|
|
415
|
+
const shown = material.show ?? true;
|
|
416
|
+
this.visible = enhancer.states().isTexturized ? shown : shown && active;
|
|
409
417
|
this.castShadow = !!material.castShadow;
|
|
410
418
|
this.receiveShadow = !!material.receiveShadow;
|
|
411
419
|
|