@navaramap/three 0.0.6 → 0.0.7
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/dist/assets/fontWorker-DaqfXmZf.js +2 -0
- package/dist/assets/fontWorker-DaqfXmZf.js.map +1 -0
- package/dist/assets/index-Dydat1h1.js +25 -0
- package/dist/assets/index-Dydat1h1.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-DympKYVX.wasm → navara_wasm_font_worker_bg-XvE_oYPr.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-Bx_l45cW.wasm → navara_wasm_worker_bg-Dg9fnGLP.wasm} +0 -0
- package/dist/index.d.ts +140 -4
- package/dist/index.js +5385 -5022
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +164 -86
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +12 -12
- package/src/declutter/DeclutterManager.test.ts +65 -0
- package/src/declutter/DeclutterManager.ts +60 -1
- package/src/declutter/types.ts +17 -0
- package/src/event/feature.ts +45 -2
- package/src/event/worker.ts +18 -0
- package/src/index.ts +51 -2
- package/src/layers/effect/TransparentPassEffectDesc.ts +1 -1
- package/src/mesh/nvLineGeometry.test.ts +88 -0
- package/src/mesh/nvLineGeometry.ts +148 -0
- package/src/mesh/polygonOutline.ts +126 -42
- package/src/mesh/sdfText/batchedSdfText.test.ts +377 -0
- package/src/mesh/sdfText/batchedSdfText.ts +192 -21
- package/src/mesh/sdfText/labelVisibility.test.ts +106 -0
- package/src/mesh/sdfText/labelVisibility.ts +131 -0
- package/src/mesh/sdfText/pendingSettlement.test.ts +151 -0
- package/src/mesh/sdfText/pendingSettlement.ts +68 -0
- package/src/pick/pickTerrain.ts +22 -18
- package/src/terrain/sampleTerrainMostDetailed.test.ts +236 -0
- package/src/terrain/sampleTerrainMostDetailed.ts +447 -0
- package/dist/assets/fontWorker-JfTCpd38.js +0 -2
- package/dist/assets/fontWorker-JfTCpd38.js.map +0 -1
- package/dist/assets/index-B3U_DvcP.js +0 -25
- package/dist/assets/index-B3U_DvcP.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@navaramap/three",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"main": "dist/index.umd.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"astronomy-engine": "2.1.19",
|
|
31
31
|
"stats.js": "0.17.0",
|
|
32
32
|
"three-stdlib": "2.36.1",
|
|
33
|
-
"@navaramap/
|
|
34
|
-
"@navaramap/engine": "0.0.
|
|
35
|
-
"@navaramap/
|
|
36
|
-
"@navaramap/three-api": "0.0.
|
|
37
|
-
"@navaramap/
|
|
38
|
-
"@navaramap/
|
|
39
|
-
"@navaramap/worker": "0.0.
|
|
33
|
+
"@navaramap/core": "0.0.7",
|
|
34
|
+
"@navaramap/engine": "0.0.7",
|
|
35
|
+
"@navaramap/engine-api": "0.0.7",
|
|
36
|
+
"@navaramap/three-api": "0.0.7",
|
|
37
|
+
"@navaramap/font": "0.0.7",
|
|
38
|
+
"@navaramap/three-csm": "0.0.7",
|
|
39
|
+
"@navaramap/worker": "0.0.7"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@playwright/test": "1.61.1",
|
|
@@ -71,10 +71,10 @@
|
|
|
71
71
|
"tweakpane": "4.0.5",
|
|
72
72
|
"vite-plugin-glsl": "1.6.0",
|
|
73
73
|
"vite-plugin-static-copy": "4.1.1",
|
|
74
|
-
"@navaramap/three-default-descs": "0.0.
|
|
75
|
-
"@navaramap/three-
|
|
76
|
-
"@navaramap/three-
|
|
77
|
-
"@navaramap/three-
|
|
74
|
+
"@navaramap/three-default-descs": "0.0.7",
|
|
75
|
+
"@navaramap/three-plugins": "0.0.7",
|
|
76
|
+
"@navaramap/three-default-plugin": "0.0.7",
|
|
77
|
+
"@navaramap/three-react": "0.0.7"
|
|
78
78
|
},
|
|
79
79
|
"scripts": {
|
|
80
80
|
"start": "vite",
|
|
@@ -232,4 +232,69 @@ describe("DeclutterManager", () => {
|
|
|
232
232
|
manager.dispose();
|
|
233
233
|
expect(manager.update(camera, 800, 600, 1000)).toBe("idle");
|
|
234
234
|
});
|
|
235
|
+
|
|
236
|
+
// Tile swaps replace a batch wholesale; the new batch's labels start hidden
|
|
237
|
+
// and would re-earn placement from scratch (throttled pass + fade-in) while
|
|
238
|
+
// the old batch vanished instantly — a tile-shaped blink. The registry lets
|
|
239
|
+
// the new batch seed a label as already-granted when the previous pass
|
|
240
|
+
// showed the same content at (nearly) the same anchor.
|
|
241
|
+
describe("shown-content registry (tile-swap handoff)", () => {
|
|
242
|
+
it("answers lookups for content shown by the last pass, within tolerance", () => {
|
|
243
|
+
const manager = new DeclutterManager(new StubKernel());
|
|
244
|
+
const p = new FakeParticipant([label({ handle: 0, contentKey: "東京" })]);
|
|
245
|
+
manager.register(p);
|
|
246
|
+
|
|
247
|
+
// Nothing recorded before any pass ran.
|
|
248
|
+
expect(manager.wasRecentlyShown("東京", R, 0, 0)).toBe(false);
|
|
249
|
+
|
|
250
|
+
manager.update(makeCamera(), 800, 600, 0);
|
|
251
|
+
|
|
252
|
+
expect(manager.wasRecentlyShown("東京", R, 0, 0)).toBe(true);
|
|
253
|
+
// Same content, slightly offset anchor (a coarser tile's quantization).
|
|
254
|
+
expect(manager.wasRecentlyShown("東京", R + 500, 200, -300)).toBe(true);
|
|
255
|
+
// Same content but far away (an unrelated same-name label).
|
|
256
|
+
expect(manager.wasRecentlyShown("東京", R + 50_000, 0, 0)).toBe(false);
|
|
257
|
+
// Different content at the same anchor.
|
|
258
|
+
expect(manager.wasRecentlyShown("大阪", R, 0, 0)).toBe(false);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("does not record candidates the pass hid", () => {
|
|
262
|
+
const kernel = new StubKernel();
|
|
263
|
+
kernel.hiddenFor = (n) => new Uint8Array(n).fill(1);
|
|
264
|
+
const manager = new DeclutterManager(kernel);
|
|
265
|
+
const p = new FakeParticipant([label({ handle: 0, contentKey: "東京" })]);
|
|
266
|
+
manager.register(p);
|
|
267
|
+
|
|
268
|
+
manager.update(makeCamera(), 800, 600, 0);
|
|
269
|
+
|
|
270
|
+
expect(manager.wasRecentlyShown("東京", R, 0, 0)).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("reflects the latest pass only", () => {
|
|
274
|
+
const kernel = new StubKernel();
|
|
275
|
+
const manager = new DeclutterManager(kernel);
|
|
276
|
+
const p = new FakeParticipant([label({ handle: 0, contentKey: "東京" })]);
|
|
277
|
+
manager.register(p);
|
|
278
|
+
const camera = makeCamera();
|
|
279
|
+
|
|
280
|
+
manager.update(camera, 800, 600, 0);
|
|
281
|
+
expect(manager.wasRecentlyShown("東京", R, 0, 0)).toBe(true);
|
|
282
|
+
|
|
283
|
+
// The next pass hides it; the registry must forget it.
|
|
284
|
+
kernel.hiddenFor = (n) => new Uint8Array(n).fill(1);
|
|
285
|
+
manager.markDirty();
|
|
286
|
+
manager.update(camera, 800, 600, DeclutterManager.MIN_INTERVAL_MS);
|
|
287
|
+
expect(manager.wasRecentlyShown("東京", R, 0, 0)).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("ignores candidates without a contentKey", () => {
|
|
291
|
+
const manager = new DeclutterManager(new StubKernel());
|
|
292
|
+
const p = new FakeParticipant([label({ handle: 0 })]);
|
|
293
|
+
manager.register(p);
|
|
294
|
+
|
|
295
|
+
manager.update(makeCamera(), 800, 600, 0);
|
|
296
|
+
|
|
297
|
+
expect(manager.wasRecentlyShown("", R, 0, 0)).toBe(false);
|
|
298
|
+
});
|
|
299
|
+
});
|
|
235
300
|
});
|
|
@@ -49,6 +49,11 @@ export class DeclutterManager {
|
|
|
49
49
|
* drift toggles frame to frame instead of flickering. Its full box is
|
|
50
50
|
* still claimed against competitors. */
|
|
51
51
|
static readonly HYSTERESIS_PX = 6;
|
|
52
|
+
/** Anchor distance (m) within which two same-content candidates count as
|
|
53
|
+
* the same label for the tile-swap handoff. Covers the anchor drift a
|
|
54
|
+
* zoom level's tile-grid quantization (and per-tile terrain heights)
|
|
55
|
+
* introduces, while staying far below distinct same-name places. */
|
|
56
|
+
static readonly HANDOFF_TOLERANCE_M = 2000;
|
|
52
57
|
|
|
53
58
|
private _participants = new Set<DeclutterParticipant>();
|
|
54
59
|
/** Set when the label set changed (register/update/text change) so the next
|
|
@@ -60,6 +65,11 @@ export class DeclutterManager {
|
|
|
60
65
|
private _lastRunAt = Number.NEGATIVE_INFINITY;
|
|
61
66
|
private _lastStepAt = Number.NaN;
|
|
62
67
|
|
|
68
|
+
/** Shown candidates of the most recent pass, keyed by content, as flat xyz
|
|
69
|
+
* anchor triplets. Read by {@link wasRecentlyShown} when a tile swap
|
|
70
|
+
* activates a replacement batch. */
|
|
71
|
+
private _shownByContent = new Map<string, number[]>();
|
|
72
|
+
|
|
63
73
|
// Reused per-pass scratch to keep steady-state allocations low.
|
|
64
74
|
private _candidates: DeclutterCandidate[] = [];
|
|
65
75
|
/** Packed candidate input for the kernel; grows as the label count does. */
|
|
@@ -100,6 +110,33 @@ export class DeclutterManager {
|
|
|
100
110
|
this._dirty = true;
|
|
101
111
|
}
|
|
102
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Whether the most recent pass showed `contentKey` anchored within
|
|
115
|
+
* {@link DeclutterManager.HANDOFF_TOLERANCE_M} of (x, y, z) — i.e. an
|
|
116
|
+
* equivalent label (typically the swapped-out tile's copy) already holds
|
|
117
|
+
* this screen space. A batch activating in a tile swap uses this to seed
|
|
118
|
+
* such labels as granted instead of fading them in from hidden, which
|
|
119
|
+
* would blink the whole tile's labels out for a pass-plus-fade.
|
|
120
|
+
*/
|
|
121
|
+
wasRecentlyShown(
|
|
122
|
+
contentKey: string,
|
|
123
|
+
x: number,
|
|
124
|
+
y: number,
|
|
125
|
+
z: number,
|
|
126
|
+
): boolean {
|
|
127
|
+
const anchors = this._shownByContent.get(contentKey);
|
|
128
|
+
if (!anchors) return false;
|
|
129
|
+
const tol = DeclutterManager.HANDOFF_TOLERANCE_M;
|
|
130
|
+
const tolSq = tol * tol;
|
|
131
|
+
for (let i = 0; i < anchors.length; i += 3) {
|
|
132
|
+
const dx = anchors[i] - x;
|
|
133
|
+
const dy = anchors[i + 1] - y;
|
|
134
|
+
const dz = anchors[i + 2] - z;
|
|
135
|
+
if (dx * dx + dy * dy + dz * dz <= tolSq) return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
103
140
|
/**
|
|
104
141
|
* Run a placement pass if one is due, then advance any active show/hide
|
|
105
142
|
* fades. `widthPx`/`heightPx` are the viewport in CSS pixels (matching the
|
|
@@ -153,6 +190,16 @@ export class DeclutterManager {
|
|
|
153
190
|
widthPx: number,
|
|
154
191
|
heightPx: number,
|
|
155
192
|
): void {
|
|
193
|
+
// Let participants start font/shape preparation for labels whose anchors
|
|
194
|
+
// became potentially visible (text batches park preparation while an
|
|
195
|
+
// anchor is out of view, so world-spanning low-zoom tiles don't fetch
|
|
196
|
+
// fonts for labels the camera can't see). Results land asynchronously and
|
|
197
|
+
// mark the pass dirty again, so this pass places only already-shaped
|
|
198
|
+
// labels.
|
|
199
|
+
for (const p of this._participants) {
|
|
200
|
+
p.prepareDeferredLabels?.(camera);
|
|
201
|
+
}
|
|
202
|
+
|
|
156
203
|
const candidates = this._candidates;
|
|
157
204
|
candidates.length = 0;
|
|
158
205
|
for (const p of this._participants) {
|
|
@@ -212,9 +259,20 @@ export class DeclutterManager {
|
|
|
212
259
|
|
|
213
260
|
// Apply results as fade targets. Order is irrelevant — applyDeclutter is
|
|
214
261
|
// idempotent per handle — so iterate in candidate order, not sorted order.
|
|
262
|
+
// Rebuild the shown-content registry from the same results.
|
|
263
|
+
this._shownByContent.clear();
|
|
215
264
|
for (let i = 0; i < n; i++) {
|
|
216
265
|
const c = candidates[i];
|
|
217
|
-
|
|
266
|
+
const isHidden = hidden[i] !== 0;
|
|
267
|
+
c.owner.applyDeclutter(c.handle, isHidden);
|
|
268
|
+
if (!isHidden && c.contentKey) {
|
|
269
|
+
let anchors = this._shownByContent.get(c.contentKey);
|
|
270
|
+
if (!anchors) {
|
|
271
|
+
anchors = [];
|
|
272
|
+
this._shownByContent.set(c.contentKey, anchors);
|
|
273
|
+
}
|
|
274
|
+
anchors.push(c.anchorX, c.anchorY, c.anchorZ);
|
|
275
|
+
}
|
|
218
276
|
}
|
|
219
277
|
|
|
220
278
|
candidates.length = 0;
|
|
@@ -226,6 +284,7 @@ export class DeclutterManager {
|
|
|
226
284
|
dispose(): void {
|
|
227
285
|
this._participants.clear();
|
|
228
286
|
this._candidates.length = 0;
|
|
287
|
+
this._shownByContent.clear();
|
|
229
288
|
}
|
|
230
289
|
|
|
231
290
|
private _snapshotChanged(
|
package/src/declutter/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PerspectiveCamera } from "three";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* One label/sprite competing for screen space in a declutter pass.
|
|
3
5
|
*
|
|
@@ -34,6 +36,13 @@ export type DeclutterCandidate = {
|
|
|
34
36
|
/** Participant-defined id (label slot for text batches, instance index for
|
|
35
37
|
* sprites) passed back to `applyDeclutter`. */
|
|
36
38
|
handle: number;
|
|
39
|
+
/**
|
|
40
|
+
* Stable identity of what the candidate displays (label text for text
|
|
41
|
+
* batches). Feeds the manager's shown-content registry so a tile swap can
|
|
42
|
+
* hand placement over to the replacing batch's identical label without a
|
|
43
|
+
* hide/fade-in blink. Omit for candidates that don't participate.
|
|
44
|
+
*/
|
|
45
|
+
contentKey?: string;
|
|
37
46
|
};
|
|
38
47
|
|
|
39
48
|
/** Duration of the show/hide fade applied to placement results (ms). */
|
|
@@ -58,4 +67,12 @@ export type DeclutterParticipant = {
|
|
|
58
67
|
* anything is fading; returns true while any label is still mid-fade.
|
|
59
68
|
*/
|
|
60
69
|
stepDeclutterFade: (deltaMs: number) => boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Optional: start preparation (font loading, shaping) for labels that
|
|
72
|
+
* parked it while their anchor was out of view. Called at the start of
|
|
73
|
+
* every placement pass with the camera the pass will use — the cadence at
|
|
74
|
+
* which visibility can actually change. Preparation is asynchronous, so
|
|
75
|
+
* promoted labels join a *later* pass once their text is applied.
|
|
76
|
+
*/
|
|
77
|
+
prepareDeferredLabels?: (camera: PerspectiveCamera) => void;
|
|
61
78
|
};
|
package/src/event/feature.ts
CHANGED
|
@@ -37,6 +37,14 @@ import { renderText, processTextChanged } from "./features/text";
|
|
|
37
37
|
|
|
38
38
|
import { setTransform } from ".";
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Upper bound on how long a text batch's render-completion report waits for
|
|
42
|
+
* label shaping (see processRenderableFeatureAdded). Settling normally takes
|
|
43
|
+
* tens of milliseconds; the cap exists so a hung font fetch cannot stall the
|
|
44
|
+
* tile LOD indefinitely.
|
|
45
|
+
*/
|
|
46
|
+
const TEXT_LABELS_SETTLE_TIMEOUT_MS = 2000;
|
|
47
|
+
|
|
40
48
|
/**
|
|
41
49
|
* Choose the render scene for a non-draped feature based on its material.
|
|
42
50
|
*
|
|
@@ -232,7 +240,16 @@ export async function processRenderableFeatureAdded(
|
|
|
232
240
|
// terrain tile's vector source to this tile once its features report rendered,
|
|
233
241
|
// so reporting before the offscreen scene existed left the terrain tile's
|
|
234
242
|
// vector drape blank until a camera-move re-traverse.
|
|
235
|
-
|
|
243
|
+
//
|
|
244
|
+
// Text batches are the exception: the report drives the Rust tile-LOD swap,
|
|
245
|
+
// and a text batch draws nothing until its labels finish async font shaping,
|
|
246
|
+
// which the evaluator only kicks off further down. Reporting here swapped
|
|
247
|
+
// the parent tile out for a child that couldn't draw yet — a tile-shaped
|
|
248
|
+
// blank on every LOD refinement. Their report is deferred below.
|
|
249
|
+
const isTextBatch = obj instanceof BatchedSdfTextMesh;
|
|
250
|
+
if (renderedType && !isTextBatch) {
|
|
251
|
+
featureHandler.markFeatureIsRendered(renderedType, bits);
|
|
252
|
+
}
|
|
236
253
|
|
|
237
254
|
if (obj instanceof PolygonMesh && polygon && polygon.outline_geometry) {
|
|
238
255
|
const outline = await renderPolygonOutline(ctx, polygon);
|
|
@@ -255,7 +272,13 @@ export async function processRenderableFeatureAdded(
|
|
|
255
272
|
featureLayerId,
|
|
256
273
|
ev.bits,
|
|
257
274
|
);
|
|
258
|
-
|
|
275
|
+
// Text batches also get their initial `featureUpdated` while still
|
|
276
|
+
// invisible: tiles spawn inactive, and apps that style text through
|
|
277
|
+
// `featureUpdated` must be able to populate label texts *before* the
|
|
278
|
+
// tile-LOD swap flips this batch visible (per-label writes persist in the
|
|
279
|
+
// label data texture, so styling hidden batches is safe). Otherwise labels
|
|
280
|
+
// could not exist until after the swap, guaranteeing a blank activation.
|
|
281
|
+
if (obj.visible || isTextBatch) {
|
|
259
282
|
handleFeatureUpdatedEventByLayerId(
|
|
260
283
|
viewEvents,
|
|
261
284
|
layersManager,
|
|
@@ -264,6 +287,26 @@ export async function processRenderableFeatureAdded(
|
|
|
264
287
|
updatedAt,
|
|
265
288
|
);
|
|
266
289
|
}
|
|
290
|
+
|
|
291
|
+
// The evaluator runs synchronously inside the emits above, so any font
|
|
292
|
+
// preparation it *started* is in flight now. Report the text batch rendered
|
|
293
|
+
// only once those land, so the Rust swap waits until this batch can draw.
|
|
294
|
+
// The timeout only bounds a hung font fetch — a late or post-removal report
|
|
295
|
+
// is a safe no-op on the Rust side.
|
|
296
|
+
//
|
|
297
|
+
// With declutter on, the evaluator's setters park unprepared text on anchor
|
|
298
|
+
// visibility instead of preparing it (BatchedSdfTextMesh.setTextByBatchIndex),
|
|
299
|
+
// so nothing is in flight here and this resolves immediately. That is
|
|
300
|
+
// deliberate and must stay that way: promotion needs the batch visible,
|
|
301
|
+
// visibility needs this report, so waiting on parked labels would deadlock
|
|
302
|
+
// the cycle until the timeout and stall every text tile's swap. Continuity
|
|
303
|
+
// across decluttered swaps comes from the placement handoff in `setActive`.
|
|
304
|
+
if (renderedType && obj instanceof BatchedSdfTextMesh) {
|
|
305
|
+
void obj.whenLabelsSettled(TEXT_LABELS_SETTLE_TIMEOUT_MS).then(() => {
|
|
306
|
+
featureHandler.markFeatureIsRendered(renderedType, bits);
|
|
307
|
+
ctx.renderFlag.forceUpdate = true;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
267
310
|
}
|
|
268
311
|
|
|
269
312
|
export async function processRenderableFeatureChanged(
|
package/src/event/worker.ts
CHANGED
|
@@ -598,6 +598,12 @@ async function processConstructPolygonBatchedFeature(
|
|
|
598
598
|
let outlineGeometry: TransferablePolygonOutlineGeometry | undefined;
|
|
599
599
|
if (result.outline_position) {
|
|
600
600
|
const outlinePosition = bufHandler.adoptF32(result.outline_position);
|
|
601
|
+
const outlinePosition3dHigh = result.outline_position_3d_high
|
|
602
|
+
? bufHandler.adoptF32(result.outline_position_3d_high)
|
|
603
|
+
: undefined;
|
|
604
|
+
const outlinePosition3dLow = result.outline_position_3d_low
|
|
605
|
+
? bufHandler.adoptF32(result.outline_position_3d_low)
|
|
606
|
+
: undefined;
|
|
601
607
|
const outlineScaleNormalAndCap = result.outline_scale_normal_and_cap
|
|
602
608
|
? bufHandler.adoptF32(result.outline_scale_normal_and_cap)
|
|
603
609
|
: undefined;
|
|
@@ -616,6 +622,18 @@ async function processConstructPolygonBatchedFeature(
|
|
|
616
622
|
result.outline_position_size ?? 3,
|
|
617
623
|
)
|
|
618
624
|
: undefined,
|
|
625
|
+
outlinePosition3dHigh
|
|
626
|
+
? new TransferableFloatAttribute(
|
|
627
|
+
outlinePosition3dHigh,
|
|
628
|
+
result.outline_position_3d_high_size ?? 3,
|
|
629
|
+
)
|
|
630
|
+
: undefined,
|
|
631
|
+
outlinePosition3dLow
|
|
632
|
+
? new TransferableFloatAttribute(
|
|
633
|
+
outlinePosition3dLow,
|
|
634
|
+
result.outline_position_3d_low_size ?? 3,
|
|
635
|
+
)
|
|
636
|
+
: undefined,
|
|
619
637
|
outlineScaleNormalAndCap
|
|
620
638
|
? new TransferableFloatAttribute(
|
|
621
639
|
outlineScaleNormalAndCap,
|
package/src/index.ts
CHANGED
|
@@ -125,6 +125,12 @@ import { TexturizedSceneByTileCoordinates, type Scenes } from "./scene";
|
|
|
125
125
|
import { ShadowMapViewers } from "./ShadowMapViewers";
|
|
126
126
|
import { Source } from "./source";
|
|
127
127
|
import { RendererStats } from "./stats";
|
|
128
|
+
import {
|
|
129
|
+
sampleTerrainMostDetailed,
|
|
130
|
+
type SampleTerrainOptions,
|
|
131
|
+
type SampledTerrainPosition,
|
|
132
|
+
type TerrainSampleSource,
|
|
133
|
+
} from "./terrain/sampleTerrainMostDetailed";
|
|
128
134
|
import type { TextureOptions } from "./textures";
|
|
129
135
|
import { TileTextureCompositor } from "./tileTexture";
|
|
130
136
|
import {
|
|
@@ -140,6 +146,7 @@ import {
|
|
|
140
146
|
type WorkerPoolPromises,
|
|
141
147
|
type RenderFlag,
|
|
142
148
|
type TileMapByHandle,
|
|
149
|
+
type SourceRef,
|
|
143
150
|
} from "./type";
|
|
144
151
|
import type { CommonUniforms } from "./uniforms";
|
|
145
152
|
import { isWorker, convertScreenPos, type TextureSlot } from "./utils";
|
|
@@ -168,6 +175,10 @@ export { CameraDirection } from "@navaramap/engine";
|
|
|
168
175
|
export { CascadedShadowMaps, CSMHelper } from "@navaramap/three-csm";
|
|
169
176
|
|
|
170
177
|
export * from "./type";
|
|
178
|
+
export type {
|
|
179
|
+
SampleTerrainOptions,
|
|
180
|
+
SampledTerrainPosition,
|
|
181
|
+
} from "./terrain/sampleTerrainMostDetailed";
|
|
171
182
|
export * from "./constants";
|
|
172
183
|
export * from "./light";
|
|
173
184
|
export * from "./mesh";
|
|
@@ -2547,14 +2558,52 @@ export default class ThreeView<
|
|
|
2547
2558
|
|
|
2548
2559
|
/**
|
|
2549
2560
|
* Samples the terrain height at a given geodetic position synchronously.
|
|
2550
|
-
* @param pos - Geodetic position (lat in radians, lng in radians
|
|
2561
|
+
* @param pos - Geodetic position (lat in radians, lng in radians)
|
|
2551
2562
|
* @returns Terrain height in meters, or undefined if terrain data not loaded
|
|
2552
2563
|
*/
|
|
2553
|
-
sampleTerrainHeight(pos:
|
|
2564
|
+
sampleTerrainHeight(pos: LatLng): number | undefined {
|
|
2554
2565
|
const lle = new LLE(pos.lat, pos.lng, 0);
|
|
2555
2566
|
return this._core?.sampleTerrainHeight(lle);
|
|
2556
2567
|
}
|
|
2557
2568
|
|
|
2569
|
+
/**
|
|
2570
|
+
* Samples terrain heights at the most detailed zoom level the terrain
|
|
2571
|
+
* source provides, fetching the needed tiles over the network — unlike
|
|
2572
|
+
* {@link sampleTerrainHeight}, which reads only tiles already resident for
|
|
2573
|
+
* rendering and returns coarse-LOD heights (or nothing) when the camera is
|
|
2574
|
+
* far away. Use this to place objects on the ground before the camera has
|
|
2575
|
+
* streamed the area in.
|
|
2576
|
+
*
|
|
2577
|
+
* `source` is a registered `quantized-mesh` / `raster-dem` source: the
|
|
2578
|
+
* {@link Source} handle returned by `addSource`, or its id. The stored
|
|
2579
|
+
* config (including engine defaults and partial-update merges) is what gets
|
|
2580
|
+
* sampled.
|
|
2581
|
+
*
|
|
2582
|
+
* @param source - A registered terrain source (handle or id)
|
|
2583
|
+
* @param positions - Geodetic positions (lat/lng in radians)
|
|
2584
|
+
* @param options - Optional abort signal and fixed-level sampling
|
|
2585
|
+
* @returns One result per input position, in the same order
|
|
2586
|
+
*/
|
|
2587
|
+
async sampleTerrainMostDetailed(
|
|
2588
|
+
source: SourceRef,
|
|
2589
|
+
positions: LatLng[],
|
|
2590
|
+
options?: SampleTerrainOptions,
|
|
2591
|
+
): Promise<SampledTerrainPosition[]> {
|
|
2592
|
+
invariant(this._core);
|
|
2593
|
+
const sourceId = source instanceof Source ? source.id : source;
|
|
2594
|
+
const description = this._core.getSourceDescription(sourceId) as
|
|
2595
|
+
TerrainSampleSource | undefined;
|
|
2596
|
+
if (
|
|
2597
|
+
description?.type !== "quantized-mesh" &&
|
|
2598
|
+
description?.type !== "raster-dem"
|
|
2599
|
+
) {
|
|
2600
|
+
throw new Error(
|
|
2601
|
+
`sampleTerrainMostDetailed: source "${sourceId}" is not a registered quantized-mesh or raster-dem source`,
|
|
2602
|
+
);
|
|
2603
|
+
}
|
|
2604
|
+
return sampleTerrainMostDetailed(description, positions, options);
|
|
2605
|
+
}
|
|
2606
|
+
|
|
2558
2607
|
/**
|
|
2559
2608
|
* Observes terrain height changes at a position. Callback is invoked each time terrain data updates.
|
|
2560
2609
|
* @param pos - Geodetic position to observe (lat in radians, lng in radians)
|
|
@@ -22,7 +22,7 @@ export class TransparentPassEffectDesc extends EffectDesc<
|
|
|
22
22
|
RenderPass
|
|
23
23
|
> {
|
|
24
24
|
static key = "transparent";
|
|
25
|
-
static
|
|
25
|
+
static insertBefore = ["final"];
|
|
26
26
|
|
|
27
27
|
light = new Group();
|
|
28
28
|
lightsSyncMap = new Map();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { InterleavedBufferAttribute } from "three";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { NvLineGeometry } from "./nvLineGeometry";
|
|
5
|
+
|
|
6
|
+
// 4 vertices → 3 candidate segments (0-1, 1-2, 2-3)
|
|
7
|
+
const HIGH = new Float32Array([0, 0, 0, 10, 0, 0, 20, 0, 0, 30, 0, 0]);
|
|
8
|
+
const LOW = new Float32Array([0.1, 0, 0, 0.2, 0, 0, 0.3, 0, 0, 0.4, 0, 0]);
|
|
9
|
+
|
|
10
|
+
describe("NvLineGeometry.setPositionsHighLow", () => {
|
|
11
|
+
it("builds interleaved start/end high/low attributes per segment", () => {
|
|
12
|
+
const geometry = new NvLineGeometry();
|
|
13
|
+
geometry.setPositionsHighLow(HIGH, LOW);
|
|
14
|
+
|
|
15
|
+
const startHigh = geometry.getAttribute(
|
|
16
|
+
"instanceStartHigh",
|
|
17
|
+
) as InterleavedBufferAttribute;
|
|
18
|
+
const endHigh = geometry.getAttribute(
|
|
19
|
+
"instanceEndHigh",
|
|
20
|
+
) as InterleavedBufferAttribute;
|
|
21
|
+
const startLow = geometry.getAttribute(
|
|
22
|
+
"instanceStartLow",
|
|
23
|
+
) as InterleavedBufferAttribute;
|
|
24
|
+
const endLow = geometry.getAttribute(
|
|
25
|
+
"instanceEndLow",
|
|
26
|
+
) as InterleavedBufferAttribute;
|
|
27
|
+
|
|
28
|
+
// Same layout as LineSegmentsGeometry.setPositions: one interleaved
|
|
29
|
+
// buffer of stride 6, start at offset 0, end at offset 3.
|
|
30
|
+
expect(startHigh.data).toBe(endHigh.data);
|
|
31
|
+
expect(startLow.data).toBe(endLow.data);
|
|
32
|
+
expect(startHigh.data.stride).toBe(6);
|
|
33
|
+
expect(startHigh.offset).toBe(0);
|
|
34
|
+
expect(endHigh.offset).toBe(3);
|
|
35
|
+
|
|
36
|
+
// 3 segments
|
|
37
|
+
expect(startHigh.count).toBe(3);
|
|
38
|
+
expect(startLow.count).toBe(3);
|
|
39
|
+
|
|
40
|
+
// segment 1 = vertices 1 → 2
|
|
41
|
+
expect(startHigh.getX(1)).toBe(10);
|
|
42
|
+
expect(endHigh.getX(1)).toBe(20);
|
|
43
|
+
expect(startLow.getX(1)).toBeCloseTo(0.2);
|
|
44
|
+
expect(endLow.getX(1)).toBeCloseTo(0.3);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("computes bounds from reconstructed high+low positions", () => {
|
|
48
|
+
// setPositions is not called in RTE mode, so setPositionsHighLow must
|
|
49
|
+
// provide the bounding volumes itself (culling / depth sorting read them).
|
|
50
|
+
const geometry = new NvLineGeometry();
|
|
51
|
+
geometry.setPositionsHighLow(HIGH, LOW);
|
|
52
|
+
|
|
53
|
+
const box = geometry.boundingBox;
|
|
54
|
+
const sphere = geometry.boundingSphere;
|
|
55
|
+
expect(box).not.toBeNull();
|
|
56
|
+
expect(sphere).not.toBeNull();
|
|
57
|
+
|
|
58
|
+
// absolute x spans [0.1, 30.4], y = z = 0
|
|
59
|
+
expect(box?.min.x).toBeCloseTo(0.1, 5);
|
|
60
|
+
expect(box?.max.x).toBeCloseTo(30.4, 5);
|
|
61
|
+
expect(box?.min.y).toBe(0);
|
|
62
|
+
expect(box?.max.z).toBe(0);
|
|
63
|
+
|
|
64
|
+
expect(sphere?.center.x).toBeCloseTo(15.25, 5);
|
|
65
|
+
expect(sphere?.radius).toBeCloseTo(15.15, 5);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("skips the same segments as setPositions", () => {
|
|
69
|
+
const geometry = new NvLineGeometry();
|
|
70
|
+
const skip = new Uint32Array([1]); // drop segment starting at vertex 1
|
|
71
|
+
geometry.setPositions(HIGH, skip);
|
|
72
|
+
geometry.setPositionsHighLow(HIGH, LOW, skip);
|
|
73
|
+
|
|
74
|
+
const start = geometry.getAttribute(
|
|
75
|
+
"instanceStart",
|
|
76
|
+
) as InterleavedBufferAttribute;
|
|
77
|
+
const startHigh = geometry.getAttribute(
|
|
78
|
+
"instanceStartHigh",
|
|
79
|
+
) as InterleavedBufferAttribute;
|
|
80
|
+
|
|
81
|
+
expect(startHigh.count).toBe(2);
|
|
82
|
+
expect(startHigh.count).toBe(start.count);
|
|
83
|
+
|
|
84
|
+
// remaining segments are 0→1 and 2→3
|
|
85
|
+
expect(startHigh.getX(0)).toBe(0);
|
|
86
|
+
expect(startHigh.getX(1)).toBe(20);
|
|
87
|
+
});
|
|
88
|
+
});
|