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,8 +21,10 @@
21
21
  * MapLibre / Mapbox 的胶水层在 s3m-maplibre 包的 S3MLayer.ts。
22
22
  */
23
23
  import * as THREE from 'three';
24
+ import { usableGeoBounds } from '../tileset/S3MTileset.js';
24
25
  import { setMaxAnisotropy } from './TextureFactory.js';
25
26
  import { STYLE, STYLE_LABELS, SELECT_MODE, DEFAULT_STYLE, createSharedUniforms, createOutlineMaterial, } from './StyleMaterial.js';
27
+ import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
26
28
  import { Picker } from './Picker.js';
27
29
  import { buildFeatureEdgeSegments, buildThickLineGeometry, createLineSharedUniforms, createThickLineMaterial, } from './EdgeLines.js';
28
30
  import { resolveFeature, measureSegments, buildAttributes, buildInstanceLookup, pickEntryId, } from '../core/FeatureInfo.js';
@@ -38,6 +40,10 @@ function styledMaterialOf(mesh) {
38
40
  const styled = m;
39
41
  return styled?.userData?.isStyled ? styled : null;
40
42
  }
43
+ /** 点云是 THREE.Points 不是 Mesh,样式 / 拾取 / 卸载链路都靠这个判定放行 */
44
+ function isPointCloudObject(o) {
45
+ return o.isPoints === true;
46
+ }
41
47
  /* ------------------------------------------------------------------ 图层 */
42
48
  export class S3MViewer {
43
49
  options;
@@ -103,8 +109,24 @@ export class S3MViewer {
103
109
  _needsRepaint = true;
104
110
  _frame = 0;
105
111
  _fps = { value: 0, last: 0 };
106
- _keyLight = null;
107
- _fillLight = null;
112
+ _ambientLight;
113
+ _hemiLight;
114
+ _keyLight;
115
+ _fillLight;
116
+ _lightOptions = {
117
+ ambientIntensity: 0.4,
118
+ ambientColor: 0xffffff,
119
+ hemisphereIntensity: 0.8,
120
+ hemisphereColor: 0xffffff,
121
+ hemisphereGroundColor: 0xb6c0cb,
122
+ keyLightIntensity: 1.0,
123
+ keyLightColor: 0xffffff,
124
+ fillLightIntensity: 0.35,
125
+ fillLightColor: 0xffffff,
126
+ mode: 'headlamp',
127
+ azimuth: 150,
128
+ altitude: 50,
129
+ };
108
130
  _datasetQueue;
109
131
  /** 宿主重画信号与地图朝向,构造时注入 */
110
132
  _onRequestRepaint = () => { };
@@ -114,10 +136,19 @@ export class S3MViewer {
114
136
  _requestRepaint() {
115
137
  this._onRequestRepaint();
116
138
  }
139
+ triggerRepaint() {
140
+ this._requestRepaint();
141
+ }
117
142
  constructor(options = {}) {
118
143
  this.options = options;
119
144
  this.pickable = options.pickable !== false;
120
145
  this._lodRangeScale = options.lodRangeScale ?? 1.0;
146
+ if (options.light) {
147
+ Object.assign(this._lightOptions, options.light);
148
+ }
149
+ if (options.totalMaxTiles) {
150
+ this._datasetManager.totalMaxTiles = options.totalMaxTiles;
151
+ }
121
152
  this._onRequestRepaint = options.onRequestRepaint ?? (() => { });
122
153
  this._getBearing = options.getBearing;
123
154
  this.onStats = options.onStats ?? (() => { });
@@ -125,6 +156,11 @@ export class S3MViewer {
125
156
  this.onPick = options.onPick ?? (() => { });
126
157
  this.onError = options.onError ?? ((e) => console.warn('[s3m]', e));
127
158
  this.groundManager = new GroundManager(options.ground);
159
+ if (!this.scene) {
160
+ this.scene = new THREE.Scene();
161
+ this.scene.add(this.groundManager.group);
162
+ this._addLights();
163
+ }
128
164
  this._featureStyleManager = new FeatureStyleManager(() => this.style.floorHeight, () => this._requestRepaint(), () => {
129
165
  if (this._selection)
130
166
  this._syncSelectionStyle();
@@ -201,24 +237,64 @@ export class S3MViewer {
201
237
  this.renderer = null;
202
238
  this.canvas = null;
203
239
  }
240
+ _createLights() {
241
+ this._ambientLight = new THREE.AmbientLight(this._lightOptions.ambientColor, this._lightOptions.ambientIntensity);
242
+ this._hemiLight = new THREE.HemisphereLight(this._lightOptions.hemisphereColor, this._lightOptions.hemisphereGroundColor, this._lightOptions.hemisphereIntensity);
243
+ this._keyLight = new THREE.DirectionalLight(this._lightOptions.keyLightColor, this._lightOptions.keyLightIntensity);
244
+ this._fillLight = new THREE.DirectionalLight(this._lightOptions.fillLightColor, this._lightOptions.fillLightIntensity);
245
+ this._updateLights();
246
+ }
204
247
  _addLights() {
205
248
  if (!this.scene)
206
249
  return;
207
- // 环境光与天光打底,保证背光面和折叠处不死黑
208
- this.scene.add(new THREE.AmbientLight(0xffffff, 0.4));
209
- this.scene.add(new THREE.HemisphereLight(0xffffff, 0xb6c0cb, 0.8));
210
- // 主光 / 补光的方向每帧按地图朝向重算(见 _updateLights),
211
- // 这样旋转视角时白模始终是亮的,不会转到背光面就一片死灰。
212
- this._keyLight = new THREE.DirectionalLight(0xffffff, 1.0);
213
- this._fillLight = new THREE.DirectionalLight(0xffffff, 0.35);
214
- this.scene.add(this._keyLight, this._fillLight);
250
+ if (!this._ambientLight)
251
+ this._createLights();
252
+ this.scene.add(this._ambientLight, this._hemiLight, this._keyLight, this._fillLight);
253
+ }
254
+ _applyLightOptions() {
255
+ if (this._ambientLight) {
256
+ this._ambientLight.intensity = this._lightOptions.ambientIntensity;
257
+ this._ambientLight.color.set(this._lightOptions.ambientColor);
258
+ }
259
+ if (this._hemiLight) {
260
+ this._hemiLight.intensity = this._lightOptions.hemisphereIntensity;
261
+ this._hemiLight.color.set(this._lightOptions.hemisphereColor);
262
+ this._hemiLight.groundColor.set(this._lightOptions.hemisphereGroundColor);
263
+ }
264
+ if (this._keyLight) {
265
+ this._keyLight.intensity = this._lightOptions.keyLightIntensity;
266
+ this._keyLight.color.set(this._lightOptions.keyLightColor);
267
+ }
268
+ if (this._fillLight) {
269
+ this._fillLight.intensity = this._lightOptions.fillLightIntensity;
270
+ this._fillLight.color.set(this._lightOptions.fillLightColor);
271
+ }
272
+ }
273
+ /**
274
+ * 动态设置光照配置
275
+ */
276
+ setLight(options) {
277
+ Object.assign(this._lightOptions, options);
278
+ this._applyLightOptions();
279
+ this._updateLights();
280
+ this._needsRepaint = true;
281
+ this.triggerRepaint();
215
282
  }
216
283
  /**
217
- * 让光照跟着地图朝向走:主光在镜头后上方偏左,补光在后上方偏右。
218
- * 相当于一盏头灯,看到的立面永远是受光面。
284
+ * 获取当前光照配置快照
285
+ */
286
+ getLight() {
287
+ return { ...this._lightOptions };
288
+ }
289
+ /**
290
+ * 更新光照位置与方向:
291
+ * - 'headlamp': 随地图方位角 bearing 旋转,主光始终在镜头上方,受光面面向镜头;
292
+ * - 'fixed': 固定世界方位角(如模拟恒定日光)。
219
293
  */
220
294
  _updateLights() {
221
- const bearing = ((this._getBearing?.() ?? 0) * Math.PI) / 180;
295
+ if (!this._keyLight || !this._fillLight)
296
+ return;
297
+ const bearing = this._lightOptions.mode === 'fixed' ? 0 : (((this._getBearing?.() ?? 0) * Math.PI) / 180);
222
298
  const place = (light, azOffsetDeg, elevDeg) => {
223
299
  if (!light)
224
300
  return;
@@ -227,8 +303,8 @@ export class S3MViewer {
227
303
  const c = Math.cos(el);
228
304
  light.position.set(Math.sin(az) * c, Math.cos(az) * c, Math.sin(el)).multiplyScalar(1000);
229
305
  };
230
- place(this._keyLight, 150, 50);
231
- place(this._fillLight, 215, 30);
306
+ place(this._keyLight, this._lightOptions.azimuth, this._lightOptions.altitude);
307
+ place(this._fillLight, this._lightOptions.azimuth + 65, Math.max(10, this._lightOptions.altitude - 20));
232
308
  }
233
309
  /* ---------------------------------------------------------------- 数据集 */
234
310
  _normalizeDatasets(options) {
@@ -323,8 +399,12 @@ export class S3MViewer {
323
399
  /**
324
400
  * 物理地表网格的高度跟随「主数据集的应用层抬升量」,
325
401
  * 保证贴地后地表覆盖 / 地下透视的物理关系与被抬升的模型一致。
402
+ * `ground.trackHeight = false` 时不跟随:地表钉在当前高度,
403
+ * 高程调整只动模型不动切割面。
326
404
  */
327
405
  _syncGroundHeight() {
406
+ if (!this.groundManager.trackHeight)
407
+ return;
328
408
  const primaryId = this.datasets.keys().next().value;
329
409
  if (!primaryId)
330
410
  return;
@@ -455,6 +535,14 @@ export class S3MViewer {
455
535
  this.groundManager.setExcavationBounds(bounds);
456
536
  this._requestRepaint();
457
537
  }
538
+ /**
539
+ * 开关「物理地表作为深度遮挡源」:开启后地表以下的几何被真正切掉。
540
+ * 不依赖宿主是否开启 MapLibre terrain——深度缓冲是共用的,谁更近谁遮挡。
541
+ */
542
+ setGroundOcclude(enabled) {
543
+ this.groundManager.setOcclude(enabled);
544
+ this._requestRepaint();
545
+ }
458
546
  /* ---------------------------------------------------------------- 状态纹理同步 */
459
547
  _isFeatureHidden(smid, _batchId) {
460
548
  if (smid == null)
@@ -529,6 +617,8 @@ export class S3MViewer {
529
617
  const styled = styledMaterialOf(mesh);
530
618
  if (styled)
531
619
  styled.dispose();
620
+ if (isOwnedPointCloudMaterial(mesh.material))
621
+ mesh.material.dispose();
532
622
  this._featureStyleManager.disposeMesh(mesh);
533
623
  });
534
624
  });
@@ -561,8 +651,10 @@ export class S3MViewer {
561
651
  this._styleEdgeLine(child);
562
652
  continue;
563
653
  }
654
+ // 点云是 THREE.Points 不是 Mesh,但同样要进拾取表、吃不透明度、跟着着色模式走
655
+ const isPoints = isPointCloudObject(child);
564
656
  const mesh = child;
565
- if (!mesh.isMesh)
657
+ if (!mesh.isMesh && !isPoints)
566
658
  continue;
567
659
  const ud = mesh.userData;
568
660
  ud.pickId = this._allocPickId();
@@ -576,42 +668,145 @@ export class S3MViewer {
576
668
  });
577
669
  this._indexMeshSmids(mesh);
578
670
  this._addMeshHeight(mesh);
579
- if (this._edgesEnabled && !hasNativeEdges)
671
+ // 点云没有「棱」:给一堆孤立点建 EdgesGeometry 又慢又没意义
672
+ if (this._edgesEnabled && !hasNativeEdges && !isPoints)
580
673
  this._ensureEdges(mesh);
581
674
  // 新瓦片要继承当前的不透明度与着色方式
582
- const mat = styledMaterialOf(mesh);
583
- if (mat) {
584
- const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
585
- mat.opacity = o;
586
- mat.transparent = o < 0.999;
587
- mat.depthWrite = o >= 0.999;
588
- mat.flatShading = this.style.shading !== 'smooth';
589
- mat.needsUpdate = true;
590
- this._featureStyleManager.applyOverridesToMesh(mesh);
675
+ if (isPoints) {
676
+ this._applyPointCloudStyle(child);
677
+ }
678
+ else {
679
+ const mat = styledMaterialOf(mesh);
680
+ if (mat) {
681
+ const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
682
+ mat.opacity = o;
683
+ mat.transparent = o < 0.999;
684
+ mat.depthWrite = o >= 0.999;
685
+ mat.flatShading = this.style.shading !== 'smooth';
686
+ mat.needsUpdate = true;
687
+ this._featureStyleManager.applyOverridesToMesh(mesh);
688
+ }
591
689
  }
690
+ // 选中状态也要跟着继承:LOD 细化 / 瓦片重载会换掉 mesh,
691
+ // 不重放就出现「点中的那栋楼一拉近就没高亮了」
692
+ this._replaySelectionOnto(mesh);
592
693
  }
593
694
  }
594
- /** 瓦片被淘汰:从拾取表和场景里摘掉,并释放它自己的 GPU 资源 */
595
- _unregisterTile(node, entry) {
596
- for (const child of [...node.group.children]) {
597
- const mesh = child;
598
- if (!mesh.isMesh)
599
- continue;
600
- const ud = mesh.userData;
601
- if (this._selection?.mesh === mesh || this._selectedMeshes.includes(mesh))
602
- this.select(null);
603
- if (ud.pickId != null) {
604
- this._pickables.delete(ud.pickId);
605
- this._releasePickId(ud.pickId);
606
- ud.pickId = undefined;
695
+ /**
696
+ * 把「当前选中」重放到一块新登记的 mesh 上。
697
+ *
698
+ * 为什么需要:选中高亮是直接写在各 mesh 材质的 uSelBatch / uSelWhole 上的,
699
+ * 而 LOD 细化、瓦片重载、淘汰回补都会换掉 mesh —— 不重放的话,用户点中的那栋楼
700
+ * 一细化就失去高亮(状态还在 _selection.smid 里,只是没人往新 mesh 上刷)。
701
+ */
702
+ /**
703
+ * 判断一块无 SMID 单体化信息的网格(如倾斜摄影或白模整块)是否空间覆盖当前选中命中点
704
+ */
705
+ _isMeshCoveringSelection(mesh, sel) {
706
+ if (!sel.point)
707
+ return false;
708
+ const geom = mesh.geometry;
709
+ if (!geom)
710
+ return false;
711
+ if (!geom.boundingBox)
712
+ geom.computeBoundingBox();
713
+ if (!geom.boundingBox)
714
+ return false;
715
+ // 将选中的三维场景点转入网格局部坐标系进行包围盒检测
716
+ const pt = new THREE.Vector3(sel.point[0], sel.point[1], sel.point[2]);
717
+ mesh.updateWorldMatrix(true, false);
718
+ const inv = mesh.matrixWorld.clone().invert();
719
+ const localPt = pt.applyMatrix4(inv);
720
+ // 留出 0.5 米容差,避免刚好处于三角网格边缘缝隙时漏判
721
+ const box = geom.boundingBox.clone().expandByScalar(0.5);
722
+ return box.containsPoint(localPt);
723
+ }
724
+ _replaySelectionOnto(mesh) {
725
+ const sel = this._selection;
726
+ if (!sel)
727
+ return;
728
+ const mat = styledMaterialOf(mesh);
729
+ if (!mat)
730
+ return;
731
+ const ud = mesh.userData;
732
+ let batchId = -1;
733
+ if (sel.smid != null) {
734
+ const pickInfo = ud.geoRef?.pickInfo;
735
+ const entry = pickInfo?.[sel.smid] ?? pickInfo?.[String(sel.smid)];
736
+ batchId = entry ? pickEntryId(entry) : -1;
737
+ }
738
+ // 拾取命中的那块 mesh 用原始 batchId;同一 smid 散在别的几何里时按各自的区间算
739
+ if (mesh === sel.mesh)
740
+ batchId = sel.batchId;
741
+ // 这块几何里根本没有这个单体 → 不动它(否则会把无关瓦片一起点亮)
742
+ if (batchId < 0 && mesh !== sel.mesh) {
743
+ if (sel.smid == null && ud.datasetId === sel.datasetId && this._isMeshCoveringSelection(mesh, sel)) {
744
+ batchId = -1;
745
+ }
746
+ else {
747
+ return;
607
748
  }
608
- this._unindexMeshSmids(mesh);
609
- this._removeMeshHeight(mesh);
610
- this._featureStyleManager.disposeMesh(mesh);
611
- const styled = styledMaterialOf(mesh);
612
- styled?.dispose?.();
613
- mesh.geometry?.dispose?.();
614
749
  }
750
+ mat.userData.uSelBatch.value = batchId;
751
+ mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
752
+ if (!this._selectedMeshes.includes(mesh))
753
+ this._selectedMeshes.push(mesh);
754
+ if (isPointCloudObject(mesh))
755
+ this._applyPointCloudStyle(mesh, true);
756
+ this.uniforms.uSelMix.value = 1;
757
+ this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
758
+ }
759
+ /** 瓦片被淘汰:从拾取表和场景里摘掉,并深度彻底释放所有几何体与材质显存 */
760
+ _unregisterTile(node, entry) {
761
+ node.group.traverse((child) => {
762
+ const obj = child;
763
+ if (obj.userData?.pickId != null) {
764
+ this._pickables.delete(obj.userData.pickId);
765
+ this._releasePickId(obj.userData.pickId);
766
+ obj.userData.pickId = undefined;
767
+ }
768
+ if (obj.isMesh) {
769
+ const wasSelected = this._selectedMeshes.includes(obj);
770
+ if (wasSelected) {
771
+ this._selectedMeshes = this._selectedMeshes.filter((m) => m !== obj);
772
+ }
773
+ const currentSel = this._selection;
774
+ if (currentSel && currentSel.mesh === obj) {
775
+ const smid = currentSel.smid;
776
+ const carrier = (smid != null
777
+ ? [...(this._smidMeshes.get(smid) ?? [])].find((m) => m !== obj && m.parent)
778
+ : this._selectedMeshes.find((m) => m !== obj && m.parent && m.parent.visible) ??
779
+ this._selectedMeshes.find((m) => m !== obj && m.parent));
780
+ if (carrier) {
781
+ // 选中身份交给承载同一 smid 的另一块 mesh(通常是接替显示的子瓦)
782
+ this._selection = { ...currentSel, mesh: carrier };
783
+ this._replaySelectionOnto(carrier);
784
+ }
785
+ else {
786
+ this.select(null);
787
+ }
788
+ }
789
+ this._unindexMeshSmids(obj);
790
+ this._removeMeshHeight(obj);
791
+ this._featureStyleManager.disposeMesh(obj);
792
+ const styled = styledMaterialOf(obj);
793
+ styled?.dispose?.();
794
+ }
795
+ // 深度释放材质(点云、普通材质、材质数组)
796
+ if (isOwnedPointCloudMaterial(obj.material)) {
797
+ obj.material.dispose();
798
+ }
799
+ else if (obj.material) {
800
+ if (Array.isArray(obj.material)) {
801
+ obj.material.forEach((m) => m?.dispose?.());
802
+ }
803
+ else {
804
+ obj.material.dispose?.();
805
+ }
806
+ }
807
+ // 释放几何体显存缓冲
808
+ obj.geometry?.dispose?.();
809
+ });
615
810
  entry.root.remove(node.group);
616
811
  }
617
812
  /**
@@ -644,6 +839,9 @@ export class S3MViewer {
644
839
  this._applyStyleUniforms();
645
840
  if ('opacity' in patch || 'depthWrite' in patch)
646
841
  this._applyOpacity();
842
+ // 点云不走 StyleMaterial 的 uMode,模式切换要单独同步它的材质
843
+ if ('mode' in patch)
844
+ this._applyPointCloudStyles();
647
845
  if ('shading' in patch)
648
846
  this._applyShading();
649
847
  if ('edges' in patch)
@@ -718,6 +916,10 @@ export class S3MViewer {
718
916
  // 关键:若 depthWrite 开启(默认开启),即使半透明也保持深度写入,完美遮挡背面棱线,防止背面线条透出
719
917
  const keepDepth = this.style.depthWrite !== false;
720
918
  for (const p of this._pickables.values()) {
919
+ if (isPointCloudObject(p.mesh)) {
920
+ this._applyPointCloudStyle(p.mesh);
921
+ continue;
922
+ }
721
923
  const m = styledMaterialOf(p.mesh);
722
924
  if (!m)
723
925
  continue;
@@ -727,6 +929,18 @@ export class S3MViewer {
727
929
  }
728
930
  this._requestRepaint();
729
931
  }
932
+ /** 单个点云对象同步当前样式(着色模式 + 不透明度 + 选中高亮) */
933
+ _applyPointCloudStyle(points, selected = this._selectedMeshes.includes(points)) {
934
+ applyPointCloudStyle(points, this.style, this.uniforms, selected);
935
+ }
936
+ /** 全部点云刷新:着色模式切换时用(uniform 是共享引用,改色不用走这里) */
937
+ _applyPointCloudStyles() {
938
+ for (const p of this._pickables.values()) {
939
+ if (isPointCloudObject(p.mesh))
940
+ this._applyPointCloudStyle(p.mesh);
941
+ }
942
+ this._requestRepaint();
943
+ }
730
944
  /** 平面/平滑着色:FLAT_SHADED 是编译期宏,改它必须重编译 */
731
945
  _applyShading() {
732
946
  const flat = this.style.shading !== 'smooth';
@@ -871,7 +1085,8 @@ export class S3MViewer {
871
1085
  .map((p) => p.mesh)
872
1086
  .filter((m) => {
873
1087
  const ud = m.userData;
874
- return !ud.edges && !ud.nativeEdges;
1088
+ // 点云没有棱可描
1089
+ return !ud.edges && !ud.nativeEdges && !isPointCloudObject(m);
875
1090
  });
876
1091
  let i = 0;
877
1092
  const step = () => {
@@ -1379,8 +1594,13 @@ export class S3MViewer {
1379
1594
  select(result, { silent = false } = {}) {
1380
1595
  // 清掉上一次
1381
1596
  if (this._selection) {
1382
- for (const mesh of this._selectedMeshes) {
1383
- const mat = styledMaterialOf(mesh);
1597
+ for (const obj of this._selectedMeshes) {
1598
+ // 点云:恢复成当前着色模式(去掉选中混色)
1599
+ if (isPointCloudObject(obj)) {
1600
+ this._applyPointCloudStyle(obj, false);
1601
+ continue;
1602
+ }
1603
+ const mat = styledMaterialOf(obj);
1384
1604
  if (mat) {
1385
1605
  mat.userData.uSelBatch.value = -1;
1386
1606
  mat.userData.uSelWhole.value = 0;
@@ -1432,14 +1652,23 @@ export class S3MViewer {
1432
1652
  mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
1433
1653
  this._selectedMeshes.push(mesh);
1434
1654
  }
1655
+ // 点云没有单体粒度:整块混成 selectColor,让选中至少看得见
1656
+ for (const mesh of this._selectedMeshes) {
1657
+ if (isPointCloudObject(mesh))
1658
+ this._applyPointCloudStyle(mesh, true);
1659
+ }
1435
1660
  this.uniforms.uSelMix.value = 1;
1436
1661
  this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
1437
1662
  if (this.style.outline) {
1438
- if ((this.style.outlineStyle ?? 'edges') === 'edges') {
1439
- this._addSelectionEdges(result, targetMeshes);
1440
- }
1441
- else {
1442
- this._addSelectionHull(result, targetMeshes);
1663
+ // 棱线 / 外扩轮廓都是按「面」设计的,点云画不出来也不该画
1664
+ const decoMeshes = [...targetMeshes].filter((m) => !isPointCloudObject(m));
1665
+ if (decoMeshes.length) {
1666
+ if ((this.style.outlineStyle ?? 'edges') === 'edges') {
1667
+ this._addSelectionEdges(result, decoMeshes);
1668
+ }
1669
+ else {
1670
+ this._addSelectionHull(result, decoMeshes);
1671
+ }
1443
1672
  }
1444
1673
  }
1445
1674
  this._requestRepaint();
@@ -1706,7 +1935,8 @@ export class S3MViewer {
1706
1935
  let maxLon = -Infinity;
1707
1936
  let maxLat = -Infinity;
1708
1937
  for (const entry of this.datasets.values()) {
1709
- const b = entry.tileset?.config?.geoBounds;
1938
+ // 同 DatasetManager.getBounds:退化 / 越界的 geoBounds 不参与,避免视角甩到 (0,0)
1939
+ const b = usableGeoBounds(entry.tileset?.config?.geoBounds);
1710
1940
  if (!b)
1711
1941
  continue;
1712
1942
  minLon = Math.min(minLon, b.left);