@navaramap/three 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/dist/assets/fontWorker-8oWvyvwL.js +2 -0
- package/dist/assets/fontWorker-8oWvyvwL.js.map +1 -0
- package/dist/assets/index-D9G7kIEj.js +25 -0
- package/dist/assets/index-D9G7kIEj.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-Bnck8OGf.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-DG9ORAmj.wasm → navara_wasm_worker_bg-Q8zrkLCR.wasm} +0 -0
- package/dist/draco_decoder.wasm +0 -0
- package/dist/draco_decoder2.wasm +0 -0
- package/dist/index.d.ts +445 -16
- package/dist/index.js +6151 -5717
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +241 -222
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +34 -33
- 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/features/modelGpuBytes.ts +1 -3
- 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/material/enhancer/ShaderReplacer.ts +1 -2
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +6 -0
- package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +11 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.test.ts +83 -0
- package/src/material/enhancer/tileComposite/tileCompositeBaseEnhancer/mutates.ts +13 -3
- package/src/mesh/batchTexture.ts +2 -4
- package/src/mesh/batchedSdfText.ts +61 -1
- package/src/mesh/sdfText.ts +170 -5
- 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/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
|
@@ -44,6 +44,7 @@ export const createBaseMutates = (
|
|
|
44
44
|
uTextWidth: { value: 0.0 },
|
|
45
45
|
uTextHeight: { value: 0.0 },
|
|
46
46
|
uBgYBounds: { value: new Vector2(0.0, 1.0) },
|
|
47
|
+
uDeclutterHide: { value: 0.0 },
|
|
47
48
|
uRTCCenter: {
|
|
48
49
|
value: new Vector3(
|
|
49
50
|
rtcCenter?.[0] ?? 0,
|
|
@@ -113,6 +114,7 @@ export const createBaseMutates = (
|
|
|
113
114
|
uniforms.uTextWidth = refs.uTextWidth;
|
|
114
115
|
uniforms.uTextHeight = refs.uTextHeight;
|
|
115
116
|
uniforms.uBgYBounds = refs.uBgYBounds;
|
|
117
|
+
uniforms.uDeclutterHide = refs.uDeclutterHide;
|
|
116
118
|
uniforms.uRTCCenter = refs.uRTCCenter;
|
|
117
119
|
uniforms.uRTCCenterView = refs.uRTCCenterView;
|
|
118
120
|
uniforms.uEyeRTELow = refs.uEyeRTELow;
|
|
@@ -228,5 +230,9 @@ export const createBaseMutates = (
|
|
|
228
230
|
setBatchId: (id: number) => {
|
|
229
231
|
refs.nvr_uBatchId.value = id;
|
|
230
232
|
},
|
|
233
|
+
|
|
234
|
+
setDeclutterHide: (value: number) => {
|
|
235
|
+
refs.uDeclutterHide.value = value;
|
|
236
|
+
},
|
|
231
237
|
};
|
|
232
238
|
};
|
|
@@ -103,6 +103,10 @@ export type SdfTextBaseRefs = {
|
|
|
103
103
|
uTextWidth: UniformValue<number>;
|
|
104
104
|
uTextHeight: UniformValue<number>;
|
|
105
105
|
uBgYBounds: UniformValue<Vector2>;
|
|
106
|
+
/** Screen-space declutter hide factor (0 = shown … 1 = hidden), animated
|
|
107
|
+
* by the declutter fade. Driven by the declutter pass, not by material
|
|
108
|
+
* style updates. */
|
|
109
|
+
uDeclutterHide: UniformValue<number>;
|
|
106
110
|
uRTCCenter: UniformValue<Vector3>;
|
|
107
111
|
uRTCCenterView: UniformValue<Vector3>;
|
|
108
112
|
uEyeRTELow: UniformValue<Vector3>;
|
|
@@ -180,5 +184,12 @@ export type SdfTextBaseMutates = Mutates<
|
|
|
180
184
|
* Set the batch ID uniform (immutable after mount, but needed for batch creation).
|
|
181
185
|
*/
|
|
182
186
|
setBatchId: (batchId: number) => void;
|
|
187
|
+
/**
|
|
188
|
+
* Set the declutter hide factor (0 = shown … 1 = hidden); the fragment
|
|
189
|
+
* shader scales opacity by `1 - value` and the vertex shader hard-culls
|
|
190
|
+
* at 1. Kept separate from `show`/`opacity` so user-driven visibility and
|
|
191
|
+
* declutter results compose instead of clobbering each other.
|
|
192
|
+
*/
|
|
193
|
+
setDeclutterHide: (value: number) => void;
|
|
183
194
|
}
|
|
184
195
|
>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Color, Texture, Vector2 } from "three";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { createTestTileHandler } from "../../../../test-utils/engine";
|
|
5
|
+
import type { RasterCompositeLayer } from "../../../../tileTexture/types";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
createCoreUniformMutates,
|
|
9
|
+
type CompositeUniformTarget,
|
|
10
|
+
} from "./mutates";
|
|
11
|
+
|
|
12
|
+
// The real Rust `mercator_y` (clamped to the WM band), reached through the
|
|
13
|
+
// same context handler the app injects — the clamp regression below exercises
|
|
14
|
+
// the production implementation, not a reimplementation.
|
|
15
|
+
const { mercatorY } = createTestTileHandler();
|
|
16
|
+
|
|
17
|
+
const rasterLayer = (
|
|
18
|
+
reproject: [number, number] | undefined,
|
|
19
|
+
): RasterCompositeLayer => ({
|
|
20
|
+
absSlot: 0,
|
|
21
|
+
region: "raster",
|
|
22
|
+
texture: null,
|
|
23
|
+
uvOffset: new Vector2(0, 0),
|
|
24
|
+
uvScale: new Vector2(1, 1),
|
|
25
|
+
kind: "raster",
|
|
26
|
+
color: new Color(1, 1, 1),
|
|
27
|
+
opacity: 1,
|
|
28
|
+
water: false,
|
|
29
|
+
reproject,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const bind = (reproject: [number, number] | undefined) => {
|
|
33
|
+
const mutates = createCoreUniformMutates(mercatorY);
|
|
34
|
+
const target: CompositeUniformTarget = {};
|
|
35
|
+
mutates.attachUniforms(target, 1, new Texture());
|
|
36
|
+
mutates.bindSlot(0, rasterLayer(reproject));
|
|
37
|
+
return target;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe("bindSlot Mercator reprojection constants", () => {
|
|
41
|
+
it("clamps a polar latitude band to the WebMercator limit instead of NaN", () => {
|
|
42
|
+
// A z0 Geographic terrain tile's band is ±90°, and Rust hands it over as
|
|
43
|
+
// f32 — slightly PAST ±π/2, where an unclamped log(tan(...)) is NaN. A
|
|
44
|
+
// NaN span fails the shader's `abs(mDen) > 1e-3` guard, silently skipping
|
|
45
|
+
// reprojection for the whole tile (imagery squashes toward the equator).
|
|
46
|
+
const f32HalfPi = Math.fround(Math.PI / 2);
|
|
47
|
+
const target = bind([-f32HalfPi, f32HalfPi]);
|
|
48
|
+
|
|
49
|
+
const merc = target.uReprojectMerc.value[0];
|
|
50
|
+
// Band start/span from the clamped WM limit: mRs = -π, mDen = 2π.
|
|
51
|
+
expect(merc.x).toBeCloseTo(-Math.PI, 5);
|
|
52
|
+
expect(merc.y).toBeCloseTo(2 * Math.PI, 5);
|
|
53
|
+
// Both edges sit on the WM limit → both polar-cap clamp flags set.
|
|
54
|
+
expect(merc.z).toBe(1);
|
|
55
|
+
expect(merc.w).toBe(1);
|
|
56
|
+
expect(target.uReproject.value[0]).toBe(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("keeps an interior band un-clamped and un-flagged", () => {
|
|
60
|
+
const south = 0.2;
|
|
61
|
+
const north = 0.7;
|
|
62
|
+
const target = bind([south, north]);
|
|
63
|
+
|
|
64
|
+
const merc = target.uReprojectMerc.value[0];
|
|
65
|
+
expect(merc.x).toBeCloseTo(mercatorY(south), 9);
|
|
66
|
+
expect(merc.y).toBeCloseTo(mercatorY(north) - mercatorY(south), 9);
|
|
67
|
+
expect(merc.z).toBe(0);
|
|
68
|
+
expect(merc.w).toBe(0);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("flags but does not NaN a band touching one WM edge", () => {
|
|
72
|
+
// z1 north Geographic tile: [0, 90°]. South edge is fine; the north edge
|
|
73
|
+
// must clamp to the WM limit and set only the top polar-cap flag.
|
|
74
|
+
const target = bind([0, Math.fround(Math.PI / 2)]);
|
|
75
|
+
|
|
76
|
+
const merc = target.uReprojectMerc.value[0];
|
|
77
|
+
expect(merc.x).toBeCloseTo(0, 9);
|
|
78
|
+
expect(merc.y).toBeCloseTo(Math.PI, 5);
|
|
79
|
+
expect(merc.z).toBe(1);
|
|
80
|
+
expect(merc.w).toBe(0);
|
|
81
|
+
expect(Number.isFinite(merc.y)).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -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;
|
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) {
|
|
@@ -6,6 +6,10 @@ import type { FontManager } from "@navaramap/font";
|
|
|
6
6
|
import { type 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";
|
|
@@ -36,7 +40,7 @@ type PositionsInfo = PositionsInfoBase &
|
|
|
36
40
|
|
|
37
41
|
export class BatchedSdfTextMesh
|
|
38
42
|
extends InstancedMesh<SDFTextMesh>
|
|
39
|
-
implements PickableMesh
|
|
43
|
+
implements PickableMesh, DeclutterParticipant
|
|
40
44
|
{
|
|
41
45
|
readonly ctx: EventContext;
|
|
42
46
|
/** The font identifier from material — may be a family name or a URL. */
|
|
@@ -79,6 +83,41 @@ export class BatchedSdfTextMesh
|
|
|
79
83
|
this._highQuality,
|
|
80
84
|
() => this._revalidateStaleMeshes(),
|
|
81
85
|
);
|
|
86
|
+
ctx.declutter?.register(this);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// --- DeclutterParticipant ---
|
|
90
|
+
|
|
91
|
+
collectDeclutterCandidates(out: DeclutterCandidate[]): void {
|
|
92
|
+
if (!this.visible) return;
|
|
93
|
+
const meshes = this.meshes();
|
|
94
|
+
for (let i = 0; i < meshes.length; i++) {
|
|
95
|
+
const candidate = meshes[i].getDeclutterCandidate(this, i);
|
|
96
|
+
if (candidate) out.push(candidate);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
applyDeclutter(handle: number, hidden: boolean): void {
|
|
101
|
+
this.meshes()[handle]?.setDeclutterHidden(hidden);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
stepDeclutterFade(deltaMs: number): boolean {
|
|
105
|
+
let animating = false;
|
|
106
|
+
for (const mesh of this.meshes()) {
|
|
107
|
+
animating = mesh.stepDeclutterFade(deltaMs) || animating;
|
|
108
|
+
}
|
|
109
|
+
return animating;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Candidates changed (text, style, position, visibility): ask the shared
|
|
113
|
+
* declutter pass to re-place on its next update. */
|
|
114
|
+
private _markDeclutterDirty(): void {
|
|
115
|
+
this.ctx.declutter?.markDirty();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
override setActive(active: boolean) {
|
|
119
|
+
super.setActive(active);
|
|
120
|
+
this._markDeclutterDirty();
|
|
82
121
|
}
|
|
83
122
|
|
|
84
123
|
/**
|
|
@@ -111,6 +150,7 @@ export class BatchedSdfTextMesh
|
|
|
111
150
|
// Force a rebuild even though the text string is unchanged: the baked
|
|
112
151
|
// atlas rects and glyph retains must refresh against the new metrics.
|
|
113
152
|
mesh.setText(text, true);
|
|
153
|
+
this._markDeclutterDirty();
|
|
114
154
|
this._needRender?.();
|
|
115
155
|
})
|
|
116
156
|
.catch((err: unknown) => {
|
|
@@ -222,6 +262,8 @@ export class BatchedSdfTextMesh
|
|
|
222
262
|
: position.subarray(posIdx, posIdx + positionSize);
|
|
223
263
|
this.meshes()[i].setPosition(pos, RTE, transform);
|
|
224
264
|
}
|
|
265
|
+
// Anchors moved (e.g. terrain height resolution) — re-place labels.
|
|
266
|
+
this._markDeclutterDirty();
|
|
225
267
|
}
|
|
226
268
|
|
|
227
269
|
const fontIdentifier = m.material.font ?? this._fontIdentifier;
|
|
@@ -292,6 +334,7 @@ export class BatchedSdfTextMesh
|
|
|
292
334
|
mesh.update(material, forceUpdate);
|
|
293
335
|
this.markVisibility(mesh);
|
|
294
336
|
}
|
|
337
|
+
this._markDeclutterDirty();
|
|
295
338
|
|
|
296
339
|
if (needRender) needRender();
|
|
297
340
|
}
|
|
@@ -412,6 +455,7 @@ export class BatchedSdfTextMesh
|
|
|
412
455
|
// Restore intended visibility now that text content is available
|
|
413
456
|
mesh.visible = intendedVisible;
|
|
414
457
|
this.markVisibility(mesh);
|
|
458
|
+
this._markDeclutterDirty();
|
|
415
459
|
this._needRender?.();
|
|
416
460
|
})
|
|
417
461
|
.catch((err: unknown) => {
|
|
@@ -422,6 +466,7 @@ export class BatchedSdfTextMesh
|
|
|
422
466
|
}
|
|
423
467
|
mesh.setText(text);
|
|
424
468
|
this.markVisibility(mesh);
|
|
469
|
+
this._markDeclutterDirty();
|
|
425
470
|
this._needRender?.();
|
|
426
471
|
}
|
|
427
472
|
}
|
|
@@ -438,6 +483,7 @@ export class BatchedSdfTextMesh
|
|
|
438
483
|
if (mesh) {
|
|
439
484
|
mesh._setFeatureShow(rawVisible);
|
|
440
485
|
this.markVisibility(mesh);
|
|
486
|
+
this._markDeclutterDirty();
|
|
441
487
|
}
|
|
442
488
|
}
|
|
443
489
|
|
|
@@ -445,6 +491,7 @@ export class BatchedSdfTextMesh
|
|
|
445
491
|
const mesh = this.meshes()[batchIndex];
|
|
446
492
|
if (mesh) {
|
|
447
493
|
mesh.setHeight(height);
|
|
494
|
+
this._markDeclutterDirty();
|
|
448
495
|
}
|
|
449
496
|
}
|
|
450
497
|
|
|
@@ -452,6 +499,18 @@ export class BatchedSdfTextMesh
|
|
|
452
499
|
const mesh = this.meshes()[batchIndex];
|
|
453
500
|
if (mesh) {
|
|
454
501
|
mesh.setSize(size);
|
|
502
|
+
this._markDeclutterDirty();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
setFeatureDeclutterPriorityByBatchIndex(
|
|
507
|
+
batchIndex: number,
|
|
508
|
+
priority: number,
|
|
509
|
+
) {
|
|
510
|
+
const mesh = this.meshes()[batchIndex];
|
|
511
|
+
if (mesh) {
|
|
512
|
+
mesh.setDeclutterPriority(priority);
|
|
513
|
+
this._markDeclutterDirty();
|
|
455
514
|
}
|
|
456
515
|
}
|
|
457
516
|
|
|
@@ -466,6 +525,7 @@ export class BatchedSdfTextMesh
|
|
|
466
525
|
}
|
|
467
526
|
|
|
468
527
|
dispose() {
|
|
528
|
+
this.ctx.declutter?.unregister(this);
|
|
469
529
|
this._unsubscribeEvict?.();
|
|
470
530
|
this._unsubscribeEvict = undefined;
|
|
471
531
|
const q = this._highQuality;
|
package/src/mesh/sdfText.ts
CHANGED
|
@@ -26,6 +26,11 @@ import {
|
|
|
26
26
|
Vector2,
|
|
27
27
|
} from "three";
|
|
28
28
|
|
|
29
|
+
import {
|
|
30
|
+
DECLUTTER_FADE_MS,
|
|
31
|
+
type DeclutterCandidate,
|
|
32
|
+
type DeclutterParticipant,
|
|
33
|
+
} from "../declutter/types";
|
|
29
34
|
import type { MaterialEnhancer } from "../material/enhancer/MaterialEnhancer";
|
|
30
35
|
import {
|
|
31
36
|
createSdfTextMaterialEnhancer,
|
|
@@ -219,6 +224,27 @@ export class SDFTextMesh
|
|
|
219
224
|
private _requestRender?: () => void;
|
|
220
225
|
// The COLRv1 color atlas is always FontManager-owned; no local field needed.
|
|
221
226
|
|
|
227
|
+
/** World-space anchor in ECEF meters (f64), kept in sync with the position
|
|
228
|
+
* uniforms so the declutter pass can project it without touching WASM. */
|
|
229
|
+
private _worldAnchor = new Float64Array(3);
|
|
230
|
+
/** Whether this label participates in screen-space decluttering. */
|
|
231
|
+
private _declutter = false;
|
|
232
|
+
/** Layer-level placement priority from the material. */
|
|
233
|
+
private _declutterPriority = 0;
|
|
234
|
+
/** Per-feature priority set through the evaluator; overrides the layer
|
|
235
|
+
* value when defined. */
|
|
236
|
+
private _declutterPriorityOverride: number | undefined;
|
|
237
|
+
/** Current animated hide factor (mirrors the uDeclutterHide uniform) and
|
|
238
|
+
* the placement target it is fading toward. */
|
|
239
|
+
private _declutterHide = 0;
|
|
240
|
+
private _declutterTarget = 0;
|
|
241
|
+
/** Text block dimensions in em units, mirroring the uTextWidth/uTextHeight/
|
|
242
|
+
* uBgYBounds uniforms; 0 width means "no collision box" (empty text). */
|
|
243
|
+
private _labelWidthEm = 0;
|
|
244
|
+
private _labelHeightEm = 0;
|
|
245
|
+
private _labelMinYEm = 0;
|
|
246
|
+
private _labelMaxYEm = 0;
|
|
247
|
+
|
|
222
248
|
private _enhancer: MaterialEnhancer<
|
|
223
249
|
ShaderMaterial,
|
|
224
250
|
{ base?: SdfTextBaseProps },
|
|
@@ -311,6 +337,22 @@ export class SDFTextMesh
|
|
|
311
337
|
transform.ty,
|
|
312
338
|
transform.tz,
|
|
313
339
|
]);
|
|
340
|
+
this._cacheWorldAnchor(position, RTE, [
|
|
341
|
+
transform.tx,
|
|
342
|
+
transform.ty,
|
|
343
|
+
transform.tz,
|
|
344
|
+
]);
|
|
345
|
+
|
|
346
|
+
// Decluttered labels start hidden and fade in once the placement pass
|
|
347
|
+
// grants them space — otherwise dense tiles flash their full clutter for
|
|
348
|
+
// a frame before the first pass runs.
|
|
349
|
+
this._declutter = material.declutter ?? true;
|
|
350
|
+
this._declutterPriority = material.declutterPriority ?? 0;
|
|
351
|
+
if (this._declutter) {
|
|
352
|
+
this._declutterHide = 1;
|
|
353
|
+
this._declutterTarget = 1;
|
|
354
|
+
mutates.setDeclutterHide(1);
|
|
355
|
+
}
|
|
314
356
|
|
|
315
357
|
// Register shader hook
|
|
316
358
|
mat.onBeforeCompile = this._enhancer.transformShader;
|
|
@@ -446,6 +488,7 @@ export class SDFTextMesh
|
|
|
446
488
|
|
|
447
489
|
if (!text) {
|
|
448
490
|
this.geometry.instanceCount = 0;
|
|
491
|
+
this._labelWidthEm = 0;
|
|
449
492
|
this._setGlyphKeys([]);
|
|
450
493
|
return;
|
|
451
494
|
}
|
|
@@ -457,6 +500,7 @@ export class SDFTextMesh
|
|
|
457
500
|
);
|
|
458
501
|
if (!shapeResult) {
|
|
459
502
|
this.geometry.instanceCount = 0;
|
|
503
|
+
this._labelWidthEm = 0;
|
|
460
504
|
this._setGlyphKeys([]);
|
|
461
505
|
return;
|
|
462
506
|
}
|
|
@@ -546,6 +590,36 @@ export class SDFTextMesh
|
|
|
546
590
|
this._enhancer.update({ base: { opacity: clampedOpacity } });
|
|
547
591
|
}
|
|
548
592
|
|
|
593
|
+
/**
|
|
594
|
+
* Set this label's declutter fade target; the actual hide factor animates
|
|
595
|
+
* toward it in {@link stepDeclutterFade}. Deliberately separate from
|
|
596
|
+
* `visible`/`show`: it neither churns scene-graph membership nor releases
|
|
597
|
+
* glyph atlas retains, so the declutter pass can retarget it every
|
|
598
|
+
* placement run at no cost.
|
|
599
|
+
*/
|
|
600
|
+
setDeclutterHidden(hidden: boolean): void {
|
|
601
|
+
this._declutterTarget = hidden ? 1 : 0;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Advance the hide factor toward its target by `deltaMs / DECLUTTER_FADE_MS`
|
|
606
|
+
* and push it to the uniform. Returns true while still mid-fade.
|
|
607
|
+
*/
|
|
608
|
+
stepDeclutterFade(deltaMs: number): boolean {
|
|
609
|
+
const target = this._declutterTarget;
|
|
610
|
+
let value = this._declutterHide;
|
|
611
|
+
if (value === target) return false;
|
|
612
|
+
|
|
613
|
+
const step = deltaMs / DECLUTTER_FADE_MS;
|
|
614
|
+
value =
|
|
615
|
+
value < target
|
|
616
|
+
? Math.min(value + step, target)
|
|
617
|
+
: Math.max(value - step, target);
|
|
618
|
+
this._declutterHide = value;
|
|
619
|
+
this._enhancer.mutates().setDeclutterHide(value);
|
|
620
|
+
return value !== target;
|
|
621
|
+
}
|
|
622
|
+
|
|
549
623
|
_setFeatureWidth(_width: number): void {
|
|
550
624
|
// Width is not applicable to text meshes.
|
|
551
625
|
// This method is intentionally a no-op to satisfy the FeatureMesh guard.
|
|
@@ -565,6 +639,80 @@ export class SDFTextMesh
|
|
|
565
639
|
this._enhancer
|
|
566
640
|
.mutates()
|
|
567
641
|
.setPosition(position, RTE, [transform.tx, transform.ty, transform.tz]);
|
|
642
|
+
this._cacheWorldAnchor(position, RTE, [
|
|
643
|
+
transform.tx,
|
|
644
|
+
transform.ty,
|
|
645
|
+
transform.tz,
|
|
646
|
+
]);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/** Reconstruct the absolute ECEF anchor from the same inputs the position
|
|
650
|
+
* uniforms receive (RTE high+low split, or RTC-relative + center). */
|
|
651
|
+
private _cacheWorldAnchor(
|
|
652
|
+
position: Float32Array | { high: Float32Array; low: Float32Array },
|
|
653
|
+
RTE: boolean,
|
|
654
|
+
rtcCenter: [number, number, number],
|
|
655
|
+
): void {
|
|
656
|
+
const anchor = this._worldAnchor;
|
|
657
|
+
if (RTE) {
|
|
658
|
+
const p = position as { high: Float32Array; low: Float32Array };
|
|
659
|
+
anchor[0] = p.high[0] + p.low[0];
|
|
660
|
+
anchor[1] = p.high[1] + p.low[1];
|
|
661
|
+
anchor[2] = (p.high[2] ?? 0.0) + (p.low[2] ?? 0.0);
|
|
662
|
+
} else {
|
|
663
|
+
const p = position as Float32Array;
|
|
664
|
+
anchor[0] = p[0] + rtcCenter[0];
|
|
665
|
+
anchor[1] = p[1] + rtcCenter[1];
|
|
666
|
+
anchor[2] = (p[2] ?? 0.0) + rtcCenter[2];
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Build this label's collision candidate for the shared declutter pass, or
|
|
672
|
+
* null when it doesn't participate (declutter disabled, hidden, or empty).
|
|
673
|
+
* The local box mirrors the vertex shader's layout: glyphs span
|
|
674
|
+
* [0, textWidth] × [bgMinY, bgMaxY] in em units, shifted by the `center`
|
|
675
|
+
* anchor and scaled by the font size (sdfText.vert.glsl:111-115).
|
|
676
|
+
*/
|
|
677
|
+
getDeclutterCandidate(
|
|
678
|
+
owner: DeclutterParticipant,
|
|
679
|
+
handle: number,
|
|
680
|
+
): DeclutterCandidate | null {
|
|
681
|
+
if (!this._declutter || !this.visible || this._labelWidthEm <= 0) {
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
const state = this._enhancer.states();
|
|
685
|
+
const size = state.fontSize;
|
|
686
|
+
if (size <= 0) return null;
|
|
687
|
+
|
|
688
|
+
const cx = Math.min(Math.max(state.center[0], -0.5), 0.5);
|
|
689
|
+
const cy = Math.min(Math.max(state.center[1], -0.5), 0.5);
|
|
690
|
+
const w = this._labelWidthEm;
|
|
691
|
+
const h = this._labelHeightEm;
|
|
692
|
+
return {
|
|
693
|
+
anchorX: this._worldAnchor[0],
|
|
694
|
+
anchorY: this._worldAnchor[1],
|
|
695
|
+
anchorZ: this._worldAnchor[2],
|
|
696
|
+
addHeight: state.addHeight,
|
|
697
|
+
minX: (0 - cx * w) * size,
|
|
698
|
+
maxX: (w - cx * w) * size,
|
|
699
|
+
minY: (this._labelMinYEm - cy * h) * size,
|
|
700
|
+
maxY: (this._labelMaxYEm - cy * h) * size,
|
|
701
|
+
sizeInMeters: state.sizeInMeters,
|
|
702
|
+
priority: this._declutterPriorityOverride ?? this._declutterPriority,
|
|
703
|
+
isShown: this._declutterTarget === 0,
|
|
704
|
+
owner,
|
|
705
|
+
handle,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Set a per-feature placement priority (higher wins), overriding the
|
|
711
|
+
* layer-level `declutterPriority` for this label. Pass `undefined` to fall
|
|
712
|
+
* back to the layer value. Driven by the feature evaluator.
|
|
713
|
+
*/
|
|
714
|
+
setDeclutterPriority(priority: number | undefined): void {
|
|
715
|
+
this._declutterPriorityOverride = priority;
|
|
568
716
|
}
|
|
569
717
|
|
|
570
718
|
/**
|
|
@@ -596,6 +744,17 @@ export class SDFTextMesh
|
|
|
596
744
|
this.setText(this._text, true);
|
|
597
745
|
}
|
|
598
746
|
|
|
747
|
+
// Read before the visibility early-return: the declutter pass consults
|
|
748
|
+
// these even while other style state is skipped for hidden labels.
|
|
749
|
+
const nextDeclutter = material.declutter ?? true;
|
|
750
|
+
if (this._declutter && !nextDeclutter) {
|
|
751
|
+
// Leaving declutter mode: clear any hide the pass applied — the mesh
|
|
752
|
+
// stops producing candidates, so nothing else would ever re-show it.
|
|
753
|
+
this.setDeclutterHidden(false);
|
|
754
|
+
}
|
|
755
|
+
this._declutter = nextDeclutter;
|
|
756
|
+
this._declutterPriority = material.declutterPriority ?? 0;
|
|
757
|
+
|
|
599
758
|
this.visible = (material.show ?? true) && !!this._text;
|
|
600
759
|
this._syncGlyphRefs();
|
|
601
760
|
if (!this.visible) return;
|
|
@@ -921,6 +1080,7 @@ export class SDFTextMesh
|
|
|
921
1080
|
const count = renderable.length;
|
|
922
1081
|
if (count === 0) {
|
|
923
1082
|
this.geometry.instanceCount = 0;
|
|
1083
|
+
this._labelWidthEm = 0;
|
|
924
1084
|
return;
|
|
925
1085
|
}
|
|
926
1086
|
|
|
@@ -976,14 +1136,19 @@ export class SDFTextMesh
|
|
|
976
1136
|
|
|
977
1137
|
this.geometry.instanceCount = count + 1;
|
|
978
1138
|
|
|
979
|
-
// Update text dimension uniforms via mutates
|
|
1139
|
+
// Update text dimension uniforms via mutates, mirroring them CPU-side for
|
|
1140
|
+
// the declutter pass's collision box.
|
|
1141
|
+
this._labelWidthEm = textWidth / SDF_PX_SIZE;
|
|
1142
|
+
this._labelHeightEm = textHeight / SDF_PX_SIZE;
|
|
1143
|
+
this._labelMinYEm = bgMinY === Infinity ? 0.0 : bgMinY;
|
|
1144
|
+
this._labelMaxYEm = bgMaxY === -Infinity ? 1.0 : bgMaxY;
|
|
980
1145
|
this._enhancer
|
|
981
1146
|
.mutates()
|
|
982
1147
|
.updateTextDimensions(
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1148
|
+
this._labelWidthEm,
|
|
1149
|
+
this._labelHeightEm,
|
|
1150
|
+
this._labelMinYEm,
|
|
1151
|
+
this._labelMaxYEm,
|
|
987
1152
|
);
|
|
988
1153
|
}
|
|
989
1154
|
|