@navaramap/three 0.0.3 → 0.0.5

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 (64) hide show
  1. package/README.md +7 -7
  2. package/dist/assets/fontWorker-OwvKiQ8o.js +2 -0
  3. package/dist/assets/fontWorker-OwvKiQ8o.js.map +1 -0
  4. package/dist/assets/{index-ywHSGImj.js → index-TKhRxurv.js} +2 -2
  5. package/dist/assets/index-TKhRxurv.js.map +1 -0
  6. package/dist/assets/{navara_wasm_font_worker_bg-Du9nb2Pn.wasm → navara_wasm_font_worker_bg-Kk06ArRB.wasm} +0 -0
  7. package/dist/assets/{navara_wasm_worker_bg-Xadu67fw.wasm → navara_wasm_worker_bg-BZjLKf4i.wasm} +0 -0
  8. package/dist/index.d.ts +351 -284
  9. package/dist/index.js +5394 -5470
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.umd.cjs +287 -235
  12. package/dist/index.umd.cjs.map +1 -1
  13. package/package.json +12 -12
  14. package/src/ShadowMapViewers.ts +1 -1
  15. package/src/atmosphere.test.ts +227 -28
  16. package/src/atmosphere.ts +219 -55
  17. package/src/constants/assets.ts +63 -13
  18. package/src/declutter/DeclutterManager.ts +1 -1
  19. package/src/declutter/types.ts +1 -1
  20. package/src/evaluations/FeatureEvaluator.test.ts +152 -0
  21. package/src/evaluations/FeatureEvaluator.ts +73 -2
  22. package/src/event/features/billboard.ts +1 -0
  23. package/src/event/features/model.ts +1 -1
  24. package/src/event/features/point.ts +1 -0
  25. package/src/event/index.ts +1 -1
  26. package/src/index.iife.ts +3 -3
  27. package/src/index.ts +10 -12
  28. package/src/layers/effect/README.md +1 -1
  29. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +2 -2
  30. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +1 -1
  31. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +1 -1
  32. package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +1 -1
  33. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/index.ts +10 -7
  34. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.test.ts +74 -81
  35. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +19 -86
  36. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.test.ts +77 -0
  37. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.ts +9 -1
  38. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.test.ts +15 -19
  39. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.ts +0 -13
  40. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +46 -48
  41. package/src/mesh/constants.ts +22 -0
  42. package/src/mesh/index.ts +1 -1
  43. package/src/mesh/model.ts +6 -0
  44. package/src/mesh/polygon.ts +6 -0
  45. package/src/mesh/polyline.ts +6 -0
  46. package/src/mesh/rtcRteHelper.ts +1 -1
  47. package/src/mesh/sdfText/batchedSdfText.ts +1223 -0
  48. package/src/mesh/sdfText/glyphBuffers.ts +222 -0
  49. package/src/mesh/sdfText/glyphSlots.test.ts +151 -0
  50. package/src/mesh/sdfText/glyphSlots.ts +98 -0
  51. package/src/mesh/sdfText/index.ts +10 -0
  52. package/src/mesh/sdfText/labelData.test.ts +178 -0
  53. package/src/mesh/sdfText/labelData.ts +187 -0
  54. package/src/mesh/{sdfText.test.ts → sdfText/layout.test.ts} +1 -1
  55. package/src/mesh/sdfText/layout.ts +306 -0
  56. package/src/mesh/sprite/instancedSprite.ts +33 -1
  57. package/src/utils/screen.ts +1 -1
  58. package/dist/assets/fontWorker-wz61edjR.js +0 -2
  59. package/dist/assets/fontWorker-wz61edjR.js.map +0 -1
  60. package/dist/assets/index-ywHSGImj.js.map +0 -1
  61. package/src/mesh/batchedSdfText.ts +0 -708
  62. package/src/mesh/sdfText.ts +0 -1189
  63. /package/dist/{draco_decoder.wasm → assets/draco_decoder-C32yEggz.wasm} +0 -0
  64. /package/dist/{draco_decoder2.wasm → assets/draco_decoder-Z1_iN-Ht.wasm} +0 -0
@@ -1,708 +0,0 @@
1
- import type {
2
- TextMesh as NavaraTextMesh,
3
- TextMaterial as NavaraTextMaterial,
4
- } from "@navaramap/engine";
5
- import type { FontManager } from "@navaramap/font";
6
- import { Color } from "three";
7
- import invariant from "tiny-invariant";
8
-
9
- import type {
10
- DeclutterCandidate,
11
- DeclutterParticipant,
12
- } from "../declutter/types";
13
- import type { EventContext } from "../event/context";
14
-
15
- import { InstancedMesh, type InstancedMeshOptions } from "./instanced";
16
- import type { PickableMesh } from "./pickableMesh";
17
- import { SDFTextMesh } from "./sdfText";
18
-
19
- type PositionsInfoBase = {
20
- batchIDs: Float32Array<ArrayBufferLike> | null;
21
- positionSize: number;
22
- batchIDSize: number;
23
- nPositions: number;
24
- };
25
-
26
- type PositionsInfo = PositionsInfoBase &
27
- (
28
- | {
29
- RTE: true;
30
- position: {
31
- high: Float32Array<ArrayBufferLike>;
32
- low: Float32Array<ArrayBufferLike>;
33
- };
34
- }
35
- | {
36
- RTE: false;
37
- position: Float32Array<ArrayBufferLike>;
38
- }
39
- );
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
-
57
- export class BatchedSdfTextMesh
58
- extends InstancedMesh<SDFTextMesh>
59
- implements PickableMesh, DeclutterParticipant
60
- {
61
- readonly ctx: EventContext;
62
- /** The font identifier from material — may be a family name or a URL. */
63
- private _fontIdentifier: string;
64
- /** Per-batch text quality. All instanced meshes in this batch share it
65
- * because they all sample the same atlas texture; flipping quality requires
66
- * a new BatchedSdfTextMesh. */
67
- private _highQuality: boolean;
68
- private _fontManager: FontManager;
69
- private _needRender?: () => void;
70
- /** Unsubscribe from the font manager's atlas-eviction notifications. */
71
- private _unsubscribeEvict?: () => void;
72
- /**
73
- * Face URLs loaded by this mesh for font-family fonts.
74
- * Each URL in this set has had loadFont() called exactly once by this mesh
75
- * and must be balanced with unloadFont() on dispose or font change.
76
- */
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"];
87
-
88
- constructor(
89
- ctx: EventContext,
90
- m: NavaraTextMesh,
91
- fontIdentifier: string,
92
- options: InstancedMeshOptions,
93
- loadedFaceUrls?: Set<string>,
94
- ) {
95
- super(options);
96
- this.ctx = ctx;
97
- this._fontIdentifier = fontIdentifier;
98
- this._highQuality = m.material.highQuality ?? false;
99
- invariant(ctx.fontManager);
100
- this._fontManager = ctx.fontManager;
101
- this._loadedFaceUrls = loadedFaceUrls ?? new Set();
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();
108
- // When the shared atlas evicts glyphs, a still-in-flight glyph this batch
109
- // already baked into a visible mesh may have had its rect reused. Rebuild
110
- // any such stale mesh so its UVs and retains refresh.
111
- this._unsubscribeEvict = this._fontManager.onAtlasEvicted(
112
- this._fontIdentifier,
113
- this._highQuality,
114
- () => this._revalidateStaleMeshes(),
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();
151
- }
152
-
153
- /**
154
- * After an atlas eviction, re-prepare and force-rebuild any visible mesh whose
155
- * shaped text is now stale (its pinned glyphs may have been evicted before the
156
- * retain landed and its rect reused). Fresh meshes short-circuit on the cheap
157
- * `isTextPrepared` check, so this is inexpensive to run per eviction.
158
- */
159
- private _revalidateStaleMeshes(): void {
160
- const q = this._highQuality;
161
- for (const mesh of this.meshes()) {
162
- const text = mesh.text;
163
- if (!mesh.visible || !text) continue;
164
- if (this._fontManager.isTextPrepared(this._fontIdentifier, text, q)) {
165
- continue;
166
- }
167
- this._fontManager
168
- .prepareText(this._fontIdentifier, text, q, this._loadedFaceUrls)
169
- .then(() => {
170
- // Text may have changed (or the mesh hidden) while re-preparing.
171
- if (mesh.text !== text || !mesh.visible) return;
172
- const sharedTex = this._fontManager.getAtlasTexture(
173
- this._fontIdentifier,
174
- q,
175
- );
176
- if (sharedTex) mesh.setAtlasTexture(sharedTex);
177
- mesh.setColorAtlasTexture(
178
- this._fontManager.getColorAtlasTexture(this._fontIdentifier, q),
179
- );
180
- // Force a rebuild even though the text string is unchanged: the baked
181
- // atlas rects and glyph retains must refresh against the new metrics.
182
- mesh.setText(text, true);
183
- this._markDeclutterDirty();
184
- this._needRender?.();
185
- })
186
- .catch((err: unknown) => {
187
- console.error("Failed to revalidate text after eviction:", err);
188
- });
189
- }
190
- }
191
-
192
- get fontIdentifier(): string {
193
- return this._fontIdentifier;
194
- }
195
-
196
- get highQuality(): boolean {
197
- return this._highQuality;
198
- }
199
-
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;
211
- if (!positionInfo) {
212
- return;
213
- }
214
-
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
- }
224
-
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
- }
239
-
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,
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).
278
- this._highQuality,
279
- );
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?.(),
292
- );
293
-
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);
301
-
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));
308
- }
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
- }
325
-
326
- this._meshByIndex[batchIndex] = mesh;
327
- this.addWithBatchIndex(mesh, batchIndex);
328
- return mesh;
329
- }
330
-
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);
341
- }
342
- return state;
343
- }
344
-
345
- async _update(m: NavaraTextMesh, needRender?: () => void) {
346
- if (needRender) this._needRender = needRender;
347
-
348
- const material = m.material;
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;
353
-
354
- const positionInfo = this.extractPositions(m);
355
- if (positionInfo) {
356
- invariant(
357
- this._positions === null ||
358
- positionInfo.nPositions === this._positions.nPositions,
359
- "Number of positions in the updated geometry must match the initial geometry",
360
- );
361
- this._positions = positionInfo;
362
-
363
- const { position, positionSize, RTE } = positionInfo;
364
- const transform = m.transform;
365
- for (const mesh of this.meshes()) {
366
- const posIdx = (mesh.userData.batchIndex as number) * positionSize;
367
- const pos = RTE
368
- ? {
369
- high: position.high.subarray(posIdx, posIdx + positionSize),
370
- low: position.low.subarray(posIdx, posIdx + positionSize),
371
- }
372
- : position.subarray(posIdx, posIdx + positionSize);
373
- mesh.setPosition(pos, RTE, transform);
374
- }
375
- // Anchors moved (e.g. terrain height resolution) — re-place labels.
376
- this._markDeclutterDirty();
377
- }
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
-
384
- const fontIdentifier = m.material.font ?? this._fontIdentifier;
385
- const needFontUpdate = fontIdentifier !== this._fontIdentifier;
386
-
387
- // Quality is immutable per batch (see _highQuality docs); use the batch's
388
- // quality everywhere, ignoring `m.material.highQuality` on updates.
389
- const q = this._highQuality;
390
-
391
- if (needFontUpdate) {
392
- // Unload old font resources.
393
- if (this._loadedFaceUrls.size > 0) {
394
- await Promise.all(
395
- [...this._loadedFaceUrls].map((url) =>
396
- this._fontManager.unloadFont(url, q),
397
- ),
398
- );
399
- this._loadedFaceUrls.clear();
400
- } else if (!this._fontManager.isFamily(this._fontIdentifier)) {
401
- await this._fontManager.unloadFont(this._fontIdentifier, q);
402
- }
403
- // For standalone new fonts load upfront; family faces are loaded lazily
404
- // by prepareText below.
405
- if (!this._fontManager.isFamily(fontIdentifier)) {
406
- await this._fontManager.loadFont(fontIdentifier, q);
407
- }
408
- }
409
- this._fontIdentifier = fontIdentifier;
410
-
411
- // If the text hasn't been prepared in the worker yet, schedule async preparation
412
- if (
413
- (needFontUpdate || text) &&
414
- !this._fontManager.isTextPrepared(this._fontIdentifier, text, q)
415
- ) {
416
- this._fontManager
417
- .prepareText(this._fontIdentifier, text, q, this._loadedFaceUrls)
418
- .then(() => {
419
- this._applyUpdate(material, needRender, needFontUpdate);
420
- })
421
- .catch((err: unknown) => {
422
- console.error("Failed to prepare text:", err);
423
- needRender?.();
424
- });
425
- return;
426
- }
427
-
428
- this._applyUpdate(material, needRender, needFontUpdate);
429
- }
430
-
431
- private _applyUpdate(
432
- material: NavaraTextMaterial,
433
- needRender?: () => void,
434
- forceUpdate = false,
435
- ) {
436
- // Update shared textures (in-place update if either atlas grew)
437
- const sharedTex = this._fontManager.getAtlasTexture(
438
- this._fontIdentifier,
439
- this._highQuality,
440
- );
441
- const sharedColorTex = this._fontManager.getColorAtlasTexture(
442
- this._fontIdentifier,
443
- this._highQuality,
444
- );
445
-
446
- for (const mesh of this.meshes()) {
447
- if (sharedTex) mesh.setAtlasTexture(sharedTex);
448
- mesh.setColorAtlasTexture(sharedColorTex);
449
- mesh.update(material, forceUpdate);
450
- this.markVisibility(mesh);
451
- }
452
- this._markDeclutterDirty();
453
-
454
- if (needRender) needRender();
455
- }
456
-
457
- override onBeforePicking() {
458
- for (const mesh of this.meshes()) {
459
- mesh.onBeforePicking();
460
- }
461
- }
462
-
463
- override onAfterPicking() {
464
- for (const mesh of this.meshes()) {
465
- mesh.onAfterPicking();
466
- }
467
- }
468
-
469
- private extractPositions(m: NavaraTextMesh): PositionsInfo | null {
470
- const { buf } = this.ctx;
471
- const g = m.geometry;
472
-
473
- const batchIdsData = g.batch_ids;
474
- const batchIDs = buf.removeF32(batchIdsData.data);
475
- const batchIDSize = batchIdsData.size;
476
-
477
- const positionData = g.position;
478
- const position = positionData
479
- ? buf.removeF32(positionData.data)
480
- : undefined;
481
-
482
- if (position && positionData) {
483
- const positionSize = positionData.size;
484
- const nPositions = position.length / positionSize;
485
-
486
- return {
487
- position,
488
- batchIDs,
489
- batchIDSize,
490
- positionSize,
491
- nPositions,
492
- RTE: false,
493
- };
494
- }
495
-
496
- const positionHighData = g.position_3d_high;
497
- const positionLowData = g.position_3d_low;
498
- const positionHigh = positionHighData
499
- ? buf.removeF32(positionHighData.data)
500
- : undefined;
501
- const positionLow = positionLowData
502
- ? buf.removeF32(positionLowData.data)
503
- : undefined;
504
-
505
- if (positionHigh && positionLow && positionHighData && positionLowData) {
506
- const positionLowSize = positionLowData.size;
507
- const positionHighSize = positionHighData.size;
508
- invariant(
509
- positionLowSize === positionHighSize,
510
- "Position high and low size mismatch",
511
- );
512
-
513
- const nPositions = positionHigh.length / positionHighSize;
514
-
515
- return {
516
- position: { high: positionHigh, low: positionLow },
517
- batchIDs,
518
- batchIDSize,
519
- positionSize: positionHighSize,
520
- nPositions,
521
- RTE: true,
522
- };
523
- }
524
-
525
- return null;
526
- }
527
-
528
- setTextByBatchIndex(batchIndex: number, text: string) {
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);
535
-
536
- if (mesh) {
537
- // If the text hasn't been prepared in the worker yet, schedule async preparation
538
- if (
539
- text &&
540
- !this._fontManager.isTextPrepared(
541
- this._fontIdentifier,
542
- text,
543
- this._highQuality,
544
- )
545
- ) {
546
- // Capture the intended visibility before the async font prep begins.
547
- // A concurrent processTextChanged → mesh.update(material) call may
548
- // reset mesh.visible to false (because this._text is still empty)
549
- // while the font is loading.
550
- const intendedVisible = mesh.visible;
551
-
552
- this._fontManager
553
- .prepareText(
554
- this._fontIdentifier,
555
- text,
556
- this._highQuality,
557
- this._loadedFaceUrls,
558
- )
559
- .then(() => {
560
- // Refresh shared atlas textures if the worker rasterized new glyphs
561
- const sharedTex = this._fontManager.getAtlasTexture(
562
- this._fontIdentifier,
563
- this._highQuality,
564
- );
565
- if (sharedTex) {
566
- mesh.setAtlasTexture(sharedTex);
567
- }
568
- mesh.setColorAtlasTexture(
569
- this._fontManager.getColorAtlasTexture(
570
- this._fontIdentifier,
571
- this._highQuality,
572
- ),
573
- );
574
- mesh.setText(text);
575
- // Restore intended visibility now that text content is available
576
- mesh.visible = intendedVisible;
577
- this.markVisibility(mesh);
578
- this._markDeclutterDirty();
579
- this._needRender?.();
580
- })
581
- .catch((err: unknown) => {
582
- console.error("Failed to prepare text:", err);
583
- this._needRender?.();
584
- });
585
- return;
586
- }
587
- mesh.setText(text);
588
- this.markVisibility(mesh);
589
- this._markDeclutterDirty();
590
- this._needRender?.();
591
- }
592
- }
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
-
606
- setFeatureColorByBatchIndex(batchIndex: number, color: Color) {
607
- const mesh = this.getMeshByBatchIndex(batchIndex);
608
- if (mesh) {
609
- mesh.setColor(color);
610
- return;
611
- }
612
- const pending = this._pendingAt(batchIndex);
613
- if (pending) pending.colorHex = color.getHex();
614
- }
615
-
616
- setFeatureShowByBatchIndex(batchIndex: number, rawVisible: boolean) {
617
- const mesh = this.getMeshByBatchIndex(batchIndex);
618
- if (mesh) {
619
- mesh._setFeatureShow(rawVisible);
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;
636
- }
637
- }
638
-
639
- setFeatureHeightByBatchIndex(batchIndex: number, height: number) {
640
- const mesh = this.getMeshByBatchIndex(batchIndex);
641
- if (mesh) {
642
- mesh.setHeight(height);
643
- this._markDeclutterDirty();
644
- return;
645
- }
646
- const pending = this._pendingAt(batchIndex);
647
- if (pending) pending.height = height;
648
- }
649
-
650
- setFeatureSizeByBatchIndex(batchIndex: number, size: number) {
651
- const mesh = this.getMeshByBatchIndex(batchIndex);
652
- if (mesh) {
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;
675
- }
676
- }
677
-
678
- setFeatureOpacityByBatchIndex(batchIndex: number, opacity: number) {
679
- const clampedOpacity = Number.isFinite(opacity)
680
- ? Math.max(0, Math.min(1, opacity))
681
- : 1.0;
682
- const mesh = this.getMeshByBatchIndex(batchIndex);
683
- if (mesh) {
684
- mesh.setOpacity(clampedOpacity);
685
- return;
686
- }
687
- const pending = this._pendingAt(batchIndex);
688
- if (pending) pending.opacity = clampedOpacity;
689
- }
690
-
691
- dispose() {
692
- this.ctx.declutter?.unregister(this);
693
- this._unsubscribeEvict?.();
694
- this._unsubscribeEvict = undefined;
695
- const q = this._highQuality;
696
- const unload =
697
- this._loadedFaceUrls.size > 0
698
- ? Promise.all(
699
- [...this._loadedFaceUrls].map((url) =>
700
- this._fontManager.unloadFont(url, q),
701
- ),
702
- )
703
- : this._fontManager.unloadFont(this._fontIdentifier, q);
704
- void unload.catch((err: unknown) => {
705
- console.error("Failed to unload font during dispose:", err);
706
- });
707
- }
708
- }