s3m-three 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +202 -202
  2. package/dist/render/DatasetManager.d.ts +10 -3
  3. package/dist/render/DatasetManager.d.ts.map +1 -1
  4. package/dist/render/DatasetManager.js +14 -5
  5. package/dist/render/DatasetManager.js.map +1 -1
  6. package/dist/render/EdgeLines.js +46 -46
  7. package/dist/render/GroundManager.d.ts +9 -0
  8. package/dist/render/GroundManager.d.ts.map +1 -1
  9. package/dist/render/GroundManager.js +13 -1
  10. package/dist/render/GroundManager.js.map +1 -1
  11. package/dist/render/Picker.js +66 -66
  12. package/dist/render/S3MViewer.d.ts +11 -3
  13. package/dist/render/S3MViewer.d.ts.map +1 -1
  14. package/dist/render/S3MViewer.js +18 -3
  15. package/dist/render/S3MViewer.js.map +1 -1
  16. package/dist/render/StyleMaterial.js +265 -265
  17. package/dist/vendor/meshopt_decoder.module.js +118 -118
  18. package/docs/API.md +20 -0
  19. package/package.json +1 -1
  20. package/src/core/BinaryReader.ts +116 -116
  21. package/src/core/DXTTextureDecode.ts +260 -260
  22. package/src/core/FeatureInfo.ts +150 -150
  23. package/src/core/MeshOpt.ts +401 -401
  24. package/src/core/S3MBParser.ts +590 -590
  25. package/src/core/types.ts +222 -222
  26. package/src/render/DatasetManager.ts +17 -6
  27. package/src/render/EdgeLines.ts +343 -343
  28. package/src/render/GeometryBuilder.ts +840 -840
  29. package/src/render/GroundManager.ts +14 -1
  30. package/src/render/Picker.ts +293 -293
  31. package/src/render/S3MViewer.ts +18 -3
  32. package/src/render/StyleMaterial.ts +602 -602
  33. package/src/render/TextureFactory.ts +134 -134
  34. package/src/tileset/S3MTileset.ts +552 -552
  35. package/src/vendor/meshopt_decoder.module.d.ts +31 -31
  36. package/src/vendor/meshopt_decoder.module.js +118 -118
  37. package/dist/render/CoordinateHelper.d.ts +0 -90
  38. package/dist/render/CoordinateHelper.d.ts.map +0 -1
  39. package/dist/render/CoordinateHelper.js +0 -85
  40. package/dist/render/CoordinateHelper.js.map +0 -1
  41. package/dist/render/S3MLayer.d.ts +0 -457
  42. package/dist/render/S3MLayer.d.ts.map +0 -1
  43. package/dist/render/S3MLayer.js +0 -1860
  44. package/dist/render/S3MLayer.js.map +0 -1
@@ -1,1860 +0,0 @@
1
- /**
2
- * S3M × MapLibre / Mapbox 图层
3
- *
4
- * 坐标链路:
5
- * S3M 顶点(数据集局部 ENU,米)
6
- * → 数据集 Group 偏移:把各数据集锚点对齐到「场景锚点」
7
- * → L 矩阵:ENU → Web Mercator 归一化坐标
8
- * → 宿主传入的 mercator→clip 矩阵
9
- *
10
- * 关键点:three 场景原点固定在**场景锚点**(第一个数据集的位置),
11
- * 所有顶点只保留「相对锚点的米」,避免 float32 在墨卡托大坐标下的抖动(RTC 思路)。
12
- * 多数据集叠加时,每个数据集一个 Group,用 position 承担锚点差。
13
- *
14
- * 兼容性:图层只依赖 `MapLike` 这组鸭子类型接口,MapLibre GL JS v4/v5 与
15
- * Mapbox GL JS v2/v3 都可以直接 `map.addLayer(layer)`,不引入任一方的类型依赖。
16
- */
17
- import * as THREE from 'three';
18
- import { setMaxAnisotropy } from './TextureFactory.js';
19
- import { STYLE, STYLE_LABELS, SELECT_MODE, SELECT_MODE_LABELS, createSharedUniforms, createOutlineMaterial, } from './StyleMaterial.js';
20
- import { Picker } from './Picker.js';
21
- import { buildFeatureEdgeSegments, buildThickLineGeometry, createLineSharedUniforms, createThickLineMaterial, } from './EdgeLines.js';
22
- import { buildOrthoMVP, extractFovRadians, extractNearFar, extractProjectionMatrix, } from './OrthoProjection.js';
23
- import { resolveFeature, measureSegments, buildAttributes, buildInstanceLookup, pickEntryId, DEFAULT_FLOOR_HEIGHT, } from '../core/FeatureInfo.js';
24
- import { AttributeQuery } from '../core/AttributeQuery.js';
25
- import { DEFAULT_FOV, lngLatFromWorld, } from './CoordinateHelper.js';
26
- import { DatasetManager, } from './DatasetManager.js';
27
- import { FeatureStyleManager, } from './FeatureStyleManager.js';
28
- import { GroundManager, } from './GroundManager.js';
29
- export { GroundManager, DatasetManager, FeatureStyleManager };
30
- const MAX_PICK_ID = 65534;
31
- export const DEFAULT_STYLE = {
32
- mode: STYLE.TEXTURE,
33
- colorA: '#dfe6ee',
34
- colorB: '#1d4ed8',
35
- colorC: '#fbbf24',
36
- selectColor: '#22d3ee',
37
- selectMode: SELECT_MODE.OVERLAY,
38
- selectOpacity: 0.65,
39
- selectEmissive: 0.35,
40
- selectElevation: 0,
41
- floorHeight: DEFAULT_FLOOR_HEIGHT,
42
- speed: 1,
43
- opacity: 1,
44
- shading: 'flat',
45
- dimOthers: true,
46
- dimFactor: 0.42,
47
- outline: true,
48
- outlineStyle: 'edges',
49
- outlineColor: '#22d3ee',
50
- outlineWidth: 0.9,
51
- outlineWidthPx: 3,
52
- edges: false,
53
- edgeWidth: 1.2,
54
- edgeColor: '#334155',
55
- edgeOpacity: 0.45,
56
- roofThreshold: 0.65,
57
- texMix: 0,
58
- depthWrite: true,
59
- };
60
- function styledMaterialOf(mesh) {
61
- const m = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
62
- const styled = m;
63
- return styled?.userData?.isStyled ? styled : null;
64
- }
65
- /* ------------------------------------------------------------------ 图层 */
66
- export class S3MLayer {
67
- id;
68
- type = 'custom';
69
- renderingMode = '3d';
70
- options;
71
- pickable;
72
- onStats;
73
- onReady;
74
- onPick;
75
- onError;
76
- map = null;
77
- renderer = null;
78
- scene = null;
79
- camera = null;
80
- picker = null;
81
- uniforms;
82
- style;
83
- outlineMaterial;
84
- /** 等宽线共享的分辨率 uniform(全局棱线 + 选中棱线共用) */
85
- lineShared;
86
- /** 全局棱线材质 */
87
- edgeMaterial;
88
- /** 选中棱线材质 */
89
- selectionEdgeMaterial;
90
- /** 3D 物理地表面 / 基坑开挖管理器 */
91
- groundManager;
92
- /** 多数据集生命周期与锚点管理器 */
93
- _datasetManager = new DatasetManager();
94
- /** 单体样式、专题着色与状态纹理管理器 */
95
- _featureStyleManager;
96
- /** 兼容 datasets 映射表暴露 */
97
- get datasets() {
98
- return this._datasetManager.datasets;
99
- }
100
- /** 场景主锚点(ENU 原点对应的世界坐标) */
101
- get sceneAnchor() {
102
- return this._datasetManager.sceneAnchor;
103
- }
104
- set sceneAnchor(val) {
105
- this._datasetManager.sceneAnchor = val;
106
- }
107
- _pickables = new Map();
108
- _pickSeq = 0;
109
- _freePickIds = [];
110
- /** SMID → 包含它的网格。改单个单体样式时只重算这几块,不用全量扫 */
111
- _smidMeshes = new Map();
112
- /** 每块网格的世界高度范围,用来增量维护 uHeightMin / uHeightMax */
113
- _meshHeight = new Map();
114
- _heightMin = Infinity;
115
- _heightMax = -Infinity;
116
- _boxScratch = new THREE.Box3();
117
- _selection = null;
118
- _selectedMeshes = [];
119
- _outlineMesh = null;
120
- _outlineMeshes = [];
121
- _selectionEdges = null;
122
- _selectionEdgeMeshes = [];
123
- _edgesEnabled = false;
124
- /** 业务属性查询(可选,配了 `attributes` 才有) */
125
- attributeQuery = null;
126
- _enuMatrix = new THREE.Matrix4();
127
- _needsRepaint = true;
128
- _frame = 0;
129
- _fps = { value: 0, last: 0 };
130
- _keyLight = null;
131
- _fillLight = null;
132
- _datasetQueue;
133
- /** 投影模式;正交不可用时(老版本 MapLibre)自动退回透视并告警一次 */
134
- _projection = 'perspective';
135
- _orthoWarned = false;
136
- _lodRangeScale = 1.0;
137
- constructor(options = {}) {
138
- this.id = options.id ?? 's3m';
139
- this.options = options;
140
- this.pickable = options.pickable !== false;
141
- this._lodRangeScale = options.lodRangeScale ?? 1.0;
142
- this._projection = options.projection === 'orthographic' ? 'orthographic' : 'perspective';
143
- this.onStats = options.onStats ?? (() => { });
144
- this.onReady = options.onReady ?? (() => { });
145
- this.onPick = options.onPick ?? (() => { });
146
- this.onError = options.onError ?? ((e) => console.warn('[s3m]', e));
147
- this.groundManager = new GroundManager(options.ground);
148
- this._featureStyleManager = new FeatureStyleManager(() => this.style.floorHeight, () => this.map?.triggerRepaint(), () => {
149
- if (this._selection)
150
- this._syncSelectionStyle();
151
- }, () => this.getAllMeshes(), (mesh) => this._applyOverridesToMesh(mesh));
152
- this.uniforms = createSharedUniforms();
153
- this.style = { ...DEFAULT_STYLE, ...(options.style ?? {}) };
154
- this.outlineMaterial = createOutlineMaterial(this.uniforms);
155
- this.lineShared = createLineSharedUniforms();
156
- this.edgeMaterial = createThickLineMaterial(this.lineShared, {
157
- color: this.style.edgeColor,
158
- opacity: this.style.edgeOpacity ?? 0.45,
159
- width: this.style.edgeWidth ?? 1.2,
160
- });
161
- this.selectionEdgeMaterial = createThickLineMaterial(this.lineShared, {
162
- color: this.style.outlineColor ?? this.style.selectColor,
163
- opacity: 1,
164
- width: this.style.outlineWidthPx ?? 3,
165
- });
166
- this._fps.last = performance.now();
167
- this._datasetQueue = this._normalizeDatasets(options);
168
- if (options.attributes)
169
- this.attributeQuery = new AttributeQuery(options.attributes);
170
- }
171
- _onProjectionChange = () => {
172
- this._recomputeAnchors();
173
- };
174
- /* ---------------------------------------------------------------- 生命周期 */
175
- onAdd(map, gl) {
176
- this.map = map;
177
- this.renderer = new THREE.WebGLRenderer({
178
- canvas: map.getCanvas(),
179
- context: gl,
180
- antialias: true,
181
- });
182
- this.renderer.autoClear = false;
183
- this.renderer.setPixelRatio(1);
184
- // 白模 albedo 接近 1,不做色调映射会过曝成一片纯白、丢失明暗关系
185
- this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
186
- this.renderer.toneMappingExposure = 1.0;
187
- setMaxAnisotropy(this.renderer.capabilities.getMaxAnisotropy());
188
- this.scene = new THREE.Scene();
189
- this.scene.add(this.groundManager.group);
190
- this._addLights();
191
- this.camera = new THREE.Camera();
192
- this.camera.matrixAutoUpdate = false;
193
- this.picker = new Picker(this.renderer);
194
- this._applyStyleUniforms();
195
- for (const ds of this._datasetQueue)
196
- void this.addDataset(ds).catch((e) => this.onError(e));
197
- this._recomputeAnchors();
198
- map.on('click', this._onClick);
199
- // MapLibre v6 起 setProjection() 会 fire 'projectiontransition'(style.ts 的
200
- // _setProjectionInternal 里触发)。事件名必须是全称,写 'projection' 永远不会触发。
201
- // v4 / v5 / Mapbox 没有这个事件,监听一个不存在的事件名是安全的(只是不 fire),
202
- // 而 v4/v5 本来也不支持运行时切投影,初始投影在 onAdd 的 _recomputeAnchors 里已覆盖。
203
- if (typeof map.on === 'function') {
204
- map.on('projectiontransition', this._onProjectionChange);
205
- }
206
- }
207
- onRemove() {
208
- this.map?.off('click', this._onClick);
209
- if (typeof this.map?.off === 'function') {
210
- this.map.off('projectiontransition', this._onProjectionChange);
211
- }
212
- for (const id of [...this.datasets.keys()])
213
- this.removeDataset(id);
214
- this.groundManager.dispose();
215
- this.picker?.dispose();
216
- this.picker = null;
217
- this.outlineMaterial.dispose();
218
- this.edgeMaterial.dispose();
219
- this.selectionEdgeMaterial.dispose();
220
- this.scene?.clear();
221
- this._selectedMeshes = [];
222
- this._outlineMeshes = [];
223
- this._selectionEdgeMeshes = [];
224
- this._outlineMesh = null;
225
- this._selectionEdges = null;
226
- this._smidMeshes.clear();
227
- this._meshHeight.clear();
228
- this._heightMin = Infinity;
229
- this._heightMax = -Infinity;
230
- this.renderer?.dispose();
231
- this.renderer = null;
232
- this.map = null;
233
- }
234
- _addLights() {
235
- if (!this.scene)
236
- return;
237
- // 环境光与天光打底,保证背光面和折叠处不死黑
238
- this.scene.add(new THREE.AmbientLight(0xffffff, 0.4));
239
- this.scene.add(new THREE.HemisphereLight(0xffffff, 0xb6c0cb, 0.8));
240
- // 主光 / 补光的方向每帧按地图朝向重算(见 _updateLights),
241
- // 这样旋转视角时白模始终是亮的,不会转到背光面就一片死灰。
242
- this._keyLight = new THREE.DirectionalLight(0xffffff, 1.0);
243
- this._fillLight = new THREE.DirectionalLight(0xffffff, 0.35);
244
- this.scene.add(this._keyLight, this._fillLight);
245
- }
246
- /**
247
- * 让光照跟着地图朝向走:主光在镜头后上方偏左,补光在后上方偏右。
248
- * 相当于一盏头灯,看到的立面永远是受光面。
249
- */
250
- _updateLights() {
251
- const bearing = ((this.map?.getBearing?.() ?? 0) * Math.PI) / 180;
252
- const place = (light, azOffsetDeg, elevDeg) => {
253
- if (!light)
254
- return;
255
- const az = bearing + (azOffsetDeg * Math.PI) / 180;
256
- const el = (elevDeg * Math.PI) / 180;
257
- const c = Math.cos(el);
258
- light.position.set(Math.sin(az) * c, Math.cos(az) * c, Math.sin(el)).multiplyScalar(1000);
259
- };
260
- place(this._keyLight, 150, 50);
261
- place(this._fillLight, 215, 30);
262
- }
263
- /* ---------------------------------------------------------------- 数据集 */
264
- _normalizeDatasets(options) {
265
- if (Array.isArray(options.datasets))
266
- return options.datasets.filter((d) => !!d?.configUrl);
267
- if (options.configUrl)
268
- return [{ id: 'default', name: '默认数据集', configUrl: options.configUrl }];
269
- return [];
270
- }
271
- /** 追加一个数据集。图层 addLayer 之前调用会抛错。 */
272
- async addDataset(ds) {
273
- if (!this.scene)
274
- throw new Error('[s3m] 图层尚未 addLayer,不能添加数据集');
275
- const entry = await this._datasetManager.addDataset(ds, this.scene, this.map, this.uniforms, this._lodRangeScale, this.options.buildTile, this.options.maxTiles ?? 64, this.options.maxConcurrent ?? 4, {
276
- onTile: (node, e) => {
277
- this._registerTile(node, e);
278
- this.onStats(this.stats);
279
- this._needsRepaint = true;
280
- this.map?.triggerRepaint();
281
- },
282
- onTileRemoved: (node, e) => {
283
- this._unregisterTile(node, e);
284
- this.onStats(this.stats);
285
- this._needsRepaint = true;
286
- this.map?.triggerRepaint();
287
- },
288
- onStats: () => {
289
- this.onStats(this.stats);
290
- this.map?.triggerRepaint();
291
- },
292
- onError: (e) => {
293
- this.onError(e);
294
- this.onStats(this.stats);
295
- },
296
- onReady: (e) => {
297
- this.onReady(e);
298
- this._refreshAllHeights();
299
- if (!this.groundManager.excavationBounds) {
300
- this.groundManager.autoFitExcavation(this.datasets.values());
301
- }
302
- },
303
- triggerRepaint: () => {
304
- this.map?.triggerRepaint();
305
- },
306
- });
307
- return entry;
308
- }
309
- /**
310
- * 归一化世界坐标 → 经纬度 [lng, lat]
311
- */
312
- _lngLatFromWorld(x, y) {
313
- return lngLatFromWorld(this.map, x, y);
314
- }
315
- /** 当地图 CRS 投影切换或宿主 map 挂载后,重新计算场景锚点与各数据集 offset */
316
- _recomputeAnchors() {
317
- this._datasetManager.recomputeAnchors(this.map);
318
- this._refreshAllHeights();
319
- this.map?.triggerRepaint();
320
- }
321
- /**
322
- * 获取指定数据集的垂直高程偏移(米)。
323
- * 若不传 id,默认获取第一个(或默认)数据集的高程偏移。
324
- */
325
- getDatasetHeight(id) {
326
- return this._datasetManager.getDatasetHeight(id);
327
- }
328
- /**
329
- * 设置指定数据集的垂直高程偏移(米),正数抬升,负数下降。
330
- */
331
- setDatasetHeight(idOrHeight, heightOffset) {
332
- let id;
333
- let h;
334
- if (typeof idOrHeight === 'number') {
335
- id = this.datasets.keys().next().value ?? '';
336
- h = idOrHeight;
337
- }
338
- else {
339
- id = idOrHeight;
340
- h = heightOffset ?? 0;
341
- }
342
- this._datasetManager.setDatasetHeight(id, h, this.map);
343
- this._refreshAllHeights();
344
- this.map?.triggerRepaint();
345
- }
346
- /**
347
- * 动态设置指定数据集的垂直对齐基准('origin' | 'bottom' | 'top' | 'center')。
348
- */
349
- setDatasetAlign(idOrAlign, align) {
350
- let id;
351
- let a;
352
- if (align === undefined && (idOrAlign === 'origin' || idOrAlign === 'bottom' || idOrAlign === 'top' || idOrAlign === 'center')) {
353
- id = this.datasets.keys().next().value ?? '';
354
- a = idOrAlign;
355
- }
356
- else {
357
- id = idOrAlign;
358
- a = align ?? 'origin';
359
- }
360
- this._datasetManager.setDatasetAlign(id, a, this.map);
361
- this._refreshAllHeights();
362
- this.map?.triggerRepaint();
363
- }
364
- /** @internal 语义高度对齐计算代理,供内部与测试使用 */
365
- _computeAlignOffset(tileset, align) {
366
- return this._datasetManager.computeAlignOffset(tileset, align);
367
- }
368
- /* ---------------------------------------------------------------- 单体自定义样式 & 过滤 */
369
- /**
370
- * 为指定单体(SMID)设置独立自定义样式(颜色、拔高、透明度)。
371
- */
372
- setFeatureStyle(smid, style) {
373
- this._featureStyleManager.setFeatureStyle(smid, style, this._meshesOf([smid]));
374
- }
375
- /** 批量为多个单体设置样式 */
376
- setFeaturesStyle(smids, style) {
377
- this._featureStyleManager.setFeaturesStyle(smids, style, this._meshesOf(smids));
378
- }
379
- /** 获取指定单体的自定义样式 */
380
- getFeatureStyle(smid) {
381
- return this._featureStyleManager.getFeatureStyle(smid);
382
- }
383
- /** 获取所有自定义样式的单体映射表 */
384
- getFeatureStyles() {
385
- return this._featureStyleManager.getFeatureStyles();
386
- }
387
- /** 移除指定单体的自定义样式,恢复为图层全局样式 */
388
- removeFeatureStyle(smid) {
389
- this._featureStyleManager.removeFeatureStyle(smid, this._meshesOf([smid]));
390
- }
391
- /** 清空所有单体的自定义样式 */
392
- clearFeatureStyles() {
393
- this._featureStyleManager.clearFeatureStyles();
394
- }
395
- /* ---------------------------------------------------------------- 按业务字段着色 */
396
- setAttributeSource(options) {
397
- this.attributeQuery = options ? new AttributeQuery(options) : null;
398
- }
399
- getLoadedSmids() {
400
- return [...this._smidMeshes.keys()];
401
- }
402
- auditBatches() {
403
- const res = this._featureStyleManager.auditBatches(this._pickables.values());
404
- res.smids = this._smidMeshes.size;
405
- return res;
406
- }
407
- getThematicField() {
408
- return this._featureStyleManager.thematicField;
409
- }
410
- setThematicColors(colors, field = 'custom') {
411
- this._featureStyleManager.setThematicColors(colors, field);
412
- }
413
- clearThematic() {
414
- this._featureStyleManager.clearThematicColors();
415
- }
416
- /* ---------------------------------------------------------------- 按业务字段过滤 / 抬高 */
417
- getFilterField() {
418
- return this._featureStyleManager.fieldFilter?.field ?? null;
419
- }
420
- filterByField(options) {
421
- return this._featureStyleManager.filterByField(options, this.attributeQuery, this.getLoadedSmids());
422
- }
423
- clearFieldFilter() {
424
- this._featureStyleManager.clearFieldFilter();
425
- }
426
- getElevationField() {
427
- return this._featureStyleManager.elevationField;
428
- }
429
- elevateByField(options) {
430
- return this._featureStyleManager.elevateByField(options, this.attributeQuery, this.getLoadedSmids());
431
- }
432
- setThematicElevation(elevations, field = 'custom') {
433
- this._featureStyleManager.setThematicElevation(elevations, field);
434
- }
435
- clearThematicElevation() {
436
- this._featureStyleManager.clearThematicElevation();
437
- }
438
- colorByField(options) {
439
- return this._featureStyleManager.colorByField(options, this.attributeQuery, this.getLoadedSmids());
440
- }
441
- setFilter(filter) {
442
- this._featureStyleManager.setFilter(filter, this.uniforms);
443
- }
444
- getFilter() {
445
- return this._featureStyleManager.filter;
446
- }
447
- clearFilter() {
448
- this._featureStyleManager.clearFilter(this.uniforms);
449
- }
450
- /* ---------------------------------------------------------------- 3D 物理地表与基坑 */
451
- /** 设置 3D 物理地表面渲染模式:'none' 关闭 | 'translucent' 半透明透视 | 'excavate' 基坑开挖 */
452
- setGroundMode(mode) {
453
- this.groundManager.setMode(mode);
454
- this.map?.triggerRepaint();
455
- }
456
- /** 设置 3D 物理地表面不透明度 (0~1) */
457
- setGroundOpacity(opacity) {
458
- this.groundManager.setOpacity(opacity);
459
- this.map?.triggerRepaint();
460
- }
461
- /** 设置基坑开挖范围 [minX, minY, maxX, maxY](米,场景 ENU 坐标) */
462
- setExcavationBounds(bounds) {
463
- this.groundManager.setExcavationBounds(bounds);
464
- this.map?.triggerRepaint();
465
- }
466
- /* ---------------------------------------------------------------- 状态纹理同步 */
467
- _isFeatureHidden(smid, _batchId) {
468
- if (smid == null)
469
- return false;
470
- const ff = this._featureStyleManager.fieldFilter;
471
- if (ff) {
472
- const inSet = ff.matched.has(smid);
473
- if (ff.mode === 'keep' ? !inSet : inSet)
474
- return true;
475
- }
476
- const filter = this._featureStyleManager.filter;
477
- if (filter) {
478
- if (filter.include && filter.include.length > 0 && !filter.include.includes(smid))
479
- return true;
480
- if (filter.exclude && filter.exclude.length > 0 && filter.exclude.includes(smid))
481
- return true;
482
- }
483
- const st = this._featureStyleManager.featureStyles.get(smid);
484
- if (st?.hidden)
485
- return true;
486
- return false;
487
- }
488
- getAllMeshes() {
489
- const list = [];
490
- for (const p of this._pickables.values())
491
- list.push(p.mesh);
492
- return list;
493
- }
494
- _meshesOf(smids) {
495
- const out = new Set();
496
- for (const smid of smids) {
497
- const set = this._smidMeshes.get(smid);
498
- if (set)
499
- for (const mesh of set)
500
- out.add(mesh);
501
- }
502
- return out;
503
- }
504
- _markOverridesDirty(scope = 'all') {
505
- if (scope === 'all') {
506
- this._featureStyleManager.markOverridesDirty(undefined, this.getAllMeshes());
507
- }
508
- else {
509
- this._featureStyleManager.markOverridesDirty(scope);
510
- }
511
- }
512
- flushOverrides() {
513
- this._featureStyleManager.flushOverrides();
514
- }
515
- /** @internal 内部状态纹理烘焙代理,保留供回归单测与钩子使用 */
516
- _applyOverridesToMesh(mesh) {
517
- this._featureStyleManager.applyOverridesToMesh(mesh);
518
- }
519
- removeDataset(id) {
520
- const entry = this.datasets.get(id);
521
- if (!entry)
522
- return;
523
- if (this._selection?.datasetId === id)
524
- this.select(null);
525
- this._datasetManager.removeDataset(id, this.scene, this.map, (e) => {
526
- e.root.traverse((o) => {
527
- const mesh = o;
528
- const ud = mesh.userData;
529
- if (ud.pickId != null) {
530
- this._pickables.delete(ud.pickId);
531
- this._releasePickId(ud.pickId);
532
- ud.pickId = undefined;
533
- }
534
- this._unindexMeshSmids(mesh);
535
- this._removeMeshHeight(mesh);
536
- mesh.geometry?.dispose?.();
537
- const styled = styledMaterialOf(mesh);
538
- if (styled)
539
- styled.dispose();
540
- this._featureStyleManager.disposeMesh(mesh);
541
- });
542
- });
543
- for (const [pickId, p] of this._pickables)
544
- if (p.datasetId === id)
545
- this._pickables.delete(pickId);
546
- this.onStats(this.stats);
547
- this.map?.triggerRepaint();
548
- }
549
- /** 清空当前所有数据集 */
550
- clearDatasets() {
551
- for (const id of [...this.datasets.keys()])
552
- this.removeDataset(id);
553
- }
554
- setDatasetVisible(id, visible) {
555
- this._datasetManager.setDatasetVisible(id, visible);
556
- if (!visible && this._selection?.datasetId === id)
557
- this.select(null);
558
- this.map?.triggerRepaint();
559
- }
560
- /* ---------------------------------------------------------------- 瓦片注册 */
561
- _registerTile(node, entry) {
562
- entry.root.add(node.group);
563
- node.group.updateMatrixWorld(true);
564
- const hasNativeEdges = node.group.userData.hasNativeEdges === true;
565
- for (const child of node.group.children) {
566
- // 数据自带的棱线包:只跟着 edges 开关显示,不参与拾取、不参与着色
567
- if (child.isLineSegments && child.userData.isEdgeLines) {
568
- child.visible = this._edgesEnabled;
569
- this._styleEdgeLine(child);
570
- continue;
571
- }
572
- const mesh = child;
573
- if (!mesh.isMesh)
574
- continue;
575
- const ud = mesh.userData;
576
- ud.pickId = this._allocPickId();
577
- ud.datasetId = entry.id;
578
- ud.nativeEdges = hasNativeEdges;
579
- this._pickables.set(ud.pickId, {
580
- datasetId: entry.id,
581
- mesh,
582
- tile: node.tile,
583
- skeletonName: ud.skeletonName ?? '',
584
- });
585
- this._indexMeshSmids(mesh);
586
- this._addMeshHeight(mesh);
587
- if (this._edgesEnabled && !hasNativeEdges)
588
- this._ensureEdges(mesh);
589
- // 新瓦片要继承当前的不透明度与着色方式
590
- const mat = styledMaterialOf(mesh);
591
- if (mat) {
592
- const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
593
- mat.opacity = o;
594
- mat.transparent = o < 0.999;
595
- mat.depthWrite = o >= 0.999;
596
- mat.flatShading = this.style.shading !== 'smooth';
597
- mat.needsUpdate = true;
598
- this._featureStyleManager.applyOverridesToMesh(mesh);
599
- }
600
- }
601
- }
602
- /** 瓦片被淘汰:从拾取表和场景里摘掉,并释放它自己的 GPU 资源 */
603
- _unregisterTile(node, entry) {
604
- for (const child of [...node.group.children]) {
605
- const mesh = child;
606
- if (!mesh.isMesh)
607
- continue;
608
- const ud = mesh.userData;
609
- if (this._selection?.mesh === mesh || this._selectedMeshes.includes(mesh))
610
- this.select(null);
611
- if (ud.pickId != null) {
612
- this._pickables.delete(ud.pickId);
613
- this._releasePickId(ud.pickId);
614
- ud.pickId = undefined;
615
- }
616
- this._unindexMeshSmids(mesh);
617
- this._removeMeshHeight(mesh);
618
- this._featureStyleManager.disposeMesh(mesh);
619
- const styled = styledMaterialOf(mesh);
620
- styled?.dispose?.();
621
- mesh.geometry?.dispose?.();
622
- }
623
- entry.root.remove(node.group);
624
- }
625
- /**
626
- * 拾取 ID 只有 2 个字节(RG 两个通道),65535 个网格就到头了,
627
- * 所以必须回收:瓦片淘汰 / 数据集移除时把 id 还回池子。
628
- */
629
- _allocPickId() {
630
- const reused = this._freePickIds.pop();
631
- if (reused !== undefined)
632
- return reused;
633
- if (this._pickSeq > MAX_PICK_ID) {
634
- for (let id = 0; id <= MAX_PICK_ID; id++)
635
- if (!this._pickables.has(id))
636
- return id;
637
- throw new Error(`[s3m] 可拾取网格超过 ${MAX_PICK_ID + 1} 个,2 字节 ID 编码不够用`);
638
- }
639
- return this._pickSeq++;
640
- }
641
- _releasePickId(id) {
642
- this._freePickIds.push(id);
643
- }
644
- /* ---------------------------------------------------------------- 样式 */
645
- /** 动态设置图层整体透明度 (0.0 ~ 1.0) */
646
- setOpacity(opacity) {
647
- this.setStyle({ opacity });
648
- }
649
- /** 合并并应用样式。只改 uniform 的项不会触发几何/着色器重建。 */
650
- setStyle(patch = {}) {
651
- this.style = { ...this.style, ...patch };
652
- this._applyStyleUniforms();
653
- if ('opacity' in patch || 'depthWrite' in patch)
654
- this._applyOpacity();
655
- if ('shading' in patch)
656
- this._applyShading();
657
- if ('edges' in patch)
658
- this.setEdges(!!this.style.edges);
659
- else if ('edgeColor' in patch || 'edgeWidth' in patch || 'opacity' in patch || 'edgeOpacity' in patch) {
660
- this._refreshEdgeLines();
661
- }
662
- if (this._selection) {
663
- // 换描边画法要重建几何;颜色 / 粗细 / 拔高只是 uniform,拖着滑块也不卡
664
- if ('outline' in patch || 'outlineStyle' in patch)
665
- this.select(this._selection, { silent: true });
666
- else
667
- this._syncSelectionStyle();
668
- }
669
- this.map?.triggerRepaint();
670
- }
671
- getStyle() {
672
- return { ...this.style, modeLabel: STYLE_LABELS[this.style.mode] };
673
- }
674
- _applyStyleUniforms() {
675
- const u = this.uniforms;
676
- const s = this.style;
677
- u.uMode.value = s.mode;
678
- u.uColorA.value.set(s.colorA);
679
- u.uColorB.value.set(s.colorB);
680
- u.uColorC.value.set(s.colorC);
681
- u.uSelColor.value.set(s.selectColor);
682
- u.uFloorHeight.value = s.floorHeight;
683
- const selMode = s.selectMode ?? SELECT_MODE.OVERLAY;
684
- if (u.uSelMode) {
685
- u.uSelMode.value =
686
- selMode === SELECT_MODE.REPLACE
687
- ? 0
688
- : selMode === SELECT_MODE.MULTIPLY
689
- ? 2
690
- : selMode === SELECT_MODE.GLOW_ONLY
691
- ? 3
692
- : 1;
693
- }
694
- if (u.uSelOpacity && s.selectOpacity != null)
695
- u.uSelOpacity.value = s.selectOpacity;
696
- if (u.uSelEmissive && s.selectEmissive != null)
697
- u.uSelEmissive.value = s.selectEmissive;
698
- if (u.uSelElevation && s.selectElevation != null)
699
- u.uSelElevation.value = s.selectElevation;
700
- if (u.uDimFactor && s.dimFactor != null)
701
- u.uDimFactor.value = s.dimFactor;
702
- if (u.uRoofThreshold && s.roofThreshold != null) {
703
- u.uRoofThreshold.value = s.roofThreshold;
704
- }
705
- if (u.uTexMix && s.texMix != null) {
706
- u.uTexMix.value = s.texMix;
707
- }
708
- if (this.outlineMaterial) {
709
- if (this.outlineMaterial.uniforms.uElevation && s.selectElevation != null) {
710
- this.outlineMaterial.uniforms.uElevation.value = s.selectElevation;
711
- }
712
- if (s.outlineWidth != null)
713
- this.outlineMaterial.uniforms.uWidth.value = s.outlineWidth;
714
- if (s.outlineColor != null) {
715
- this.outlineMaterial.uniforms.uColor.value.set(s.outlineColor);
716
- }
717
- else {
718
- this.outlineMaterial.uniforms.uColor.value.set(s.selectColor);
719
- }
720
- }
721
- }
722
- /** 透明度:只改材质状态,不触发着色器重编译,所以可以跟着滑块实时拖 */
723
- _applyOpacity() {
724
- const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
725
- const transparent = o < 0.999;
726
- // 关键:若 depthWrite 开启(默认开启),即使半透明也保持深度写入,完美遮挡背面棱线,防止背面线条透出
727
- const keepDepth = this.style.depthWrite !== false;
728
- for (const p of this._pickables.values()) {
729
- const m = styledMaterialOf(p.mesh);
730
- if (!m)
731
- continue;
732
- m.opacity = o;
733
- m.transparent = transparent;
734
- m.depthWrite = transparent ? keepDepth : true;
735
- }
736
- this.map?.triggerRepaint();
737
- }
738
- /** 平面/平滑着色:FLAT_SHADED 是编译期宏,改它必须重编译 */
739
- _applyShading() {
740
- const flat = this.style.shading !== 'smooth';
741
- for (const p of this._pickables.values()) {
742
- const m = styledMaterialOf(p.mesh);
743
- if (!m || m.flatShading === flat)
744
- continue;
745
- m.flatShading = flat;
746
- m.needsUpdate = true;
747
- }
748
- this.map?.triggerRepaint();
749
- }
750
- /**
751
- * 高度范围增量维护。
752
- *
753
- * 原来每注册一块瓦片都要把所有网格重新遍历一遍、每个都 clone 一个 Box3 再乘矩阵,
754
- * 加载 n 块瓦片就是 O(n²) 次矩阵乘 + n² 个垃圾对象。现在只在网格进出的记一次,
755
- * 全局值从缓存里滚:只有被移除的那块正好卡在边界上时,才回退到一次 O(n) 重算。
756
- */
757
- _addMeshHeight(mesh) {
758
- const bb = mesh.geometry?.boundingBox;
759
- if (!bb)
760
- return;
761
- this._boxScratch.copy(bb).applyMatrix4(mesh.matrixWorld);
762
- const min = this._boxScratch.min.z;
763
- const max = this._boxScratch.max.z;
764
- if (!Number.isFinite(min) || !Number.isFinite(max))
765
- return;
766
- this._meshHeight.set(mesh, { min, max });
767
- if (min < this._heightMin)
768
- this._heightMin = min;
769
- if (max > this._heightMax)
770
- this._heightMax = max;
771
- this._pushHeightRange();
772
- }
773
- _removeMeshHeight(mesh) {
774
- const rec = this._meshHeight.get(mesh);
775
- if (!rec)
776
- return;
777
- this._meshHeight.delete(mesh);
778
- // 只有正好卡在边界上才需要重算,否则滚动的 min/max 依然成立
779
- if (rec.min === this._heightMin || rec.max === this._heightMax)
780
- this._recomputeHeightRange();
781
- else
782
- this._pushHeightRange();
783
- }
784
- /** 数据集整体位移(换高程偏移 / 重新布局)后,缓存的世界高度全部失效 */
785
- _refreshAllHeights() {
786
- if (this._meshHeight.size === 0)
787
- return;
788
- const meshes = [...this._meshHeight.keys()];
789
- this._meshHeight.clear();
790
- this._heightMin = Infinity;
791
- this._heightMax = -Infinity;
792
- for (const mesh of meshes)
793
- this._addMeshHeight(mesh);
794
- this._pushHeightRange();
795
- }
796
- _recomputeHeightRange() {
797
- let min = Infinity;
798
- let max = -Infinity;
799
- for (const h of this._meshHeight.values()) {
800
- if (h.min < min)
801
- min = h.min;
802
- if (h.max > max)
803
- max = h.max;
804
- }
805
- this._heightMin = min;
806
- this._heightMax = max;
807
- this._pushHeightRange();
808
- }
809
- _pushHeightRange() {
810
- let min = this._heightMin;
811
- let max = this._heightMax;
812
- if (!Number.isFinite(min) || !Number.isFinite(max) || max <= min) {
813
- min = 0;
814
- max = 100;
815
- }
816
- this.uniforms.uHeightMin.value = min;
817
- this.uniforms.uHeightMax.value = max;
818
- }
819
- /** 建 SMID → 网格 反查表,逐单体改样式时才知道该重算哪几块 */
820
- _indexMeshSmids(mesh) {
821
- const pickInfo = mesh.userData.geoRef?.pickInfo;
822
- if (!pickInfo)
823
- return;
824
- for (const key of Object.keys(pickInfo)) {
825
- const smid = Number(key);
826
- if (!Number.isFinite(smid) || smid < 0)
827
- continue;
828
- let set = this._smidMeshes.get(smid);
829
- if (!set)
830
- this._smidMeshes.set(smid, (set = new Set()));
831
- set.add(mesh);
832
- }
833
- }
834
- _unindexMeshSmids(mesh) {
835
- const pickInfo = mesh.userData.geoRef?.pickInfo;
836
- if (!pickInfo)
837
- return;
838
- for (const key of Object.keys(pickInfo)) {
839
- const smid = Number(key);
840
- const set = this._smidMeshes.get(smid);
841
- if (!set)
842
- continue;
843
- set.delete(mesh);
844
- if (set.size === 0)
845
- this._smidMeshes.delete(smid);
846
- }
847
- }
848
- /**
849
- * 全局棱线开关。
850
- *
851
- * 数据自带棱线包(primitiveType 13)时直接用,成本为零;
852
- * 没有的话才用 EdgesGeometry 现算 —— 那是 CPU 端的活,几十万面一次性建会卡住主线程
853
- * (实测 20 个网格 1.9s),所以按 8ms 一帧切片,边建边出。
854
- *
855
- * 棱线本身用「屏幕空间等宽线」画(见 EdgeLines.ts):WebGL 的 lineWidth 基本无效,
856
- * 只有摊成面向屏幕的四边形,`edgeWidth` 才能真正调粗。
857
- */
858
- setEdges(enabled) {
859
- this._edgesEnabled = enabled;
860
- this.style.edges = enabled;
861
- // 数据自带的棱线:直接切可见性
862
- for (const entry of this.datasets.values()) {
863
- entry.root.traverse((o) => {
864
- const ud = o.userData;
865
- if (ud.isEdgeLines) {
866
- o.visible = enabled;
867
- this._styleEdgeLine(o);
868
- }
869
- });
870
- }
871
- this._syncEdgeStyle();
872
- if (!enabled) {
873
- for (const p of this._pickables.values())
874
- this._removeEdges(p.mesh);
875
- this.map?.triggerRepaint();
876
- return;
877
- }
878
- const pending = [...this._pickables.values()]
879
- .map((p) => p.mesh)
880
- .filter((m) => {
881
- const ud = m.userData;
882
- return !ud.edges && !ud.nativeEdges;
883
- });
884
- let i = 0;
885
- const step = () => {
886
- const t0 = performance.now();
887
- while (i < pending.length && performance.now() - t0 < 8)
888
- this._ensureEdges(pending[i++]);
889
- if (i < pending.length)
890
- setTimeout(step, 0);
891
- else
892
- this.map?.triggerRepaint();
893
- };
894
- step();
895
- }
896
- /** 让全局棱线材质跟上 style.edgeColor / edgeWidth / edgeOpacity */
897
- _syncEdgeStyle() {
898
- const s = this.style;
899
- const u = this.edgeMaterial.uniforms;
900
- u.uColor.value.set(s.edgeColor);
901
- u.uWidth.value = Math.max(0.1, s.edgeWidth ?? 1.2);
902
- u.uOpacity.value =
903
- Math.min(1, Math.max(0, s.edgeOpacity ?? 0.45)) * Math.min(1, Math.max(0.05, s.opacity ?? 1));
904
- }
905
- /** 让数据自带的棱线跟着 style.edgeColor / opacity 走(这些是 LineBasicMaterial,改不了粗细) */
906
- _styleEdgeLine(lines) {
907
- const mat = lines.material;
908
- if (!mat || !mat.color)
909
- return;
910
- mat.color.set(this.style.edgeColor);
911
- const baseOpacity = this.style.edgeOpacity ?? 0.45;
912
- mat.opacity = baseOpacity * Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
913
- mat.transparent = true;
914
- mat.polygonOffset = true;
915
- mat.polygonOffsetFactor = -1;
916
- mat.polygonOffsetUnits = -1;
917
- mat.needsUpdate = true;
918
- }
919
- _refreshEdgeLines() {
920
- this._syncEdgeStyle();
921
- for (const entry of this.datasets.values()) {
922
- entry.root.traverse((o) => {
923
- if (o.userData.isEdgeLines)
924
- this._styleEdgeLine(o);
925
- });
926
- }
927
- }
928
- _ensureEdges(mesh) {
929
- const ud = mesh.userData;
930
- if (ud.edges || ud.nativeEdges || !mesh.geometry)
931
- return;
932
- const indexCount = mesh.geometry.index?.count ?? 0;
933
- // 棱线是 CPU 端建的,超大几何直接跳过,避免拖慢加载
934
- if (indexCount > 120000)
935
- return;
936
- const source = new THREE.EdgesGeometry(mesh.geometry, 40);
937
- const attr = source.getAttribute('position');
938
- const segments = attr ? attr.array : null;
939
- source.dispose();
940
- if (!segments || segments.length < 6)
941
- return;
942
- const lines = new THREE.Mesh(buildThickLineGeometry(segments), this.edgeMaterial);
943
- lines.renderOrder = 2;
944
- // 屏幕空间外扩后可能超出原包围球,关掉剔除更稳
945
- lines.frustumCulled = false;
946
- lines.raycast = () => { };
947
- lines.userData.isThickEdge = true;
948
- ud.edges = lines;
949
- mesh.add(lines); // 作为子节点,自动继承 mesh 的世界变换
950
- }
951
- _removeEdges(mesh) {
952
- const ud = mesh.userData;
953
- const lines = ud.edges;
954
- if (!lines)
955
- return;
956
- mesh.remove(lines);
957
- lines.geometry?.dispose();
958
- ud.edges = null;
959
- }
960
- /* ---------------------------------------------------------------- 拾取 */
961
- _onClick = (e) => {
962
- if (!this.pickable)
963
- return;
964
- const hit = this.pickFromEvent(e);
965
- if (!hit) {
966
- this.select(null);
967
- return;
968
- }
969
- this.select(hit);
970
- this.onPick(hit);
971
- };
972
- /** 事件对象(宿主 MouseEvent 或原生事件)→ 拾取结果 */
973
- pickFromEvent(e) {
974
- const canvas = this.map?.getCanvas();
975
- if (!canvas)
976
- return null;
977
- const rect = canvas.getBoundingClientRect();
978
- const oe = e.originalEvent ?? e;
979
- return this.pick(oe.clientX - rect.left, oe.clientY - rect.top);
980
- }
981
- /** 获取指定屏幕像素对应的场景射线(用于高精度求交与穿透拾取) */
982
- getRayAt(x, y) {
983
- if (!this.camera || !this.map)
984
- return null;
985
- const canvas = this.map.getCanvas();
986
- const w = canvas.clientWidth || canvas.width;
987
- const h = canvas.clientHeight || canvas.height;
988
- if (!w || !h)
989
- return null;
990
- const ndcX = (x / w) * 2 - 1;
991
- const ndcY = -(y / h) * 2 + 1;
992
- const near = new THREE.Vector3(ndcX, ndcY, -1).unproject(this.camera);
993
- const far = new THREE.Vector3(ndcX, ndcY, 1).unproject(this.camera);
994
- const dir = far.clone().sub(near).normalize();
995
- return new THREE.Ray(near, dir);
996
- }
997
- _intersectMeshAtRay(mesh, ray, instanceIndex) {
998
- const raycaster = new THREE.Raycaster();
999
- raycaster.ray.copy(ray);
1000
- const hits = raycaster.intersectObject(mesh, false);
1001
- if (!hits.length)
1002
- return null;
1003
- if (instanceIndex != null) {
1004
- const match = hits.find((h) => h.instanceId === instanceIndex);
1005
- return match ? match.point : hits[0].point;
1006
- }
1007
- return hits[0].point;
1008
- }
1009
- _buildPickResult(hit, ref, exactPoint, ray) {
1010
- const ud = ref.mesh.userData;
1011
- let smid = null;
1012
- let batchId = hit.batchId;
1013
- let instanceIndex = hit.instanceIndex;
1014
- let geoRef = ud.geoRef;
1015
- let segments = [];
1016
- if (instanceIndex != null) {
1017
- const feature = this._instanceFeature(ref.mesh, geoRef, instanceIndex);
1018
- if (!feature)
1019
- return null;
1020
- smid = feature.smid;
1021
- batchId = feature.batchId;
1022
- }
1023
- else {
1024
- geoRef = ud.geoByBatchId?.get(hit.batchId) ?? ud.geoRef;
1025
- const resolved = resolveFeature(geoRef, hit.batchId);
1026
- smid = resolved.smid;
1027
- segments = resolved.segments;
1028
- }
1029
- if (this._isFeatureHidden(smid, batchId))
1030
- return null;
1031
- let box = null;
1032
- let totalTriangles = 0;
1033
- if (instanceIndex != null) {
1034
- box = this._instanceBox(ref.mesh, instanceIndex);
1035
- totalTriangles = ref.mesh.geometry.index ? Math.round(ref.mesh.geometry.index.count / 3) : 0;
1036
- }
1037
- else if (smid != null && this._smidMeshes.has(smid)) {
1038
- const meshes = this._smidMeshes.get(smid);
1039
- let minX = Infinity;
1040
- let minY = Infinity;
1041
- let minZ = Infinity;
1042
- let maxX = -Infinity;
1043
- let maxY = -Infinity;
1044
- let maxZ = -Infinity;
1045
- let count = 0;
1046
- for (const m of meshes) {
1047
- const mUd = m.userData;
1048
- const mPick = mUd.geoRef?.pickInfo;
1049
- const entry = mPick?.[smid] ??
1050
- mPick?.[String(smid)];
1051
- const bId = entry ? pickEntryId(entry) : -1;
1052
- const mSegs = entry?.segments ?? (bId >= 0 ? resolveFeature(mUd.geoRef, bId).segments : []);
1053
- const mBox = measureSegments(m.geometry, mSegs);
1054
- if (mBox) {
1055
- if (mBox.minX < minX)
1056
- minX = mBox.minX;
1057
- if (mBox.minY < minY)
1058
- minY = mBox.minY;
1059
- if (mBox.minZ < minZ)
1060
- minZ = mBox.minZ;
1061
- if (mBox.maxX > maxX)
1062
- maxX = mBox.maxX;
1063
- if (mBox.maxY > maxY)
1064
- maxY = mBox.maxY;
1065
- if (mBox.maxZ > maxZ)
1066
- maxZ = mBox.maxZ;
1067
- count += mBox.count;
1068
- totalTriangles += mBox.triangles;
1069
- }
1070
- }
1071
- if (count > 0) {
1072
- box = { minX, minY, minZ, maxX, maxY, maxZ, count, triangles: totalTriangles };
1073
- }
1074
- }
1075
- if (!box) {
1076
- box = measureSegments(ref.mesh.geometry, segments);
1077
- if (!box && ref.mesh.geometry) {
1078
- if (!ref.mesh.geometry.boundingBox)
1079
- ref.mesh.geometry.computeBoundingBox();
1080
- const bb = ref.mesh.geometry.boundingBox;
1081
- if (bb) {
1082
- box = {
1083
- minX: bb.min.x,
1084
- maxX: bb.max.x,
1085
- minY: bb.min.y,
1086
- maxY: bb.max.y,
1087
- minZ: bb.min.z,
1088
- maxZ: bb.max.z,
1089
- count: 0,
1090
- triangles: 0,
1091
- };
1092
- }
1093
- }
1094
- }
1095
- const geometry = ref.mesh.geometry;
1096
- const triangles = totalTriangles > 0 ? totalTriangles : (geometry.index ? Math.round(geometry.index.count / 3) : 0);
1097
- const attributes = buildAttributes({ smid, box, triangles, floorHeight: this.style.floorHeight });
1098
- let centerLngLat = null;
1099
- if (box && this.sceneAnchor) {
1100
- const v = new THREE.Vector3((box.minX + box.maxX) / 2, (box.minY + box.maxY) / 2, (box.minZ + box.maxZ) / 2);
1101
- v.applyMatrix4(ref.mesh.matrixWorld);
1102
- const s = this.sceneAnchor.meterInMercator;
1103
- centerLngLat = this._lngLatFromWorld(v.x * s + this.sceneAnchor.x, -v.y * s + this.sceneAnchor.y);
1104
- }
1105
- // 精确命中点求交计算
1106
- let hitPoint = exactPoint ?? null;
1107
- if (!hitPoint && ray) {
1108
- hitPoint = this._intersectMeshAtRay(ref.mesh, ray, instanceIndex);
1109
- }
1110
- let point = null;
1111
- let lngLat = null;
1112
- let altitude = null;
1113
- if (hitPoint && this.sceneAnchor) {
1114
- const off = smid != null ? this._featureStyleManager.getFeatureEffectiveOffset(smid) : {};
1115
- const trans = off.translation;
1116
- const elev = off.elevation;
1117
- const finalPos = hitPoint.clone();
1118
- if (trans)
1119
- finalPos.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
1120
- else if (elev)
1121
- finalPos.z += elev;
1122
- point = [finalPos.x, finalPos.y, finalPos.z];
1123
- const s = this.sceneAnchor.meterInMercator;
1124
- lngLat = this._lngLatFromWorld(finalPos.x * s + this.sceneAnchor.x, -finalPos.y * s + this.sceneAnchor.y);
1125
- altitude = finalPos.z + (this.sceneAnchor.altitude ?? 0);
1126
- }
1127
- else if (centerLngLat && box && this.sceneAnchor) {
1128
- lngLat = centerLngLat;
1129
- altitude = (box.minZ + box.maxZ) / 2 + (this.sceneAnchor.altitude ?? 0);
1130
- }
1131
- return {
1132
- datasetId: ref.datasetId,
1133
- datasetName: this.datasets.get(ref.datasetId)?.name ?? ref.datasetId,
1134
- objectId: hit.objectId,
1135
- batchId,
1136
- instanceIndex,
1137
- skeletonName: ref.skeletonName,
1138
- centerLngLat,
1139
- point,
1140
- lngLat,
1141
- altitude,
1142
- ...attributes,
1143
- mesh: ref.mesh,
1144
- };
1145
- }
1146
- /** 按 canvas 左上角的 CSS 像素坐标拾取。 */
1147
- pick(x, y) {
1148
- if (!this.picker || !this.scene || !this.camera || !this.sceneAnchor)
1149
- return null;
1150
- const hit = this.picker.pick(this.scene, this.camera, x, y);
1151
- if (!hit)
1152
- return null;
1153
- const ref = this._pickables.get(hit.objectId);
1154
- if (!ref)
1155
- return null;
1156
- const ray = this.getRayAt(x, y);
1157
- return this._buildPickResult(hit, ref, undefined, ray ?? undefined);
1158
- }
1159
- /**
1160
- * 拾取指定屏幕像素对应的世界坐标与场景位置。
1161
- * 优先与命中的建筑网格精确求交;未命中建筑时若视线与地表相交则返回地面交点。
1162
- */
1163
- pickPosition(x, y) {
1164
- if (!this.camera || !this.sceneAnchor || !this.map)
1165
- return null;
1166
- const hit = this.pick(x, y);
1167
- if (hit?.point && hit?.lngLat && hit.altitude != null) {
1168
- return {
1169
- lngLat: hit.lngLat,
1170
- altitude: hit.altitude,
1171
- point: hit.point,
1172
- };
1173
- }
1174
- const ray = this.getRayAt(x, y);
1175
- if (!ray)
1176
- return null;
1177
- const groundPlane = new THREE.Plane(new THREE.Vector3(0, 0, 1), 0);
1178
- const target = new THREE.Vector3();
1179
- const intersect = ray.intersectPlane(groundPlane, target);
1180
- if (intersect) {
1181
- const s = this.sceneAnchor.meterInMercator;
1182
- const lngLat = this._lngLatFromWorld(intersect.x * s + this.sceneAnchor.x, -intersect.y * s + this.sceneAnchor.y);
1183
- const altitude = intersect.z + (this.sceneAnchor.altitude ?? 0);
1184
- return {
1185
- lngLat,
1186
- altitude,
1187
- point: [intersect.x, intersect.y, intersect.z],
1188
- };
1189
- }
1190
- return null;
1191
- }
1192
- /**
1193
- * 沿视线方向穿透拾取多个单体,按由近至远排序返回。
1194
- */
1195
- drillPick(x, y, limit = 10) {
1196
- if (!this.sceneAnchor || !this.camera || !this.map)
1197
- return [];
1198
- const ray = this.getRayAt(x, y);
1199
- if (!ray)
1200
- return [];
1201
- const raycaster = new THREE.Raycaster();
1202
- raycaster.ray.copy(ray);
1203
- const meshes = [...this._pickables.values()]
1204
- .map((p) => p.mesh)
1205
- .filter((m) => {
1206
- let curr = m;
1207
- while (curr) {
1208
- if (!curr.visible)
1209
- return false;
1210
- curr = curr.parent;
1211
- }
1212
- return true;
1213
- });
1214
- const hits = raycaster.intersectObjects(meshes, false);
1215
- if (!hits.length)
1216
- return [];
1217
- const results = [];
1218
- const seenSmids = new Set();
1219
- const seenBatches = new Set();
1220
- for (const hit of hits) {
1221
- if (results.length >= limit)
1222
- break;
1223
- const mesh = hit.object;
1224
- const ud = mesh.userData;
1225
- if (ud.pickId == null)
1226
- continue;
1227
- const ref = this._pickables.get(ud.pickId);
1228
- if (!ref)
1229
- continue;
1230
- let smid = null;
1231
- let batchId = -1;
1232
- let instanceIndex = hit.instanceId;
1233
- if (instanceIndex != null) {
1234
- const feature = this._instanceFeature(mesh, ud.geoRef, instanceIndex);
1235
- if (!feature)
1236
- continue;
1237
- smid = feature.smid;
1238
- batchId = feature.batchId;
1239
- }
1240
- else if (hit.faceIndex != null) {
1241
- const batchAttr = mesh.geometry.attributes.aBatchId;
1242
- if (batchAttr) {
1243
- const idx = mesh.geometry.index ? mesh.geometry.index.getX(hit.faceIndex * 3) : hit.faceIndex * 3;
1244
- batchId = Math.round(batchAttr.getX(idx));
1245
- const geoRef = ud.geoByBatchId?.get(batchId) ?? ud.geoRef;
1246
- const resolved = resolveFeature(geoRef, batchId);
1247
- smid = resolved.smid;
1248
- }
1249
- }
1250
- if (smid != null) {
1251
- if (seenSmids.has(smid))
1252
- continue;
1253
- seenSmids.add(smid);
1254
- }
1255
- else {
1256
- const key = `${ud.pickId}:${batchId}:${instanceIndex ?? -1}`;
1257
- if (seenBatches.has(key))
1258
- continue;
1259
- seenBatches.add(key);
1260
- }
1261
- if (this._isFeatureHidden(smid, batchId))
1262
- continue;
1263
- const pickHit = {
1264
- objectId: ud.pickId,
1265
- batchId,
1266
- instanceIndex,
1267
- };
1268
- const res = this._buildPickResult(pickHit, ref, hit.point);
1269
- if (res)
1270
- results.push(res);
1271
- }
1272
- return results;
1273
- }
1274
- /**
1275
- * 获取指定 SMID 单体的经纬度包围盒 [[minLng, minLat], [maxLng, maxLat]]。
1276
- * 若模型尚未加载到场景中或无此单体则返回 null。
1277
- */
1278
- getFeatureBounds(smid) {
1279
- if (!this.sceneAnchor)
1280
- return null;
1281
- let minLng = Infinity;
1282
- let maxLng = -Infinity;
1283
- let minLat = Infinity;
1284
- let maxLat = -Infinity;
1285
- let found = false;
1286
- for (const ref of this._pickables.values()) {
1287
- const mesh = ref.mesh;
1288
- const ud = mesh.userData;
1289
- const geoRef = ud.geoRef;
1290
- const pickInfo = geoRef?.pickInfo;
1291
- if (!pickInfo)
1292
- continue;
1293
- let box = null;
1294
- if (mesh.isInstancedMesh) {
1295
- let lookup = ud.instanceLookup;
1296
- if (!lookup) {
1297
- lookup = buildInstanceLookup(pickInfo);
1298
- ud.instanceLookup = lookup;
1299
- }
1300
- for (const [instIdx, feat] of lookup) {
1301
- if (feat.smid === smid) {
1302
- box = this._instanceBox(mesh, instIdx);
1303
- break;
1304
- }
1305
- }
1306
- }
1307
- else {
1308
- const infoMap = pickInfo;
1309
- const entry = infoMap[smid] ?? infoMap[String(smid)];
1310
- if (entry) {
1311
- const bId = pickEntryId(entry);
1312
- const resolved = resolveFeature(geoRef, bId);
1313
- box = measureSegments(mesh.geometry, resolved.segments);
1314
- if (!box && mesh.geometry) {
1315
- if (!mesh.geometry.boundingBox)
1316
- mesh.geometry.computeBoundingBox();
1317
- const bb = mesh.geometry.boundingBox;
1318
- if (bb) {
1319
- box = {
1320
- minX: bb.min.x,
1321
- maxX: bb.max.x,
1322
- minY: bb.min.y,
1323
- maxY: bb.max.y,
1324
- minZ: bb.min.z,
1325
- maxZ: bb.max.z,
1326
- count: 0,
1327
- triangles: 0,
1328
- };
1329
- }
1330
- }
1331
- }
1332
- }
1333
- if (box) {
1334
- found = true;
1335
- const corners = [
1336
- new THREE.Vector3(box.minX, box.minY, box.minZ),
1337
- new THREE.Vector3(box.maxX, box.minY, box.minZ),
1338
- new THREE.Vector3(box.minX, box.maxY, box.minZ),
1339
- new THREE.Vector3(box.maxX, box.maxY, box.minZ),
1340
- new THREE.Vector3(box.minX, box.minY, box.maxZ),
1341
- new THREE.Vector3(box.maxX, box.minY, box.maxZ),
1342
- new THREE.Vector3(box.minX, box.maxY, box.maxZ),
1343
- new THREE.Vector3(box.maxX, box.maxY, box.maxZ),
1344
- ];
1345
- const off = smid != null ? this._featureStyleManager.getFeatureEffectiveOffset(smid) : {};
1346
- const trans = off.translation;
1347
- const elev = off.elevation;
1348
- for (const c of corners) {
1349
- if (trans)
1350
- c.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
1351
- else if (elev)
1352
- c.z += elev;
1353
- c.applyMatrix4(mesh.matrixWorld);
1354
- const s = this.sceneAnchor.meterInMercator;
1355
- const [lng, lat] = this._lngLatFromWorld(c.x * s + this.sceneAnchor.x, -c.y * s + this.sceneAnchor.y);
1356
- if (lng < minLng)
1357
- minLng = lng;
1358
- if (lng > maxLng)
1359
- maxLng = lng;
1360
- if (lat < minLat)
1361
- minLat = lat;
1362
- if (lat > maxLat)
1363
- maxLat = lat;
1364
- }
1365
- }
1366
- }
1367
- if (!found)
1368
- return null;
1369
- return [
1370
- [minLng, minLat],
1371
- [maxLng, maxLat],
1372
- ];
1373
- }
1374
- /**
1375
- * 视角聚焦飞向指定 SMID 单体。
1376
- * @param smid 单体 SMID
1377
- * @param options 传给宿主 map.fitBounds 的参数(如 padding, maxZoom, duration 等)
1378
- * @returns 是否成功找到并触发飞向
1379
- */
1380
- flyToFeature(smid, options) {
1381
- const bounds = this.getFeatureBounds(smid);
1382
- if (!bounds || !this.map)
1383
- return false;
1384
- const mapAny = this.map;
1385
- if (typeof mapAny.fitBounds === 'function') {
1386
- mapAny.fitBounds(bounds, {
1387
- padding: 50,
1388
- maxZoom: 19,
1389
- duration: 1200,
1390
- ...options,
1391
- });
1392
- return true;
1393
- }
1394
- return false;
1395
- }
1396
- /** 实例序号 → 所属单体(按网格缓存反查表,实例多的数据不用每次重建) */
1397
- _instanceFeature(mesh, geoRef, instanceIndex) {
1398
- const ud = mesh.userData;
1399
- if (!ud.instanceLookup)
1400
- ud.instanceLookup = buildInstanceLookup(geoRef?.pickInfo);
1401
- return ud.instanceLookup.get(instanceIndex) ?? null;
1402
- }
1403
- /** 实例化几何的单体包围盒:base mesh 的包围盒套上该实例的矩阵 */
1404
- _instanceBox(mesh, instanceIndex) {
1405
- const im = mesh;
1406
- if (!im.isInstancedMesh)
1407
- return null;
1408
- const geometry = mesh.geometry;
1409
- if (!geometry.boundingBox)
1410
- geometry.computeBoundingBox();
1411
- const bb = geometry.boundingBox;
1412
- if (!bb)
1413
- return null;
1414
- const m = new THREE.Matrix4();
1415
- im.getMatrixAt(instanceIndex, m);
1416
- const box = bb.clone().applyMatrix4(m);
1417
- return {
1418
- minX: box.min.x,
1419
- minY: box.min.y,
1420
- minZ: box.min.z,
1421
- maxX: box.max.x,
1422
- maxY: box.max.y,
1423
- maxZ: box.max.z,
1424
- count: geometry.getAttribute('position')?.count ?? 0,
1425
- triangles: geometry.index ? Math.round(geometry.index.count / 3) : 0,
1426
- };
1427
- }
1428
- /** 选中/取消选中单体。`silent` 为 true 时不回调 onPick。 */
1429
- select(result, { silent = false } = {}) {
1430
- // 清掉上一次
1431
- if (this._selection) {
1432
- for (const mesh of this._selectedMeshes) {
1433
- const mat = styledMaterialOf(mesh);
1434
- if (mat) {
1435
- mat.userData.uSelBatch.value = -1;
1436
- mat.userData.uSelWhole.value = 0;
1437
- }
1438
- }
1439
- this._selectedMeshes = [];
1440
- this._clearSelectionDecoration();
1441
- this._selection = null;
1442
- }
1443
- if (!result) {
1444
- this.uniforms.uSelMix.value = 0;
1445
- this.uniforms.uDim.value = 0;
1446
- this.outlineMaterial.uniforms.uSelBatch.value = -1;
1447
- this.outlineMaterial.uniforms.uSelWhole.value = 0;
1448
- this.outlineMaterial.uniforms.uElevation.value = 0;
1449
- this.map?.triggerRepaint();
1450
- if (!silent)
1451
- this.onPick(null);
1452
- return;
1453
- }
1454
- this._selection = result;
1455
- const targetMeshes = new Set();
1456
- if (result.smid != null) {
1457
- const smidSet = this._smidMeshes.get(result.smid);
1458
- if (smidSet) {
1459
- for (const m of smidSet)
1460
- targetMeshes.add(m);
1461
- }
1462
- }
1463
- targetMeshes.add(result.mesh);
1464
- for (const mesh of targetMeshes) {
1465
- const mat = styledMaterialOf(mesh);
1466
- if (!mat)
1467
- continue;
1468
- let batchId = result.batchId;
1469
- if (result.instanceIndex != null && mesh === result.mesh) {
1470
- batchId = result.batchId;
1471
- }
1472
- else if (result.smid != null) {
1473
- const ud = mesh.userData;
1474
- const pickInfo = ud.geoRef?.pickInfo;
1475
- const entry = pickInfo?.[result.smid] ??
1476
- pickInfo?.[String(result.smid)];
1477
- const bId = entry ? pickEntryId(entry) : -1;
1478
- batchId = bId >= 0 ? bId : (mesh === result.mesh ? result.batchId : -1);
1479
- }
1480
- mat.userData.uSelBatch.value = batchId;
1481
- // 没有单体化信息时整块高亮,否则会出现「点中了但什么都没亮、其他全暗了」
1482
- mat.userData.uSelWhole.value = batchId < 0 ? 1 : 0;
1483
- this._selectedMeshes.push(mesh);
1484
- }
1485
- this.uniforms.uSelMix.value = 1;
1486
- this.uniforms.uDim.value = this.style.dimOthers ? 1 : 0;
1487
- if (this.style.outline) {
1488
- if ((this.style.outlineStyle ?? 'edges') === 'edges') {
1489
- this._addSelectionEdges(result, targetMeshes);
1490
- }
1491
- else {
1492
- this._addSelectionHull(result, targetMeshes);
1493
- }
1494
- }
1495
- this.map?.triggerRepaint();
1496
- }
1497
- /**
1498
- * 选中单体的棱线:只挑这个单体自己的三角面算折边,再画成屏幕空间等宽线。
1499
- * 抽出来的线段数很少(一栋楼通常几百到几千条),所以选中开销可以忽略。
1500
- */
1501
- _addSelectionEdges(result, meshes) {
1502
- this._syncSelectionStyle();
1503
- for (const mesh of meshes) {
1504
- const ud = mesh.userData;
1505
- const geoRef = ud.geoRef;
1506
- let segments = [];
1507
- if (mesh === result.mesh && result.instanceIndex != null) {
1508
- // 实例化几何没有逐单体的顶点区间,整个 base mesh 就是这一栋的外形
1509
- segments = [{ vertexOffset: 0, vertexCount: mesh.geometry.getAttribute('position')?.count ?? 0 }];
1510
- }
1511
- else if (result.smid != null) {
1512
- const pickInfo = geoRef?.pickInfo;
1513
- const entry = pickInfo?.[result.smid] ??
1514
- pickInfo?.[String(result.smid)];
1515
- const bId = entry ? pickEntryId(entry) : -1;
1516
- segments = entry?.segments ?? (bId >= 0 ? resolveFeature(geoRef, bId).segments : []);
1517
- }
1518
- else if (mesh === result.mesh) {
1519
- const localGeo = ud.geoByBatchId?.get(result.batchId) ?? geoRef;
1520
- segments = resolveFeature(localGeo, result.batchId).segments;
1521
- }
1522
- const pairs = buildFeatureEdgeSegments(mesh.geometry, segments);
1523
- if (pairs && pairs.length > 0) {
1524
- const lines = new THREE.Mesh(buildThickLineGeometry(pairs), this.selectionEdgeMaterial);
1525
- lines.renderOrder = 4;
1526
- // 屏幕空间外扩后可能超出原包围球,关掉剔除更稳
1527
- lines.frustumCulled = false;
1528
- lines.raycast = () => { };
1529
- lines.userData.isSelectionEdge = true;
1530
- if (mesh === result.mesh && result.instanceIndex != null) {
1531
- this._applyInstanceTransform(lines, result);
1532
- }
1533
- mesh.add(lines); // 作为子节点,直接继承世界变换
1534
- this._selectionEdgeMeshes.push(lines);
1535
- }
1536
- }
1537
- if (this._selectionEdgeMeshes.length === 0) {
1538
- // 抽不出折边(退化几何 / 没有任何夹角)时退回外扩轮廓,至少保证选中可见
1539
- this._addSelectionHull(result, meshes);
1540
- return;
1541
- }
1542
- this._selectionEdges = this._selectionEdgeMeshes[0] ?? null;
1543
- }
1544
- /** 实例化几何:把某个实例的矩阵套到一个普通 Mesh/子节点上(子节点不参与父级的 instanceMatrix) */
1545
- _applyInstanceTransform(obj, result) {
1546
- const im = result.mesh;
1547
- if (result.instanceIndex == null || !im.isInstancedMesh)
1548
- return;
1549
- const m = new THREE.Matrix4();
1550
- im.getMatrixAt(result.instanceIndex, m);
1551
- m.decompose(obj.position, obj.quaternion, obj.scale);
1552
- }
1553
- /** 旧的描边画法:背面外扩法,整块几何一起上,靠 uSelBatch 只显示选中那栋 */
1554
- _addSelectionHull(result, meshes) {
1555
- const instanced = result.instanceIndex != null;
1556
- // 实例化:轮廓是这个实例的单独一份副本(普通 Mesh),所以整份都画;
1557
- // 顶点区间型:整块几何一起画,靠 uSelBatch 只显示选中那栋。
1558
- this.outlineMaterial.uniforms.uSelBatch.value = instanced ? -1 : result.batchId;
1559
- this.outlineMaterial.uniforms.uSelWhole.value = instanced || result.batchId < 0 ? 1 : 0;
1560
- this.outlineMaterial.uniforms.uElevation.value = instanced
1561
- ? this._selectionElevation(result)
1562
- : this.style.selectElevation ?? 0;
1563
- for (const mesh of meshes) {
1564
- const mat = styledMaterialOf(mesh);
1565
- if (mat?.userData?.uBatchTex) {
1566
- this.outlineMaterial.uniforms.uBatchTex.value = mat.userData.uBatchTex.value;
1567
- this.outlineMaterial.uniforms.uBatchTexWidth.value = mat.userData.uBatchTexWidth.value;
1568
- this.outlineMaterial.uniforms.uBatchTexActive.value = mat.userData.uBatchTexActive.value;
1569
- }
1570
- const outline = new THREE.Mesh(mesh.geometry, this.outlineMaterial);
1571
- outline.renderOrder = 3;
1572
- outline.frustumCulled = false;
1573
- outline.raycast = () => { };
1574
- if (mesh === result.mesh && result.instanceIndex != null) {
1575
- this._applyInstanceTransform(outline, result);
1576
- }
1577
- mesh.add(outline); // 作为子节点,直接继承世界变换
1578
- this._outlineMeshes.push(outline);
1579
- }
1580
- this._outlineMesh = this._outlineMeshes[0] ?? null;
1581
- }
1582
- /** 只改 uniform:颜色 / 粗细 / 拔高,拖着滑块实时生效,不重建几何 */
1583
- _syncSelectionStyle() {
1584
- const s = this.style;
1585
- const u = this.selectionEdgeMaterial.uniforms;
1586
- u.uColor.value.set(s.outlineColor ?? s.selectColor);
1587
- u.uWidth.value = Math.max(0.1, s.outlineWidthPx ?? 3);
1588
- u.uOpacity.value = Math.min(1, Math.max(0.05, s.opacity ?? 1));
1589
- u.uElevation.value = this._selection ? this._selectionElevation(this._selection) : 0;
1590
- // 外扩描边是另一个材质实例,单体状态纹理的开关是建的时候抄过去的。
1591
- // 先选中、再抬高(或反过来)时模板会停留在旧值,这里补一次同步。
1592
- for (const outline of this._outlineMeshes) {
1593
- const parent = outline.parent;
1594
- const ud = parent ? styledMaterialOf(parent)?.userData : null;
1595
- if (ud?.uBatchTex) {
1596
- this.outlineMaterial.uniforms.uBatchTex.value = ud.uBatchTex.value;
1597
- this.outlineMaterial.uniforms.uBatchTexWidth.value = ud.uBatchTexWidth.value;
1598
- this.outlineMaterial.uniforms.uBatchTexActive.value = ud.uBatchTexActive.value;
1599
- }
1600
- }
1601
- }
1602
- /** 选中单体的实际拔高 = 全局选中拔高 + 该单体的独立拔高(与着色器口径一致) */
1603
- _selectionElevation(result) {
1604
- let elev = this.style.selectElevation ?? 0;
1605
- if (result.smid != null) {
1606
- const off = this._featureStyleManager.getFeatureEffectiveOffset(result.smid);
1607
- if (off.elevation)
1608
- elev += off.elevation;
1609
- }
1610
- return elev;
1611
- }
1612
- _clearSelectionDecoration() {
1613
- for (const outline of this._outlineMeshes) {
1614
- outline.parent?.remove(outline);
1615
- }
1616
- this._outlineMeshes = [];
1617
- this._outlineMesh = null;
1618
- for (const edges of this._selectionEdgeMeshes) {
1619
- edges.parent?.remove(edges);
1620
- edges.geometry.dispose();
1621
- }
1622
- this._selectionEdgeMeshes = [];
1623
- this._selectionEdges = null;
1624
- }
1625
- /* ---------------------------------------------------------------- 渲染 */
1626
- /** 等宽线要按设备像素换算线宽:uResolution 用绘图缓冲尺寸,uDpr 是缓冲 / CSS 的比值 */
1627
- _updateLineResolution(canvas) {
1628
- const w = canvas.width || 1;
1629
- const h = canvas.height || 1;
1630
- const cssW = canvas.clientWidth || w;
1631
- this.lineShared.uResolution.value.set(w, h);
1632
- this.lineShared.uDpr.value = Math.max(1, w / cssW);
1633
- }
1634
- /** 从 render 参数里取出 mercator→clip 矩阵,兼容 MapLibre v4 / v5 与 Mapbox 两种签名 */
1635
- _extractMatrix(arg) {
1636
- if (Array.isArray(arg))
1637
- return arg;
1638
- if (arg instanceof Float64Array || arg instanceof Float32Array)
1639
- return arg;
1640
- const o = arg;
1641
- return o?.defaultProjectionData?.mainMatrix ?? o?.modelViewProjectionMatrix ?? null;
1642
- }
1643
- /**
1644
- * 正交模式:把透视 MVP 换成同视角正交 MVP。
1645
- * 返回 null = 本帧保持透视(拿不到独立投影矩阵 / 矩阵不同源),只告警一次。
1646
- */
1647
- _applyOrtho(arg, mvp) {
1648
- const { nearZ, farZ } = extractNearFar(arg);
1649
- const projectionMatrix = extractProjectionMatrix(arg);
1650
- if (!projectionMatrix) {
1651
- this._warnOrtho('render() 输入里没有独立的投影矩阵(需要 MapLibre ≥ 5)');
1652
- return null;
1653
- }
1654
- const built = buildOrthoMVP({ projectionMatrix, mvp: mvp.elements, nearZ, farZ }, {
1655
- distance: this.map?.transform?.cameraToCenterDistance ?? 0,
1656
- fov: this._frameFov(arg),
1657
- aspect: this._canvasAspect(),
1658
- });
1659
- if (!built) {
1660
- this._warnOrtho('拿到的投影矩阵与 MVP 不同源(反推结果不是仿射变换)');
1661
- return null;
1662
- }
1663
- return built.matrix;
1664
- }
1665
- /** 正交不可用只告警一次,不每帧刷屏 */
1666
- _warnOrtho(reason) {
1667
- if (this._orthoWarned)
1668
- return;
1669
- this._orthoWarned = true;
1670
- console.warn(`[s3m] 正交模式不可用:${reason},本帧保持透视`);
1671
- }
1672
- /**
1673
- * 本帧垂直视场角(弧度)。取 render() 入参的 fov(≥5 直接给弧度),
1674
- * 退回 transform.fovInRadians。**不用 transform.fov**:v5 起它是「度」。
1675
- */
1676
- _frameFov(arg) {
1677
- return extractFovRadians(arg, this.map?.transform?.fovInRadians) ?? DEFAULT_FOV;
1678
- }
1679
- _canvasAspect() {
1680
- const canvas = this.map?.getCanvas?.();
1681
- if (!canvas || !canvas.height)
1682
- return 1;
1683
- return canvas.width / canvas.height;
1684
- }
1685
- /**
1686
- * 切换投影模式,下一帧生效。
1687
- *
1688
- * 正交只影响本层:底图瓦片、地形、DOM Marker / 弹窗仍走 MapLibre 的透视
1689
- * 投影,和正交层差 ≈ 楼高 / 相机距(城市尺度几十到上百像素),App 需要
1690
- * 自行把这些层退出。拾取、选中描边、按单体着色/隐藏/抬高不受影响。
1691
- */
1692
- setProjection(mode) {
1693
- const next = mode === 'orthographic' ? 'orthographic' : 'perspective';
1694
- if (this._projection === next)
1695
- return;
1696
- this._projection = next;
1697
- this._orthoWarned = false;
1698
- this.map?.triggerRepaint();
1699
- }
1700
- /** 当前 LOD 切换距离缩放系数 */
1701
- get lodRangeScale() {
1702
- return this._lodRangeScale;
1703
- }
1704
- /**
1705
- * 动态调整 LOD 切换范围缩放系数。
1706
- * scale > 1 时倾向保留粗模(降低瓦片请求与面数);scale < 1 时提前细化加载高精瓦片。
1707
- */
1708
- setLodRangeScale(scale, datasetId) {
1709
- if (scale <= 0)
1710
- return;
1711
- if (datasetId) {
1712
- const entry = this.datasets.get(datasetId);
1713
- if (entry?.tileset)
1714
- entry.tileset.lodRangeScale = scale;
1715
- }
1716
- else {
1717
- this._lodRangeScale = scale;
1718
- for (const entry of this.datasets.values()) {
1719
- if (entry.tileset)
1720
- entry.tileset.lodRangeScale = scale;
1721
- }
1722
- }
1723
- this.map?.triggerRepaint();
1724
- }
1725
- /** 当前投影模式 */
1726
- get projection() {
1727
- return this._projection;
1728
- }
1729
- render(gl, arg) {
1730
- if (!this.renderer || !this.scene || !this.camera || !this.sceneAnchor || !this.map)
1731
- return;
1732
- // 脏标记正常由微任务刷掉;这里再兜一次底,保证「本帧画出来的」一定是最新样式
1733
- this.flushOverrides();
1734
- const matrix = this._extractMatrix(arg);
1735
- if (!matrix)
1736
- return;
1737
- const canvas = this.map.getCanvas();
1738
- const s = this.sceneAnchor.meterInMercator;
1739
- // ENU → Mercator:x 向东,y 向北(墨卡托 y 朝南,所以取负),z 向上
1740
- this._enuMatrix.set(s, 0, 0, this.sceneAnchor.x, 0, -s, 0, this.sceneAnchor.y, 0, 0, s, this.sceneAnchor.z, 0, 0, 0, 1);
1741
- // 正交模式:把本帧的透视投影换成同视角正交投影。视图矩阵从 MapLibre 输入
1742
- // 反推(P⁻¹×MVP),不是自建相机,所以不会和宿主的 pan/zoom/pitch 错位。
1743
- let mvp = new THREE.Matrix4().fromArray(matrix);
1744
- if (this._projection === 'orthographic')
1745
- mvp = this._applyOrtho(arg, mvp) ?? mvp;
1746
- mvp.multiply(this._enuMatrix);
1747
- this.camera.projectionMatrix.copy(mvp);
1748
- this.camera.projectionMatrixInverse.copy(mvp).invert();
1749
- // 相机在场景 ENU 中的位置
1750
- let camEnu = [0, 0, 0];
1751
- try {
1752
- const p = this.map.getFreeCameraOptions?.()?.position;
1753
- if (p) {
1754
- camEnu = [
1755
- (p.x - this.sceneAnchor.x) / s,
1756
- -(p.y - this.sceneAnchor.y) / s,
1757
- (p.z - this.sceneAnchor.z) / s,
1758
- ];
1759
- }
1760
- }
1761
- catch {
1762
- /* FreeCamera 不可用时退回原点,只影响 LOD 精度 */
1763
- }
1764
- // 补齐相机的真实场景坐标与世界矩阵逆矩阵,确保依赖 matrixWorldInverse 的场景正常运作
1765
- this.camera.position.set(camEnu[0], camEnu[1], camEnu[2]);
1766
- this.camera.matrixWorld.setPosition(camEnu[0], camEnu[1], camEnu[2]);
1767
- this.camera.matrixWorldInverse.copy(this.camera.matrixWorld).invert();
1768
- // 更新 3D 物理地表面中心随相机延伸
1769
- this.groundManager.update(this.camera);
1770
- // 同上:弧度视场角。MapLibre v5 起 transform.fov 是「度」,当弧度用会让
1771
- // LOD 屏幕误差算大 20% 以上(提前加载更细的瓦片)
1772
- const fov = this._frameFov(arg);
1773
- let tilesetChanged = false;
1774
- for (const entry of this.datasets.values()) {
1775
- if (!entry.visible || !entry.tileset)
1776
- continue;
1777
- const local = [
1778
- camEnu[0] - entry.offset[0],
1779
- camEnu[1] - entry.offset[1],
1780
- camEnu[2] - entry.offset[2],
1781
- ];
1782
- if (entry.tileset.update(local, fov, canvas.height)) {
1783
- tilesetChanged = true;
1784
- }
1785
- }
1786
- if (this.style.mode === STYLE.DYNAMIC) {
1787
- this.uniforms.uTime.value += 0.016 * (this.style.speed ?? 1);
1788
- }
1789
- this._updateLights();
1790
- // 等宽棱线按设备像素撑开,分辨率每帧同步一次
1791
- this._updateLineResolution(canvas);
1792
- this.renderer.setViewport(0, 0, canvas.width, canvas.height);
1793
- this.renderer.resetState();
1794
- this.renderer.render(this.scene, this.camera);
1795
- // 动态着色 / 瓦片加载中 / 瓦片层级或可见性改变,都需要继续驱动下一帧
1796
- if (this.style.mode === STYLE.DYNAMIC || this.stats.loading > 0 || tilesetChanged)
1797
- this.map.triggerRepaint();
1798
- this._frame++;
1799
- const now = performance.now();
1800
- if (now - this._fps.last > 500) {
1801
- this._fps.value = Math.round((this._frame * 1000) / (now - this._fps.last));
1802
- this._frame = 0;
1803
- this._fps.last = now;
1804
- }
1805
- }
1806
- /* ---------------------------------------------------------------- 只读属性 */
1807
- get fps() {
1808
- return this._fps.value;
1809
- }
1810
- get stats() {
1811
- const total = {
1812
- tiles: 0,
1813
- loading: 0,
1814
- failed: 0,
1815
- meshes: 0,
1816
- vertices: 0,
1817
- triangles: 0,
1818
- datasets: this.datasets.size,
1819
- };
1820
- for (const entry of this.datasets.values()) {
1821
- const s = entry.tileset?.stats;
1822
- if (!s)
1823
- continue;
1824
- total.tiles += s.tiles;
1825
- total.loading += s.loading;
1826
- total.failed += s.failed;
1827
- total.meshes += s.meshes;
1828
- total.vertices += s.vertices;
1829
- total.triangles += s.triangles;
1830
- }
1831
- return total;
1832
- }
1833
- /** 所有数据集合并后的经纬度包围盒 */
1834
- getBounds() {
1835
- let minLon = Infinity;
1836
- let minLat = Infinity;
1837
- let maxLon = -Infinity;
1838
- let maxLat = -Infinity;
1839
- for (const entry of this.datasets.values()) {
1840
- const b = entry.tileset?.config?.geoBounds;
1841
- if (!b)
1842
- continue;
1843
- minLon = Math.min(minLon, b.left);
1844
- maxLon = Math.max(maxLon, b.right);
1845
- minLat = Math.min(minLat, b.bottom);
1846
- maxLat = Math.max(maxLat, b.top);
1847
- }
1848
- if (!isFinite(minLon))
1849
- return null;
1850
- return [
1851
- [minLon, minLat],
1852
- [maxLon, maxLat],
1853
- ];
1854
- }
1855
- dispose() {
1856
- this.onRemove();
1857
- }
1858
- }
1859
- export { STYLE, STYLE_LABELS, SELECT_MODE, SELECT_MODE_LABELS };
1860
- //# sourceMappingURL=S3MLayer.js.map