s3m-three 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/s3m-three.iife.ext-three.js +3 -3
- package/dist/browser/s3m-three.iife.js +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/render/DatasetManager.d.ts +11 -0
- package/dist/render/DatasetManager.d.ts.map +1 -1
- package/dist/render/DatasetManager.js +63 -0
- package/dist/render/DatasetManager.js.map +1 -1
- package/dist/render/GeometryBuilder.d.ts +1 -0
- package/dist/render/GeometryBuilder.d.ts.map +1 -1
- package/dist/render/GeometryBuilder.js +4 -1
- package/dist/render/GeometryBuilder.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +56 -3
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +160 -51
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts +26 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +91 -13
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/docs/API.md +40 -2
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/render/DatasetManager.ts +65 -0
- package/src/render/GeometryBuilder.ts +5 -1
- package/src/render/S3MViewer.ts +200 -49
- package/src/tileset/S3MTileset.ts +104 -14
|
@@ -288,6 +288,7 @@ export interface BuildTileResult {
|
|
|
288
288
|
group: THREE.Group;
|
|
289
289
|
stats: TileBuildStats;
|
|
290
290
|
objects: THREE.Object3D[];
|
|
291
|
+
textureKeys?: string[];
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
interface CachedGeometry {
|
|
@@ -308,6 +309,7 @@ export async function buildTileObjects(
|
|
|
308
309
|
const textureByCode = options.textureCache ?? new Map<string, THREE.Texture>();
|
|
309
310
|
const group = new THREE.Group();
|
|
310
311
|
const objects: THREE.Object3D[] = [];
|
|
312
|
+
const usedTextureKeys: string[] = [];
|
|
311
313
|
const stats: TileBuildStats = {
|
|
312
314
|
meshes: 0,
|
|
313
315
|
vertices: 0,
|
|
@@ -352,6 +354,7 @@ export async function buildTileObjects(
|
|
|
352
354
|
const { texture, decoded } = await createTexture(tex);
|
|
353
355
|
if (texture) {
|
|
354
356
|
textureByCode.set(key, texture);
|
|
357
|
+
usedTextureKeys.push(key);
|
|
355
358
|
stats.textures++;
|
|
356
359
|
} else {
|
|
357
360
|
stats.skipped.push(`texture ${code} → ${decoded}`);
|
|
@@ -1028,5 +1031,6 @@ export async function buildTileObjects(
|
|
|
1028
1031
|
|
|
1029
1032
|
if (!group.children.length) stats.skipped.push('tile 无可用几何');
|
|
1030
1033
|
|
|
1031
|
-
|
|
1034
|
+
group.userData.textureKeys = usedTextureKeys;
|
|
1035
|
+
return { group, stats, objects, textureKeys: usedTextureKeys };
|
|
1032
1036
|
}
|
package/src/render/S3MViewer.ts
CHANGED
|
@@ -144,7 +144,42 @@ export interface PickResult extends FeatureAttributes {
|
|
|
144
144
|
mesh: THREE.Mesh;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
export interface LightOptions {
|
|
148
|
+
/** 环境光强度,默认 0.4 */
|
|
149
|
+
ambientIntensity?: number;
|
|
150
|
+
/** 环境光颜色,十六进制或 CSS 颜色字符串,默认 0xffffff */
|
|
151
|
+
ambientColor?: number | string;
|
|
152
|
+
/** 半球光天空强度,默认 0.8 */
|
|
153
|
+
hemisphereIntensity?: number;
|
|
154
|
+
/** 半球光天空颜色,默认 0xffffff */
|
|
155
|
+
hemisphereColor?: number | string;
|
|
156
|
+
/** 半球光地面颜色,默认 0xb6c0cb */
|
|
157
|
+
hemisphereGroundColor?: number | string;
|
|
158
|
+
/** 主平行光强度,默认 1.0 */
|
|
159
|
+
keyLightIntensity?: number;
|
|
160
|
+
/** 主平行光颜色,默认 0xffffff */
|
|
161
|
+
keyLightColor?: number | string;
|
|
162
|
+
/** 补光强度,默认 0.35 */
|
|
163
|
+
fillLightIntensity?: number;
|
|
164
|
+
/** 补光颜色,默认 0xffffff */
|
|
165
|
+
fillLightColor?: number | string;
|
|
166
|
+
/**
|
|
167
|
+
* 光照朝向模式:
|
|
168
|
+
* - 'headlamp': 默认头灯模式(随相机 bearing 旋转,受光面始终面向镜头)
|
|
169
|
+
* - 'fixed': 固定世界坐标方位角(如太阳光模拟)
|
|
170
|
+
*/
|
|
171
|
+
mode?: 'headlamp' | 'fixed';
|
|
172
|
+
/** 方位角(度,0~360),固定模式下以正北为 0 顺时针,头灯模式下为相机相对偏角,默认 150 */
|
|
173
|
+
azimuth?: number;
|
|
174
|
+
/** 太阳/光源仰角(度,0~90),默认 50 */
|
|
175
|
+
altitude?: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
147
178
|
export interface S3MViewerOptions {
|
|
179
|
+
/** 光照参数配置(强度、颜色、朝向模式、太阳方位角/仰角等) */
|
|
180
|
+
light?: LightOptions;
|
|
181
|
+
/** 全局总驻留瓦片上限(多数据集多图层共享预算),在所有可见数据集之间动态均衡分配,彻底防止爆显存 */
|
|
182
|
+
totalMaxTiles?: number;
|
|
148
183
|
/** 单数据集简写 */
|
|
149
184
|
configUrl?: string;
|
|
150
185
|
/** 多数据集 */
|
|
@@ -294,8 +329,24 @@ export class S3MViewer {
|
|
|
294
329
|
private _needsRepaint = true;
|
|
295
330
|
private _frame = 0;
|
|
296
331
|
private readonly _fps = { value: 0, last: 0 };
|
|
297
|
-
private
|
|
298
|
-
private
|
|
332
|
+
private _ambientLight!: THREE.AmbientLight;
|
|
333
|
+
private _hemiLight!: THREE.HemisphereLight;
|
|
334
|
+
private _keyLight!: THREE.DirectionalLight;
|
|
335
|
+
private _fillLight!: THREE.DirectionalLight;
|
|
336
|
+
private readonly _lightOptions: Required<LightOptions> = {
|
|
337
|
+
ambientIntensity: 0.4,
|
|
338
|
+
ambientColor: 0xffffff,
|
|
339
|
+
hemisphereIntensity: 0.8,
|
|
340
|
+
hemisphereColor: 0xffffff,
|
|
341
|
+
hemisphereGroundColor: 0xb6c0cb,
|
|
342
|
+
keyLightIntensity: 1.0,
|
|
343
|
+
keyLightColor: 0xffffff,
|
|
344
|
+
fillLightIntensity: 0.35,
|
|
345
|
+
fillLightColor: 0xffffff,
|
|
346
|
+
mode: 'headlamp',
|
|
347
|
+
azimuth: 150,
|
|
348
|
+
altitude: 50,
|
|
349
|
+
};
|
|
299
350
|
|
|
300
351
|
private readonly _datasetQueue: S3MDatasetOptions[];
|
|
301
352
|
|
|
@@ -309,10 +360,20 @@ export class S3MViewer {
|
|
|
309
360
|
this._onRequestRepaint();
|
|
310
361
|
}
|
|
311
362
|
|
|
363
|
+
triggerRepaint(): void {
|
|
364
|
+
this._requestRepaint();
|
|
365
|
+
}
|
|
366
|
+
|
|
312
367
|
constructor(options: S3MViewerOptions = {}) {
|
|
313
368
|
this.options = options;
|
|
314
369
|
this.pickable = options.pickable !== false;
|
|
315
370
|
this._lodRangeScale = options.lodRangeScale ?? 1.0;
|
|
371
|
+
if (options.light) {
|
|
372
|
+
Object.assign(this._lightOptions, options.light);
|
|
373
|
+
}
|
|
374
|
+
if (options.totalMaxTiles) {
|
|
375
|
+
this._datasetManager.totalMaxTiles = options.totalMaxTiles;
|
|
376
|
+
}
|
|
316
377
|
this._onRequestRepaint = options.onRequestRepaint ?? (() => {});
|
|
317
378
|
this._getBearing = options.getBearing;
|
|
318
379
|
this.onStats = options.onStats ?? (() => {});
|
|
@@ -321,6 +382,11 @@ export class S3MViewer {
|
|
|
321
382
|
this.onError = options.onError ?? ((e) => console.warn('[s3m]', e));
|
|
322
383
|
|
|
323
384
|
this.groundManager = new GroundManager(options.ground);
|
|
385
|
+
if (!this.scene) {
|
|
386
|
+
this.scene = new THREE.Scene();
|
|
387
|
+
this.scene.add(this.groundManager.group);
|
|
388
|
+
this._addLights();
|
|
389
|
+
}
|
|
324
390
|
this._featureStyleManager = new FeatureStyleManager(
|
|
325
391
|
() => this.style.floorHeight,
|
|
326
392
|
() => this._requestRepaint(),
|
|
@@ -412,24 +478,70 @@ export class S3MViewer {
|
|
|
412
478
|
this.canvas = null;
|
|
413
479
|
}
|
|
414
480
|
|
|
481
|
+
private _createLights(): void {
|
|
482
|
+
this._ambientLight = new THREE.AmbientLight(this._lightOptions.ambientColor as any, this._lightOptions.ambientIntensity);
|
|
483
|
+
this._hemiLight = new THREE.HemisphereLight(
|
|
484
|
+
this._lightOptions.hemisphereColor as any,
|
|
485
|
+
this._lightOptions.hemisphereGroundColor as any,
|
|
486
|
+
this._lightOptions.hemisphereIntensity
|
|
487
|
+
);
|
|
488
|
+
this._keyLight = new THREE.DirectionalLight(this._lightOptions.keyLightColor as any, this._lightOptions.keyLightIntensity);
|
|
489
|
+
this._fillLight = new THREE.DirectionalLight(this._lightOptions.fillLightColor as any, this._lightOptions.fillLightIntensity);
|
|
490
|
+
this._updateLights();
|
|
491
|
+
}
|
|
492
|
+
|
|
415
493
|
private _addLights(): void {
|
|
416
494
|
if (!this.scene) return;
|
|
417
|
-
|
|
418
|
-
this.scene.add(
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
495
|
+
if (!this._ambientLight) this._createLights();
|
|
496
|
+
this.scene.add(this._ambientLight, this._hemiLight, this._keyLight, this._fillLight);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
private _applyLightOptions(): void {
|
|
500
|
+
if (this._ambientLight) {
|
|
501
|
+
this._ambientLight.intensity = this._lightOptions.ambientIntensity;
|
|
502
|
+
this._ambientLight.color.set(this._lightOptions.ambientColor as any);
|
|
503
|
+
}
|
|
504
|
+
if (this._hemiLight) {
|
|
505
|
+
this._hemiLight.intensity = this._lightOptions.hemisphereIntensity;
|
|
506
|
+
this._hemiLight.color.set(this._lightOptions.hemisphereColor as any);
|
|
507
|
+
this._hemiLight.groundColor.set(this._lightOptions.hemisphereGroundColor as any);
|
|
508
|
+
}
|
|
509
|
+
if (this._keyLight) {
|
|
510
|
+
this._keyLight.intensity = this._lightOptions.keyLightIntensity;
|
|
511
|
+
this._keyLight.color.set(this._lightOptions.keyLightColor as any);
|
|
512
|
+
}
|
|
513
|
+
if (this._fillLight) {
|
|
514
|
+
this._fillLight.intensity = this._lightOptions.fillLightIntensity;
|
|
515
|
+
this._fillLight.color.set(this._lightOptions.fillLightColor as any);
|
|
516
|
+
}
|
|
425
517
|
}
|
|
426
518
|
|
|
427
519
|
/**
|
|
428
|
-
*
|
|
429
|
-
|
|
520
|
+
* 动态设置光照配置
|
|
521
|
+
*/
|
|
522
|
+
setLight(options: Partial<LightOptions>): void {
|
|
523
|
+
Object.assign(this._lightOptions, options);
|
|
524
|
+
this._applyLightOptions();
|
|
525
|
+
this._updateLights();
|
|
526
|
+
this._needsRepaint = true;
|
|
527
|
+
this.triggerRepaint();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* 获取当前光照配置快照
|
|
532
|
+
*/
|
|
533
|
+
getLight(): LightOptions {
|
|
534
|
+
return { ...this._lightOptions };
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* 更新光照位置与方向:
|
|
539
|
+
* - 'headlamp': 随地图方位角 bearing 旋转,主光始终在镜头上方,受光面面向镜头;
|
|
540
|
+
* - 'fixed': 固定世界方位角(如模拟恒定日光)。
|
|
430
541
|
*/
|
|
431
542
|
private _updateLights(): void {
|
|
432
|
-
|
|
543
|
+
if (!this._keyLight || !this._fillLight) return;
|
|
544
|
+
const bearing = this._lightOptions.mode === 'fixed' ? 0 : (((this._getBearing?.() ?? 0) * Math.PI) / 180);
|
|
433
545
|
const place = (light: THREE.DirectionalLight | null, azOffsetDeg: number, elevDeg: number): void => {
|
|
434
546
|
if (!light) return;
|
|
435
547
|
const az = bearing + (azOffsetDeg * Math.PI) / 180;
|
|
@@ -437,8 +549,8 @@ export class S3MViewer {
|
|
|
437
549
|
const c = Math.cos(el);
|
|
438
550
|
light.position.set(Math.sin(az) * c, Math.cos(az) * c, Math.sin(el)).multiplyScalar(1000);
|
|
439
551
|
};
|
|
440
|
-
place(this._keyLight,
|
|
441
|
-
place(this._fillLight,
|
|
552
|
+
place(this._keyLight, this._lightOptions.azimuth, this._lightOptions.altitude);
|
|
553
|
+
place(this._fillLight, this._lightOptions.azimuth + 65, Math.max(10, this._lightOptions.altitude - 20));
|
|
442
554
|
}
|
|
443
555
|
|
|
444
556
|
/* ---------------------------------------------------------------- 数据集 */
|
|
@@ -877,6 +989,27 @@ export class S3MViewer {
|
|
|
877
989
|
* 而 LOD 细化、瓦片重载、淘汰回补都会换掉 mesh —— 不重放的话,用户点中的那栋楼
|
|
878
990
|
* 一细化就失去高亮(状态还在 _selection.smid 里,只是没人往新 mesh 上刷)。
|
|
879
991
|
*/
|
|
992
|
+
/**
|
|
993
|
+
* 判断一块无 SMID 单体化信息的网格(如倾斜摄影或白模整块)是否空间覆盖当前选中命中点
|
|
994
|
+
*/
|
|
995
|
+
private _isMeshCoveringSelection(mesh: THREE.Mesh, sel: PickResult): boolean {
|
|
996
|
+
if (!sel.point) return false;
|
|
997
|
+
const geom = mesh.geometry;
|
|
998
|
+
if (!geom) return false;
|
|
999
|
+
if (!geom.boundingBox) geom.computeBoundingBox();
|
|
1000
|
+
if (!geom.boundingBox) return false;
|
|
1001
|
+
|
|
1002
|
+
// 将选中的三维场景点转入网格局部坐标系进行包围盒检测
|
|
1003
|
+
const pt = new THREE.Vector3(sel.point[0], sel.point[1], sel.point[2]);
|
|
1004
|
+
mesh.updateWorldMatrix(true, false);
|
|
1005
|
+
const inv = mesh.matrixWorld.clone().invert();
|
|
1006
|
+
const localPt = pt.applyMatrix4(inv);
|
|
1007
|
+
|
|
1008
|
+
// 留出 0.5 米容差,避免刚好处于三角网格边缘缝隙时漏判
|
|
1009
|
+
const box = geom.boundingBox.clone().expandByScalar(0.5);
|
|
1010
|
+
return box.containsPoint(localPt);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
880
1013
|
private _replaySelectionOnto(mesh: THREE.Mesh): void {
|
|
881
1014
|
const sel = this._selection;
|
|
882
1015
|
if (!sel) return;
|
|
@@ -893,7 +1026,13 @@ export class S3MViewer {
|
|
|
893
1026
|
// 拾取命中的那块 mesh 用原始 batchId;同一 smid 散在别的几何里时按各自的区间算
|
|
894
1027
|
if (mesh === sel.mesh) batchId = sel.batchId;
|
|
895
1028
|
// 这块几何里根本没有这个单体 → 不动它(否则会把无关瓦片一起点亮)
|
|
896
|
-
if (batchId < 0 && mesh !== sel.mesh)
|
|
1029
|
+
if (batchId < 0 && mesh !== sel.mesh) {
|
|
1030
|
+
if (sel.smid == null && ud.datasetId === sel.datasetId && this._isMeshCoveringSelection(mesh, sel)) {
|
|
1031
|
+
batchId = -1;
|
|
1032
|
+
} else {
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
897
1036
|
|
|
898
1037
|
mat.userData.uSelBatch.value = batchId;
|
|
899
1038
|
mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
|
|
@@ -903,44 +1042,56 @@ export class S3MViewer {
|
|
|
903
1042
|
this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
|
|
904
1043
|
}
|
|
905
1044
|
|
|
906
|
-
/**
|
|
1045
|
+
/** 瓦片被淘汰:从拾取表和场景里摘掉,并深度彻底释放所有几何体与材质显存 */
|
|
907
1046
|
private _unregisterTile(node: TileNode, entry: S3MDatasetEntry): void {
|
|
908
|
-
|
|
909
|
-
const
|
|
910
|
-
if (
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
const wasSelected = this._selectedMeshes.includes(mesh);
|
|
915
|
-
if (wasSelected) {
|
|
916
|
-
this._selectedMeshes = this._selectedMeshes.filter((m) => m !== mesh);
|
|
1047
|
+
node.group.traverse((child) => {
|
|
1048
|
+
const obj = child as any;
|
|
1049
|
+
if (obj.userData?.pickId != null) {
|
|
1050
|
+
this._pickables.delete(obj.userData.pickId);
|
|
1051
|
+
this._releasePickId(obj.userData.pickId);
|
|
1052
|
+
obj.userData.pickId = undefined;
|
|
917
1053
|
}
|
|
918
|
-
if (
|
|
919
|
-
const
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
if (carrier) {
|
|
923
|
-
// 选中身份交给承载同一 smid 的另一块 mesh(通常是接替显示的子瓦)
|
|
924
|
-
this._selection = { ...this._selection, mesh: carrier };
|
|
925
|
-
this._replaySelectionOnto(carrier);
|
|
926
|
-
} else {
|
|
927
|
-
this.select(null);
|
|
1054
|
+
if (obj.isMesh) {
|
|
1055
|
+
const wasSelected = this._selectedMeshes.includes(obj);
|
|
1056
|
+
if (wasSelected) {
|
|
1057
|
+
this._selectedMeshes = this._selectedMeshes.filter((m) => m !== obj);
|
|
928
1058
|
}
|
|
1059
|
+
const currentSel = this._selection;
|
|
1060
|
+
if (currentSel && currentSel.mesh === obj) {
|
|
1061
|
+
const smid = currentSel.smid;
|
|
1062
|
+
const carrier = (
|
|
1063
|
+
smid != null
|
|
1064
|
+
? [...(this._smidMeshes.get(smid) ?? [])].find((m) => m !== obj && m.parent)
|
|
1065
|
+
: this._selectedMeshes.find((m) => m !== obj && m.parent && (m.parent as any).visible) ??
|
|
1066
|
+
this._selectedMeshes.find((m) => m !== obj && m.parent)
|
|
1067
|
+
) as THREE.Mesh | undefined;
|
|
1068
|
+
if (carrier) {
|
|
1069
|
+
// 选中身份交给承载同一 smid 的另一块 mesh(通常是接替显示的子瓦)
|
|
1070
|
+
this._selection = { ...currentSel, mesh: carrier };
|
|
1071
|
+
this._replaySelectionOnto(carrier);
|
|
1072
|
+
} else {
|
|
1073
|
+
this.select(null);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
this._unindexMeshSmids(obj);
|
|
1077
|
+
this._removeMeshHeight(obj);
|
|
1078
|
+
this._featureStyleManager.disposeMesh(obj);
|
|
1079
|
+
const styled = styledMaterialOf(obj);
|
|
1080
|
+
styled?.dispose?.();
|
|
929
1081
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1082
|
+
// 深度释放材质(点云、普通材质、材质数组)
|
|
1083
|
+
if (isOwnedPointCloudMaterial(obj.material)) {
|
|
1084
|
+
(obj.material as THREE.Material).dispose();
|
|
1085
|
+
} else if (obj.material) {
|
|
1086
|
+
if (Array.isArray(obj.material)) {
|
|
1087
|
+
obj.material.forEach((m: any) => m?.dispose?.());
|
|
1088
|
+
} else {
|
|
1089
|
+
obj.material.dispose?.();
|
|
1090
|
+
}
|
|
934
1091
|
}
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
const styled = styledMaterialOf(mesh);
|
|
939
|
-
styled?.dispose?.();
|
|
940
|
-
// 点云材质是本模块按模式建的,不走 styled 那条链
|
|
941
|
-
if (isOwnedPointCloudMaterial(mesh.material)) (mesh.material as THREE.Material).dispose();
|
|
942
|
-
mesh.geometry?.dispose?.();
|
|
943
|
-
}
|
|
1092
|
+
// 释放几何体显存缓冲
|
|
1093
|
+
obj.geometry?.dispose?.();
|
|
1094
|
+
});
|
|
944
1095
|
entry.root.remove(node.group);
|
|
945
1096
|
}
|
|
946
1097
|
|
|
@@ -216,6 +216,10 @@ export interface TileNode {
|
|
|
216
216
|
parentUrl?: string;
|
|
217
217
|
/** 当前帧是否满足细化条件 */
|
|
218
218
|
needRefine?: boolean;
|
|
219
|
+
/** 该瓦片引用的纹理 key 清单,用于引用计数与显存释放 */
|
|
220
|
+
textureKeys?: string[];
|
|
221
|
+
/** 瓦片构建产生的统计信息,用于淘汰时准确扣减 stats */
|
|
222
|
+
stats?: { meshes: number; vertices: number; triangles: number };
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
/** 瓦片构建器可注入,用于测试 / 服务端渲染(此时不需要 three) */
|
|
@@ -228,6 +232,8 @@ export interface S3MTilesetOptions {
|
|
|
228
232
|
configUrl: string;
|
|
229
233
|
maxTiles?: number;
|
|
230
234
|
maxConcurrent?: number;
|
|
235
|
+
/** 原始图像/子图集缓存上限(LRU),默认 128 */
|
|
236
|
+
maxImageCache?: number;
|
|
231
237
|
buildTile?: BuildTileFn | null;
|
|
232
238
|
uniforms?: StyleUniforms | null;
|
|
233
239
|
onTile?: (node: TileNode) => void;
|
|
@@ -266,6 +272,9 @@ export class S3MTileset {
|
|
|
266
272
|
readonly nodes = new Map<string, TileNode>();
|
|
267
273
|
readonly failed = new Set<string>();
|
|
268
274
|
readonly textureCache = new Map<string, Texture>();
|
|
275
|
+
maxImageCache: number;
|
|
276
|
+
private readonly _textureRefs = new Map<string, number>();
|
|
277
|
+
private readonly _tileTextureKeys = new Map<string, string[]>();
|
|
269
278
|
readonly pending = new Set<string>();
|
|
270
279
|
/** 是否请求 `.texblock` 子图集,默认关,见 S3MTilesetOptions.atlasLod */
|
|
271
280
|
atlasLod: boolean;
|
|
@@ -316,7 +325,7 @@ export class S3MTileset {
|
|
|
316
325
|
/** 格式类永久错误只向上报一次,避免整棵瓦片树刷同样的日志 */
|
|
317
326
|
private _permanentReported = false;
|
|
318
327
|
/** 请求量的硬上限(软上限 maxTiles 的超额余量),见构造函数注释 */
|
|
319
|
-
|
|
328
|
+
hardCap = 96;
|
|
320
329
|
/** 本数据集是 S3M 1.x(SuperMapCache,.s3m):子瓦地址规则与 3.x 不同 */
|
|
321
330
|
private isS3MV1 = false;
|
|
322
331
|
private _buildTile: BuildTileFn | null;
|
|
@@ -341,6 +350,7 @@ export class S3MTileset {
|
|
|
341
350
|
// 并发与总量限制:iServer 公开服务上别把人家打爆
|
|
342
351
|
this.maxConcurrent = options.maxConcurrent ?? 4;
|
|
343
352
|
this.maxTiles = options.maxTiles ?? 64;
|
|
353
|
+
this.maxImageCache = options.maxImageCache ?? 128;
|
|
344
354
|
// 硬上限 = 软上限 + 余量。淘汰只在「新瓦片已经落地并接管显示」之后做(make-before-break),
|
|
345
355
|
// 所以中间态允许临时超出 maxTiles;硬上限只用来挡住失控的请求量,不用于挤掉正在显示的内容。
|
|
346
356
|
this.hardCap = this.maxTiles + Math.max(8, Math.ceil(this.maxTiles * 0.5));
|
|
@@ -419,19 +429,91 @@ export class S3MTileset {
|
|
|
419
429
|
* 两轮都按「最久没进有效范围」排,同帧再比距离。
|
|
420
430
|
* 淘汰最细一级时,如果上一级也不在内存里,顺手把上一级排回队列补洞。
|
|
421
431
|
*/
|
|
432
|
+
/** 记录瓦片引用的纹理 key,并递增其引用计数 */
|
|
433
|
+
registerTileTextures(url: string, textureKeys: string[]): void {
|
|
434
|
+
this._tileTextureKeys.set(url, textureKeys);
|
|
435
|
+
const node = this.nodes.get(url);
|
|
436
|
+
if (node) {
|
|
437
|
+
node.textureKeys = textureKeys;
|
|
438
|
+
}
|
|
439
|
+
for (const key of textureKeys) {
|
|
440
|
+
this._textureRefs.set(key, (this._textureRefs.get(key) ?? 0) + 1);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/** 淘汰单块瓦片节点并释放专属纹理 */
|
|
445
|
+
_evictTileNode(victim: TileNode): void {
|
|
446
|
+
this.nodes.delete(victim.url);
|
|
447
|
+
victim.group?.parent?.remove(victim.group);
|
|
448
|
+
this._onTileRemoved(victim);
|
|
449
|
+
this._parentOf.delete(victim.url);
|
|
450
|
+
|
|
451
|
+
this.stats.tiles = Math.max(0, this.stats.tiles - 1);
|
|
452
|
+
if (victim.stats) {
|
|
453
|
+
this.stats.meshes = Math.max(0, this.stats.meshes - (victim.stats.meshes ?? 0));
|
|
454
|
+
this.stats.vertices = Math.max(0, this.stats.vertices - (victim.stats.vertices ?? 0));
|
|
455
|
+
this.stats.triangles = Math.max(0, this.stats.triangles - (victim.stats.triangles ?? 0));
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// 释放此瓦片引用的纹理计数;引用归零时从 GPU / 缓存彻底 dispose
|
|
459
|
+
const keys = victim.textureKeys ?? this._tileTextureKeys.get(victim.url) ?? (victim.group?.userData?.textureKeys as string[] | undefined);
|
|
460
|
+
this._tileTextureKeys.delete(victim.url);
|
|
461
|
+
if (keys?.length) {
|
|
462
|
+
for (const key of keys) {
|
|
463
|
+
const ref = (this._textureRefs.get(key) ?? 1) - 1;
|
|
464
|
+
if (ref <= 0) {
|
|
465
|
+
this._textureRefs.delete(key);
|
|
466
|
+
const tex = this.textureCache.get(key);
|
|
467
|
+
if (tex) {
|
|
468
|
+
tex.dispose?.();
|
|
469
|
+
this.textureCache.delete(key);
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
this._textureRefs.set(key, ref);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** 存入外部图集(带 LRU 容量上限) */
|
|
479
|
+
putExternalAtlas(id: string, img: S3MBlockAtlasImage | null): void {
|
|
480
|
+
if (this.externalAtlasCache.size >= this.maxImageCache && !this.externalAtlasCache.has(id)) {
|
|
481
|
+
const oldestKey = this.externalAtlasCache.keys().next().value;
|
|
482
|
+
if (oldestKey !== undefined) this.externalAtlasCache.delete(oldestKey);
|
|
483
|
+
}
|
|
484
|
+
Map.prototype.set.call(this.externalAtlasCache, id, img);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** 读取外部图集 */
|
|
488
|
+
getExternalAtlas(id: string): S3MBlockAtlasImage | null | undefined {
|
|
489
|
+
return this.externalAtlasCache.get(id);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** 存入 leaf 记录段(带 LRU 容量上限) */
|
|
493
|
+
putLeafRecords(name: string, recs: S3MBlockAtlasImage[] | null): void {
|
|
494
|
+
if (this.leafRequestCache.size >= this.maxImageCache && !this.leafRequestCache.has(name)) {
|
|
495
|
+
const oldestKey = this.leafRequestCache.keys().next().value;
|
|
496
|
+
if (oldestKey !== undefined) this.leafRequestCache.delete(oldestKey);
|
|
497
|
+
}
|
|
498
|
+
Map.prototype.set.call(this.leafRequestCache, name, recs);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/** 读取 leaf 记录段 */
|
|
502
|
+
getLeafRecords(name: string): S3MBlockAtlasImage[] | null | undefined {
|
|
503
|
+
return this.leafRequestCache.get(name);
|
|
504
|
+
}
|
|
505
|
+
|
|
422
506
|
private _evictOne(allowUncovered = false): boolean {
|
|
507
|
+
|
|
423
508
|
// 只挑安全候选:叶子 → 已被子瓦接手的父瓦。allowUncovered 仅在超出硬上限时开启(内存兜底)
|
|
424
|
-
const victim = this._pickVictim(
|
|
509
|
+
const victim = this._pickVictim(true, allowUncovered) ?? this._pickVictim(false, allowUncovered);
|
|
425
510
|
if (!victim) return false;
|
|
426
511
|
|
|
427
512
|
const parentUrl = victim.parentUrl ?? this._parentOf.get(victim.url);
|
|
428
513
|
const childUrls = victim.childUrls ?? (victim.childUrl ? [victim.childUrl] : []);
|
|
429
514
|
const isLeaf = !childUrls.some((u) => u !== victim.url && this.nodes.has(u));
|
|
430
515
|
|
|
431
|
-
this.
|
|
432
|
-
victim.group.parent?.remove(victim.group);
|
|
433
|
-
this._onTileRemoved(victim);
|
|
434
|
-
this._parentOf.delete(victim.url);
|
|
516
|
+
this._evictTileNode(victim);
|
|
435
517
|
|
|
436
518
|
// 淘汰的是中间层时,把已加载的子孙改挂到它的父级上:
|
|
437
519
|
// 不然它们瞬间"无父",父级那边还留着指向 victim 的 childUrls,
|
|
@@ -459,8 +541,8 @@ export class S3MTileset {
|
|
|
459
541
|
}
|
|
460
542
|
|
|
461
543
|
// 删的是最细一级、而上一级又不在内存里 → 把上一级排回去,别让那块地空着
|
|
462
|
-
if (isLeaf && parentUrl && !this.nodes.has(parentUrl) && !this.pending.has(parentUrl)) {
|
|
463
|
-
|
|
544
|
+
if (false as boolean && isLeaf && parentUrl && !this.nodes.has(parentUrl) && !this.pending.has(parentUrl)) {
|
|
545
|
+
// 淘汰阶段不再重新入队父瓦,防止预算满载时引发父子死循环震荡(thrashing loop)
|
|
464
546
|
}
|
|
465
547
|
return true;
|
|
466
548
|
}
|
|
@@ -773,7 +855,7 @@ export class S3MTileset {
|
|
|
773
855
|
break;
|
|
774
856
|
}
|
|
775
857
|
}
|
|
776
|
-
this.
|
|
858
|
+
this.putExternalAtlas(id, candidate);
|
|
777
859
|
}
|
|
778
860
|
if (candidate) {
|
|
779
861
|
if (accept(candidate)) apply(candidate);
|
|
@@ -818,10 +900,10 @@ export class S3MTileset {
|
|
|
818
900
|
if (segments.length !== names.length) {
|
|
819
901
|
throw new Error(`pack 段数 ${segments.length} ≠ 请求名 ${names.length}`);
|
|
820
902
|
}
|
|
821
|
-
names.forEach((n, i) => this.
|
|
903
|
+
names.forEach((n, i) => this.putLeafRecords(n, segments[i]!));
|
|
822
904
|
} catch (e) {
|
|
823
905
|
console.warn('[s3m] leaf pack 请求失败:', u, e);
|
|
824
|
-
for (const n of names) if (!this.leafRequestCache.has(n)) this.
|
|
906
|
+
for (const n of names) if (!this.leafRequestCache.has(n)) this.putLeafRecords(n, null);
|
|
825
907
|
}
|
|
826
908
|
}
|
|
827
909
|
|
|
@@ -833,10 +915,10 @@ export class S3MTileset {
|
|
|
833
915
|
const res = await fetch(u);
|
|
834
916
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
835
917
|
const records = (await parseTexblockSegments(await res.arrayBuffer())).flat();
|
|
836
|
-
this.
|
|
918
|
+
this.putLeafRecords(stem, records);
|
|
837
919
|
} catch (e) {
|
|
838
920
|
console.warn('[s3m] leaf texblock 请求失败:', u, e);
|
|
839
|
-
if (!this.leafRequestCache.has(stem)) this.
|
|
921
|
+
if (!this.leafRequestCache.has(stem)) this.putLeafRecords(stem, null);
|
|
840
922
|
}
|
|
841
923
|
}
|
|
842
924
|
|
|
@@ -898,7 +980,10 @@ export class S3MTileset {
|
|
|
898
980
|
}
|
|
899
981
|
|
|
900
982
|
const build = await this._builder();
|
|
901
|
-
const
|
|
983
|
+
const buildRes = await build(tile, { textureCache: this.textureCache, uniforms: this._uniforms });
|
|
984
|
+
const { group, stats } = buildRes;
|
|
985
|
+
const texKeys = (buildRes as any).textureKeys ?? (group.userData?.textureKeys as string[] | undefined) ?? Object.keys(tile.textures ?? {});
|
|
986
|
+
this.registerTileTextures(url, texKeys);
|
|
902
987
|
const lod = this._pickLod(tile);
|
|
903
988
|
|
|
904
989
|
const childUrls: string[] = [];
|
|
@@ -932,6 +1017,11 @@ export class S3MTileset {
|
|
|
932
1017
|
childUrls,
|
|
933
1018
|
parentUrl: this._parentOf.get(url),
|
|
934
1019
|
state: 'ready',
|
|
1020
|
+
stats: {
|
|
1021
|
+
meshes: stats.meshes ?? 0,
|
|
1022
|
+
vertices: stats.vertices ?? 0,
|
|
1023
|
+
triangles: stats.triangles ?? 0,
|
|
1024
|
+
},
|
|
935
1025
|
};
|
|
936
1026
|
|
|
937
1027
|
this.nodes.set(url, node);
|