@navaramap/three 0.0.0 → 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-665OLxjy.wasm → navara_wasm_font_worker_bg-CkZP9l2i.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-BXhKC5bE.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 +587 -67
- package/dist/index.js +5640 -4944
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +238 -211
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +35 -34
- 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 +17 -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 +143 -33
- 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 +19 -4
- package/src/material/enhancer/tileComposite/tileElevationHeatmapEnhancer/shader.ts +9 -2
- 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/mesh/tile/drapeResolver.ts +76 -0
- package/src/mesh/{tile.test.ts → tile/index.test.ts} +16 -6
- package/src/mesh/{tile.ts → tile/index.ts} +124 -392
- package/src/mesh/tile/rasterDrapeResolver.ts +423 -0
- package/src/mesh/tile/vectorDrapeResolver.ts +404 -0
- 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/TileTextureCache.test.ts +2 -0
- package/src/tileTexture/TileTextureCache.ts +1 -0
- package/src/tileTexture/TileTextureCompositor.test.ts +107 -0
- package/src/tileTexture/TileTextureCompositor.ts +248 -32
- package/src/tileTexture/__snapshots__/CompositeShader.snapshot.test.ts.snap +36 -16
- package/src/tileTexture/types.ts +7 -0
- package/src/utils/demNoDataColor.test.ts +47 -0
- package/src/utils/demNoDataColor.ts +48 -0
- 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-CEJEfRpb.js +0 -2
- package/dist/assets/fontWorker-CEJEfRpb.js.map +0 -1
- package/dist/assets/index-BUB-ijMO.js +0 -12
- package/dist/assets/index-BUB-ijMO.js.map +0 -1
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
|
|
|
@@ -10,14 +10,22 @@ import {
|
|
|
10
10
|
Object3D,
|
|
11
11
|
ShaderMaterial,
|
|
12
12
|
BufferAttribute,
|
|
13
|
+
type BufferGeometry,
|
|
13
14
|
Color,
|
|
15
|
+
type Material,
|
|
14
16
|
PerspectiveCamera,
|
|
15
17
|
Vector2,
|
|
16
18
|
} from "three";
|
|
17
19
|
import invariant from "tiny-invariant";
|
|
18
20
|
|
|
21
|
+
import {
|
|
22
|
+
DECLUTTER_FADE_MS,
|
|
23
|
+
type DeclutterCandidate,
|
|
24
|
+
type DeclutterParticipant,
|
|
25
|
+
} from "../../declutter/types";
|
|
19
26
|
import type { EventContext } from "../../event/context";
|
|
20
27
|
import { createInstancedSpriteMaterialEnhancer } from "../../material/enhancer";
|
|
28
|
+
import type { CustomObject3DEventMap } from "../../object3DEvent";
|
|
21
29
|
import { PickableMesh } from "../pickableMesh";
|
|
22
30
|
|
|
23
31
|
import { BillboardAtlas, type AtlasRect } from "./billboardAtlas";
|
|
@@ -46,7 +54,10 @@ type PositionsInfo = {
|
|
|
46
54
|
const _tmpSize = new Vector2();
|
|
47
55
|
|
|
48
56
|
// Coupled with crates/navara_feature/src/geometry/point.rs::pixel_to_world
|
|
49
|
-
export class InstancedSpriteMesh
|
|
57
|
+
export class InstancedSpriteMesh
|
|
58
|
+
extends Mesh<BufferGeometry, Material | Material[], CustomObject3DEventMap>
|
|
59
|
+
implements PickableMesh, DeclutterParticipant
|
|
60
|
+
{
|
|
50
61
|
private _batchIdToInstance = new Map<number, number>();
|
|
51
62
|
private _initialColor: Color = new Color(0xffffff);
|
|
52
63
|
private _initialHeight = 0.0;
|
|
@@ -72,15 +83,182 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
72
83
|
private _enhancedMaterial?: ReturnType<
|
|
73
84
|
typeof createInstancedSpriteMaterialEnhancer
|
|
74
85
|
>;
|
|
86
|
+
/** Per-instance world anchors in ECEF meters (f64, 3 per instance), kept in
|
|
87
|
+
* sync with the position attributes for the declutter pass. */
|
|
88
|
+
private _anchors: Float64Array | null = null;
|
|
89
|
+
/** Whether this mesh's instances participate in screen-space decluttering. */
|
|
90
|
+
private _declutter = false;
|
|
91
|
+
/** Layer-level placement priority from the material. */
|
|
92
|
+
private _declutterPriority = 0;
|
|
93
|
+
/** Per-instance priorities set through the evaluator (NaN = no override,
|
|
94
|
+
* fall back to the layer value). Lazily allocated on first use. */
|
|
95
|
+
private _declutterPriorityOverrides: Float32Array | null = null;
|
|
96
|
+
/** Per-instance fade targets for `instanceDeclutterHide` (0 = shown,
|
|
97
|
+
* 1 = hidden); the attribute animates toward these in stepDeclutterFade. */
|
|
98
|
+
private _declutterTargets: Float32Array | null = null;
|
|
99
|
+
/** True while any instance's hide factor may differ from its target. */
|
|
100
|
+
private _declutterAnimating = false;
|
|
101
|
+
|
|
75
102
|
constructor(options: InstancedSpriteOptions) {
|
|
76
103
|
super();
|
|
77
104
|
this.renderOrder = options.renderOrder ?? this.renderOrder;
|
|
78
105
|
this.ctx = options.ctx;
|
|
106
|
+
this.ctx.declutter?.register(this);
|
|
107
|
+
// `processObjectRemoved` dispatches this for every removed mesh; it is the
|
|
108
|
+
// reliable teardown signal (this class's dispose() is not called there).
|
|
109
|
+
this.addEventListener("removedFromWorld", () => {
|
|
110
|
+
this.ctx.declutter?.unregister(this);
|
|
111
|
+
});
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
setActive(active: boolean) {
|
|
82
115
|
this._active = active;
|
|
83
116
|
this.updateVisibility();
|
|
117
|
+
this.ctx.declutter?.markDirty();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- DeclutterParticipant ---
|
|
121
|
+
|
|
122
|
+
collectDeclutterCandidates(out: DeclutterCandidate[]): void {
|
|
123
|
+
if (!this.visible || !this._declutter || !this._anchors) return;
|
|
124
|
+
const enhancer = this._enhancedMaterial;
|
|
125
|
+
const params = this.geometry?.getAttribute("instanceParams") as
|
|
126
|
+
InstancedBufferAttribute | undefined;
|
|
127
|
+
if (!enhancer || !params) return;
|
|
128
|
+
|
|
129
|
+
const state = enhancer.states();
|
|
130
|
+
const cx = Math.min(Math.max(state.center[0], -0.5), 0.5);
|
|
131
|
+
const cy = Math.min(Math.max(state.center[1], -0.5), 0.5);
|
|
132
|
+
// Mirror of instancedSprite.vert.glsl:100-104 — aspect is per-instance
|
|
133
|
+
// (from the atlas rect), not a material-level uniform; there is no
|
|
134
|
+
// material-wide "aspect" state to read.
|
|
135
|
+
const uvRect = state.billboard
|
|
136
|
+
? (this.geometry?.getAttribute("instanceUvRect") as
|
|
137
|
+
InstancedBufferAttribute | undefined)
|
|
138
|
+
: undefined;
|
|
139
|
+
const anchors = this._anchors;
|
|
140
|
+
const overrides = this._declutterPriorityOverrides;
|
|
141
|
+
const targets = this._declutterTargets;
|
|
142
|
+
const count = Math.min(params.count, anchors.length / 3);
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < count; i++) {
|
|
145
|
+
if (params.getZ(i) <= 0.5) continue; // hidden by user `show`
|
|
146
|
+
const instanceSize = params.getY(i);
|
|
147
|
+
const size = instanceSize >= 0.0 ? instanceSize : state.scale;
|
|
148
|
+
if (size <= 0.0) continue;
|
|
149
|
+
|
|
150
|
+
const override = overrides ? overrides[i] : Number.NaN;
|
|
151
|
+
const rectH = uvRect ? uvRect.getW(i) : 0;
|
|
152
|
+
const aspect = uvRect && rectH > 0.0 ? uvRect.getZ(i) / rectH : 1.0;
|
|
153
|
+
|
|
154
|
+
// Mirror of instancedSprite.vert.glsl:95-97 — the quad spans
|
|
155
|
+
// (position.xy - center) * vec2(aspect, 1) * size around the anchor.
|
|
156
|
+
out.push({
|
|
157
|
+
anchorX: anchors[i * 3],
|
|
158
|
+
anchorY: anchors[i * 3 + 1],
|
|
159
|
+
anchorZ: anchors[i * 3 + 2],
|
|
160
|
+
addHeight: params.getX(i),
|
|
161
|
+
minX: (-0.5 - cx) * aspect * size,
|
|
162
|
+
maxX: (0.5 - cx) * aspect * size,
|
|
163
|
+
minY: (-0.5 - cy) * size,
|
|
164
|
+
maxY: (0.5 - cy) * size,
|
|
165
|
+
sizeInMeters: state.sizeInMeters,
|
|
166
|
+
// NaN-safe: an unset override falls back to the layer priority.
|
|
167
|
+
priority: Number.isNaN(override) ? this._declutterPriority : override,
|
|
168
|
+
isShown: targets ? targets[i] === 0 : true,
|
|
169
|
+
owner: this,
|
|
170
|
+
handle: i,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
applyDeclutter(handle: number, hidden: boolean): void {
|
|
176
|
+
this.setDeclutterHiddenByInstance(handle, hidden);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
stepDeclutterFade(deltaMs: number): boolean {
|
|
180
|
+
if (!this._declutterAnimating) return false;
|
|
181
|
+
const attr = this.geometry?.getAttribute("instanceDeclutterHide") as
|
|
182
|
+
InstancedBufferAttribute | undefined;
|
|
183
|
+
const targets = this._declutterTargets;
|
|
184
|
+
if (!attr || !targets) {
|
|
185
|
+
this._declutterAnimating = false;
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const step = deltaMs / DECLUTTER_FADE_MS;
|
|
190
|
+
const count = Math.min(attr.count, targets.length);
|
|
191
|
+
let stillAnimating = false;
|
|
192
|
+
let changed = false;
|
|
193
|
+
for (let i = 0; i < count; i++) {
|
|
194
|
+
let value = attr.getX(i);
|
|
195
|
+
const target = targets[i];
|
|
196
|
+
if (value === target) continue;
|
|
197
|
+
value =
|
|
198
|
+
value < target
|
|
199
|
+
? Math.min(value + step, target)
|
|
200
|
+
: Math.max(value - step, target);
|
|
201
|
+
attr.setX(i, value);
|
|
202
|
+
changed = true;
|
|
203
|
+
if (value !== target) stillAnimating = true;
|
|
204
|
+
}
|
|
205
|
+
if (changed) attr.needsUpdate = true;
|
|
206
|
+
this._declutterAnimating = stillAnimating;
|
|
207
|
+
return stillAnimating;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private _cacheDeclutterState(m: NavaraPointMesh | NavaraBillboardMesh) {
|
|
211
|
+
const nextDeclutter = m.material.declutter ?? true;
|
|
212
|
+
if (this._declutter && !nextDeclutter) {
|
|
213
|
+
// Leaving declutter mode: clear hides the pass applied — the mesh stops
|
|
214
|
+
// producing candidates, so nothing else would ever re-show them.
|
|
215
|
+
this._clearDeclutterHidden();
|
|
216
|
+
}
|
|
217
|
+
this._declutter = nextDeclutter;
|
|
218
|
+
this._declutterPriority = m.material.declutterPriority ?? 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private _clearDeclutterHidden(): void {
|
|
222
|
+
const targets = this._declutterTargets;
|
|
223
|
+
if (!targets) return;
|
|
224
|
+
targets.fill(0.0);
|
|
225
|
+
// Everything fades back in from wherever its hide factor currently is.
|
|
226
|
+
this._declutterAnimating = true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Reconstruct absolute ECEF anchors from the same arrays the position
|
|
230
|
+
* attributes receive (RTE high+low split, or RTC-relative + center). */
|
|
231
|
+
private _cacheAnchors(
|
|
232
|
+
positionsInfo: PositionsInfo,
|
|
233
|
+
transform: { tx: number; ty: number; tz: number },
|
|
234
|
+
): void {
|
|
235
|
+
const { nPositions, positionSize, RTE } = positionsInfo;
|
|
236
|
+
const anchors =
|
|
237
|
+
this._anchors && this._anchors.length === nPositions * 3
|
|
238
|
+
? this._anchors
|
|
239
|
+
: new Float64Array(nPositions * 3);
|
|
240
|
+
|
|
241
|
+
if (RTE) {
|
|
242
|
+
const pos = positionsInfo.position as {
|
|
243
|
+
high: Float32Array<ArrayBufferLike>;
|
|
244
|
+
low: Float32Array<ArrayBufferLike>;
|
|
245
|
+
};
|
|
246
|
+
for (let i = 0; i < nPositions; i++) {
|
|
247
|
+
const s = i * positionSize;
|
|
248
|
+
anchors[i * 3] = pos.high[s] + pos.low[s];
|
|
249
|
+
anchors[i * 3 + 1] = pos.high[s + 1] + pos.low[s + 1];
|
|
250
|
+
anchors[i * 3 + 2] = (pos.high[s + 2] ?? 0.0) + (pos.low[s + 2] ?? 0.0);
|
|
251
|
+
}
|
|
252
|
+
} else {
|
|
253
|
+
const pos = positionsInfo.position as Float32Array<ArrayBufferLike>;
|
|
254
|
+
for (let i = 0; i < nPositions; i++) {
|
|
255
|
+
const s = i * positionSize;
|
|
256
|
+
anchors[i * 3] = pos[s] + transform.tx;
|
|
257
|
+
anchors[i * 3 + 1] = pos[s + 1] + transform.ty;
|
|
258
|
+
anchors[i * 3 + 2] = (pos[s + 2] ?? 0.0) + transform.tz;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
this._anchors = anchors;
|
|
84
262
|
}
|
|
85
263
|
|
|
86
264
|
async _init(m: NavaraPointMesh | NavaraBillboardMesh) {
|
|
@@ -90,6 +268,9 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
90
268
|
return;
|
|
91
269
|
}
|
|
92
270
|
|
|
271
|
+
this._cacheDeclutterState(m);
|
|
272
|
+
this._cacheAnchors(positionsInfo, m.transform);
|
|
273
|
+
|
|
93
274
|
// Create Geometry
|
|
94
275
|
this.geometry = this._initGeometry(positionsInfo, m);
|
|
95
276
|
|
|
@@ -97,12 +278,15 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
97
278
|
this.material = await this._initMaterial(positionsInfo, m);
|
|
98
279
|
|
|
99
280
|
this.frustumCulled = false; // Disable since bounding box doesn't account for instance positions
|
|
281
|
+
this.ctx.declutter?.markDirty();
|
|
100
282
|
}
|
|
101
283
|
|
|
102
284
|
async _update(m: NavaraPointMesh | NavaraBillboardMesh) {
|
|
103
285
|
const enhancer = this.getEnhancer();
|
|
104
286
|
const material = this.material as ShaderMaterial;
|
|
105
287
|
|
|
288
|
+
this._cacheDeclutterState(m);
|
|
289
|
+
|
|
106
290
|
if (material.visible !== m.material.show) {
|
|
107
291
|
material.visible = m.material.show ?? true;
|
|
108
292
|
this.updateVisibility();
|
|
@@ -162,6 +346,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
162
346
|
const positionsInfo = this.extractPositions(m);
|
|
163
347
|
|
|
164
348
|
if (positionsInfo) {
|
|
349
|
+
this._cacheAnchors(positionsInfo, m.transform);
|
|
165
350
|
if (positionsInfo.RTE) {
|
|
166
351
|
const pos = positionsInfo.position as {
|
|
167
352
|
high: Float32Array<ArrayBufferLike>;
|
|
@@ -198,6 +383,8 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
198
383
|
await this._setDefaultImage(m.material.url);
|
|
199
384
|
}
|
|
200
385
|
}
|
|
386
|
+
|
|
387
|
+
this.ctx.declutter?.markDirty();
|
|
201
388
|
}
|
|
202
389
|
|
|
203
390
|
private _initGeometry(
|
|
@@ -307,6 +494,16 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
307
494
|
"instanceColor",
|
|
308
495
|
new InstancedBufferAttribute(colorBuffer, 3),
|
|
309
496
|
);
|
|
497
|
+
// Declutter hide factors (0 = shown … 1 = hidden). Decluttered instances
|
|
498
|
+
// start hidden and fade in once the placement pass grants them space, so
|
|
499
|
+
// dense tiles don't flash their full clutter before the first pass runs.
|
|
500
|
+
const initialHide = this._declutter ? 1.0 : 0.0;
|
|
501
|
+
const declutterBuffer = new Float32Array(instanceCount).fill(initialHide);
|
|
502
|
+
this._declutterTargets = new Float32Array(instanceCount).fill(initialHide);
|
|
503
|
+
instancedGeometry.setAttribute(
|
|
504
|
+
"instanceDeclutterHide",
|
|
505
|
+
new InstancedBufferAttribute(declutterBuffer, 1),
|
|
506
|
+
);
|
|
310
507
|
instancedGeometry.setAttribute(
|
|
311
508
|
"instanceBatchID",
|
|
312
509
|
new InstancedBufferAttribute(
|
|
@@ -572,6 +769,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
572
769
|
) as InstancedBufferAttribute;
|
|
573
770
|
paramsAttr.setZ(instanceId, rawVisible ? 1.0 : 0.0);
|
|
574
771
|
paramsAttr.needsUpdate = true;
|
|
772
|
+
this.ctx.declutter?.markDirty();
|
|
575
773
|
}
|
|
576
774
|
|
|
577
775
|
setFeatureOpacityByBatchId(batchId: number, opacity: number) {
|
|
@@ -599,6 +797,46 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
599
797
|
const sanitizedHeight = Number.isFinite(height) ? height : 0.0;
|
|
600
798
|
paramsAttr.setX(instanceId, sanitizedHeight);
|
|
601
799
|
paramsAttr.needsUpdate = true;
|
|
800
|
+
this.ctx.declutter?.markDirty();
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Set one instance's declutter fade target; the attribute animates toward
|
|
805
|
+
* it in {@link stepDeclutterFade}. Deliberately separate from the `show`
|
|
806
|
+
* component of `instanceParams` so user-driven visibility and declutter
|
|
807
|
+
* results compose instead of clobbering each other.
|
|
808
|
+
*/
|
|
809
|
+
setDeclutterHiddenByInstance(instanceIndex: number, hidden: boolean) {
|
|
810
|
+
const targets = this._declutterTargets;
|
|
811
|
+
if (!targets || instanceIndex < 0 || instanceIndex >= targets.length) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
targets[instanceIndex] = hidden ? 1.0 : 0.0;
|
|
815
|
+
// Cheap over-approximation; stepDeclutterFade clears it when everything
|
|
816
|
+
// has reached its target.
|
|
817
|
+
this._declutterAnimating = true;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* Set a per-feature placement priority (higher wins), overriding the
|
|
822
|
+
* layer-level `declutterPriority` for this instance. Driven by the feature
|
|
823
|
+
* evaluator.
|
|
824
|
+
*/
|
|
825
|
+
setFeatureDeclutterPriorityByBatchId(batchId: number, priority: number) {
|
|
826
|
+
const instanceId = this._batchIdToInstance.get(batchId);
|
|
827
|
+
if (instanceId === undefined) return;
|
|
828
|
+
|
|
829
|
+
if (!this._declutterPriorityOverrides) {
|
|
830
|
+
const count = this._anchors ? this._anchors.length / 3 : 0;
|
|
831
|
+
if (count === 0) return;
|
|
832
|
+
this._declutterPriorityOverrides = new Float32Array(count).fill(
|
|
833
|
+
Number.NaN,
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
if (instanceId >= this._declutterPriorityOverrides.length) return;
|
|
837
|
+
if (this._declutterPriorityOverrides[instanceId] === priority) return;
|
|
838
|
+
this._declutterPriorityOverrides[instanceId] = priority;
|
|
839
|
+
this.ctx.declutter?.markDirty();
|
|
602
840
|
}
|
|
603
841
|
|
|
604
842
|
setFeatureSizeByBatchId(batchId: number, size: number) {
|
|
@@ -616,6 +854,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
616
854
|
: -1.0;
|
|
617
855
|
paramsAttr.setY(instanceId, sanitizedSize);
|
|
618
856
|
paramsAttr.needsUpdate = true;
|
|
857
|
+
this.ctx.declutter?.markDirty();
|
|
619
858
|
}
|
|
620
859
|
|
|
621
860
|
/**
|
|
@@ -634,8 +873,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
634
873
|
if (instanceId === undefined) return;
|
|
635
874
|
|
|
636
875
|
const rectAttr = this.geometry.getAttribute("instanceUvRect") as
|
|
637
|
-
|
|
|
638
|
-
| undefined;
|
|
876
|
+
InstancedBufferAttribute | undefined;
|
|
639
877
|
if (!rectAttr) return;
|
|
640
878
|
|
|
641
879
|
if (url == null) {
|
|
@@ -669,6 +907,7 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
669
907
|
}
|
|
670
908
|
|
|
671
909
|
dispose(): void {
|
|
910
|
+
this.ctx.declutter?.unregister(this);
|
|
672
911
|
this.geometry?.dispose();
|
|
673
912
|
|
|
674
913
|
// The material's uTexture points at the atlas texture; the atlas owns it.
|
|
@@ -688,6 +927,9 @@ export class InstancedSpriteMesh extends Mesh implements PickableMesh {
|
|
|
688
927
|
|
|
689
928
|
// Clear internal collections to release references
|
|
690
929
|
this._batchIdToInstance.clear();
|
|
930
|
+
this._anchors = null;
|
|
931
|
+
this._declutterTargets = null;
|
|
932
|
+
this._declutterPriorityOverrides = null;
|
|
691
933
|
this._imageOverrides.clear();
|
|
692
934
|
this._requestedImageUrls.clear();
|
|
693
935
|
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
MagnificationTextureFilter,
|
|
3
|
+
MinificationTextureFilter,
|
|
4
|
+
Texture,
|
|
5
|
+
} from "three";
|
|
6
|
+
|
|
7
|
+
import type { TextureOptions } from "../../textures";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A drape resolver owns one family of composite material slots for a terrain
|
|
11
|
+
* tile: how its layers' pixels reach those slots (direct texture binding or an
|
|
12
|
+
* offscreen bake into per-layer render targets), gated so the work only runs
|
|
13
|
+
* when the Rust-side resolve or the backing content actually changed.
|
|
14
|
+
*
|
|
15
|
+
* Three implementations, side by side, spell out the rendering differences:
|
|
16
|
+
* - {@link VectorDrapeResolver} (`vectorDrapeResolver.ts`) — clamp-to-ground
|
|
17
|
+
* vector layers. Always baked on every tiling scheme (vector content is an
|
|
18
|
+
* offscreen scene, so rasterizing is intrinsic); WebMercator terrain is the
|
|
19
|
+
* degenerate single-source identity case, Geographic terrain the N:M mosaic.
|
|
20
|
+
* - `DirectRasterDrapeResolver` (`rasterDrapeResolver.ts`) — raster layers on
|
|
21
|
+
* WebMercator terrain. Same-scheme 1:1: the fetched tile texture is bound
|
|
22
|
+
* straight to its slot by `setupTextures`; every hook is a no-op.
|
|
23
|
+
* - `BakedRasterDrapeResolver` (`rasterDrapeResolver.ts`) — raster layers on
|
|
24
|
+
* Geographic (quantized-mesh) terrain. Cross-scheme N:M mosaic bake with
|
|
25
|
+
* per-slot Mercator latitude reprojection.
|
|
26
|
+
*/
|
|
27
|
+
export type DrapeResolver = {
|
|
28
|
+
/**
|
|
29
|
+
* Per-frame hook (`_onBeforeRender`): refresh the Rust-resolved slots when
|
|
30
|
+
* the resolve revision moved, and re-bake when the slot/content signature
|
|
31
|
+
* changed (which also catches async content landing after the resolve).
|
|
32
|
+
*/
|
|
33
|
+
update(): void;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* `setupTextures` hook, after the material's per-slot arrays were rebuilt:
|
|
37
|
+
* re-derive the resolver's slot pairing and rebind (or invalidate) its
|
|
38
|
+
* textures. `textureFragments` holds the raster region's per-slot fragment
|
|
39
|
+
* ids; the vector resolver ignores it (its region is positional).
|
|
40
|
+
*/
|
|
41
|
+
syncMaterialSlots(
|
|
42
|
+
textureFragments: readonly (string | null | undefined)[] | undefined,
|
|
43
|
+
): void;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Mercator latitude reprojection band (`[south, north]`, radians) for one of
|
|
47
|
+
* this resolver's composite slots, or undefined when the slot samples
|
|
48
|
+
* without reprojection.
|
|
49
|
+
*/
|
|
50
|
+
slotReproject(absSlot: number): [number, number] | undefined;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Force a re-bake + re-bind on the next `update()` even though the resolved
|
|
54
|
+
* slots are unchanged (e.g. picking passes repaint the offscreen content).
|
|
55
|
+
*/
|
|
56
|
+
invalidate(): void;
|
|
57
|
+
|
|
58
|
+
/** Live drape render targets, counted into the tile's GPU byte report. */
|
|
59
|
+
liveRenderTargetCount(): number;
|
|
60
|
+
|
|
61
|
+
dispose(): void;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Apply the shared texturized-slot sampler settings to a drape render target's
|
|
65
|
+
* texture. Returns the texture for call-site chaining. */
|
|
66
|
+
export function configureTexturizedTexture(
|
|
67
|
+
tex: Texture,
|
|
68
|
+
textureOptions: TextureOptions,
|
|
69
|
+
): Texture {
|
|
70
|
+
tex.minFilter = textureOptions.minFilter as MinificationTextureFilter;
|
|
71
|
+
tex.magFilter = textureOptions.magFilter as MagnificationTextureFilter;
|
|
72
|
+
tex.anisotropy = textureOptions.maxAnisotropy;
|
|
73
|
+
tex.generateMipmaps = textureOptions.useMipmaps;
|
|
74
|
+
tex.needsUpdate = true;
|
|
75
|
+
return tex;
|
|
76
|
+
}
|