@navaramap/three 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +7 -7
  2. package/dist/assets/fontWorker-OwvKiQ8o.js +2 -0
  3. package/dist/assets/fontWorker-OwvKiQ8o.js.map +1 -0
  4. package/dist/assets/{index-ywHSGImj.js → index-TKhRxurv.js} +2 -2
  5. package/dist/assets/index-TKhRxurv.js.map +1 -0
  6. package/dist/assets/{navara_wasm_font_worker_bg-Du9nb2Pn.wasm → navara_wasm_font_worker_bg-Kk06ArRB.wasm} +0 -0
  7. package/dist/assets/{navara_wasm_worker_bg-Xadu67fw.wasm → navara_wasm_worker_bg-BZjLKf4i.wasm} +0 -0
  8. package/dist/index.d.ts +351 -284
  9. package/dist/index.js +5394 -5470
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.umd.cjs +287 -235
  12. package/dist/index.umd.cjs.map +1 -1
  13. package/package.json +12 -12
  14. package/src/ShadowMapViewers.ts +1 -1
  15. package/src/atmosphere.test.ts +227 -28
  16. package/src/atmosphere.ts +219 -55
  17. package/src/constants/assets.ts +63 -13
  18. package/src/declutter/DeclutterManager.ts +1 -1
  19. package/src/declutter/types.ts +1 -1
  20. package/src/evaluations/FeatureEvaluator.test.ts +152 -0
  21. package/src/evaluations/FeatureEvaluator.ts +73 -2
  22. package/src/event/features/billboard.ts +1 -0
  23. package/src/event/features/model.ts +1 -1
  24. package/src/event/features/point.ts +1 -0
  25. package/src/event/index.ts +1 -1
  26. package/src/index.iife.ts +3 -3
  27. package/src/index.ts +10 -12
  28. package/src/layers/effect/README.md +1 -1
  29. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.test.ts +2 -2
  30. package/src/material/enhancer/instancedSprite/instancedSpriteBaseEnhancer/mutates.ts +1 -1
  31. package/src/material/enhancer/polygon/polygonBaseEnhancer/mutates.ts +1 -1
  32. package/src/material/enhancer/polyline/polylineBaseEnhancer/mutates.ts +1 -1
  33. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/index.ts +10 -7
  34. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.test.ts +74 -81
  35. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/mutates.ts +19 -86
  36. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.test.ts +77 -0
  37. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/shader.ts +9 -1
  38. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.test.ts +15 -19
  39. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/state.ts +0 -13
  40. package/src/material/enhancer/sdfText/sdfTextBaseEnhancer/types.ts +46 -48
  41. package/src/mesh/constants.ts +22 -0
  42. package/src/mesh/index.ts +1 -1
  43. package/src/mesh/model.ts +6 -0
  44. package/src/mesh/polygon.ts +6 -0
  45. package/src/mesh/polyline.ts +6 -0
  46. package/src/mesh/rtcRteHelper.ts +1 -1
  47. package/src/mesh/sdfText/batchedSdfText.ts +1223 -0
  48. package/src/mesh/sdfText/glyphBuffers.ts +222 -0
  49. package/src/mesh/sdfText/glyphSlots.test.ts +151 -0
  50. package/src/mesh/sdfText/glyphSlots.ts +98 -0
  51. package/src/mesh/sdfText/index.ts +10 -0
  52. package/src/mesh/sdfText/labelData.test.ts +178 -0
  53. package/src/mesh/sdfText/labelData.ts +187 -0
  54. package/src/mesh/{sdfText.test.ts → sdfText/layout.test.ts} +1 -1
  55. package/src/mesh/sdfText/layout.ts +306 -0
  56. package/src/mesh/sprite/instancedSprite.ts +33 -1
  57. package/src/utils/screen.ts +1 -1
  58. package/dist/assets/fontWorker-wz61edjR.js +0 -2
  59. package/dist/assets/fontWorker-wz61edjR.js.map +0 -1
  60. package/dist/assets/index-ywHSGImj.js.map +0 -1
  61. package/src/mesh/batchedSdfText.ts +0 -708
  62. package/src/mesh/sdfText.ts +0 -1189
  63. /package/dist/{draco_decoder.wasm → assets/draco_decoder-C32yEggz.wasm} +0 -0
  64. /package/dist/{draco_decoder2.wasm → assets/draco_decoder-Z1_iN-Ht.wasm} +0 -0
@@ -14,7 +14,7 @@ import { BufferGeometry, Mesh, Object3D } from "three";
14
14
  import type { NormalBufferAttributes } from "three";
15
15
 
16
16
  import { Color } from "../Color";
17
- import type { FeatureHandler } from "../event";
17
+ import type { FeatureHandler } from "../event/context";
18
18
  import {
19
19
  InstancedMesh,
20
20
  isFeatureMesh,
@@ -25,6 +25,8 @@ import {
25
25
  type BatchedAttributeName,
26
26
  InstancedSpriteMesh,
27
27
  BatchedSdfTextMesh,
28
+ type GeometryType,
29
+ VALID_GEOMETRY_TYPES,
28
30
  } from "../mesh";
29
31
 
30
32
  type AvailableMaterialProperty = ExtractProperties<
@@ -86,6 +88,13 @@ export type EvaluatedValue = {
86
88
  [K in EvaluatableMaterialPropertyKey]: EvaluatedMaterialProperty[K];
87
89
  };
88
90
 
91
+ /**
92
+ * Re-export GeometryType from mesh constants for convenience.
93
+ * Geometry type for feature evaluation context.
94
+ * Tells the evaluator which geometry type is being rendered.
95
+ */
96
+ export type { GeometryType };
97
+
89
98
  /**
90
99
  * Information about a feature batch, passed to evaluation callbacks.
91
100
  */
@@ -93,6 +102,8 @@ export type FeatureInfo = {
93
102
  batchId: number;
94
103
  properties: Record<string, unknown> | undefined;
95
104
  layerId: LayerId | undefined;
105
+ /** Mesh geometry type (e.g., "billboard", "text"). Not GeoJSON geometry ("Point", "Polygon"). */
106
+ meshGeomType?: GeometryType;
96
107
  };
97
108
 
98
109
  /**
@@ -185,6 +196,49 @@ export class FeatureEvaluator {
185
196
  this.obj = obj;
186
197
  }
187
198
 
199
+ /**
200
+ * Track objects we've already warned about to avoid log spam.
201
+ * Uses WeakSet so objects can be garbage collected.
202
+ */
203
+ private static readonly warnedObjects = new WeakSet<Object3D>();
204
+
205
+ /**
206
+ * Type guard to check if an Object3D has a geometryType getter.
207
+ */
208
+ private hasGeometryType(
209
+ obj: Object3D,
210
+ ): obj is Object3D & { geometryType: unknown } {
211
+ return "geometryType" in obj;
212
+ }
213
+
214
+ /**
215
+ * Determine geometry type from the mesh object.
216
+ * Reads geometryType getter if available on the mesh.
217
+ * Validates that the returned value is a valid GeometryType literal.
218
+ */
219
+ private determineGeometryType(obj: Object3D): GeometryType | undefined {
220
+ if (this.hasGeometryType(obj)) {
221
+ const value = obj.geometryType;
222
+
223
+ // Runtime validation: ensure the returned value is a valid GeometryType
224
+ if (
225
+ typeof value === "string" &&
226
+ VALID_GEOMETRY_TYPES.includes(value as GeometryType)
227
+ ) {
228
+ return value as GeometryType;
229
+ }
230
+
231
+ // Invalid value from geometryType - warn once per object to avoid log spam
232
+ if (!FeatureEvaluator.warnedObjects.has(obj)) {
233
+ FeatureEvaluator.warnedObjects.add(obj);
234
+ console.warn(
235
+ `Invalid geometry type returned from geometryType getter: "${value}". Expected one of: ${VALID_GEOMETRY_TYPES.join(", ")}`,
236
+ );
237
+ }
238
+ }
239
+ return undefined;
240
+ }
241
+
188
242
  /**
189
243
  * Gets the unique identifier of this feature set.
190
244
  */
@@ -214,6 +268,9 @@ export class FeatureEvaluator {
214
268
  * ```
215
269
  */
216
270
  readFeatureProperties(f: (info: FeatureInfo, batchIndex: number) => void) {
271
+ // Read geometry type dynamically to handle cases where it changes after evaluator creation
272
+ const meshGeomType = this.determineGeometryType(this.obj);
273
+
217
274
  if (this.cachedBatchedProperties) {
218
275
  for (const [batchIdx, properties] of this.cachedBatchedProperties) {
219
276
  f(
@@ -221,6 +278,7 @@ export class FeatureEvaluator {
221
278
  layerId: this.layerId,
222
279
  batchId: this.batchIds[batchIdx],
223
280
  properties,
281
+ meshGeomType,
224
282
  },
225
283
  batchIdx,
226
284
  );
@@ -241,6 +299,7 @@ export class FeatureEvaluator {
241
299
  layerId: this.layerId,
242
300
  batchId,
243
301
  properties,
302
+ meshGeomType,
244
303
  },
245
304
  batchIdx,
246
305
  );
@@ -267,6 +326,9 @@ export class FeatureEvaluator {
267
326
  keys: string[],
268
327
  f: (info: FeatureInfo, batchIndex: number) => void,
269
328
  ) {
329
+ // Read geometry type dynamically to handle cases where it changes after evaluator creation
330
+ const meshGeomType = this.determineGeometryType(this.obj);
331
+
270
332
  const cacheKey = keys.join("|");
271
333
  if (this.filterCacheKey === cacheKey && this.cachedFilteredProperties) {
272
334
  for (const [batchIndex, filtered] of this.cachedFilteredProperties) {
@@ -275,6 +337,7 @@ export class FeatureEvaluator {
275
337
  layerId: this.layerId,
276
338
  batchId: this.batchIds[batchIndex],
277
339
  properties: filtered,
340
+ meshGeomType,
278
341
  },
279
342
  batchIndex,
280
343
  );
@@ -293,7 +356,15 @@ export class FeatureEvaluator {
293
356
  }
294
357
  this.cachedFilteredProperties?.set(batchIndex, properties);
295
358
  this.batchIds[batchIndex] = batchId;
296
- f({ layerId: this.layerId, batchId, properties }, batchIndex);
359
+ f(
360
+ {
361
+ layerId: this.layerId,
362
+ batchId,
363
+ properties,
364
+ meshGeomType,
365
+ },
366
+ batchIndex,
367
+ );
297
368
  },
298
369
  );
299
370
  }
@@ -13,6 +13,7 @@ export async function renderBillboard(
13
13
  const mesh = new InstancedSpriteMesh({
14
14
  renderOrder: FEATURE_RENDER_ORDER,
15
15
  ctx,
16
+ geometryType: "billboard",
16
17
  });
17
18
  await mesh._init(m);
18
19
  mesh.setActive(m.active);
@@ -1,5 +1,5 @@
1
1
  import { ModelMesh as NavaraModelMesh } from "@navaramap/engine";
2
- import { degreeToRadian } from "@navaramap/three_api";
2
+ import { degreeToRadian } from "@navaramap/three-api";
3
3
  import {
4
4
  BufferGeometry,
5
5
  Points,
@@ -8,6 +8,7 @@ export async function renderPoint(ctx: EventContext, m: NavaraPointMesh) {
8
8
  const mesh = new InstancedSpriteMesh({
9
9
  renderOrder: FEATURE_RENDER_ORDER,
10
10
  ctx,
11
+ geometryType: "point",
11
12
  });
12
13
  await mesh._init(m);
13
14
  mesh.setActive(m.active);
@@ -15,7 +15,7 @@ import {
15
15
  RenderableFeatureRemovedEvent,
16
16
  Events,
17
17
  } from "@navaramap/engine";
18
- import { radianToDegree } from "@navaramap/three_api";
18
+ import { radianToDegree } from "@navaramap/three-api";
19
19
  import { canWorkerProcessImmediately } from "@navaramap/worker";
20
20
  import { Mesh, Object3D, Sprite } from "three";
21
21
 
package/src/index.iife.ts CHANGED
@@ -1,11 +1,11 @@
1
1
  export * from "./index";
2
2
  export { default } from "./index";
3
- export { DefaultPlugin } from "@navaramap/three_default_plugin";
3
+ export { DefaultPlugin } from "@navaramap/three-default-plugin";
4
4
  export type {
5
5
  DefaultDescriptions,
6
6
  DefaultMeshDescription,
7
7
  DefaultLightDescription,
8
8
  DefaultEffectDescription,
9
- } from "@navaramap/three_default_plugin";
10
- export * from "@navaramap/three_default_descs";
9
+ } from "@navaramap/three-default-plugin";
10
+ export * from "@navaramap/three-default-descs";
11
11
  export { AttributionPlugin } from "./plugins";
package/src/index.ts CHANGED
@@ -23,7 +23,7 @@ import {
23
23
  type FontWorkerMemoryStats,
24
24
  } from "@navaramap/font";
25
25
  import FontWorkerURL from "@navaramap/font/fontWorker?worker&url";
26
- import { initNavaraApi } from "@navaramap/three_api";
26
+ import { initNavaraApi } from "@navaramap/three-api";
27
27
  import {
28
28
  initializeWorkerPool,
29
29
  probeWorkerPoolHeap,
@@ -158,7 +158,7 @@ export type {
158
158
  } from "@navaramap/core";
159
159
  export { CameraDirection } from "@navaramap/engine";
160
160
  // CSM exports for advanced users
161
- export { CascadedShadowMaps, CSMHelper } from "@navaramap/three_csm";
161
+ export { CascadedShadowMaps, CSMHelper } from "@navaramap/three-csm";
162
162
 
163
163
  export * from "./type";
164
164
  export * from "./constants";
@@ -176,7 +176,7 @@ export * from "./layers";
176
176
  export * from "./passes";
177
177
  export * from "./evaluations";
178
178
  export { SKY_RENDER_ORDER, STARS_RENDER_ORDER } from "./renderOrder";
179
- export * from "@navaramap/three_api";
179
+ export * from "@navaramap/three-api";
180
180
  export * from "./Color";
181
181
  export { type BlendMode, blendFunction, createReplacer } from "./utils";
182
182
  export { Atmosphere, type AtmosphereOptions } from "./atmosphere";
@@ -1044,6 +1044,9 @@ export default class ThreeView<
1044
1044
  this._camera,
1045
1045
  );
1046
1046
  this._atmosphere.on("needsUpdate", this.forceUpdate);
1047
+ // Textures load lazily, kicked off by the first consumer registering via
1048
+ // onTexturesReady(); re-render whenever they arrive.
1049
+ this._atmosphere.on("textureLoaded", this.forceUpdate);
1047
1050
 
1048
1051
  this.on("layer", (e, id, ...args) => {
1049
1052
  this.layersManager.emitById(e, id, ...args);
@@ -1327,14 +1330,9 @@ export default class ThreeView<
1327
1330
  budgets.maxWorkerHeapBytes,
1328
1331
  });
1329
1332
 
1330
- // 1. Pre-fetch atmosphere resources. Non-blocking: consumers receive
1331
- // the textures through onTexturesReady() once they arrive.
1332
- this._atmosphere._init().then(
1333
- () => this.forceUpdate(),
1334
- (e) => console.error("Failed to load atmosphere textures:", e),
1335
- );
1336
-
1337
- // 2. Fetch WASM resources.
1333
+ // Fetch WASM resources. Atmosphere textures are not fetched here: they
1334
+ // load lazily once a consumer (sky, sun light, aerial perspective, ...)
1335
+ // registers via atmosphere.onTexturesReady().
1338
1336
  await Promise.all([initCore(), initNavaraApi()]);
1339
1337
 
1340
1338
  this._core = new Core(newId());
@@ -1480,7 +1478,7 @@ export default class ThreeView<
1480
1478
 
1481
1479
  this.viewContext._setRenderPass(this.renderPass);
1482
1480
 
1483
- // 3. Warm up workers. Non-blocking: warms one worker, the rest follow
1481
+ // Warm up workers. Non-blocking: warms one worker, the rest follow
1484
1482
  // in the background off the HTTP cache; tasks dispatched to a still-cold
1485
1483
  // worker just await WASM inside the task.
1486
1484
  warmUpWorkerPool().catch((e) =>
@@ -5,5 +5,5 @@ internal rendering pipeline (MRT passes, transparent pass, final copy, selective
5
5
  effects).
6
6
 
7
7
  Independent, self-contained effect descriptors should be implemented as external
8
- packages (e.g., `@navaramap/three_default_descs`) using the public
8
+ packages (e.g., `@navaramap/three-default-descs`) using the public
9
9
  `EffectDesc` API and registered via plugins.
@@ -1,4 +1,4 @@
1
- import { degreeToRadian } from "@navaramap/three_api";
1
+ import { degreeToRadian } from "@navaramap/three-api";
2
2
  import { Matrix4, type Texture } from "three";
3
3
  import { describe, expect, it, vi } from "vitest";
4
4
 
@@ -8,7 +8,7 @@ import { createBaseMutates } from "./mutates";
8
8
  import { DEFAULT_BASE_STATE } from "./state";
9
9
  import type { InstancedSpriteBaseState } from "./types";
10
10
 
11
- vi.mock("@navaramap/three_api", () => ({
11
+ vi.mock("@navaramap/three-api", () => ({
12
12
  degreeToRadian: (degree: number) => (degree * Math.PI) / 180,
13
13
  RTE_ONE_UNIFORM: { value: 1.0 },
14
14
  }));
@@ -1,5 +1,5 @@
1
1
  import { encodePosition } from "@navaramap/engine-api";
2
- import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
2
+ import { RTE_ONE_UNIFORM } from "@navaramap/three-api";
3
3
  import {
4
4
  type Matrix4,
5
5
  type Texture,
@@ -1,4 +1,4 @@
1
- import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
1
+ import { RTE_ONE_UNIFORM } from "@navaramap/three-api";
2
2
  import type { Matrix4, Texture, Vector3 } from "three";
3
3
  import { Matrix4 as ThreeMatrix4, Vector3 as ThreeVector3 } from "three";
4
4
 
@@ -1,4 +1,4 @@
1
- import { RTE_ONE_UNIFORM } from "@navaramap/three_api";
1
+ import { RTE_ONE_UNIFORM } from "@navaramap/three-api";
2
2
  import {
3
3
  Color as ThreeColor,
4
4
  Matrix4 as ThreeMatrix4,
@@ -21,15 +21,18 @@ import type {
21
21
  /**
22
22
  * Factory function to create an sdfText base enhancer.
23
23
  *
24
- * This enhancer handles:
25
- * - Text color, font size, centering
26
- * - Outline (width, color, opacity)
24
+ * One enhancer drives the whole tile-layer batch, so it owns only batch-wide
25
+ * state:
26
+ * - Anchor centering, outline (width, color, opacity)
27
27
  * - Background quad (color, border)
28
- * - Height offset, depth offset
29
- * - Picking
28
+ * - Depth offset and picking mode
30
29
  * - RTE (Relative-To-Eye) support for high-precision coordinates
31
30
  * - Billboard scaling by distance
32
- * - SDF atlas texture management
31
+ * - SDF atlas and per-label data texture management
32
+ *
33
+ * Per-label values (position, colour, opacity, font size, height, block
34
+ * metrics, batch id, declutter fade) are not here — they live in the label
35
+ * data texture the mesh owns.
33
36
  *
34
37
  * Supports ShaderMaterial with custom sdfText shaders.
35
38
  *
@@ -71,7 +74,7 @@ export function createSdfTextBaseEnhancer(
71
74
  };
72
75
  state = updateState(mergedProps, initialState);
73
76
  // Create mutates (refs are created inside with default values)
74
- mutates = createBaseMutates(mergedProps.useRTE, mergedProps.rtcCenter);
77
+ mutates = createBaseMutates(mergedProps.rtcCenter);
75
78
  mutates.update(state);
76
79
 
77
80
  // Set atlas texture external ref if provided
@@ -19,11 +19,8 @@ describe("sdfTextBaseEnhancer/mutates", () => {
19
19
  it("should sync core refs from state", () => {
20
20
  const state: SdfTextBaseState = {
21
21
  ...DEFAULT_BASE_STATE,
22
- color: new Color(0xff0000),
23
- fontSize: 24,
24
22
  center: [0.5, 0.5],
25
23
  sizeInMeters: true,
26
- addHeight: 50,
27
24
  offsetDepth: false,
28
25
  outlineWidth: 0.1,
29
26
  outlineColor: new Color(0x00ff00),
@@ -34,18 +31,15 @@ describe("sdfTextBaseEnhancer/mutates", () => {
34
31
  backgroundOutlineWidth: 0.2,
35
32
  pickable: true,
36
33
  };
37
- const mutates = createBaseMutates(false);
34
+ const mutates = createBaseMutates();
38
35
  mutates.update(state);
39
36
 
40
37
  const uniforms: ShaderUniforms = {};
41
38
  mutates.updateUniforms(uniforms, state);
42
39
 
43
- expect(uniforms.uColor?.value.getHex()).toBe(0xff0000);
44
- expect(uniforms.uFontSize?.value).toBe(24);
45
40
  expect(uniforms.uCenter?.value.x).toBe(0.5);
46
41
  expect(uniforms.uCenter?.value.y).toBe(0.5);
47
42
  expect(uniforms.uSizeInMeters?.value).toBe(true);
48
- expect(uniforms.uAddHeight?.value).toBe(50);
49
43
  expect(uniforms.uOffsetDepth?.value).toBe(false);
50
44
  expect(uniforms.uOutlineWidth?.value).toBe(0.1);
51
45
  expect(uniforms.uOutlineColor?.value.getHex()).toBe(0x00ff00);
@@ -61,7 +55,7 @@ describe("sdfTextBaseEnhancer/mutates", () => {
61
55
  describe("RTC center", () => {
62
56
  it("should initialize uRTCCenter from constructor args", () => {
63
57
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
64
- const mutates = createBaseMutates(false, [10, 20, 30]);
58
+ const mutates = createBaseMutates([10, 20, 30]);
65
59
  mutates.update(state);
66
60
 
67
61
  const uniforms: ShaderUniforms = {};
@@ -73,41 +67,60 @@ describe("sdfTextBaseEnhancer/mutates", () => {
73
67
  });
74
68
  });
75
69
 
76
- describe("RTE position refs", () => {
77
- it("should create RTE position uniforms when useRTE=true", () => {
78
- const state: SdfTextBaseState = {
79
- ...DEFAULT_BASE_STATE,
80
- useRTE: true,
81
- };
82
- const mutates = createBaseMutates(true);
83
- mutates.update(state);
84
-
85
- const uniforms: ShaderUniforms = {};
86
- mutates.updateUniforms(uniforms, state);
87
-
88
- expect(uniforms.uRTEPositionLOW).toBeDefined();
89
- expect(uniforms.uRTEPositionHIGH).toBeDefined();
90
- expect(uniforms.uRTCPosition).toBeUndefined();
91
- });
92
-
93
- it("should create RTC position uniform when useRTE=false", () => {
70
+ describe("per-label state is not uniform state", () => {
71
+ // Anchors used to be uRTEPositionHIGH/LOW (RTE) or uRTCPosition (RTC) —
72
+ // one material per label. Batched, every label's anchor is a texel in
73
+ // uLabelData, so these uniforms must not come back: a single one would
74
+ // silently place the whole batch at one point.
75
+ it.each([true, false])(
76
+ "exposes no position uniforms (useRTE=%s)",
77
+ (useRTE) => {
78
+ const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE, useRTE };
79
+ const mutates = createBaseMutates();
80
+ mutates.update(state);
81
+
82
+ const uniforms: ShaderUniforms = {};
83
+ mutates.updateUniforms(uniforms, state);
84
+
85
+ expect(uniforms.uRTEPositionLOW).toBeUndefined();
86
+ expect(uniforms.uRTEPositionHIGH).toBeUndefined();
87
+ expect(uniforms.uRTCPosition).toBeUndefined();
88
+ // The batch-wide RTC origin does stay a uniform.
89
+ expect(uniforms.uRTCCenter).toBeDefined();
90
+ expect(uniforms.uLabelData).toBeDefined();
91
+ },
92
+ );
93
+
94
+ it("exposes no per-label style or batch-id uniforms", () => {
94
95
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
95
- const mutates = createBaseMutates(false);
96
+ const mutates = createBaseMutates();
96
97
  mutates.update(state);
97
98
 
98
99
  const uniforms: ShaderUniforms = {};
99
100
  mutates.updateUniforms(uniforms, state);
100
101
 
101
- expect(uniforms.uRTCPosition).toBeDefined();
102
- expect(uniforms.uRTEPositionLOW).toBeUndefined();
103
- expect(uniforms.uRTEPositionHIGH).toBeUndefined();
102
+ for (const name of [
103
+ "uColor",
104
+ "uOpacity",
105
+ "uFontSize",
106
+ "uAddHeight",
107
+ "uTextWidth",
108
+ "uTextHeight",
109
+ "uBgYBounds",
110
+ "uDeclutterHide",
111
+ "nvr_uBatchId",
112
+ ]) {
113
+ expect(uniforms).not.toHaveProperty(name);
114
+ }
115
+ // Pick mode is still batch-wide.
116
+ expect(uniforms.nvr_uPickable).toBeDefined();
104
117
  });
105
118
  });
106
119
 
107
120
  describe("atlas texture ref", () => {
108
121
  it("should update uAtlas value without replacing uniform ref", () => {
109
122
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
110
- const mutates = createBaseMutates(false);
123
+ const mutates = createBaseMutates();
111
124
  mutates.update(state);
112
125
 
113
126
  const uniforms: ShaderUniforms = {};
@@ -130,7 +143,7 @@ describe("sdfTextBaseEnhancer/mutates", () => {
130
143
  describe("updateAtlasSizes", () => {
131
144
  it("should sync uSdfAtlasSize and uColorAtlasSize from bound texture images", () => {
132
145
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
133
- const mutates = createBaseMutates(false);
146
+ const mutates = createBaseMutates();
134
147
  mutates.update(state);
135
148
 
136
149
  const sdfTex = {
@@ -155,7 +168,7 @@ describe("sdfTextBaseEnhancer/mutates", () => {
155
168
 
156
169
  it("should pick up new dimensions after the atlas grows", () => {
157
170
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
158
- const mutates = createBaseMutates(false);
171
+ const mutates = createBaseMutates();
159
172
  mutates.update(state);
160
173
 
161
174
  const sdfTex = {
@@ -184,7 +197,7 @@ describe("sdfTextBaseEnhancer/mutates", () => {
184
197
 
185
198
  it("should leave atlas size uniforms unchanged when textures are null", () => {
186
199
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
187
- const mutates = createBaseMutates(false);
200
+ const mutates = createBaseMutates();
188
201
  mutates.update(state);
189
202
 
190
203
  // Both uAtlas and uColorAtlas default to null.
@@ -202,7 +215,7 @@ describe("sdfTextBaseEnhancer/mutates", () => {
202
215
 
203
216
  it("should leave atlas size uniforms unchanged when image has zero dimensions", () => {
204
217
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
205
- const mutates = createBaseMutates(false);
218
+ const mutates = createBaseMutates();
206
219
  mutates.update(state);
207
220
 
208
221
  const sdfTex = {
@@ -222,7 +235,7 @@ describe("sdfTextBaseEnhancer/mutates", () => {
222
235
  describe("updatePerFrame", () => {
223
236
  it("should update camera uniforms", () => {
224
237
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
225
- const mutates = createBaseMutates(false);
238
+ const mutates = createBaseMutates();
226
239
  mutates.update(state);
227
240
 
228
241
  mutates.updatePerFrame(1.5, 1080, 10000, 0, 0, 0, new Matrix4(), state);
@@ -240,7 +253,7 @@ describe("sdfTextBaseEnhancer/mutates", () => {
240
253
  ...DEFAULT_BASE_STATE,
241
254
  useRTE: true,
242
255
  };
243
- const mutates = createBaseMutates(true);
256
+ const mutates = createBaseMutates();
244
257
  mutates.update(state);
245
258
 
246
259
  mutates.updatePerFrame(
@@ -268,7 +281,7 @@ describe("sdfTextBaseEnhancer/mutates", () => {
268
281
 
269
282
  it("should not update RTE eye uniforms when useRTE=false", () => {
270
283
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
271
- const mutates = createBaseMutates(false);
284
+ const mutates = createBaseMutates();
272
285
  mutates.update(state);
273
286
 
274
287
  mutates.updatePerFrame(
@@ -292,7 +305,7 @@ describe("sdfTextBaseEnhancer/mutates", () => {
292
305
 
293
306
  it("should transform the RTC center into view space when useRTE=false", () => {
294
307
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
295
- const mutates = createBaseMutates(false, [6_400_000, 0, 0]);
308
+ const mutates = createBaseMutates([6_400_000, 0, 0]);
296
309
  mutates.update(state);
297
310
 
298
311
  // View matrix cancels the center's large X so it lands near the origin.
@@ -308,74 +321,54 @@ describe("sdfTextBaseEnhancer/mutates", () => {
308
321
  });
309
322
  });
310
323
 
311
- describe("updateTextDimensions", () => {
312
- it("should update text dimension uniforms", () => {
324
+ describe("setLabelDataTexture", () => {
325
+ it("should bind the texture and its dimensions", () => {
313
326
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
314
- const mutates = createBaseMutates(false);
315
- mutates.update(state);
316
-
317
- mutates.updateTextDimensions(5.0, 1.0, -0.2, 0.8);
318
-
319
- const uniforms: ShaderUniforms = {};
320
- mutates.updateUniforms(uniforms, state);
321
-
322
- expect(uniforms.uTextWidth?.value).toBe(5.0);
323
- expect(uniforms.uTextHeight?.value).toBe(1.0);
324
- expect(uniforms.uBgYBounds?.value.x).toBe(-0.2);
325
- expect(uniforms.uBgYBounds?.value.y).toBe(0.8);
326
- });
327
- });
328
-
329
- describe("setPosition", () => {
330
- it("should set RTE position uniforms", () => {
331
- const state: SdfTextBaseState = {
332
- ...DEFAULT_BASE_STATE,
333
- useRTE: true,
334
- };
335
- const mutates = createBaseMutates(true);
327
+ const mutates = createBaseMutates();
336
328
  mutates.update(state);
337
329
 
338
- const high = new Float32Array([100, 200, 300]);
339
- const low = new Float32Array([0.1, 0.2, 0.3]);
340
- mutates.setPosition({ high, low }, true);
330
+ const tex = { isDataTexture: true } as unknown as DataTexture;
331
+ mutates.setLabelDataTexture(tex, 64, 5);
341
332
 
342
333
  const uniforms: ShaderUniforms = {};
343
334
  mutates.updateUniforms(uniforms, state);
344
335
 
345
- expect(uniforms.uRTEPositionHIGH?.value.x).toBe(100);
346
- expect(uniforms.uRTEPositionLOW?.value.x).toBeCloseTo(0.1);
336
+ expect(uniforms.uLabelData?.value).toBe(tex);
337
+ expect(uniforms.uLabelTexSize?.value.x).toBe(64);
338
+ expect(uniforms.uLabelTexSize?.value.y).toBe(5);
347
339
  });
348
340
 
349
- it("should set RTC position uniforms", () => {
341
+ // The shader does `i % size.x` and `i / size.x`; a zero would divide by
342
+ // zero before the mesh has bound anything.
343
+ it("should never expose a zero dimension", () => {
350
344
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
351
- const mutates = createBaseMutates(false);
345
+ const mutates = createBaseMutates();
352
346
  mutates.update(state);
353
347
 
354
- const pos = new Float32Array([10, 20, 30]);
355
- mutates.setPosition(pos, false, [100, 200, 300]);
348
+ mutates.setLabelDataTexture(null, 0, 0);
356
349
 
357
350
  const uniforms: ShaderUniforms = {};
358
351
  mutates.updateUniforms(uniforms, state);
359
352
 
360
- expect(uniforms.uRTCPosition?.value.x).toBe(10);
361
- expect(uniforms.uRTCPosition?.value.y).toBe(20);
362
- expect(uniforms.uRTCCenter?.value.x).toBe(100);
363
- expect(uniforms.uRTCCenter?.value.y).toBe(200);
353
+ expect(uniforms.uLabelTexSize?.value.x).toBe(1);
354
+ expect(uniforms.uLabelTexSize?.value.y).toBe(1);
364
355
  });
365
356
  });
366
357
 
367
- describe("setBatchId", () => {
368
- it("should update nvr_uBatchId value", () => {
358
+ describe("setRtcCenter", () => {
359
+ it("should update uRTCCenter after a transform change", () => {
369
360
  const state: SdfTextBaseState = { ...DEFAULT_BASE_STATE };
370
- const mutates = createBaseMutates(false);
361
+ const mutates = createBaseMutates([1, 2, 3]);
371
362
  mutates.update(state);
372
363
 
373
- mutates.setBatchId(42);
364
+ mutates.setRtcCenter([100, 200, 300]);
374
365
 
375
366
  const uniforms: ShaderUniforms = {};
376
367
  mutates.updateUniforms(uniforms, state);
377
368
 
378
- expect(uniforms.nvr_uBatchId?.value).toBe(42);
369
+ expect(uniforms.uRTCCenter?.value.x).toBe(100);
370
+ expect(uniforms.uRTCCenter?.value.y).toBe(200);
371
+ expect(uniforms.uRTCCenter?.value.z).toBe(300);
379
372
  });
380
373
  });
381
374
  });