s3m-three 0.4.19 → 0.4.21

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 (87) hide show
  1. package/dist/browser/assets/{s3mWorker-4iTAmWIu.js → s3mWorker-DvAUA8NJ.js} +1 -1
  2. package/dist/browser/s3m-three.iife.ext-three.js +243 -243
  3. package/dist/browser/s3m-three.iife.js +227 -227
  4. package/dist/core/Draco.d.ts.map +1 -1
  5. package/dist/core/Draco.js +0 -1
  6. package/dist/core/Draco.js.map +1 -1
  7. package/dist/core/S3MBlockParser.d.ts.map +1 -1
  8. package/dist/core/S3MBlockParser.js +27 -14
  9. package/dist/core/S3MBlockParser.js.map +1 -1
  10. package/dist/core/SplatSort.d.ts +14 -1
  11. package/dist/core/SplatSort.d.ts.map +1 -1
  12. package/dist/core/SplatSort.js +51 -4
  13. package/dist/core/SplatSort.js.map +1 -1
  14. package/dist/core/TileGeometry.d.ts +10 -0
  15. package/dist/core/TileGeometry.d.ts.map +1 -1
  16. package/dist/core/TileGeometry.js +17 -14
  17. package/dist/core/TileGeometry.js.map +1 -1
  18. package/dist/render/DatasetManager.d.ts +3 -1
  19. package/dist/render/DatasetManager.d.ts.map +1 -1
  20. package/dist/render/DatasetManager.js +2 -1
  21. package/dist/render/DatasetManager.js.map +1 -1
  22. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  23. package/dist/render/GeometryBuilder.d.ts +8 -0
  24. package/dist/render/GeometryBuilder.d.ts.map +1 -1
  25. package/dist/render/GeometryBuilder.js +62 -7
  26. package/dist/render/GeometryBuilder.js.map +1 -1
  27. package/dist/render/S3MViewer.d.ts +106 -440
  28. package/dist/render/S3MViewer.d.ts.map +1 -1
  29. package/dist/render/S3MViewer.js +297 -1653
  30. package/dist/render/S3MViewer.js.map +1 -1
  31. package/dist/render/SplatSorter.d.ts.map +1 -1
  32. package/dist/render/SplatSorter.js +8 -1
  33. package/dist/render/SplatSorter.js.map +1 -1
  34. package/dist/render/viewer/EdgeDecorator.d.ts +206 -0
  35. package/dist/render/viewer/EdgeDecorator.d.ts.map +1 -0
  36. package/dist/render/viewer/EdgeDecorator.js +628 -0
  37. package/dist/render/viewer/EdgeDecorator.js.map +1 -0
  38. package/dist/render/viewer/HeightIndex.d.ts +36 -0
  39. package/dist/render/viewer/HeightIndex.d.ts.map +1 -0
  40. package/dist/render/viewer/HeightIndex.js +98 -0
  41. package/dist/render/viewer/HeightIndex.js.map +1 -0
  42. package/dist/render/viewer/LightRig.d.ts +88 -0
  43. package/dist/render/viewer/LightRig.d.ts.map +1 -0
  44. package/dist/render/viewer/LightRig.js +229 -0
  45. package/dist/render/viewer/LightRig.js.map +1 -0
  46. package/dist/render/viewer/PickService.d.ts +109 -0
  47. package/dist/render/viewer/PickService.d.ts.map +1 -0
  48. package/dist/render/viewer/PickService.js +591 -0
  49. package/dist/render/viewer/PickService.js.map +1 -0
  50. package/dist/render/viewer/SelectionDecorator.d.ts +84 -0
  51. package/dist/render/viewer/SelectionDecorator.d.ts.map +1 -0
  52. package/dist/render/viewer/SelectionDecorator.js +339 -0
  53. package/dist/render/viewer/SelectionDecorator.js.map +1 -0
  54. package/dist/render/viewer/SplatLayer.d.ts +26 -0
  55. package/dist/render/viewer/SplatLayer.d.ts.map +1 -0
  56. package/dist/render/viewer/SplatLayer.js +44 -0
  57. package/dist/render/viewer/SplatLayer.js.map +1 -0
  58. package/dist/render/viewer/types.d.ts +256 -0
  59. package/dist/render/viewer/types.d.ts.map +1 -0
  60. package/dist/render/viewer/types.js +3 -0
  61. package/dist/render/viewer/types.js.map +1 -0
  62. package/dist/tileset/S3MTileset.d.ts +30 -3
  63. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  64. package/dist/tileset/S3MTileset.js +99 -5
  65. package/dist/tileset/S3MTileset.js.map +1 -1
  66. package/dist/worker/s3mWorker.js +1 -1
  67. package/dist/worker/s3mWorker.js.map +1 -1
  68. package/docs/API.md +9 -28
  69. package/docs/FORMAT-NOTES.md +38 -14
  70. package/package.json +1 -1
  71. package/src/core/Draco.ts +1 -2
  72. package/src/core/S3MBlockParser.ts +27 -16
  73. package/src/core/SplatSort.ts +67 -5
  74. package/src/core/TileGeometry.ts +24 -11
  75. package/src/render/DatasetManager.ts +5 -1
  76. package/src/render/FeatureStyleManager.ts +1 -1
  77. package/src/render/GeometryBuilder.ts +59 -7
  78. package/src/render/S3MViewer.ts +327 -1916
  79. package/src/render/SplatSorter.ts +8 -1
  80. package/src/render/viewer/EdgeDecorator.ts +722 -0
  81. package/src/render/viewer/HeightIndex.ts +94 -0
  82. package/src/render/viewer/LightRig.ts +267 -0
  83. package/src/render/viewer/PickService.ts +651 -0
  84. package/src/render/viewer/SelectionDecorator.ts +377 -0
  85. package/src/render/viewer/SplatLayer.ts +44 -0
  86. package/src/render/viewer/types.ts +264 -0
  87. package/src/tileset/S3MTileset.ts +106 -6
@@ -0,0 +1,377 @@
1
+ /**
2
+ * 选中态:高亮写入、轮廓 / 棱线装饰、跨瓦片重放。
3
+ *
4
+ * 从 `S3MViewer` 搬出来的第三块。它持有 `selection` 与 `selectedMeshes` 两份状态,
5
+ * 其余(uniform、样式、轮廓材质、逐网格棱线材质)都从宿主取。
6
+ *
7
+ * ## 为什么需要「重放」
8
+ *
9
+ * 选中高亮是直接写在各 mesh 材质的 `uSelBatch` / `uSelWhole` 上的,而 LOD 细化、
10
+ * 瓦片重载、淘汰回补都会换掉 mesh —— 不重放的话,用户点中的那栋楼一细化就失去高亮
11
+ * (状态还在 `selection.smid` 里,只是没人往新 mesh 上刷)。
12
+ *
13
+ * ## 没有单体化信息时的退路
14
+ *
15
+ * 倾斜摄影 / 白模整块没有 smid,`uSelBatch` 给 -1 会让「点中了但什么都没亮」。
16
+ * 这时改置 `uSelWhole = 1` 整块高亮,至少让用户看见自己点到了什么。
17
+ */
18
+ import * as THREE from 'three';
19
+ import type { OutlineMaterial, S3MStyle, StyledMaterial } from '../StyleMaterial.js';
20
+ import type { ThickLineMaterial } from '../EdgeLines.js';
21
+ import type { FeatureStyleManager } from '../FeatureStyleManager.js';
22
+ import type { GeoPackageEntry, StyleUniforms, PickSegment } from '../../core/types.js';
23
+ import { pickEntryId, resolveFeature } from '../../core/FeatureInfo.js';
24
+ import { buildFeatureEdgeSegments, buildThickLineGeometry } from '../EdgeLines.js';
25
+ import type { PickResult } from '../S3MViewer.js';
26
+
27
+ /** 取网格上的 styled 材质(与 S3MViewer 里那份逐字一致) */
28
+ function styledMaterialOf(mesh: THREE.Mesh): StyledMaterial | null {
29
+ const m = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
30
+ return (m as StyledMaterial | undefined)?.userData?.isStyled ? (m as StyledMaterial) : null;
31
+ }
32
+
33
+ function clampOpacity(v: number | null | undefined): number {
34
+ const n = v ?? 1;
35
+ return n < 0 ? 0 : n > 1 ? 1 : n;
36
+ }
37
+
38
+ function isPointCloud(o: THREE.Object3D): boolean {
39
+ return (o as THREE.Points).isPoints === true;
40
+ }
41
+
42
+ interface MeshUserData {
43
+ datasetId?: string;
44
+ skeletonName?: string;
45
+ geoRef?: GeoPackageEntry;
46
+ [key: string]: unknown;
47
+ }
48
+
49
+ export interface SelectionHost {
50
+ readonly uniforms: StyleUniforms;
51
+ readonly style: S3MStyle;
52
+ readonly outlineMaterial: OutlineMaterial;
53
+ /** 选中棱线的全局材质(外观 uniform 的持有者) */
54
+ readonly selectionEdgeMaterial: ThickLineMaterial;
55
+ /** 单体样式 / 过滤状态(判断被隐藏的单体不该被高亮) */
56
+ readonly featureStyleManager: FeatureStyleManager;
57
+ /** SMID → 包含它的网格 */
58
+ readonly smidMeshes: Map<number, Set<THREE.Mesh>>;
59
+ readonly pointClouds: Set<THREE.Points>;
60
+ applyPointCloudStyle(pc: THREE.Points, selected?: boolean): void;
61
+ onPick(result: PickResult | null): void;
62
+ requestRepaint(): void;
63
+ /** 选中棱线的逐网格材质(由 EdgeDecorator 提供) */
64
+ createSelectionEdgeMaterial(mesh: THREE.Mesh): ThickLineMaterial;
65
+ }
66
+
67
+ export class SelectionDecorator {
68
+ /** 当前选中(null = 没选中) */
69
+ selection: PickResult | null = null;
70
+ /** 被高亮的网格(点云也在这份列表里) */
71
+ selectedMeshes: THREE.Object3D[] = [];
72
+
73
+ private _outlineMeshes: THREE.Mesh[] = [];
74
+ private _selectionEdgeMeshes: THREE.Mesh[] = [];
75
+
76
+ constructor(private readonly host: SelectionHost) {}
77
+
78
+ /** @internal 测试用:选中棱线的网格列表 */
79
+ get selectionEdgeMeshes(): THREE.Mesh[] {
80
+ return this._selectionEdgeMeshes;
81
+ }
82
+
83
+ /** 场景清空 / 卸载时调用:丢掉所有装饰引用,不做逐对象清理 */
84
+ reset(): void {
85
+ this.selection = null;
86
+ this.selectedMeshes = [];
87
+ this._outlineMeshes = [];
88
+ this._selectionEdgeMeshes = [];
89
+ }
90
+
91
+ select(result: PickResult | null, { silent = false }: { silent?: boolean } = {}): void {
92
+ // 清掉上一次
93
+ if (this.selection) {
94
+ for (const obj of this.selectedMeshes) {
95
+ // 点云:恢复成当前着色模式(去掉选中混色)
96
+ if (isPointCloud(obj)) {
97
+ this.host.applyPointCloudStyle(obj as unknown as THREE.Points, false);
98
+ continue;
99
+ }
100
+ const mat = styledMaterialOf(obj as THREE.Mesh);
101
+ if (mat) {
102
+ mat.userData.uSelBatch.value = -1;
103
+ mat.userData.uSelWhole.value = 0;
104
+ }
105
+ }
106
+ this.selectedMeshes = [];
107
+ this.clearDecoration();
108
+ this.selection = null;
109
+ }
110
+
111
+ if (!result) {
112
+ this.host.uniforms.uSelMix.value = 0;
113
+ this.host.uniforms.uDim.value = 0;
114
+ this.host.outlineMaterial.uniforms.uSelBatch.value = -1;
115
+ this.host.outlineMaterial.uniforms.uSelWhole.value = 0;
116
+ this.host.outlineMaterial.uniforms.uElevation.value = 0;
117
+ this.host.requestRepaint();
118
+ if (!silent) this.host.onPick(null);
119
+ return;
120
+ }
121
+
122
+ this.selection = result;
123
+ const targetMeshes = new Set<THREE.Mesh>();
124
+ if (result.smid != null) {
125
+ const smidSet = this.host.smidMeshes.get(result.smid);
126
+ if (smidSet) {
127
+ for (const m of smidSet) targetMeshes.add(m);
128
+ }
129
+ }
130
+ targetMeshes.add(result.mesh);
131
+
132
+ for (const mesh of targetMeshes) {
133
+ const mat = styledMaterialOf(mesh);
134
+ if (!mat) continue;
135
+ let batchId = result.batchId;
136
+ if (result.instanceIndex != null && mesh === result.mesh) {
137
+ batchId = result.batchId;
138
+ } else if (result.smid != null) {
139
+ const ud = mesh.userData as MeshUserData;
140
+ const pickInfo = ud.geoRef?.pickInfo;
141
+ const entry = (pickInfo as Record<string | number, unknown> | undefined)?.[result.smid] ??
142
+ (pickInfo as Record<string | number, unknown> | undefined)?.[String(result.smid)];
143
+ const bId = entry ? pickEntryId(entry as any) : -1;
144
+ batchId = bId >= 0 ? bId : (mesh === result.mesh ? result.batchId : -1);
145
+ }
146
+ mat.userData.uSelBatch.value = batchId;
147
+ // 没有单体化信息时整块高亮,否则会出现「点中了但什么都没亮、其他全暗了」
148
+ mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
149
+ this.selectedMeshes.push(mesh);
150
+ }
151
+ // 点云没有单体粒度:整块混成 selectColor,让选中至少看得见
152
+ for (const mesh of this.selectedMeshes) {
153
+ if (isPointCloud(mesh)) this.host.applyPointCloudStyle(mesh as unknown as THREE.Points, true);
154
+ }
155
+ this.host.uniforms.uSelMix.value = 1;
156
+ this.host.uniforms.uDim.value = this.host.style.dimOthers ? 1 : 0;
157
+
158
+ if (this.host.style.outline) {
159
+ // 棱线 / 外扩轮廓都是按「面」设计的,点云画不出来也不该画
160
+ const decoMeshes = [...targetMeshes].filter((m) => !isPointCloud(m));
161
+ if (decoMeshes.length) {
162
+ if ((this.host.style.outlineStyle ?? 'edges') === 'edges') {
163
+ this.addSelectionEdges(result, decoMeshes);
164
+ } else {
165
+ this.addSelectionHull(result, decoMeshes);
166
+ }
167
+ }
168
+ }
169
+ this.host.requestRepaint();
170
+ }
171
+
172
+ /**
173
+ * 选中单体的棱线:只挑这个单体自己的三角面算折边,再画成屏幕空间等宽线。
174
+ * 抽出来的线段数很少(一栋楼通常几百到几千条),所以选中开销可以忽略。
175
+ */
176
+ addSelectionEdges(result: PickResult, meshes: Iterable<THREE.Mesh>): void {
177
+ this.syncStyle();
178
+ for (const mesh of meshes) {
179
+ const ud = mesh.userData as MeshUserData;
180
+ const geoRef = ud.geoRef;
181
+ let segments: PickSegment[] = [];
182
+ // 这块网格上的棱线全部属于选中的那个单体,整批填同一个 batchId,
183
+ // 顶点着色器才能从状态纹理里取到平移 / 拔高——选中跟随移动靠它。
184
+ let batchId = -1;
185
+ if (mesh === result.mesh && result.instanceIndex != null) {
186
+ // 实例化几何没有逐单体的顶点区间,整个 base mesh 就是这一栋的外形
187
+ segments = [{ vertexOffset: 0, vertexCount: mesh.geometry.getAttribute('position')?.count ?? 0 }];
188
+ const pickInfo = geoRef?.pickInfo;
189
+ const entry = (pickInfo as Record<string | number, unknown> | undefined)?.[result.smid ?? ''] ??
190
+ (pickInfo as Record<string | number, unknown> | undefined)?.[String(result.smid)];
191
+ batchId = entry ? pickEntryId(entry as any) : -1;
192
+ } else if (result.smid != null) {
193
+ const pickInfo = geoRef?.pickInfo;
194
+ const entry = (pickInfo as Record<string | number, unknown> | undefined)?.[result.smid] ??
195
+ (pickInfo as Record<string | number, unknown> | undefined)?.[String(result.smid)];
196
+ const bId = entry ? pickEntryId(entry as any) : -1;
197
+ batchId = bId;
198
+ segments = (entry as any)?.segments ?? (bId >= 0 ? resolveFeature(geoRef, bId).segments : []);
199
+ } else if (mesh === result.mesh) {
200
+ const localGeo = (ud.geoByBatchId as Map<number, GeoPackageEntry> | undefined)?.get(result.batchId) ?? geoRef;
201
+ segments = resolveFeature(localGeo, result.batchId).segments;
202
+ batchId = result.batchId;
203
+ }
204
+
205
+ const pairs = buildFeatureEdgeSegments(mesh.geometry, segments);
206
+ if (pairs && pairs.length > 0) {
207
+ const batchIds = batchId >= 0 ? new Float32Array(pairs.length / 6).fill(batchId) : undefined;
208
+ const lines = new THREE.Mesh(
209
+ buildThickLineGeometry(pairs, batchIds),
210
+ this.host.createSelectionEdgeMaterial(mesh)
211
+ );
212
+ lines.renderOrder = 4;
213
+ // 屏幕空间外扩后可能超出原包围球,关掉剔除更稳
214
+ lines.frustumCulled = false;
215
+ lines.raycast = () => {};
216
+ (lines.userData as MeshUserData).isSelectionEdge = true;
217
+ if (mesh === result.mesh && result.instanceIndex != null) {
218
+ this.applyInstanceTransform(lines, result);
219
+ }
220
+ mesh.add(lines); // 作为子节点,直接继承世界变换
221
+ this._selectionEdgeMeshes.push(lines);
222
+ }
223
+ }
224
+
225
+ if (this._selectionEdgeMeshes.length === 0) {
226
+ // 抽不出折边(退化几何 / 没有任何夹角)时退回外扩轮廓,至少保证选中可见
227
+ this.addSelectionHull(result, meshes);
228
+ return;
229
+ }
230
+ }
231
+
232
+ /** 实例化几何:把某个实例的矩阵套到一个普通 Mesh/子节点上(子节点不参与父级的 instanceMatrix) */
233
+ applyInstanceTransform(obj: THREE.Object3D, result: PickResult): void {
234
+ const im = result.mesh as THREE.InstancedMesh;
235
+ if (result.instanceIndex == null || !im.isInstancedMesh) return;
236
+ const m = new THREE.Matrix4();
237
+ im.getMatrixAt(result.instanceIndex, m);
238
+ m.decompose(obj.position, obj.quaternion, obj.scale);
239
+ }
240
+
241
+ /** 旧的描边画法:背面外扩法,整块几何一起上,靠 uSelBatch 只显示选中那栋 */
242
+ addSelectionHull(result: PickResult, meshes: Iterable<THREE.Mesh>): void {
243
+ const instanced = result.instanceIndex != null;
244
+ // 实例化:轮廓是这个实例的单独一份副本(普通 Mesh),所以整份都画;
245
+ // 顶点区间型:整块几何一起画,靠 uSelBatch 只显示选中那栋。
246
+ this.host.outlineMaterial.uniforms.uSelBatch.value = instanced ? -1 : result.batchId;
247
+ this.host.outlineMaterial.uniforms.uSelWhole.value = instanced || result.batchId < 0 ? 1 : 0;
248
+ this.host.outlineMaterial.uniforms.uElevation.value = instanced
249
+ ? this.selectionElevation(result)
250
+ : this.host.style.selectElevation ?? 0;
251
+
252
+ for (const mesh of meshes) {
253
+ const mat = styledMaterialOf(mesh);
254
+ if (mat?.userData?.uBatchTex) {
255
+ this.host.outlineMaterial.uniforms.uBatchTex.value = mat.userData.uBatchTex.value as THREE.Texture;
256
+ this.host.outlineMaterial.uniforms.uBatchTexWidth.value = mat.userData.uBatchTexWidth.value as number;
257
+ this.host.outlineMaterial.uniforms.uBatchTexActive.value = mat.userData.uBatchTexActive.value as number;
258
+ }
259
+ const outline = new THREE.Mesh(mesh.geometry, this.host.outlineMaterial);
260
+ outline.renderOrder = 3;
261
+ outline.frustumCulled = false;
262
+ outline.raycast = () => {};
263
+ if (mesh === result.mesh && result.instanceIndex != null) {
264
+ this.applyInstanceTransform(outline, result);
265
+ }
266
+ mesh.add(outline); // 作为子节点,直接继承世界变换
267
+ this._outlineMeshes.push(outline);
268
+ }
269
+ }
270
+
271
+ /** 只改 uniform:颜色 / 粗细 / 拔高,拖着滑块实时生效,不重建几何 */
272
+ syncStyle(): void {
273
+ const s = this.host.style;
274
+ const u = this.host.selectionEdgeMaterial.uniforms;
275
+ u.uColor.value.set(s.outlineColor ?? s.selectColor);
276
+ u.uWidth.value = Math.max(0.1, s.outlineWidthPx ?? 3);
277
+ u.uOpacity.value = clampOpacity(s.opacity);
278
+ // 只加全局 selectElevation:单体自己的拔高已经写在状态纹理第 2 行 z 里,
279
+ // 顶点着色器采样时带出来了,这里再加一次就会双重拔高
280
+ if (u.uElevation) u.uElevation.value = s.selectElevation ?? 0;
281
+
282
+ // 外扩描边是另一个材质实例,单体状态纹理的开关是建的时候抄过去的。
283
+ // 先选中、再抬高(或反过来)时模板会停留在旧值,这里补一次同步。
284
+ for (const outline of this._outlineMeshes) {
285
+ const parent = outline.parent as THREE.Mesh | null;
286
+ const ud = parent ? styledMaterialOf(parent)?.userData : null;
287
+ if (ud?.uBatchTex) {
288
+ this.host.outlineMaterial.uniforms.uBatchTex.value = ud.uBatchTex.value;
289
+ this.host.outlineMaterial.uniforms.uBatchTexWidth.value = ud.uBatchTexWidth.value;
290
+ this.host.outlineMaterial.uniforms.uBatchTexActive.value = ud.uBatchTexActive.value;
291
+ }
292
+ }
293
+ }
294
+
295
+ /** 选中单体的实际拔高 = 全局选中拔高 + 该单体的独立拔高(与着色器口径一致) */
296
+ selectionElevation(result: PickResult): number {
297
+ let elev = this.host.style.selectElevation ?? 0;
298
+ if (result.smid != null) {
299
+ const off = this.host.featureStyleManager.getFeatureEffectiveOffset(result.smid);
300
+ if (off.elevation) elev += off.elevation;
301
+ }
302
+ return elev;
303
+ }
304
+
305
+ clearDecoration(): void {
306
+ for (const outline of this._outlineMeshes) {
307
+ outline.parent?.remove(outline);
308
+ }
309
+ this._outlineMeshes = [];
310
+
311
+ for (const edges of this._selectionEdgeMeshes) {
312
+ edges.parent?.remove(edges);
313
+ edges.geometry.dispose();
314
+ }
315
+ this._selectionEdgeMeshes = [];
316
+ }
317
+
318
+ /**
319
+ * 把「当前选中」重放到一块新登记的 mesh 上。
320
+ *
321
+ * 为什么需要:选中高亮是直接写在各 mesh 材质的 uSelBatch / uSelWhole 上的,
322
+ * 而 LOD 细化、瓦片重载、淘汰回补都会换掉 mesh —— 不重放的话,用户点中的那栋楼
323
+ * 一细化就失去高亮(状态还在 selection.smid 里,只是没人往新 mesh 上刷)。
324
+ */
325
+ /**
326
+ * 判断一块无 SMID 单体化信息的网格(如倾斜摄影或白模整块)是否空间覆盖当前选中命中点
327
+ */
328
+ isMeshCoveringSelection(mesh: THREE.Mesh, sel: PickResult): boolean {
329
+ if (!sel.point) return false;
330
+ const geom = mesh.geometry;
331
+ if (!geom) return false;
332
+ if (!geom.boundingBox) geom.computeBoundingBox();
333
+ if (!geom.boundingBox) return false;
334
+
335
+ // 将选中的三维场景点转入网格局部坐标系进行包围盒检测
336
+ const pt = new THREE.Vector3(sel.point[0], sel.point[1], sel.point[2]);
337
+ mesh.updateWorldMatrix(true, false);
338
+ const inv = mesh.matrixWorld.clone().invert();
339
+ const localPt = pt.applyMatrix4(inv);
340
+
341
+ // 留出 0.5 米容差,避免刚好处于三角网格边缘缝隙时漏判
342
+ const box = geom.boundingBox.clone().expandByScalar(0.5);
343
+ return box.containsPoint(localPt);
344
+ }
345
+
346
+ replayOnto(mesh: THREE.Mesh): void {
347
+ const sel = this.selection;
348
+ if (!sel) return;
349
+ const mat = styledMaterialOf(mesh);
350
+ if (!mat) return;
351
+
352
+ const ud = mesh.userData as MeshUserData;
353
+ let batchId = -1;
354
+ if (sel.smid != null) {
355
+ const pickInfo = ud.geoRef?.pickInfo as Record<string | number, unknown> | undefined;
356
+ const entry = pickInfo?.[sel.smid] ?? pickInfo?.[String(sel.smid)];
357
+ batchId = entry ? pickEntryId(entry as any) : -1;
358
+ }
359
+ // 拾取命中的那块 mesh 用原始 batchId;同一 smid 散在别的几何里时按各自的区间算
360
+ if (mesh === sel.mesh) batchId = sel.batchId;
361
+ // 这块几何里根本没有这个单体 → 不动它(否则会把无关瓦片一起点亮)
362
+ if (batchId < 0 && mesh !== sel.mesh) {
363
+ if (sel.smid == null && ud.datasetId === sel.datasetId && this.isMeshCoveringSelection(mesh, sel)) {
364
+ batchId = -1;
365
+ } else {
366
+ return;
367
+ }
368
+ }
369
+
370
+ mat.userData.uSelBatch.value = batchId;
371
+ mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
372
+ if (!this.selectedMeshes.includes(mesh)) this.selectedMeshes.push(mesh);
373
+ if (isPointCloud(mesh)) this.host.applyPointCloudStyle(mesh as unknown as THREE.Points, true);
374
+ this.host.uniforms.uSelMix.value = 1;
375
+ this.host.uniforms.uDim.value = this.host.style.dimOthers ? 1 : 0;
376
+ }
377
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * 泼溅排序的调度:什么时候重排、什么时候整帧跳过。
3
+ *
4
+ * 排序算法本身在 `core/SplatSort.ts`,单瓦片的重写在 `render/SplatSorter.ts`。
5
+ * 这里只管一件事:**相机没动且没有新瓦片时,一帧都不要排**。
6
+ *
7
+ * 跳过条件用的是「视图矩阵完全相等」。地图交互时相机每帧都在变,所以这个条件
8
+ * 实际上只在静止时命中 —— 这是刻意的:移动中必须每帧重排,否则 alpha 混合顺序
9
+ * 会错,画面是彩色噪点。真正把代价压下去的是排序算法本身(基数排序,见 SplatSort)。
10
+ */
11
+ import * as THREE from 'three';
12
+ import { sortSplatMesh } from '../SplatSorter.js';
13
+
14
+ export class SplatLayer {
15
+ private readonly _lastView = new THREE.Matrix4();
16
+ private _sorted = false;
17
+
18
+ /** 有新瓦片进来 / 瓦片被淘汰:下一帧必须重排 */
19
+ markDirty(): void {
20
+ this._sorted = false;
21
+ }
22
+
23
+ /**
24
+ * 按当前视角重排所有泼溅瓦片。
25
+ *
26
+ * @param pointClouds 场景里的点云集合(含非泼溅的,按 `userData.isGaussianSplat` 过滤)
27
+ */
28
+ sort(camera: THREE.Camera | null, pointClouds: Iterable<THREE.Points>): void {
29
+ if (!camera) return;
30
+ if (this._sorted && this._lastView.equals(camera.matrixWorldInverse)) return;
31
+ for (const pc of pointClouds) {
32
+ if (!(pc.userData as { isGaussianSplat?: boolean }).isGaussianSplat) continue;
33
+ sortSplatMesh(pc as unknown as THREE.Mesh, camera.matrixWorldInverse);
34
+ }
35
+ this._lastView.copy(camera.matrixWorldInverse);
36
+ this._sorted = true;
37
+ }
38
+
39
+ /** 场景清空 / 换相机后重置,避免拿旧矩阵误判「已排过」 */
40
+ reset(): void {
41
+ this._sorted = false;
42
+ this._lastView.identity();
43
+ }
44
+ }
@@ -0,0 +1,264 @@
1
+ /**
2
+ * `S3MViewer` 的公共类型:选项、光照、拾取结果、路径跟随。
3
+ *
4
+ * 从 `S3MViewer.ts` 整块搬出来的纯类型定义(零运行时)。搬出去的原因只有一个:
5
+ * 这些类型占了两百多行,和「类怎么实现」没有关系,留在主类里会把真正的逻辑冲淡。
6
+ *
7
+ * `S3MViewer.ts` 仍然 re-export 它们,所以 `import { S3MViewerOptions } from '.../S3MViewer'`
8
+ * 这类既有写法不受影响。
9
+ */
10
+ import * as THREE from 'three';
11
+ import type { Path } from '../../anim/Path.js';
12
+ import type { EasingFn, EasingName } from '../../anim/easing.js';
13
+ import type { FeatureAttributes, GeoPackageEntry, ParsedTile, Stats } from '../../core/types.js';
14
+ import type { UnsupportedPolicy } from '../../core/degradations.js';
15
+ import type { AttributeQueryOptions } from '../../core/AttributeQuery.js';
16
+ import type { AuthenticateFn } from '../../core/AuthFetch.js';
17
+ import type { LightingPreset } from '../lightingPreset.js';
18
+ import type { S3MStyle } from '../StyleMaterial.js';
19
+ import type { BuildTileFn } from '../../tileset/S3MTileset.js';
20
+ import type { S3MDatasetEntry, S3MDatasetOptions } from '../DatasetManager.js';
21
+ import type { GroundOptions } from '../GroundManager.js';
22
+
23
+ export interface PickResult extends FeatureAttributes {
24
+ datasetId: string;
25
+ datasetName: string;
26
+ /** 网格编号 */
27
+ objectId: number;
28
+ /** 单体控制位(顶点区间型 = batchId,实例化型 = 状态纹理下标),-1 表示没有单体化信息 */
29
+ batchId: number;
30
+ /** 实例化几何命中的实例序号;非实例化为 undefined */
31
+ instanceIndex?: number;
32
+ skeletonName: string;
33
+ /** 单体中心经纬度,拿不到时为 null */
34
+ centerLngLat: [number, number] | null;
35
+ /** 命中点三维场景坐标 [x, y, z](米,场景 ENU) */
36
+ point?: [number, number, number] | null;
37
+ /** 命中点经纬度 [lng, lat] */
38
+ lngLat?: [number, number] | null;
39
+ /** 命中点高程(米) */
40
+ altitude?: number | null;
41
+ mesh: THREE.Mesh;
42
+ }
43
+
44
+ export interface LightOptions {
45
+ /** 环境光强度,默认 0.4 */
46
+ ambientIntensity?: number;
47
+ /** 环境光颜色,十六进制或 CSS 颜色字符串,默认 0xffffff */
48
+ ambientColor?: number | string;
49
+ /** 半球光天空强度,默认 0.8 */
50
+ hemisphereIntensity?: number;
51
+ /** 半球光天空颜色,默认 0xffffff */
52
+ hemisphereColor?: number | string;
53
+ /** 半球光地面颜色,默认 0xb6c0cb */
54
+ hemisphereGroundColor?: number | string;
55
+ /** 主平行光强度,默认 1.0 */
56
+ keyLightIntensity?: number;
57
+ /** 主平行光强度别名(兼容写法) */
58
+ mainIntensity?: number;
59
+ /** 主平行光颜色,默认 0xffffff */
60
+ keyLightColor?: number | string;
61
+ /** 主平行光颜色别名(兼容写法) */
62
+ mainColor?: number | string;
63
+ /** 补光强度,默认 0.35 */
64
+ fillLightIntensity?: number;
65
+ /** 补光颜色,默认 0xffffff */
66
+ fillLightColor?: number | string;
67
+ /**
68
+ * 光照朝向模式:
69
+ * - 'headlamp': 默认头灯模式(随相机 bearing 旋转,受光面始终面向镜头)
70
+ * - 'fixed': 固定世界坐标方位角(如太阳光模拟)
71
+ */
72
+ mode?: 'headlamp' | 'fixed';
73
+ /** 方位角(度,0~360),固定模式下以正北为 0 顺时针,头灯模式下为相机相对偏角,默认 150 */
74
+ azimuth?: number;
75
+ /** 太阳/光源仰角(度,0~90),默认 50 */
76
+ altitude?: number;
77
+ }
78
+
79
+ export interface S3MViewerOptions {
80
+ /**
81
+ * 光照预设:
82
+ * - `'default'`(默认):现有 PBR 打光,亮部线性总量 ≈ 2.55;
83
+ * - `'supermap'`:对齐 iClient3D 观感(暗部 0.46 / 亮部 2.44),远处不发灰、饱和度更高。
84
+ * 只改光源,不动材质与色调映射;`light` 里的显式字段仍然优先生效。
85
+ */
86
+ lightingPreset?: LightingPreset;
87
+ /** 光照参数配置(强度、颜色、朝向模式、太阳方位角/仰角等) */
88
+ light?: LightOptions;
89
+ /**
90
+ * 三角面材质深度偏移(共面摩尔纹 / 重合面归属的人工定序)。
91
+ * 例:factor -1 / units -2 把配置材质的片元深度略微拉近。
92
+ * 这是 mesh 粒度开关,见 BuildTileOptions.polygonOffset 的说明。
93
+ */
94
+ polygonOffset?: { factor: number; units: number };
95
+ /**
96
+ * 色调映射模式(默认 THREE.NoToneMapping,避免 ACESFilmic 压暗中暗部及导致底图与建筑色彩失真;
97
+ * 可传入 THREE.ACESFilmicToneMapping、THREE.LinearToneMapping、THREE.ReinhardToneMapping 等)
98
+ */
99
+ toneMapping?: THREE.ToneMapping;
100
+ /** 色调映射曝光度,默认 1.0 */
101
+ toneMappingExposure?: number;
102
+ /** 全局总驻留瓦片上限(多数据集多图层共享预算),在所有可见数据集之间动态均衡分配,彻底防止爆显存 */
103
+ totalMaxTiles?: number;
104
+ /** 单数据集简写 */
105
+ configUrl?: string;
106
+ /** 多数据集 */
107
+ datasets?: S3MDatasetOptions[];
108
+ style?: Partial<S3MStyle>;
109
+ /** 3D 物理地表面 / 基坑开挖配置 */
110
+ ground?: GroundOptions;
111
+ pickable?: boolean;
112
+ /** 每个数据集最多驻留瓦片 */
113
+ maxTiles?: number;
114
+ /** 每个数据集并发请求数 */
115
+ maxConcurrent?: number;
116
+ /**
117
+ * 图层级默认的降级处置策略(默认 `'warn'`),数据集可用 `unsupported` 单独覆盖。
118
+ * 见 `S3MTilesetOptions.unsupported`。
119
+ */
120
+ unsupported?: UnsupportedPolicy;
121
+ /** 是否用 Web Worker 解析经典 .s3mb 瓦片(默认开,崩溃自动降级主线程),见 S3MTilesetOptions.useWorker */
122
+ useWorker?: boolean;
123
+ /** LOD 切换范围缩放系数(默认 1.0),> 1 更倾向保留粗模,< 1 提前换细模 */
124
+ lodRangeScale?: number;
125
+ /**
126
+ * LOD 判定用屏幕高度时的「设备像素比上限」(默认不限制,行为与旧版一致)。
127
+ *
128
+ * LOD 的屏幕空间误差按 `canvasHeight` 物理像素算(与官方 `calcPixFromCam` 同口径),
129
+ * 于是 3x DPR 的手机上判定高度是 2556 而不是 852,细化层数与瓦片数都会远多于桌面。
130
+ * 传 2 表示「按 CSS 高度 × 2 截断」,砍掉 3x 屏的超采样余量;传 `Infinity` = 不截断。
131
+ *
132
+ * 代价:截断会把每块瓦片的屏幕投影放大同样倍数,几何误差与纹理密度同步变差,
133
+ * 所以默认不启用,由接入方按设备档位自行决定。运行时可用 `setLodPixelRatioCap()` 调整。
134
+ */
135
+ lodPixelRatioCap?: number;
136
+ /**
137
+ * 每个数据集驻留三角形总数的软上限(默认不限制)。
138
+ * 见 `S3MTilesetOptions.maxTriangles`:瓦片数管不住的场景(精模、大面片)用这条刹车。
139
+ */
140
+ maxTriangles?: number;
141
+ /**
142
+ * 每个数据集驻留显存/内存字节总数的软上限(默认 Infinity = 不限制)。
143
+ * 包含几何顶点缓冲(精确字节)与纹理显存估计。
144
+ * 超限时在每帧末尾(make-before-break)主动回收非显示的瓦片及其独占纹理。
145
+ */
146
+ maxMemory?: number;
147
+ /**
148
+ * 纹理缓存条数上限(默认 128)。一条对应一张已解码的瓦片纹理,
149
+ * 1024² 的 RGBA 一张约 4MB,128 条就是 512MB 显存 —— 低端设备建议 32~48。
150
+ * 与 `maxTextureSize` 配合使用:后者降单张尺寸,这条降张数。
151
+ */
152
+ maxImageCache?: number;
153
+ /**
154
+ * 各向异性过滤上限(默认不封顶,取 GPU 报的最大值,通常 16)。
155
+ * 移动端封到 4 或 8 肉眼通常无差,采样成本明显下降。只影响**之后创建**的纹理。
156
+ */
157
+ anisotropyCap?: number;
158
+ /**
159
+ * 纹理长边像素上限(默认不限制)。
160
+ * 非图集的 RGBA / WebP 纹理会按 2× 盒式滤波逐次减半到上限内;DXT 压缩直传与
161
+ * 图集(UV 已重映射)不受影响。手机上 1024 与 2048 肉眼几乎无差,显存差 4 倍。
162
+ */
163
+ maxTextureSize?: number;
164
+ /**
165
+ * 光照模型(默认 `'pbr'`)。
166
+ *
167
+ * - `'pbr'`:`MeshStandardMaterial`,GGX + 环境球谐,质感最好也最贵
168
+ * - `'lambert'`:`MeshLambertMaterial`,只保留漫反射 + 平行光,片元开销约省一半;
169
+ * 白模 / 纯色 / 按高度着色这类不需要高光的模式肉眼差别很小
170
+ *
171
+ * 模块级全局状态:同页多个图层会互相覆盖,后创建的那个决定全局。
172
+ * 只影响**之后创建**的材质 —— 已加载的瓦片要重新加载才会换。
173
+ */
174
+ materialQuality?: 'pbr' | 'lambert';
175
+ /** 自定义瓦片构建器(测试 / 服务端渲染时注入) */
176
+ buildTile?: BuildTileFn | null;
177
+ /**
178
+ * 可选:业务属性查询配置(iServer 数据服务)。
179
+ * 配上之后才能用 `colorByField()` 按业务字段着色 —— 属性值不在 S3M 瓦片里,只能去数据服务查。
180
+ */
181
+ attributes?: AttributeQueryOptions;
182
+ /** 宿主请求重画的信号。MapLibre 传 `() => map.triggerRepaint()`;纯 three 宿主传自己的渲染循环 */
183
+ onRequestRepaint?: () => void;
184
+ /** 地图朝向(弧度),用于让灯光跟着视角走。不传则灯光方向固定 */
185
+ getBearing?: () => number;
186
+ /**
187
+ * 鉴权适配函数(全局默认,数据集可用自己的 `authenticate` 覆盖)。
188
+ * 内核主线程发出的全部请求(配置 scp / 瓦片 / 子图集 / 棱线缓存 / 属性查询)
189
+ * 发出前都过一遍,可改写 URL(加 `?token=`)或附加请求头。
190
+ * 不传 = 行为与旧版完全一致。见 `AuthenticateFn`。
191
+ */
192
+ authenticate?: AuthenticateFn;
193
+ onStats?: (stats: Stats) => void;
194
+ onReady?: (entry: S3MDatasetEntry) => void;
195
+ onPick?: (result: PickResult | null) => void;
196
+ onError?: (error: unknown) => void;
197
+ /**
198
+ * 首次出现瓦片后自动预热另一种 `shading` 变体(默认开)。
199
+ *
200
+ * `flat` / `smooth` 是编译期变体,不预热的话第一次 `setStyle({ shading })` 会同步
201
+ * 编译着色器、卡一帧。预热在首帧之后异步做,期间跳一两帧渲染,肉眼不可见。
202
+ */
203
+ prewarmShading?: boolean;
204
+ }
205
+
206
+ /* ------------------------------------------------------------------ 内部类型 */
207
+
208
+ export interface PickableRef {
209
+ datasetId: string;
210
+ mesh: THREE.Mesh;
211
+ tile: ParsedTile;
212
+ skeletonName: string;
213
+ }
214
+
215
+ export interface MeshUserData {
216
+ pickId?: number;
217
+ datasetId?: string;
218
+ skeletonName?: string;
219
+ geoRef?: GeoPackageEntry;
220
+ tileRef?: ParsedTile;
221
+ primitiveType?: number;
222
+ /** 点云对象(THREE.Points):不吃面着色,也没有棱可描 */
223
+ isPointCloud?: boolean;
224
+ /** CPU 端现算的棱线对象(等宽线 Mesh) */
225
+ edges?: THREE.Object3D | null;
226
+ /** 背面外扩轮廓壳(edges 含 'hull' 时挂上) */
227
+ edgeHull?: THREE.Mesh | null;
228
+ /** 数据自带棱线包(此时不再用 EdgesGeometry 现算) */
229
+ nativeEdges?: boolean;
230
+ /** 数据本身有没有棱线包(不受 forceCpuEdges 影响,用来运行中切换时重算 nativeEdges) */
231
+ nativeEdgesRaw?: boolean;
232
+ /** 数据自带的棱线对象本身 */
233
+ isEdgeLines?: boolean;
234
+ /** CPU 现算的等宽棱线(与 isEdgeLines 区分,便于刷新样式) */
235
+ isThickEdge?: boolean;
236
+ /** 背面外扩轮廓壳(edges 含 'hull' 时挂上) */
237
+ isEdgeHull?: boolean;
238
+ /** 选中单体的棱线对象 */
239
+ isSelectionEdge?: boolean;
240
+ /** 该网格正在淡入时占用的动画通道 key,瓦片淘汰时要回收 */
241
+ tileFadeKey?: string;
242
+ pickMaterial?: THREE.Material;
243
+ [key: string]: unknown;
244
+ }
245
+
246
+ export interface PathFollowOptions {
247
+ /** 走完全程的时长(毫秒)。不传则按 speed 或默认 2000ms 推算 */
248
+ duration?: number;
249
+ /** 线速度(米/秒)。给了 duration 就以 duration 为准 */
250
+ speed?: number;
251
+ /** 是否循环 */
252
+ loop?: boolean;
253
+ /** 是否把折线首尾相连 */
254
+ closed?: boolean;
255
+ /** 缓动曲线:作用在**已走距离**上,默认 linear(恒速) */
256
+ easing?: EasingName | EasingFn;
257
+ /** 路径整体偏移(米),相当于路径原点 */
258
+ offset?: [number, number, number];
259
+ /** 单次走完(非循环)时回调 */
260
+ onComplete?: () => void;
261
+ }
262
+
263
+ /* ------------------------------------------------------------------ 图层 */
264
+