@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.
- package/README.md +7 -7
- package/dist/assets/fontWorker-OwvKiQ8o.js +2 -0
- package/dist/assets/fontWorker-OwvKiQ8o.js.map +1 -0
- package/dist/assets/{index-ywHSGImj.js → index-TKhRxurv.js} +2 -2
- package/dist/assets/index-TKhRxurv.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-Du9nb2Pn.wasm → navara_wasm_font_worker_bg-Kk06ArRB.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-Xadu67fw.wasm → navara_wasm_worker_bg-BZjLKf4i.wasm} +0 -0
- package/dist/index.d.ts +351 -284
- package/dist/index.js +5394 -5470
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +287 -235
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +12 -12
- package/src/ShadowMapViewers.ts +1 -1
- package/src/atmosphere.test.ts +227 -28
- package/src/atmosphere.ts +219 -55
- package/src/constants/assets.ts +63 -13
- package/src/declutter/DeclutterManager.ts +1 -1
- package/src/declutter/types.ts +1 -1
- package/src/evaluations/FeatureEvaluator.test.ts +152 -0
- package/src/evaluations/FeatureEvaluator.ts +73 -2
- package/src/event/features/billboard.ts +1 -0
- package/src/event/features/model.ts +1 -1
- package/src/event/features/point.ts +1 -0
- package/src/event/index.ts +1 -1
- package/src/index.iife.ts +3 -3
- package/src/index.ts +10 -12
- package/src/layers/effect/README.md +1 -1
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +2 -2
- package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +1 -1
- package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +1 -1
- package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +1 -1
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/index.ts +10 -7
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.test.ts +74 -81
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +19 -86
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.test.ts +77 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.ts +9 -1
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.test.ts +15 -19
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.ts +0 -13
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +46 -48
- package/src/mesh/constants.ts +22 -0
- package/src/mesh/index.ts +1 -1
- package/src/mesh/model.ts +6 -0
- package/src/mesh/polygon.ts +6 -0
- package/src/mesh/polyline.ts +6 -0
- package/src/mesh/rtcRteHelper.ts +1 -1
- package/src/mesh/sdfText/batchedSdfText.ts +1223 -0
- package/src/mesh/sdfText/glyphBuffers.ts +222 -0
- package/src/mesh/sdfText/glyphSlots.test.ts +151 -0
- package/src/mesh/sdfText/glyphSlots.ts +98 -0
- package/src/mesh/sdfText/index.ts +10 -0
- package/src/mesh/sdfText/labelData.test.ts +178 -0
- package/src/mesh/sdfText/labelData.ts +187 -0
- package/src/mesh/{sdfText.test.ts → sdfText/layout.test.ts} +1 -1
- package/src/mesh/sdfText/layout.ts +306 -0
- package/src/mesh/sprite/instancedSprite.ts +33 -1
- package/src/utils/screen.ts +1 -1
- package/dist/assets/fontWorker-wz61edjR.js +0 -2
- package/dist/assets/fontWorker-wz61edjR.js.map +0 -1
- package/dist/assets/index-ywHSGImj.js.map +0 -1
- package/src/mesh/batchedSdfText.ts +0 -708
- package/src/mesh/sdfText.ts +0 -1189
- /package/dist/{draco_decoder.wasm → assets/draco_decoder-C32yEggz.wasm} +0 -0
- /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
|
-
}
|