@navaramap/three 0.0.8 → 0.1.0
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/LICENSE +1 -1
- package/README.md +20 -8
- package/dist/assets/fontWorker-C5cHhkWQ.js +2 -0
- package/dist/assets/fontWorker-C5cHhkWQ.js.map +1 -0
- package/dist/assets/{index-DaejBdUz.js → index-VfUncKuS.js} +2 -2
- package/dist/assets/index-VfUncKuS.js.map +1 -0
- package/dist/assets/{navara_wasm_font_worker_bg-CSmoDs_Q.wasm → navara_wasm_font_worker_bg-CKqHBoCR.wasm} +0 -0
- package/dist/assets/{navara_wasm_worker_bg-C1dUd_Nk.wasm → navara_wasm_worker_bg-CT26EH41.wasm} +0 -0
- package/dist/index.d.ts +213 -34
- package/dist/index.js +1476 -1351
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +79 -58
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +12 -12
- package/src/core/MeshDesc.test.ts +6 -8
- package/src/core/MeshDesc.ts +2 -5
- package/src/evaluations/FeatureEvaluator.ts +17 -10
- package/src/event/feature.ts +15 -2
- package/src/event/features/billboard.test.ts +62 -0
- package/src/event/features/billboard.ts +4 -2
- package/src/index.ts +98 -27
- package/src/material/macro/tileShader.ts +12 -2
- package/src/mesh/polyline.ts +32 -4
- package/src/mesh/sdfText/batchedSdfText.test.ts +65 -0
- package/src/mesh/sdfText/batchedSdfText.ts +121 -43
- package/src/mesh/sprite/instancedSprite.ts +173 -65
- package/src/pick/pickHelper.test.ts +261 -3
- package/src/pick/pickHelper.ts +94 -7
- package/src/renderOrder.ts +10 -0
- package/src/terrain/sampleTerrainMostDetailed.test.ts +6 -6
- package/src/terrain/sampleTerrainMostDetailed.ts +7 -5
- package/src/type/index.ts +22 -1
- package/dist/assets/fontWorker-CkLGtrXf.js +0 -2
- package/dist/assets/fontWorker-CkLGtrXf.js.map +0 -1
- package/dist/assets/index-DaejBdUz.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
|
183
|
-
private
|
|
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(
|
|
363
|
-
const existing = this.
|
|
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 ||
|
|
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
|
-
|
|
380
|
-
batchId: info.batchIDs
|
|
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.
|
|
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.
|
|
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(
|
|
1170
|
-
: this.
|
|
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
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
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
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
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
|
|
1336
|
-
|
|
1337
|
-
|
|
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.
|
|
1448
|
+
this._labelByInstance.length = 0;
|
|
1449
|
+
this._batchIndexToInstances = null;
|
|
1372
1450
|
|
|
1373
1451
|
this._labelData.dispose();
|
|
1374
1452
|
this._glyphs.dispose();
|
|
@@ -66,7 +66,15 @@ export class InstancedSpriteMesh
|
|
|
66
66
|
implements PickableMesh, DeclutterParticipant
|
|
67
67
|
{
|
|
68
68
|
private _geometryType: SpriteGeometryType = GEOMETRY_TYPES.Point;
|
|
69
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Feature (batch) index → this feature's instance ids. A feature owns
|
|
71
|
+
* multiple instances for MultiPoint geometry and for points derived from
|
|
72
|
+
* line/polygon vertices via `geometryTypes`, so per-feature styling must
|
|
73
|
+
* fan out to all of them. `null` means instances and features are 1:1.
|
|
74
|
+
*/
|
|
75
|
+
private _batchIndexToInstances: Map<number, number[]> | null = null;
|
|
76
|
+
/** Instance count of the current geometry; bounds the identity fallback. */
|
|
77
|
+
private _instanceCount = 0;
|
|
70
78
|
private _initialColor: Color = new Color(0xffffff);
|
|
71
79
|
private _initialHeight = 0.0;
|
|
72
80
|
private _initialSize = -1.0; // Negative value indicates "use uScale" in shader
|
|
@@ -161,7 +169,7 @@ export class InstancedSpriteMesh
|
|
|
161
169
|
const state = enhancer.states();
|
|
162
170
|
const cx = Math.min(Math.max(state.center[0], -0.5), 0.5);
|
|
163
171
|
const cy = Math.min(Math.max(state.center[1], -0.5), 0.5);
|
|
164
|
-
// Mirror of instancedSprite.vert.glsl:
|
|
172
|
+
// Mirror of instancedSprite.vert.glsl:115-118 — aspect is per-instance
|
|
165
173
|
// (from the atlas rect), not a material-level uniform; there is no
|
|
166
174
|
// material-wide "aspect" state to read.
|
|
167
175
|
const uvRect = state.billboard
|
|
@@ -181,9 +189,14 @@ export class InstancedSpriteMesh
|
|
|
181
189
|
|
|
182
190
|
const override = overrides ? overrides[i] : Number.NaN;
|
|
183
191
|
const rectH = uvRect ? uvRect.getW(i) : 0;
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
//
|
|
192
|
+
const rectW = uvRect ? uvRect.getZ(i) : 0;
|
|
193
|
+
// Mirror of the shader's empty-rect cull: an instance with no image
|
|
194
|
+
// packed yet is not drawn, so it must not reserve declutter space and
|
|
195
|
+
// hide the labels around it.
|
|
196
|
+
if (uvRect && (rectW <= 0.0 || rectH <= 0.0)) continue;
|
|
197
|
+
const aspect = uvRect ? rectW / rectH : 1.0;
|
|
198
|
+
|
|
199
|
+
// Mirror of instancedSprite.vert.glsl:122-125 — the quad spans
|
|
187
200
|
// (position.xy - center) * vec2(aspect, 1) * size around the anchor.
|
|
188
201
|
out.push({
|
|
189
202
|
anchorX: anchors[i * 3],
|
|
@@ -481,17 +494,15 @@ export class InstancedSpriteMesh
|
|
|
481
494
|
colorBuffer[i * 3 + 0] = this._initialColor.r;
|
|
482
495
|
colorBuffer[i * 3 + 1] = this._initialColor.g;
|
|
483
496
|
colorBuffer[i * 3 + 2] = this._initialColor.b;
|
|
484
|
-
|
|
485
|
-
// Map batch ID to instance id
|
|
486
|
-
// assuming batch IDs are 32bit floats
|
|
487
|
-
const batchId = positionsInfo.batchIDs[i];
|
|
488
|
-
this._batchIdToInstance.set(batchId, i);
|
|
489
497
|
}
|
|
490
498
|
|
|
499
|
+
this._instanceCount = instanceCount;
|
|
500
|
+
this._rebuildBatchIndexMap(m);
|
|
501
|
+
|
|
491
502
|
if (m instanceof NavaraBillboardMesh) {
|
|
492
503
|
// instanceUvRect: vec4(x, y, w, h) — this instance's atlas sub-rect in
|
|
493
|
-
// pixels. Zeroed until an image is packed;
|
|
494
|
-
//
|
|
504
|
+
// pixels. Zeroed until an image is packed; the vertex shader culls empty
|
|
505
|
+
// rects, so instances stay invisible rather than garbled.
|
|
495
506
|
instancedGeometry.setAttribute(
|
|
496
507
|
"instanceUvRect",
|
|
497
508
|
new InstancedBufferAttribute(new Float32Array(instanceCount * 4), 4),
|
|
@@ -755,6 +766,20 @@ export class InstancedSpriteMesh
|
|
|
755
766
|
rectAttr.setXYZW(i, rect.x, rect.y, rect.w, rect.h);
|
|
756
767
|
}
|
|
757
768
|
rectAttr.needsUpdate = true;
|
|
769
|
+
this._onAtlasRectsChanged();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* An instance's atlas rect changed. The rect drives the quad's aspect and
|
|
774
|
+
* whether the instance draws at all, so the declutter bounds computed before
|
|
775
|
+
* the image landed are stale — and because packs resolve long after the
|
|
776
|
+
* frame that requested them, the view has usually gone idle by now. Without
|
|
777
|
+
* the render request the page keeps showing the pre-image state until the
|
|
778
|
+
* next camera move.
|
|
779
|
+
*/
|
|
780
|
+
private _onAtlasRectsChanged(): void {
|
|
781
|
+
this.ctx.declutter?.markDirty();
|
|
782
|
+
this.ctx.renderFlag.forceUpdate = true;
|
|
758
783
|
}
|
|
759
784
|
|
|
760
785
|
onBeforePicking(): void {
|
|
@@ -781,32 +806,85 @@ export class InstancedSpriteMesh
|
|
|
781
806
|
return this._enhancedMaterial;
|
|
782
807
|
}
|
|
783
808
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
809
|
+
/**
|
|
810
|
+
* Group instances by their feature's batch index from the geometry's
|
|
811
|
+
* per-instance `batch_index` buffer. The u32 view is consumed synchronously,
|
|
812
|
+
* so other wasm calls may detach views, so it must not be stored. The handle
|
|
813
|
+
* stays owned by the ECS geometry, whose destroy path frees it
|
|
814
|
+
* (`remove_from_buf`). An identity mapping (every feature owns exactly one
|
|
815
|
+
* instance, the common case) skips the map entirely.
|
|
816
|
+
*/
|
|
817
|
+
private _rebuildBatchIndexMap(
|
|
818
|
+
m: NavaraPointMesh | NavaraBillboardMesh,
|
|
819
|
+
): void {
|
|
820
|
+
const batchIndexData = m.geometry.batch_index?.data;
|
|
821
|
+
const batchIndices =
|
|
822
|
+
batchIndexData !== undefined ? this.ctx.buf.u32(batchIndexData) : null;
|
|
823
|
+
this._batchIndexToInstances = null;
|
|
824
|
+
if (!batchIndices) return;
|
|
825
|
+
|
|
826
|
+
let identity = true;
|
|
827
|
+
for (let i = 0; i < batchIndices.length; i++) {
|
|
828
|
+
if (batchIndices[i] !== i) {
|
|
829
|
+
identity = false;
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
if (identity) return;
|
|
834
|
+
|
|
835
|
+
const map = new Map<number, number[]>();
|
|
836
|
+
for (let i = 0; i < batchIndices.length; i++) {
|
|
837
|
+
const batchIndex = batchIndices[i];
|
|
838
|
+
let instances = map.get(batchIndex);
|
|
839
|
+
if (!instances) {
|
|
840
|
+
instances = [];
|
|
841
|
+
map.set(batchIndex, instances);
|
|
842
|
+
}
|
|
843
|
+
instances.push(i);
|
|
844
|
+
}
|
|
845
|
+
this._batchIndexToInstances = map;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/** All instances owned by the feature at `batchIndex`; empty when unknown. */
|
|
849
|
+
private instancesOfBatchIndex(batchIndex: number): number[] {
|
|
850
|
+
if (this._batchIndexToInstances) {
|
|
851
|
+
return this._batchIndexToInstances.get(batchIndex) ?? [];
|
|
852
|
+
}
|
|
853
|
+
return batchIndex >= 0 && batchIndex < this._instanceCount
|
|
854
|
+
? [batchIndex]
|
|
855
|
+
: [];
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
setFeatureColorByBatchIndex(batchIndex: number, color: Color) {
|
|
859
|
+
const instances = this.instancesOfBatchIndex(batchIndex);
|
|
860
|
+
if (instances.length === 0) return;
|
|
787
861
|
|
|
788
862
|
const colorAttr = this.geometry.getAttribute(
|
|
789
863
|
"instanceColor",
|
|
790
864
|
) as InstancedBufferAttribute;
|
|
791
|
-
|
|
865
|
+
for (const instanceId of instances) {
|
|
866
|
+
colorAttr.setXYZ(instanceId, color.r, color.g, color.b);
|
|
867
|
+
}
|
|
792
868
|
colorAttr.needsUpdate = true;
|
|
793
869
|
}
|
|
794
870
|
|
|
795
|
-
|
|
796
|
-
const
|
|
797
|
-
if (
|
|
871
|
+
setFeatureShowByBatchIndex(batchIndex: number, rawVisible: boolean) {
|
|
872
|
+
const instances = this.instancesOfBatchIndex(batchIndex);
|
|
873
|
+
if (instances.length === 0) return;
|
|
798
874
|
|
|
799
875
|
const paramsAttr = this.geometry.getAttribute(
|
|
800
876
|
"instanceParams",
|
|
801
877
|
) as InstancedBufferAttribute;
|
|
802
|
-
|
|
878
|
+
for (const instanceId of instances) {
|
|
879
|
+
paramsAttr.setZ(instanceId, rawVisible ? 1.0 : 0.0);
|
|
880
|
+
}
|
|
803
881
|
paramsAttr.needsUpdate = true;
|
|
804
882
|
this.ctx.declutter?.markDirty();
|
|
805
883
|
}
|
|
806
884
|
|
|
807
|
-
|
|
808
|
-
const
|
|
809
|
-
if (
|
|
885
|
+
setFeatureOpacityByBatchIndex(batchIndex: number, opacity: number) {
|
|
886
|
+
const instances = this.instancesOfBatchIndex(batchIndex);
|
|
887
|
+
if (instances.length === 0) return;
|
|
810
888
|
|
|
811
889
|
const paramsAttr = this.geometry.getAttribute(
|
|
812
890
|
"instanceParams",
|
|
@@ -814,20 +892,24 @@ export class InstancedSpriteMesh
|
|
|
814
892
|
const clampedOpacity = Number.isFinite(opacity)
|
|
815
893
|
? Math.max(0.0, Math.min(1.0, opacity))
|
|
816
894
|
: 1.0;
|
|
817
|
-
|
|
895
|
+
for (const instanceId of instances) {
|
|
896
|
+
paramsAttr.setW(instanceId, clampedOpacity);
|
|
897
|
+
}
|
|
818
898
|
paramsAttr.needsUpdate = true;
|
|
819
899
|
}
|
|
820
900
|
|
|
821
|
-
|
|
822
|
-
const
|
|
823
|
-
if (
|
|
901
|
+
setFeatureHeightByBatchIndex(batchIndex: number, height: number) {
|
|
902
|
+
const instances = this.instancesOfBatchIndex(batchIndex);
|
|
903
|
+
if (instances.length === 0) return;
|
|
824
904
|
|
|
825
905
|
const paramsAttr = this.geometry.getAttribute(
|
|
826
906
|
"instanceParams",
|
|
827
907
|
) as InstancedBufferAttribute;
|
|
828
908
|
|
|
829
909
|
const sanitizedHeight = Number.isFinite(height) ? height : 0.0;
|
|
830
|
-
|
|
910
|
+
for (const instanceId of instances) {
|
|
911
|
+
paramsAttr.setX(instanceId, sanitizedHeight);
|
|
912
|
+
}
|
|
831
913
|
paramsAttr.needsUpdate = true;
|
|
832
914
|
this.ctx.declutter?.markDirty();
|
|
833
915
|
}
|
|
@@ -854,9 +936,12 @@ export class InstancedSpriteMesh
|
|
|
854
936
|
* layer-level `declutterPriority` for this instance. Driven by the feature
|
|
855
937
|
* evaluator.
|
|
856
938
|
*/
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
939
|
+
setFeatureDeclutterPriorityByBatchIndex(
|
|
940
|
+
batchIndex: number,
|
|
941
|
+
priority: number,
|
|
942
|
+
) {
|
|
943
|
+
const instances = this.instancesOfBatchIndex(batchIndex);
|
|
944
|
+
if (instances.length === 0) return;
|
|
860
945
|
|
|
861
946
|
if (!this._declutterPriorityOverrides) {
|
|
862
947
|
const count = this._anchors ? this._anchors.length / 3 : 0;
|
|
@@ -865,15 +950,21 @@ export class InstancedSpriteMesh
|
|
|
865
950
|
Number.NaN,
|
|
866
951
|
);
|
|
867
952
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
953
|
+
let changed = false;
|
|
954
|
+
for (const instanceId of instances) {
|
|
955
|
+
if (instanceId >= this._declutterPriorityOverrides.length) continue;
|
|
956
|
+
if (this._declutterPriorityOverrides[instanceId] === priority) continue;
|
|
957
|
+
this._declutterPriorityOverrides[instanceId] = priority;
|
|
958
|
+
changed = true;
|
|
959
|
+
}
|
|
960
|
+
if (changed) {
|
|
961
|
+
this.ctx.declutter?.markDirty();
|
|
962
|
+
}
|
|
872
963
|
}
|
|
873
964
|
|
|
874
|
-
|
|
875
|
-
const
|
|
876
|
-
if (
|
|
965
|
+
setFeatureSizeByBatchIndex(batchIndex: number, size: number) {
|
|
966
|
+
const instances = this.instancesOfBatchIndex(batchIndex);
|
|
967
|
+
if (instances.length === 0) return;
|
|
877
968
|
|
|
878
969
|
const paramsAttr = this.geometry.getAttribute(
|
|
879
970
|
"instanceParams",
|
|
@@ -884,7 +975,9 @@ export class InstancedSpriteMesh
|
|
|
884
975
|
? -1.0
|
|
885
976
|
: size
|
|
886
977
|
: -1.0;
|
|
887
|
-
|
|
978
|
+
for (const instanceId of instances) {
|
|
979
|
+
paramsAttr.setY(instanceId, sanitizedSize);
|
|
980
|
+
}
|
|
888
981
|
paramsAttr.needsUpdate = true;
|
|
889
982
|
this.ctx.declutter?.markDirty();
|
|
890
983
|
}
|
|
@@ -897,45 +990,60 @@ export class InstancedSpriteMesh
|
|
|
897
990
|
* reverts the feature to the material's default image. No-op for
|
|
898
991
|
* non-billboard (point) meshes.
|
|
899
992
|
*/
|
|
900
|
-
async
|
|
901
|
-
|
|
993
|
+
async setFeatureImageByBatchIndex(
|
|
994
|
+
batchIndex: number,
|
|
902
995
|
url: string | null | undefined,
|
|
903
996
|
): Promise<void> {
|
|
904
|
-
const
|
|
905
|
-
if (
|
|
997
|
+
const instances = this.instancesOfBatchIndex(batchIndex);
|
|
998
|
+
if (instances.length === 0) return;
|
|
906
999
|
|
|
907
1000
|
const rectAttr = this.geometry.getAttribute("instanceUvRect") as
|
|
908
1001
|
InstancedBufferAttribute | undefined;
|
|
909
1002
|
if (!rectAttr) return;
|
|
910
1003
|
|
|
911
1004
|
if (url == null) {
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
1005
|
+
let cleared = false;
|
|
1006
|
+
for (const instanceId of instances) {
|
|
1007
|
+
this._requestedImageUrls.delete(instanceId);
|
|
1008
|
+
if (!this._imageOverrides.delete(instanceId)) continue;
|
|
1009
|
+
// Zero rect (invisible) until the default image finishes loading,
|
|
1010
|
+
// same as instances that never had an override.
|
|
1011
|
+
const rect = this._defaultRect;
|
|
1012
|
+
rectAttr.setXYZW(
|
|
1013
|
+
instanceId,
|
|
1014
|
+
rect?.x ?? 0,
|
|
1015
|
+
rect?.y ?? 0,
|
|
1016
|
+
rect?.w ?? 0,
|
|
1017
|
+
rect?.h ?? 0,
|
|
1018
|
+
);
|
|
1019
|
+
cleared = true;
|
|
1020
|
+
}
|
|
1021
|
+
if (cleared) {
|
|
1022
|
+
rectAttr.needsUpdate = true;
|
|
1023
|
+
this._onAtlasRectsChanged();
|
|
1024
|
+
}
|
|
925
1025
|
return;
|
|
926
1026
|
}
|
|
927
1027
|
|
|
928
|
-
|
|
1028
|
+
for (const instanceId of instances) {
|
|
1029
|
+
this._requestedImageUrls.set(instanceId, url);
|
|
1030
|
+
}
|
|
929
1031
|
const rect = await this._ensureAtlas().pack(url);
|
|
930
1032
|
this._reportAtlasBytes();
|
|
931
1033
|
if (!rect) return;
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
1034
|
+
let applied = false;
|
|
1035
|
+
for (const instanceId of instances) {
|
|
1036
|
+
// A newer override or a clear superseded this load while in flight.
|
|
1037
|
+
if (this._requestedImageUrls.get(instanceId) !== url) continue;
|
|
1038
|
+
this._imageOverrides.add(instanceId);
|
|
1039
|
+
rectAttr.setXYZW(instanceId, rect.x, rect.y, rect.w, rect.h);
|
|
1040
|
+
applied = true;
|
|
1041
|
+
}
|
|
1042
|
+
if (applied) {
|
|
1043
|
+
this._syncAtlasUniforms();
|
|
1044
|
+
rectAttr.needsUpdate = true;
|
|
1045
|
+
this._onAtlasRectsChanged();
|
|
1046
|
+
}
|
|
939
1047
|
}
|
|
940
1048
|
|
|
941
1049
|
dispose(): void {
|
|
@@ -958,7 +1066,7 @@ export class InstancedSpriteMesh
|
|
|
958
1066
|
(this.material as ShaderMaterial).dispose();
|
|
959
1067
|
|
|
960
1068
|
// Clear internal collections to release references
|
|
961
|
-
this.
|
|
1069
|
+
this._batchIndexToInstances = null;
|
|
962
1070
|
this._anchors = null;
|
|
963
1071
|
this._declutterTargets = null;
|
|
964
1072
|
this._declutterPriorityOverrides = null;
|