s3m-three 0.3.0 → 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.
Files changed (110) hide show
  1. package/README.md +1 -1
  2. package/dist/browser/s3m-three.iife.ext-three.js +163 -15
  3. package/dist/browser/s3m-three.iife.js +190 -42
  4. package/dist/core/AtlasAlpha.d.ts +31 -0
  5. package/dist/core/AtlasAlpha.d.ts.map +1 -0
  6. package/dist/core/AtlasAlpha.js +82 -0
  7. package/dist/core/AtlasAlpha.js.map +1 -0
  8. package/dist/core/AtlasPack.d.ts +57 -0
  9. package/dist/core/AtlasPack.d.ts.map +1 -0
  10. package/dist/core/AtlasPack.js +81 -0
  11. package/dist/core/AtlasPack.js.map +1 -0
  12. package/dist/core/AtlasUpgrade.d.ts +68 -0
  13. package/dist/core/AtlasUpgrade.d.ts.map +1 -0
  14. package/dist/core/AtlasUpgrade.js +154 -0
  15. package/dist/core/AtlasUpgrade.js.map +1 -0
  16. package/dist/core/DXTTextureDecode.d.ts.map +1 -1
  17. package/dist/core/DXTTextureDecode.js +28 -6
  18. package/dist/core/DXTTextureDecode.js.map +1 -1
  19. package/dist/core/Draco.d.ts +52 -0
  20. package/dist/core/Draco.d.ts.map +1 -0
  21. package/dist/core/Draco.js +323 -0
  22. package/dist/core/Draco.js.map +1 -0
  23. package/dist/core/MeshOpt.d.ts +14 -3
  24. package/dist/core/MeshOpt.d.ts.map +1 -1
  25. package/dist/core/MeshOpt.js +101 -18
  26. package/dist/core/MeshOpt.js.map +1 -1
  27. package/dist/core/S3M1Parser.d.ts +16 -0
  28. package/dist/core/S3M1Parser.d.ts.map +1 -0
  29. package/dist/core/S3M1Parser.js +367 -0
  30. package/dist/core/S3M1Parser.js.map +1 -0
  31. package/dist/core/S3MBParser.d.ts +43 -1
  32. package/dist/core/S3MBParser.d.ts.map +1 -1
  33. package/dist/core/S3MBParser.js +101 -23
  34. package/dist/core/S3MBParser.js.map +1 -1
  35. package/dist/core/S3MBlockParser.d.ts +209 -0
  36. package/dist/core/S3MBlockParser.d.ts.map +1 -0
  37. package/dist/core/S3MBlockParser.js +789 -0
  38. package/dist/core/S3MBlockParser.js.map +1 -0
  39. package/dist/core/TextureFormat.d.ts +0 -1
  40. package/dist/core/TextureFormat.d.ts.map +1 -1
  41. package/dist/core/TextureFormat.js +19 -2
  42. package/dist/core/TextureFormat.js.map +1 -1
  43. package/dist/core/types.d.ts +65 -0
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/index.d.ts +3 -2
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +2 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/render/DatasetManager.d.ts +11 -0
  50. package/dist/render/DatasetManager.d.ts.map +1 -1
  51. package/dist/render/DatasetManager.js +68 -3
  52. package/dist/render/DatasetManager.js.map +1 -1
  53. package/dist/render/GeometryBuilder.d.ts +3 -0
  54. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  55. package/dist/render/GeometryBuilder.js +189 -17
  56. package/dist/render/GeometryBuilder.js.map +1 -1
  57. package/dist/render/GroundManager.d.ts +57 -1
  58. package/dist/render/GroundManager.d.ts.map +1 -1
  59. package/dist/render/GroundManager.js +149 -21
  60. package/dist/render/GroundManager.js.map +1 -1
  61. package/dist/render/Picker.d.ts.map +1 -1
  62. package/dist/render/Picker.js +12 -4
  63. package/dist/render/Picker.js.map +1 -1
  64. package/dist/render/PointCloudMaterial.d.ts +36 -0
  65. package/dist/render/PointCloudMaterial.d.ts.map +1 -0
  66. package/dist/render/PointCloudMaterial.js +159 -0
  67. package/dist/render/PointCloudMaterial.js.map +1 -0
  68. package/dist/render/S3MViewer.d.ts +75 -3
  69. package/dist/render/S3MViewer.d.ts.map +1 -1
  70. package/dist/render/S3MViewer.js +284 -54
  71. package/dist/render/S3MViewer.js.map +1 -1
  72. package/dist/render/StyleMaterial.d.ts +6 -0
  73. package/dist/render/StyleMaterial.d.ts.map +1 -1
  74. package/dist/render/StyleMaterial.js +125 -2
  75. package/dist/render/StyleMaterial.js.map +1 -1
  76. package/dist/render/TextureFactory.d.ts.map +1 -1
  77. package/dist/render/TextureFactory.js +64 -10
  78. package/dist/render/TextureFactory.js.map +1 -1
  79. package/dist/tileset/S3MTileset.d.ts +177 -1
  80. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  81. package/dist/tileset/S3MTileset.js +754 -60
  82. package/dist/tileset/S3MTileset.js.map +1 -1
  83. package/dist/vendor/crunch_decoder.module.js +12 -18
  84. package/dist/vendor/draco_decoder.module.js +3178 -0
  85. package/docs/API.md +150 -11
  86. package/package.json +1 -1
  87. package/src/core/AtlasAlpha.ts +82 -0
  88. package/src/core/AtlasPack.ts +127 -0
  89. package/src/core/AtlasUpgrade.ts +182 -0
  90. package/src/core/DXTTextureDecode.ts +28 -6
  91. package/src/core/Draco.ts +409 -0
  92. package/src/core/MeshOpt.ts +124 -18
  93. package/src/core/S3M1Parser.ts +430 -0
  94. package/src/core/S3MBParser.ts +112 -21
  95. package/src/core/S3MBlockParser.ts +953 -0
  96. package/src/core/TextureFormat.ts +20 -2
  97. package/src/core/types.ts +63 -0
  98. package/src/index.ts +3 -1
  99. package/src/render/DatasetManager.ts +70 -5
  100. package/src/render/GeometryBuilder.ts +212 -16
  101. package/src/render/GroundManager.ts +174 -20
  102. package/src/render/Picker.ts +13 -4
  103. package/src/render/PointCloudMaterial.ts +174 -0
  104. package/src/render/S3MViewer.ts +322 -55
  105. package/src/render/StyleMaterial.ts +135 -2
  106. package/src/render/TextureFactory.ts +67 -11
  107. package/src/tileset/S3MTileset.ts +798 -65
  108. package/src/vendor/crunch_decoder.module.js +12 -18
  109. package/src/vendor/draco_decoder.module.d.ts +128 -0
  110. package/src/vendor/draco_decoder.module.js +3178 -0
@@ -21,7 +21,7 @@
21
21
  * MapLibre / Mapbox 的胶水层在 s3m-maplibre 包的 S3MLayer.ts。
22
22
  */
23
23
  import * as THREE from 'three';
24
- import { S3MTileset } from '../tileset/S3MTileset.js';
24
+ import { S3MTileset, usableGeoBounds } from '../tileset/S3MTileset.js';
25
25
  import type { BuildTileFn, TileNode } from '../tileset/S3MTileset.js';
26
26
  import { setMaxAnisotropy } from './TextureFactory.js';
27
27
  import {
@@ -33,6 +33,7 @@ import {
33
33
  createOutlineMaterial,
34
34
  } from './StyleMaterial.js';
35
35
  import type { OutlineMaterial, StyledMaterial, StyleMode, SelectMode, S3MStyle } from './StyleMaterial.js';
36
+ import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
36
37
  import { Picker } from './Picker.js';
37
38
  import {
38
39
  buildFeatureEdgeSegments,
@@ -143,7 +144,42 @@ export interface PickResult extends FeatureAttributes {
143
144
  mesh: THREE.Mesh;
144
145
  }
145
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
+
146
178
  export interface S3MViewerOptions {
179
+ /** 光照参数配置(强度、颜色、朝向模式、太阳方位角/仰角等) */
180
+ light?: LightOptions;
181
+ /** 全局总驻留瓦片上限(多数据集多图层共享预算),在所有可见数据集之间动态均衡分配,彻底防止爆显存 */
182
+ totalMaxTiles?: number;
147
183
  /** 单数据集简写 */
148
184
  configUrl?: string;
149
185
  /** 多数据集 */
@@ -191,6 +227,8 @@ interface MeshUserData {
191
227
  geoRef?: GeoPackageEntry;
192
228
  tileRef?: ParsedTile;
193
229
  primitiveType?: number;
230
+ /** 点云对象(THREE.Points):不吃面着色,也没有棱可描 */
231
+ isPointCloud?: boolean;
194
232
  /** CPU 端现算的棱线对象(等宽线 Mesh) */
195
233
  edges?: THREE.Object3D | null;
196
234
  /** 数据自带棱线包(此时不再用 EdgesGeometry 现算) */
@@ -211,6 +249,11 @@ function styledMaterialOf(mesh: THREE.Mesh): StyledMaterial | null {
211
249
  return styled?.userData?.isStyled ? styled : null;
212
250
  }
213
251
 
252
+ /** 点云是 THREE.Points 不是 Mesh,样式 / 拾取 / 卸载链路都靠这个判定放行 */
253
+ function isPointCloudObject(o: THREE.Object3D): o is THREE.Points {
254
+ return (o as unknown as THREE.Points).isPoints === true;
255
+ }
256
+
214
257
  /* ------------------------------------------------------------------ 图层 */
215
258
 
216
259
  export class S3MViewer {
@@ -272,7 +315,7 @@ export class S3MViewer {
272
315
  private _heightMax = -Infinity;
273
316
  private readonly _boxScratch = new THREE.Box3();
274
317
  private _selection: PickResult | null = null;
275
- private _selectedMeshes: THREE.Mesh[] = [];
318
+ private _selectedMeshes: THREE.Object3D[] = [];
276
319
  private _outlineMesh: THREE.Mesh | null = null;
277
320
  private _outlineMeshes: THREE.Mesh[] = [];
278
321
  private _selectionEdges: THREE.Mesh | null = null;
@@ -286,8 +329,24 @@ export class S3MViewer {
286
329
  private _needsRepaint = true;
287
330
  private _frame = 0;
288
331
  private readonly _fps = { value: 0, last: 0 };
289
- private _keyLight: THREE.DirectionalLight | null = null;
290
- private _fillLight: THREE.DirectionalLight | null = null;
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
+ };
291
350
 
292
351
  private readonly _datasetQueue: S3MDatasetOptions[];
293
352
 
@@ -301,10 +360,20 @@ export class S3MViewer {
301
360
  this._onRequestRepaint();
302
361
  }
303
362
 
363
+ triggerRepaint(): void {
364
+ this._requestRepaint();
365
+ }
366
+
304
367
  constructor(options: S3MViewerOptions = {}) {
305
368
  this.options = options;
306
369
  this.pickable = options.pickable !== false;
307
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
+ }
308
377
  this._onRequestRepaint = options.onRequestRepaint ?? (() => {});
309
378
  this._getBearing = options.getBearing;
310
379
  this.onStats = options.onStats ?? (() => {});
@@ -313,6 +382,11 @@ export class S3MViewer {
313
382
  this.onError = options.onError ?? ((e) => console.warn('[s3m]', e));
314
383
 
315
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
+ }
316
390
  this._featureStyleManager = new FeatureStyleManager(
317
391
  () => this.style.floorHeight,
318
392
  () => this._requestRepaint(),
@@ -404,24 +478,70 @@ export class S3MViewer {
404
478
  this.canvas = null;
405
479
  }
406
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
+
407
493
  private _addLights(): void {
408
494
  if (!this.scene) return;
409
- // 环境光与天光打底,保证背光面和折叠处不死黑
410
- this.scene.add(new THREE.AmbientLight(0xffffff, 0.4));
411
- this.scene.add(new THREE.HemisphereLight(0xffffff, 0xb6c0cb, 0.8));
412
- // 主光 / 补光的方向每帧按地图朝向重算(见 _updateLights),
413
- // 这样旋转视角时白模始终是亮的,不会转到背光面就一片死灰。
414
- this._keyLight = new THREE.DirectionalLight(0xffffff, 1.0);
415
- this._fillLight = new THREE.DirectionalLight(0xffffff, 0.35);
416
- this.scene.add(this._keyLight, this._fillLight);
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
+ }
517
+ }
518
+
519
+ /**
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 };
417
535
  }
418
536
 
419
537
  /**
420
- * 让光照跟着地图朝向走:主光在镜头后上方偏左,补光在后上方偏右。
421
- * 相当于一盏头灯,看到的立面永远是受光面。
538
+ * 更新光照位置与方向:
539
+ * - 'headlamp': 随地图方位角 bearing 旋转,主光始终在镜头上方,受光面面向镜头;
540
+ * - 'fixed': 固定世界方位角(如模拟恒定日光)。
422
541
  */
423
542
  private _updateLights(): void {
424
- const bearing = ((this._getBearing?.() ?? 0) * Math.PI) / 180;
543
+ if (!this._keyLight || !this._fillLight) return;
544
+ const bearing = this._lightOptions.mode === 'fixed' ? 0 : (((this._getBearing?.() ?? 0) * Math.PI) / 180);
425
545
  const place = (light: THREE.DirectionalLight | null, azOffsetDeg: number, elevDeg: number): void => {
426
546
  if (!light) return;
427
547
  const az = bearing + (azOffsetDeg * Math.PI) / 180;
@@ -429,8 +549,8 @@ export class S3MViewer {
429
549
  const c = Math.cos(el);
430
550
  light.position.set(Math.sin(az) * c, Math.cos(az) * c, Math.sin(el)).multiplyScalar(1000);
431
551
  };
432
- place(this._keyLight, 150, 50);
433
- place(this._fillLight, 215, 30);
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));
434
554
  }
435
555
 
436
556
  /* ---------------------------------------------------------------- 数据集 */
@@ -540,8 +660,11 @@ export class S3MViewer {
540
660
  /**
541
661
  * 物理地表网格的高度跟随「主数据集的应用层抬升量」,
542
662
  * 保证贴地后地表覆盖 / 地下透视的物理关系与被抬升的模型一致。
663
+ * `ground.trackHeight = false` 时不跟随:地表钉在当前高度,
664
+ * 高程调整只动模型不动切割面。
543
665
  */
544
666
  private _syncGroundHeight(): void {
667
+ if (!this.groundManager.trackHeight) return;
545
668
  const primaryId = this.datasets.keys().next().value;
546
669
  if (!primaryId) return;
547
670
  const userOffset = this._datasetManager.getDatasetHeight(primaryId);
@@ -704,6 +827,15 @@ export class S3MViewer {
704
827
  this._requestRepaint();
705
828
  }
706
829
 
830
+ /**
831
+ * 开关「物理地表作为深度遮挡源」:开启后地表以下的几何被真正切掉。
832
+ * 不依赖宿主是否开启 MapLibre terrain——深度缓冲是共用的,谁更近谁遮挡。
833
+ */
834
+ setGroundOcclude(enabled: boolean): void {
835
+ this.groundManager.setOcclude(enabled);
836
+ this._requestRepaint();
837
+ }
838
+
707
839
  /* ---------------------------------------------------------------- 状态纹理同步 */
708
840
 
709
841
  private _isFeatureHidden(smid: number | null, _batchId: number): boolean {
@@ -773,6 +905,7 @@ export class S3MViewer {
773
905
  mesh.geometry?.dispose?.();
774
906
  const styled = styledMaterialOf(mesh);
775
907
  if (styled) styled.dispose();
908
+ if (isOwnedPointCloudMaterial(mesh.material)) (mesh.material as THREE.Material).dispose();
776
909
  this._featureStyleManager.disposeMesh(mesh);
777
910
  });
778
911
  });
@@ -808,8 +941,10 @@ export class S3MViewer {
808
941
  continue;
809
942
  }
810
943
 
944
+ // 点云是 THREE.Points 不是 Mesh,但同样要进拾取表、吃不透明度、跟着着色模式走
945
+ const isPoints = isPointCloudObject(child);
811
946
  const mesh = child as THREE.Mesh;
812
- if (!mesh.isMesh) continue;
947
+ if (!mesh.isMesh && !isPoints) continue;
813
948
  const ud = mesh.userData as MeshUserData;
814
949
  ud.pickId = this._allocPickId();
815
950
  ud.datasetId = entry.id;
@@ -822,41 +957,141 @@ export class S3MViewer {
822
957
  });
823
958
  this._indexMeshSmids(mesh);
824
959
  this._addMeshHeight(mesh);
825
- if (this._edgesEnabled && !hasNativeEdges) this._ensureEdges(mesh);
960
+ // 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义
961
+ if (this._edgesEnabled && !hasNativeEdges && !isPoints) this._ensureEdges(mesh);
826
962
 
827
963
  // 新瓦片要继承当前的不透明度与着色方式
828
- const mat = styledMaterialOf(mesh);
829
- if (mat) {
830
- const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
831
- mat.opacity = o;
832
- mat.transparent = o < 0.999;
833
- mat.depthWrite = o >= 0.999;
834
- mat.flatShading = this.style.shading !== 'smooth';
835
- mat.needsUpdate = true;
836
- this._featureStyleManager.applyOverridesToMesh(mesh);
964
+ if (isPoints) {
965
+ this._applyPointCloudStyle(child as THREE.Points);
966
+ } else {
967
+ const mat = styledMaterialOf(mesh);
968
+ if (mat) {
969
+ const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
970
+ mat.opacity = o;
971
+ mat.transparent = o < 0.999;
972
+ mat.depthWrite = o >= 0.999;
973
+ mat.flatShading = this.style.shading !== 'smooth';
974
+ mat.needsUpdate = true;
975
+ this._featureStyleManager.applyOverridesToMesh(mesh);
976
+ }
837
977
  }
978
+
979
+ // 选中状态也要跟着继承:LOD 细化 / 瓦片重载会换掉 mesh,
980
+ // 不重放就出现「点中的那栋楼一拉近就没高亮了」
981
+ this._replaySelectionOnto(mesh);
838
982
  }
839
983
  }
840
984
 
841
- /** 瓦片被淘汰:从拾取表和场景里摘掉,并释放它自己的 GPU 资源 */
842
- private _unregisterTile(node: TileNode, entry: S3MDatasetEntry): void {
843
- for (const child of [...node.group.children]) {
844
- const mesh = child as THREE.Mesh;
845
- if (!mesh.isMesh) continue;
846
- const ud = mesh.userData as MeshUserData;
847
- if (this._selection?.mesh === mesh || this._selectedMeshes.includes(mesh)) this.select(null);
848
- if (ud.pickId != null) {
849
- this._pickables.delete(ud.pickId);
850
- this._releasePickId(ud.pickId);
851
- ud.pickId = undefined;
985
+ /**
986
+ * 把「当前选中」重放到一块新登记的 mesh 上。
987
+ *
988
+ * 为什么需要:选中高亮是直接写在各 mesh 材质的 uSelBatch / uSelWhole 上的,
989
+ * 而 LOD 细化、瓦片重载、淘汰回补都会换掉 mesh —— 不重放的话,用户点中的那栋楼
990
+ * 一细化就失去高亮(状态还在 _selection.smid 里,只是没人往新 mesh 上刷)。
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
+
1013
+ private _replaySelectionOnto(mesh: THREE.Mesh): void {
1014
+ const sel = this._selection;
1015
+ if (!sel) return;
1016
+ const mat = styledMaterialOf(mesh);
1017
+ if (!mat) return;
1018
+
1019
+ const ud = mesh.userData as MeshUserData;
1020
+ let batchId = -1;
1021
+ if (sel.smid != null) {
1022
+ const pickInfo = ud.geoRef?.pickInfo as Record<string | number, unknown> | undefined;
1023
+ const entry = pickInfo?.[sel.smid] ?? pickInfo?.[String(sel.smid)];
1024
+ batchId = entry ? pickEntryId(entry as any) : -1;
1025
+ }
1026
+ // 拾取命中的那块 mesh 用原始 batchId;同一 smid 散在别的几何里时按各自的区间算
1027
+ if (mesh === sel.mesh) batchId = sel.batchId;
1028
+ // 这块几何里根本没有这个单体 → 不动它(否则会把无关瓦片一起点亮)
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;
852
1034
  }
853
- this._unindexMeshSmids(mesh);
854
- this._removeMeshHeight(mesh);
855
- this._featureStyleManager.disposeMesh(mesh);
856
- const styled = styledMaterialOf(mesh);
857
- styled?.dispose?.();
858
- mesh.geometry?.dispose?.();
859
1035
  }
1036
+
1037
+ mat.userData.uSelBatch.value = batchId;
1038
+ mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
1039
+ if (!this._selectedMeshes.includes(mesh)) this._selectedMeshes.push(mesh);
1040
+ if (isPointCloudObject(mesh)) this._applyPointCloudStyle(mesh, true);
1041
+ this.uniforms.uSelMix.value = 1;
1042
+ this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
1043
+ }
1044
+
1045
+ /** 瓦片被淘汰:从拾取表和场景里摘掉,并深度彻底释放所有几何体与材质显存 */
1046
+ private _unregisterTile(node: TileNode, entry: S3MDatasetEntry): void {
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;
1053
+ }
1054
+ if (obj.isMesh) {
1055
+ const wasSelected = this._selectedMeshes.includes(obj);
1056
+ if (wasSelected) {
1057
+ this._selectedMeshes = this._selectedMeshes.filter((m) => m !== obj);
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?.();
1081
+ }
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
+ }
1091
+ }
1092
+ // 释放几何体显存缓冲
1093
+ obj.geometry?.dispose?.();
1094
+ });
860
1095
  entry.root.remove(node.group);
861
1096
  }
862
1097
 
@@ -890,6 +1125,8 @@ export class S3MViewer {
890
1125
  this.style = { ...this.style, ...patch };
891
1126
  this._applyStyleUniforms();
892
1127
  if ('opacity' in patch || 'depthWrite' in patch) this._applyOpacity();
1128
+ // 点云不走 StyleMaterial 的 uMode,模式切换要单独同步它的材质
1129
+ if ('mode' in patch) this._applyPointCloudStyles();
893
1130
  if ('shading' in patch) this._applyShading();
894
1131
  if ('edges' in patch) this.setEdges(!!this.style.edges);
895
1132
  else if ('edgeColor' in patch || 'edgeWidth' in patch || 'opacity' in patch || 'edgeOpacity' in patch) {
@@ -959,6 +1196,10 @@ export class S3MViewer {
959
1196
  // 关键:若 depthWrite 开启(默认开启),即使半透明也保持深度写入,完美遮挡背面棱线,防止背面线条透出
960
1197
  const keepDepth = this.style.depthWrite !== false;
961
1198
  for (const p of this._pickables.values()) {
1199
+ if (isPointCloudObject(p.mesh)) {
1200
+ this._applyPointCloudStyle(p.mesh);
1201
+ continue;
1202
+ }
962
1203
  const m = styledMaterialOf(p.mesh);
963
1204
  if (!m) continue;
964
1205
  m.opacity = o;
@@ -968,6 +1209,19 @@ export class S3MViewer {
968
1209
  this._requestRepaint();
969
1210
  }
970
1211
 
1212
+ /** 单个点云对象同步当前样式(着色模式 + 不透明度 + 选中高亮) */
1213
+ private _applyPointCloudStyle(points: THREE.Points, selected = this._selectedMeshes.includes(points)): void {
1214
+ applyPointCloudStyle(points, this.style, this.uniforms, selected);
1215
+ }
1216
+
1217
+ /** 全部点云刷新:着色模式切换时用(uniform 是共享引用,改色不用走这里) */
1218
+ private _applyPointCloudStyles(): void {
1219
+ for (const p of this._pickables.values()) {
1220
+ if (isPointCloudObject(p.mesh)) this._applyPointCloudStyle(p.mesh);
1221
+ }
1222
+ this._requestRepaint();
1223
+ }
1224
+
971
1225
  /** 平面/平滑着色:FLAT_SHADED 是编译期宏,改它必须重编译 */
972
1226
  private _applyShading(): void {
973
1227
  const flat = this.style.shading !== 'smooth';
@@ -1104,7 +1358,8 @@ export class S3MViewer {
1104
1358
  .map((p) => p.mesh)
1105
1359
  .filter((m) => {
1106
1360
  const ud = m.userData as MeshUserData;
1107
- return !ud.edges && !ud.nativeEdges;
1361
+ // 点云没有棱可描
1362
+ return !ud.edges && !ud.nativeEdges && !isPointCloudObject(m);
1108
1363
  });
1109
1364
  let i = 0;
1110
1365
  const step = (): void => {
@@ -1614,8 +1869,13 @@ export class S3MViewer {
1614
1869
  select(result: PickResult | null, { silent = false }: { silent?: boolean } = {}): void {
1615
1870
  // 清掉上一次
1616
1871
  if (this._selection) {
1617
- for (const mesh of this._selectedMeshes) {
1618
- const mat = styledMaterialOf(mesh);
1872
+ for (const obj of this._selectedMeshes) {
1873
+ // 点云:恢复成当前着色模式(去掉选中混色)
1874
+ if (isPointCloudObject(obj)) {
1875
+ this._applyPointCloudStyle(obj, false);
1876
+ continue;
1877
+ }
1878
+ const mat = styledMaterialOf(obj as THREE.Mesh);
1619
1879
  if (mat) {
1620
1880
  mat.userData.uSelBatch.value = -1;
1621
1881
  mat.userData.uSelWhole.value = 0;
@@ -1666,14 +1926,22 @@ export class S3MViewer {
1666
1926
  mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
1667
1927
  this._selectedMeshes.push(mesh);
1668
1928
  }
1929
+ // 点云没有单体粒度:整块混成 selectColor,让选中至少看得见
1930
+ for (const mesh of this._selectedMeshes) {
1931
+ if (isPointCloudObject(mesh)) this._applyPointCloudStyle(mesh, true);
1932
+ }
1669
1933
  this.uniforms.uSelMix.value = 1;
1670
1934
  this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
1671
1935
 
1672
1936
  if (this.style.outline) {
1673
- if ((this.style.outlineStyle ?? 'edges') === 'edges') {
1674
- this._addSelectionEdges(result, targetMeshes);
1675
- } else {
1676
- this._addSelectionHull(result, targetMeshes);
1937
+ // 棱线 / 外扩轮廓都是按「面」设计的,点云画不出来也不该画
1938
+ const decoMeshes = [...targetMeshes].filter((m) => !isPointCloudObject(m));
1939
+ if (decoMeshes.length) {
1940
+ if ((this.style.outlineStyle ?? 'edges') === 'edges') {
1941
+ this._addSelectionEdges(result, decoMeshes);
1942
+ } else {
1943
+ this._addSelectionHull(result, decoMeshes);
1944
+ }
1677
1945
  }
1678
1946
  }
1679
1947
  this._requestRepaint();
@@ -1973,9 +2241,8 @@ export class S3MViewer {
1973
2241
  let maxLon = -Infinity;
1974
2242
  let maxLat = -Infinity;
1975
2243
  for (const entry of this.datasets.values()) {
1976
- const b = entry.tileset?.config?.geoBounds as
1977
- | { left: number; bottom: number; right: number; top: number }
1978
- | undefined;
2244
+ // 同 DatasetManager.getBounds:退化 / 越界的 geoBounds 不参与,避免视角甩到 (0,0)
2245
+ const b = usableGeoBounds(entry.tileset?.config?.geoBounds);
1979
2246
  if (!b) continue;
1980
2247
  minLon = Math.min(minLon, b.left);
1981
2248
  maxLon = Math.max(maxLon, b.right);