@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.
Files changed (35) hide show
  1. package/dist/assets/fontWorker-DaqfXmZf.js +2 -0
  2. package/dist/assets/fontWorker-DaqfXmZf.js.map +1 -0
  3. package/dist/assets/index-Dydat1h1.js +25 -0
  4. package/dist/assets/index-Dydat1h1.js.map +1 -0
  5. package/dist/assets/{navara_wasm_font_worker_bg-DympKYVX.wasm → navara_wasm_font_worker_bg-XvE_oYPr.wasm} +0 -0
  6. package/dist/assets/{navara_wasm_worker_bg-Bx_l45cW.wasm → navara_wasm_worker_bg-Dg9fnGLP.wasm} +0 -0
  7. package/dist/index.d.ts +140 -4
  8. package/dist/index.js +5385 -5022
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.umd.cjs +164 -86
  11. package/dist/index.umd.cjs.map +1 -1
  12. package/package.json +12 -12
  13. package/src/declutter/DeclutterManager.test.ts +65 -0
  14. package/src/declutter/DeclutterManager.ts +60 -1
  15. package/src/declutter/types.ts +17 -0
  16. package/src/event/feature.ts +45 -2
  17. package/src/event/worker.ts +18 -0
  18. package/src/index.ts +51 -2
  19. package/src/layers/effect/TransparentPassEffectDesc.ts +1 -1
  20. package/src/mesh/nvLineGeometry.test.ts +88 -0
  21. package/src/mesh/nvLineGeometry.ts +148 -0
  22. package/src/mesh/polygonOutline.ts +126 -42
  23. package/src/mesh/sdfText/batchedSdfText.test.ts +377 -0
  24. package/src/mesh/sdfText/batchedSdfText.ts +192 -21
  25. package/src/mesh/sdfText/labelVisibility.test.ts +106 -0
  26. package/src/mesh/sdfText/labelVisibility.ts +131 -0
  27. package/src/mesh/sdfText/pendingSettlement.test.ts +151 -0
  28. package/src/mesh/sdfText/pendingSettlement.ts +68 -0
  29. package/src/pick/pickTerrain.ts +22 -18
  30. package/src/terrain/sampleTerrainMostDetailed.test.ts +236 -0
  31. package/src/terrain/sampleTerrainMostDetailed.ts +447 -0
  32. package/dist/assets/fontWorker-JfTCpd38.js +0 -2
  33. package/dist/assets/fontWorker-JfTCpd38.js.map +0 -1
  34. package/dist/assets/index-B3U_DvcP.js +0 -25
  35. 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.6",
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/engine-api": "0.0.6",
34
- "@navaramap/engine": "0.0.6",
35
- "@navaramap/font": "0.0.6",
36
- "@navaramap/three-api": "0.0.6",
37
- "@navaramap/three-csm": "0.0.6",
38
- "@navaramap/core": "0.0.6",
39
- "@navaramap/worker": "0.0.6"
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.6",
75
- "@navaramap/three-default-plugin": "0.0.6",
76
- "@navaramap/three-react": "0.0.6",
77
- "@navaramap/three-plugins": "0.0.6"
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
- c.owner.applyDeclutter(c.handle, hidden[i] !== 0);
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(
@@ -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
  };
@@ -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
- if (renderedType) featureHandler.markFeatureIsRendered(renderedType, bits);
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
- if (obj.visible) {
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(
@@ -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; height is ignored)
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: LatLngHeight): number | undefined {
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 insertAfter = ["ssao", "clouds", "atmosphere", "mrt"];
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
+ });