@navaramap/three 0.0.8 → 0.1.1

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 (39) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +20 -8
  3. package/dist/assets/fontWorker-C5cHhkWQ.js +2 -0
  4. package/dist/assets/fontWorker-C5cHhkWQ.js.map +1 -0
  5. package/dist/assets/{index-DaejBdUz.js → index-VfUncKuS.js} +2 -2
  6. package/dist/assets/index-VfUncKuS.js.map +1 -0
  7. package/dist/assets/{navara_wasm_font_worker_bg-CSmoDs_Q.wasm → navara_wasm_font_worker_bg-CKqHBoCR.wasm} +0 -0
  8. package/dist/assets/{navara_wasm_worker_bg-C1dUd_Nk.wasm → navara_wasm_worker_bg-CT26EH41.wasm} +0 -0
  9. package/dist/index.d.ts +298 -54
  10. package/dist/index.js +1569 -1396
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.umd.cjs +90 -69
  13. package/dist/index.umd.cjs.map +1 -1
  14. package/package.json +12 -12
  15. package/src/core/BaseDesc.ts +25 -1
  16. package/src/core/MeshDesc.test.ts +6 -8
  17. package/src/core/MeshDesc.ts +4 -6
  18. package/src/evaluations/FeatureEvaluator.ts +17 -10
  19. package/src/event/feature.ts +15 -2
  20. package/src/event/features/billboard.test.ts +62 -0
  21. package/src/event/features/billboard.ts +4 -2
  22. package/src/index.ts +266 -98
  23. package/src/material/enhancer/model/modelBaseEnhancer/shader.ts +2 -2
  24. package/src/material/enhancer/polygon/polygonBaseEnhancer/shader.ts +2 -2
  25. package/src/material/macro/tileShader.ts +12 -2
  26. package/src/mesh/polyline.ts +32 -4
  27. package/src/mesh/sdfText/batchedSdfText.test.ts +65 -0
  28. package/src/mesh/sdfText/batchedSdfText.ts +121 -43
  29. package/src/mesh/sprite/instancedSprite.ts +173 -65
  30. package/src/pick/pickHelper.test.ts +389 -4
  31. package/src/pick/pickHelper.ts +212 -37
  32. package/src/plugins/AttributionPlugin/index.ts +1 -1
  33. package/src/renderOrder.ts +10 -0
  34. package/src/terrain/sampleTerrainMostDetailed.test.ts +6 -6
  35. package/src/terrain/sampleTerrainMostDetailed.ts +7 -5
  36. package/src/type/index.ts +22 -1
  37. package/dist/assets/fontWorker-CkLGtrXf.js +0 -2
  38. package/dist/assets/fontWorker-CkLGtrXf.js.map +0 -1
  39. package/dist/assets/index-DaejBdUz.js.map +0 -1
@@ -32,6 +32,16 @@ import { setupRTECallback } from "./rtcRteHelper";
32
32
  // Sentinel value for picking coordinate when not picking (reused to avoid allocations)
33
33
  const PICKING_COORD_SENTINEL = new Vector2(-1, -1);
34
34
 
35
+ /**
36
+ * Minimum stroke width used while rendering the pick pass. A hairline stroke
37
+ * covers too few pick-buffer texels to click reliably — on the draped path the
38
+ * 512px tile atlas resolves a thin line to scattered partial texels, so the
39
+ * decoded batch id is background almost everywhere the line is visible.
40
+ * Fattening the stroke only for the pick render gives lines the same "click
41
+ * tolerance" pointing devices get elsewhere; the visible width is untouched.
42
+ */
43
+ const MIN_PICK_WIDTH = 10;
44
+
35
45
  type Attributes = BatchedFeatureAttributes<{
36
46
  position: BufferAttribute;
37
47
  // RTE mode attributes (only present when useRTE=true)
@@ -490,11 +500,21 @@ export class PolylineMesh extends BatchedFeatureMesh<
490
500
  return this.getEnhancer().states().effectIdsMask;
491
501
  }
492
502
 
503
+ /** Visible stroke width to restore after a pick-pass width bump. */
504
+ private _pickSavedWidth?: number;
505
+
493
506
  onBeforePicking(pickingCoord?: Vector2) {
494
- this.getEnhancer().update({ base: { pickable: true } });
507
+ const enhancer = this.getEnhancer();
508
+ const width = enhancer.states().width;
509
+ if (width < MIN_PICK_WIDTH) {
510
+ this._pickSavedWidth = width;
511
+ enhancer.update({ base: { pickable: true, width: MIN_PICK_WIDTH } });
512
+ } else {
513
+ enhancer.update({ base: { pickable: true } });
514
+ }
495
515
  this.needsUpdate();
496
516
 
497
- const mutates = this.getEnhancer().mutates();
517
+ const mutates = enhancer.mutates();
498
518
  if (pickingCoord) {
499
519
  mutates.setPickingCoord(pickingCoord);
500
520
  } else {
@@ -503,9 +523,17 @@ export class PolylineMesh extends BatchedFeatureMesh<
503
523
  }
504
524
 
505
525
  onAfterPicking() {
506
- this.getEnhancer().update({ base: { pickable: false } });
526
+ const enhancer = this.getEnhancer();
527
+ if (this._pickSavedWidth !== undefined) {
528
+ enhancer.update({
529
+ base: { pickable: false, width: this._pickSavedWidth },
530
+ });
531
+ this._pickSavedWidth = undefined;
532
+ } else {
533
+ enhancer.update({ base: { pickable: false } });
534
+ }
507
535
  this.needsUpdate();
508
- this.getEnhancer().mutates().setPickingCoord(PICKING_COORD_SENTINEL);
536
+ enhancer.mutates().setPickingCoord(PICKING_COORD_SENTINEL);
509
537
  }
510
538
 
511
539
  _getDefaultBatchAttributeValues(): DefaultBatchAttributeValues {
@@ -212,6 +212,71 @@ describe("BatchedSdfTextMesh material updates vs per-feature values", () => {
212
212
  });
213
213
  });
214
214
 
215
+ // A feature owns several anchors for MultiPoint geometry and for labels
216
+ // derived from line/polygon vertices via `geometryTypes`. Per-feature setters
217
+ // address features (batch indices), so they must fan out to every anchor the
218
+ // feature owns via the geometry's per-anchor `batch_index` buffer.
219
+ describe("BatchedSdfTextMesh multi-instance fan-out", () => {
220
+ /** Feature 0 owns anchors 0 and 1 (MultiPoint); feature 1 owns anchor 2. */
221
+ function multiInstanceMeshEvent(mat: NavaraTextMaterial): NavaraTextMesh {
222
+ return {
223
+ material: mat,
224
+ transform: { tx: 0, ty: 0, tz: 0 },
225
+ geometry: {
226
+ batch_ids: { data: new Float32Array([7, 8, 9]), size: 1 },
227
+ batch_index: { data: new Uint32Array([0, 0, 1]), size: 1 },
228
+ position: {
229
+ data: new Float32Array([0, 0, 0, 10, 0, 0, 20, 0, 0]),
230
+ size: 3,
231
+ },
232
+ },
233
+ } as unknown as NavaraTextMesh;
234
+ }
235
+
236
+ function makeMultiInstanceMesh() {
237
+ const fontManager = makeFontManager();
238
+ const ctx = {
239
+ buf: {
240
+ removeF32: (d: Float32Array) => d,
241
+ u32: (d: Uint32Array) => d,
242
+ },
243
+ fontManager,
244
+ renderFlag: { forceUpdate: false },
245
+ } as unknown as EventContext;
246
+ const mesh = new BatchedSdfTextMesh(
247
+ ctx,
248
+ multiInstanceMeshEvent(material()),
249
+ "font",
250
+ { layerId: "layer" },
251
+ );
252
+ return { mesh, fontManager };
253
+ }
254
+
255
+ it("applies text and styles to every anchor the feature owns", () => {
256
+ const { mesh } = makeMultiInstanceMesh();
257
+ mesh.setTextByBatchIndex(0, "AB");
258
+ mesh.setFeatureSizeByBatchIndex(0, 32);
259
+
260
+ // Feature 0's two anchors got labels (slots 0 and 1), both styled.
261
+ expect(showOf(mesh, 0)).toBe(1);
262
+ expect(showOf(mesh, 1)).toBe(1);
263
+ expect(sizeOf(mesh, 0)).toBe(32);
264
+ expect(sizeOf(mesh, 1)).toBe(32);
265
+ });
266
+
267
+ it("does not leak styling into other features' anchors", () => {
268
+ const { mesh } = makeMultiInstanceMesh();
269
+ mesh.setTextByBatchIndex(0, "AB");
270
+ mesh.setTextByBatchIndex(1, "CD");
271
+ mesh.setFeatureShowByBatchIndex(1, false);
272
+
273
+ expect(showOf(mesh, 0)).toBe(1);
274
+ expect(showOf(mesh, 1)).toBe(1);
275
+ // Feature 1 = anchor 2 = the third label slot.
276
+ expect(showOf(mesh, 2)).toBe(0);
277
+ });
278
+ });
279
+
215
280
  // Unprepared text costs a worker round-trip and font-face fetches, and a
216
281
  // low-zoom tile spans far more world than the screen shows. With declutter on,
217
282
  // `setTextByBatchIndex` therefore parks preparation until a placement pass
@@ -81,7 +81,10 @@ type PositionsInfo = PositionsInfoBase &
81
81
  type LabelRecord = {
82
82
  /** Row block in the label data texture, and this label's declutter handle. */
83
83
  slot: number;
84
- batchIndex: number;
84
+ /** Anchor (position) slot this label sits on, NOT the feature index: a
85
+ * feature spans several anchors for MultiPoint geometry and for labels
86
+ * derived from line/polygon vertices via `geometryTypes`. */
87
+ instanceIndex: number;
85
88
  batchId: number;
86
89
  /** The text currently laid out into the glyph run. */
87
90
  text: string;
@@ -179,8 +182,15 @@ export class BatchedSdfTextMesh
179
182
 
180
183
  /** Labels in slot order — `_labels[slot]` is also the declutter handle map. */
181
184
  private _labels: LabelRecord[] = [];
182
- /** Sparse batchIndex → label; labels are created on first per-feature touch. */
183
- private _labelByIndex: (LabelRecord | undefined)[] = [];
185
+ /** Sparse anchor slot → label; labels are created on first per-feature touch. */
186
+ private _labelByInstance: (LabelRecord | undefined)[] = [];
187
+ /**
188
+ * Feature (batch) index → this feature's anchor slots. A feature owns
189
+ * multiple anchors for MultiPoint geometry and for labels derived from
190
+ * line/polygon vertices via `geometryTypes`, so per-feature styling must fan
191
+ * out to all of them. `null` means anchors and features are 1:1.
192
+ */
193
+ private _batchIndexToInstances: Map<number, number[]> | null = null;
184
194
 
185
195
  /** In-flight per-feature text preparations; see {@link whenLabelsSettled}. */
186
196
  private _pendingTextPrepares = new PendingSettlement();
@@ -240,6 +250,7 @@ export class BatchedSdfTextMesh
240
250
  this._declutterPriority = material.declutterPriority ?? 0;
241
251
 
242
252
  this._positions = this.extractPositions(m);
253
+ this._rebuildBatchIndexMap(m);
243
254
  this._glyphs = new GlyphBuffers();
244
255
  this._labelData = new LabelDataTexture();
245
256
 
@@ -359,12 +370,12 @@ export class BatchedSdfTextMesh
359
370
  for (let i = 0; i < info.nPositions; i++) this._ensureLabel(i);
360
371
  }
361
372
 
362
- private _ensureLabel(batchIndex: number): LabelRecord | undefined {
363
- const existing = this._labelByIndex[batchIndex];
373
+ private _ensureLabel(instanceIndex: number): LabelRecord | undefined {
374
+ const existing = this._labelByInstance[instanceIndex];
364
375
  if (existing) return existing;
365
376
 
366
377
  const info = this._positions;
367
- if (!info || batchIndex < 0 || batchIndex >= info.nPositions) {
378
+ if (!info || instanceIndex < 0 || instanceIndex >= info.nPositions) {
368
379
  return undefined;
369
380
  }
370
381
 
@@ -376,8 +387,10 @@ export class BatchedSdfTextMesh
376
387
 
377
388
  const record: LabelRecord = {
378
389
  slot,
379
- batchIndex,
380
- batchId: info.batchIDs ? info.batchIDs[batchIndex * info.batchIDSize] : 0,
390
+ instanceIndex,
391
+ batchId: info.batchIDs
392
+ ? info.batchIDs[instanceIndex * info.batchIDSize]
393
+ : 0,
381
394
  text: "",
382
395
  requestedText: "",
383
396
  prepareDeferred: false,
@@ -405,7 +418,7 @@ export class BatchedSdfTextMesh
405
418
  };
406
419
 
407
420
  this._labels.push(record);
408
- this._labelByIndex[batchIndex] = record;
421
+ this._labelByInstance[instanceIndex] = record;
409
422
 
410
423
  this._writeAnchor(record);
411
424
  this._writeStyle(record);
@@ -419,7 +432,7 @@ export class BatchedSdfTextMesh
419
432
  private _writeAnchor(record: LabelRecord): void {
420
433
  const info = this._positions;
421
434
  if (!info) return;
422
- const idx = record.batchIndex * info.positionSize;
435
+ const idx = record.instanceIndex * info.positionSize;
423
436
  const anchor = record.anchor;
424
437
 
425
438
  if (info.RTE) {
@@ -971,6 +984,51 @@ export class BatchedSdfTextMesh
971
984
  return null;
972
985
  }
973
986
 
987
+ /**
988
+ * Group anchor slots by their feature's batch index from the geometry's
989
+ * per-anchor `batch_index` buffer. The u32 view is consumed synchronously —
990
+ * other wasm calls may detach views, so it must not be stored. An identity
991
+ * mapping (every feature owns exactly one anchor, the common MVT case) skips
992
+ * the map entirely.
993
+ */
994
+ private _rebuildBatchIndexMap(m: NavaraTextMesh): void {
995
+ const batchIndexData = m.geometry.batch_index?.data;
996
+ const batchIndices =
997
+ batchIndexData !== undefined ? this.ctx.buf.u32(batchIndexData) : null;
998
+ this._batchIndexToInstances = null;
999
+ if (!batchIndices) return;
1000
+
1001
+ let identity = true;
1002
+ for (let i = 0; i < batchIndices.length; i++) {
1003
+ if (batchIndices[i] !== i) {
1004
+ identity = false;
1005
+ break;
1006
+ }
1007
+ }
1008
+ if (identity) return;
1009
+
1010
+ const map = new Map<number, number[]>();
1011
+ for (let i = 0; i < batchIndices.length; i++) {
1012
+ const batchIndex = batchIndices[i];
1013
+ let instances = map.get(batchIndex);
1014
+ if (!instances) {
1015
+ instances = [];
1016
+ map.set(batchIndex, instances);
1017
+ }
1018
+ instances.push(i);
1019
+ }
1020
+ this._batchIndexToInstances = map;
1021
+ }
1022
+
1023
+ /** All anchor slots owned by the feature at `batchIndex`. */
1024
+ private _instancesOfBatchIndex(batchIndex: number): number[] {
1025
+ if (this._batchIndexToInstances) {
1026
+ return this._batchIndexToInstances.get(batchIndex) ?? [];
1027
+ }
1028
+ const nPositions = this._positions?.nPositions ?? 0;
1029
+ return batchIndex >= 0 && batchIndex < nPositions ? [batchIndex] : [];
1030
+ }
1031
+
974
1032
  async _update(m: NavaraTextMesh, needRender?: () => void) {
975
1033
  if (needRender) this._needRender = needRender;
976
1034
 
@@ -992,6 +1050,7 @@ export class BatchedSdfTextMesh
992
1050
  "Number of positions in the updated geometry must match the initial geometry",
993
1051
  );
994
1052
  this._positions = positionInfo;
1053
+ this._rebuildBatchIndexMap(m);
995
1054
  this._enhancer
996
1055
  .mutates()
997
1056
  .setRtcCenter([m.transform.tx, m.transform.ty, m.transform.tz]);
@@ -1162,12 +1221,18 @@ export class BatchedSdfTextMesh
1162
1221
  // --- Per-feature API (the FeatureEvaluator contract) ---
1163
1222
 
1164
1223
  setTextByBatchIndex(batchIndex: number, text: string) {
1224
+ for (const instanceIndex of this._instancesOfBatchIndex(batchIndex)) {
1225
+ this._setTextByInstance(instanceIndex, text);
1226
+ }
1227
+ }
1228
+
1229
+ private _setTextByInstance(instanceIndex: number, text: string) {
1165
1230
  // An empty text on a label that doesn't exist yet changes nothing (a
1166
1231
  // lazily-skipped label is exactly an invisible empty-text label); only a
1167
1232
  // non-empty text forces one into existence.
1168
1233
  const record = text
1169
- ? this._ensureLabel(batchIndex)
1170
- : this._labelByIndex[batchIndex];
1234
+ ? this._ensureLabel(instanceIndex)
1235
+ : this._labelByInstance[instanceIndex];
1171
1236
  if (!record) return;
1172
1237
 
1173
1238
  // Record the intent up front so a slower prepare for an older text can
@@ -1284,57 +1349,69 @@ export class BatchedSdfTextMesh
1284
1349
  }
1285
1350
 
1286
1351
  override setFeatureColorByBatchIndex(batchIndex: number, color: Color) {
1287
- const record = this._ensureLabel(batchIndex);
1288
- if (!record) return;
1289
- record.colorHex = color.getHex();
1290
- this._writeStyle(record);
1352
+ for (const instanceIndex of this._instancesOfBatchIndex(batchIndex)) {
1353
+ const record = this._ensureLabel(instanceIndex);
1354
+ if (!record) continue;
1355
+ record.colorHex = color.getHex();
1356
+ this._writeStyle(record);
1357
+ }
1291
1358
  }
1292
1359
 
1293
1360
  override setFeatureShowByBatchIndex(batchIndex: number, rawVisible: boolean) {
1294
- // A label that doesn't exist yet is already effectively `show:false`, so
1295
- // only `show:true` needs to force one into existence — the common MVT case
1296
- // is thousands of features that stay hidden and never get a label.
1297
- const record = rawVisible
1298
- ? this._ensureLabel(batchIndex)
1299
- : this._labelByIndex[batchIndex];
1300
- if (!record) return;
1301
- record.requestedShow = rawVisible;
1302
- this._recomputeShow(record);
1361
+ for (const instanceIndex of this._instancesOfBatchIndex(batchIndex)) {
1362
+ // A label that doesn't exist yet is already effectively `show:false`, so
1363
+ // only `show:true` needs to force one into existence — the common MVT
1364
+ // case is thousands of features that stay hidden and never get a label.
1365
+ const record = rawVisible
1366
+ ? this._ensureLabel(instanceIndex)
1367
+ : this._labelByInstance[instanceIndex];
1368
+ if (!record) continue;
1369
+ record.requestedShow = rawVisible;
1370
+ this._recomputeShow(record);
1371
+ }
1303
1372
  this._markDeclutterDirty();
1304
1373
  }
1305
1374
 
1306
1375
  override setFeatureHeightByBatchIndex(batchIndex: number, height: number) {
1307
- const record = this._ensureLabel(batchIndex);
1308
- if (!record) return;
1309
- record.addHeight = height;
1310
- this._writeAddHeight(record);
1376
+ for (const instanceIndex of this._instancesOfBatchIndex(batchIndex)) {
1377
+ const record = this._ensureLabel(instanceIndex);
1378
+ if (!record) continue;
1379
+ record.addHeight = height;
1380
+ this._writeAddHeight(record);
1381
+ }
1311
1382
  this._markDeclutterDirty();
1312
1383
  }
1313
1384
 
1314
1385
  setFeatureSizeByBatchIndex(batchIndex: number, size: number) {
1315
- const record = this._ensureLabel(batchIndex);
1316
- if (!record) return;
1317
- record.fontSize = Number.isFinite(size)
1318
- ? Math.max(0.0, size)
1319
- : record.fontSize;
1320
- this._writeFontSize(record);
1386
+ for (const instanceIndex of this._instancesOfBatchIndex(batchIndex)) {
1387
+ const record = this._ensureLabel(instanceIndex);
1388
+ if (!record) continue;
1389
+ record.fontSize = Number.isFinite(size)
1390
+ ? Math.max(0.0, size)
1391
+ : record.fontSize;
1392
+ this._writeFontSize(record);
1393
+ }
1321
1394
  this._markDeclutterDirty();
1322
1395
  }
1323
1396
 
1324
1397
  setFeatureOpacityByBatchIndex(batchIndex: number, opacity: number) {
1325
- const record = this._ensureLabel(batchIndex);
1326
- if (!record) return;
1327
- record.opacity = clamp01(opacity);
1328
- this._writeStyle(record);
1398
+ for (const instanceIndex of this._instancesOfBatchIndex(batchIndex)) {
1399
+ const record = this._ensureLabel(instanceIndex);
1400
+ if (!record) continue;
1401
+ record.opacity = clamp01(opacity);
1402
+ this._writeStyle(record);
1403
+ }
1329
1404
  }
1330
1405
 
1331
1406
  setFeatureDeclutterPriorityByBatchIndex(
1332
1407
  batchIndex: number,
1333
1408
  priority: number,
1334
1409
  ) {
1335
- const record = this._ensureLabel(batchIndex);
1336
- if (!record) return;
1337
- record.priorityOverride = priority;
1410
+ for (const instanceIndex of this._instancesOfBatchIndex(batchIndex)) {
1411
+ const record = this._ensureLabel(instanceIndex);
1412
+ if (!record) continue;
1413
+ record.priorityOverride = priority;
1414
+ }
1338
1415
  this._markDeclutterDirty();
1339
1416
  }
1340
1417
 
@@ -1368,7 +1445,8 @@ export class BatchedSdfTextMesh
1368
1445
  record.retainedKeys = null;
1369
1446
  }
1370
1447
  this._labels.length = 0;
1371
- this._labelByIndex.length = 0;
1448
+ this._labelByInstance.length = 0;
1449
+ this._batchIndexToInstances = null;
1372
1450
 
1373
1451
  this._labelData.dispose();
1374
1452
  this._glyphs.dispose();