s3m-three 0.3.7 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/anim/Animator.d.ts +111 -0
- package/dist/anim/Animator.d.ts.map +1 -0
- package/dist/anim/Animator.js +382 -0
- package/dist/anim/Animator.js.map +1 -0
- package/dist/anim/Path.d.ts +30 -0
- package/dist/anim/Path.d.ts.map +1 -0
- package/dist/anim/Path.js +100 -0
- package/dist/anim/Path.js.map +1 -0
- package/dist/anim/easing.d.ts +70 -0
- package/dist/anim/easing.d.ts.map +1 -0
- package/dist/anim/easing.js +170 -0
- package/dist/anim/easing.js.map +1 -0
- package/dist/anim/index.d.ts +6 -0
- package/dist/anim/index.d.ts.map +1 -0
- package/dist/anim/index.js +4 -0
- package/dist/anim/index.js.map +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +218 -39
- package/dist/browser/s3m-three.iife.js +218 -39
- package/dist/core/types.d.ts +20 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/render/EdgeLines.d.ts +68 -14
- package/dist/render/EdgeLines.d.ts.map +1 -1
- package/dist/render/EdgeLines.js +125 -8
- package/dist/render/EdgeLines.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts +62 -1
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +331 -60
- package/dist/render/FeatureStyleManager.js.map +1 -1
- package/dist/render/MergeVertices.d.ts +7 -0
- package/dist/render/MergeVertices.d.ts.map +1 -1
- package/dist/render/MergeVertices.js +10 -1
- package/dist/render/MergeVertices.js.map +1 -1
- package/dist/render/OrthoProjection.d.ts +6 -0
- package/dist/render/OrthoProjection.d.ts.map +1 -1
- package/dist/render/OrthoProjection.js +48 -0
- package/dist/render/OrthoProjection.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +235 -9
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +880 -68
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +28 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +190 -21
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +3 -1
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/docs/ANIMATION.md +534 -0
- package/docs/API.md +2 -0
- package/package.json +1 -1
- package/src/anim/Animator.ts +460 -0
- package/src/anim/Path.ts +107 -0
- package/src/anim/easing.ts +195 -0
- package/src/anim/index.ts +10 -0
- package/src/core/types.ts +10 -0
- package/src/index.ts +16 -0
- package/src/render/EdgeLines.ts +161 -15
- package/src/render/FeatureStyleManager.ts +414 -65
- package/src/render/MergeVertices.ts +11 -1
- package/src/render/OrthoProjection.ts +52 -0
- package/src/render/S3MViewer.ts +1012 -64
- package/src/render/StyleMaterial.ts +211 -22
- package/src/tileset/S3MTileset.ts +3 -1
package/dist/render/S3MViewer.js
CHANGED
|
@@ -27,13 +27,15 @@ import { decodeTextureWithWorker } from '../worker/S3MWorkerClient.js';
|
|
|
27
27
|
import { STYLE, STYLE_LABELS, SELECT_MODE, DEFAULT_STYLE, createSharedUniforms, createOutlineMaterial, } from './StyleMaterial.js';
|
|
28
28
|
import { applyPointCloudStyle, isOwnedPointCloudMaterial } from './PointCloudMaterial.js';
|
|
29
29
|
import { Picker } from './Picker.js';
|
|
30
|
-
import {
|
|
30
|
+
import { Animator } from '../anim/Animator.js';
|
|
31
|
+
import { Path } from '../anim/Path.js';
|
|
32
|
+
import { buildFeatureEdgeSegments, buildPositionBatchMap, buildThickLineGeometry, createLineSharedUniforms, createThickLineMaterial, positionQuantKey, } from './EdgeLines.js';
|
|
31
33
|
import { mergeVerticesByPosition } from './MergeVertices.js';
|
|
32
34
|
import { resolveFeature, measureSegments, buildAttributes, buildInstanceLookup, pickEntryId, } from '../core/FeatureInfo.js';
|
|
33
35
|
import { AttributeQuery } from '../core/AttributeQuery.js';
|
|
34
36
|
import { lngLatFromWorld } from './GeoAdapter.js';
|
|
35
37
|
import { DatasetManager, } from './DatasetManager.js';
|
|
36
|
-
import { FeatureStyleManager, } from './FeatureStyleManager.js';
|
|
38
|
+
import { FeatureStyleManager, resolveRotationMatrix, extractEulerAngles, } from './FeatureStyleManager.js';
|
|
37
39
|
import { GroundManager, } from './GroundManager.js';
|
|
38
40
|
export { GroundManager, DatasetManager, FeatureStyleManager };
|
|
39
41
|
const MAX_PICK_ID = 65534;
|
|
@@ -46,6 +48,31 @@ function styledMaterialOf(mesh) {
|
|
|
46
48
|
function isPointCloudObject(o) {
|
|
47
49
|
return o.isPoints === true;
|
|
48
50
|
}
|
|
51
|
+
/* ------------------------------------------------------------------ 可动画的样式键 */
|
|
52
|
+
/**
|
|
53
|
+
* 能逐帧插值的标量样式键。
|
|
54
|
+
*
|
|
55
|
+
* 判据:写一次只是「改一个 uniform 或一个字段」,不重建几何、不换 program。
|
|
56
|
+
* `mode` / `shading` / `edges` 不在此列——它们是结构变更,只能编排不能插值。
|
|
57
|
+
*/
|
|
58
|
+
const STYLE_SCALAR_KEYS = [
|
|
59
|
+
'opacity',
|
|
60
|
+
'floorHeight',
|
|
61
|
+
'selectOpacity',
|
|
62
|
+
'selectEmissive',
|
|
63
|
+
'selectElevation',
|
|
64
|
+
'dimFactor',
|
|
65
|
+
'texMix',
|
|
66
|
+
'roofThreshold',
|
|
67
|
+
'ambientGain',
|
|
68
|
+
'edgeWidth',
|
|
69
|
+
'edgeOpacity',
|
|
70
|
+
'outlineWidth',
|
|
71
|
+
];
|
|
72
|
+
/** 能逐帧插值的颜色键(在线性空间插值,与 shader 消费端一致) */
|
|
73
|
+
const STYLE_COLOR_KEYS = ['colorA', 'colorB', 'colorC', 'selectColor', 'edgeColor'];
|
|
74
|
+
/** 可动画的单体属性 */
|
|
75
|
+
const FEATURE_PROPS = ['translation', 'elevation', 'opacity', 'color', 'rotation'];
|
|
49
76
|
/* ------------------------------------------------------------------ 图层 */
|
|
50
77
|
export class S3MViewer {
|
|
51
78
|
options;
|
|
@@ -63,14 +90,21 @@ export class S3MViewer {
|
|
|
63
90
|
camera = null;
|
|
64
91
|
picker = null;
|
|
65
92
|
uniforms;
|
|
93
|
+
/** 当前**显示**的样式(动画推进中会逐帧变) */
|
|
66
94
|
style;
|
|
95
|
+
/** 用户想要的样式(getStyle 返回它,动画目标以它为准) */
|
|
96
|
+
_styleTarget;
|
|
67
97
|
outlineMaterial;
|
|
68
98
|
/** 等宽线共享的分辨率 uniform(全局棱线 + 选中棱线共用) */
|
|
69
99
|
lineShared;
|
|
70
|
-
/**
|
|
100
|
+
/** 全局棱线材质(外观 uniform 的持有者,逐网格棱线材质共享它的对象) */
|
|
71
101
|
edgeMaterial;
|
|
72
|
-
/**
|
|
102
|
+
/** 选中棱线材质(同上,选中棱线的逐网格材质共享它的对象) */
|
|
73
103
|
selectionEdgeMaterial;
|
|
104
|
+
/** 全局棱线的外观 uniform:逐网格棱线材质直接复用这些对象 */
|
|
105
|
+
_edgeStyleUniforms;
|
|
106
|
+
/** 选中棱线的外观 uniform */
|
|
107
|
+
_selEdgeStyleUniforms;
|
|
74
108
|
/** 3D 物理地表面 / 基坑开挖管理器 */
|
|
75
109
|
groundManager;
|
|
76
110
|
/** 多数据集生命周期与锚点管理器 */
|
|
@@ -105,11 +139,34 @@ export class S3MViewer {
|
|
|
105
139
|
_selectionEdges = null;
|
|
106
140
|
_selectionEdgeMeshes = [];
|
|
107
141
|
_edgesEnabled = false;
|
|
142
|
+
/** 瓦片淡入通道的自增序号(不用 pickId:pickId 会被回收复用) */
|
|
143
|
+
_tileFadeSeq = 0;
|
|
144
|
+
/** setMode 编排的代次,用来作废被新调用顶掉的旧编排 */
|
|
145
|
+
_modeGen = 0;
|
|
146
|
+
/** 路径运动的规格表:通道 key → 路径 + 偏移 */
|
|
147
|
+
_pathSpecs = new Map();
|
|
148
|
+
/** 路径采样临时对象,避免每帧 new */
|
|
149
|
+
_pathScratch = new Float32Array(3);
|
|
108
150
|
/** 预制棱线缓存(Crease Cache):从外部 source-creases 载入的全局统一 LineSegments,性能最高、视觉最干净 */
|
|
109
151
|
_creaseLines = null;
|
|
152
|
+
/** 数据自带的棱线对象(LineSegments),改棱线颜色时只刷这些,不再全场景 traverse */
|
|
153
|
+
_nativeEdgeLines = new Set();
|
|
154
|
+
/** 点云对象集合:透明度动画时只刷它们,避免每帧遍历全部 pickables */
|
|
155
|
+
_pointClouds = new Set();
|
|
156
|
+
/** 当前材质是否处于 transparent 状态,用来避免逐帧重复切换 program */
|
|
157
|
+
_opacityTransparent = false;
|
|
158
|
+
/**
|
|
159
|
+
* 统一动画调度器。
|
|
160
|
+
*
|
|
161
|
+
* 所有「有过渡」的样式 / 单体 / 棱线变更都经由它推进;公开 API 只写目标值,
|
|
162
|
+
* 真正的逐帧写入由注册在这里的通道完成。
|
|
163
|
+
*/
|
|
164
|
+
animator = new Animator();
|
|
110
165
|
/** 业务属性查询(可选,配了 `attributes` 才有) */
|
|
111
166
|
attributeQuery = null;
|
|
112
167
|
_enuMatrix = new THREE.Matrix4();
|
|
168
|
+
/** 颜色换算用的临时对象,避免每帧 new */
|
|
169
|
+
_colorScratch = new THREE.Color();
|
|
113
170
|
_needsRepaint = true;
|
|
114
171
|
_frame = 0;
|
|
115
172
|
_fps = { value: 0, last: 0 };
|
|
@@ -168,23 +225,37 @@ export class S3MViewer {
|
|
|
168
225
|
this._featureStyleManager = new FeatureStyleManager(() => this.style.floorHeight, () => this._requestRepaint(), () => {
|
|
169
226
|
if (this._selection)
|
|
170
227
|
this._syncSelectionStyle();
|
|
171
|
-
}, () => this.getAllMeshes(), (mesh) => this._applyOverridesToMesh(mesh)
|
|
228
|
+
}, () => this.getAllMeshes(), (mesh) => this._applyOverridesToMesh(mesh),
|
|
229
|
+
// 图层自身是否半透明:全局透明度改走 uOpacity 后,材质 opacity 不再代表它
|
|
230
|
+
() => this._opacityTransparent);
|
|
172
231
|
this.uniforms = createSharedUniforms();
|
|
173
232
|
// 纹理解码优先派给共享解析 Worker(崩溃自动回主线程,见 decodeTextureWithWorker)
|
|
174
233
|
setTextureOffThreadDecode(decodeTextureWithWorker);
|
|
175
234
|
this.style = { ...DEFAULT_STYLE, ...(options.style ?? {}) };
|
|
235
|
+
this._styleTarget = { ...this.style };
|
|
236
|
+
this._opacityTransparent = (this.style.opacity ?? 1) < 0.999;
|
|
237
|
+
this._applyStyleUniforms();
|
|
238
|
+
this._registerAnimChannels();
|
|
239
|
+
// 动画启动必须把按需驱动的帧循环踢起来:MapLibre 空闲时不渲染,
|
|
240
|
+
// 不踢的话动画「卡住不动」,直到别的什么碰巧触发了重绘
|
|
241
|
+
this.animator.onActive = () => this._requestRepaint();
|
|
176
242
|
this.outlineMaterial = createOutlineMaterial(this.uniforms);
|
|
177
243
|
this.lineShared = createLineSharedUniforms();
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
244
|
+
// 棱线外观 uniform:全局棱线材质与逐网格棱线材质**共用同一批对象**,
|
|
245
|
+
// _syncEdgeStyle 改一次,所有棱线(含淡入乘子)同时生效,保持 O(1)。
|
|
246
|
+
this._edgeStyleUniforms = {
|
|
247
|
+
uColor: { value: new THREE.Color(this.style.edgeColor) },
|
|
248
|
+
uWidth: { value: Math.max(0.1, this.style.edgeWidth ?? 1.5) },
|
|
249
|
+
uOpacity: { value: 0.6 },
|
|
250
|
+
};
|
|
251
|
+
this.edgeMaterial = createThickLineMaterial(this.lineShared, {}, this._edgeStyleUniforms);
|
|
252
|
+
this._selEdgeStyleUniforms = {
|
|
253
|
+
uColor: { value: new THREE.Color(this.style.outlineColor ?? this.style.selectColor) },
|
|
254
|
+
uWidth: { value: Math.max(0.1, this.style.outlineWidthPx ?? 3) },
|
|
255
|
+
uOpacity: { value: 1 },
|
|
256
|
+
uElevation: { value: this.style.selectElevation ?? 0 },
|
|
257
|
+
};
|
|
258
|
+
this.selectionEdgeMaterial = createThickLineMaterial(this.lineShared, {}, this._selEdgeStyleUniforms);
|
|
188
259
|
// 关键:style.edges 是个开关,但构造函数从没据此启用过棱线,导致
|
|
189
260
|
// `style: { edges: true }` 形同虚设、_edgesEnabled 永远是 false。
|
|
190
261
|
// 这里按 style 实际值调一次 setEdges,让开关真正生效(与 setEdges 后来的手工调用一致)。
|
|
@@ -208,9 +279,9 @@ export class S3MViewer {
|
|
|
208
279
|
});
|
|
209
280
|
r.autoClear = false;
|
|
210
281
|
r.setPixelRatio(1);
|
|
211
|
-
//
|
|
212
|
-
r.toneMapping = THREE.
|
|
213
|
-
r.toneMappingExposure = 1.0;
|
|
282
|
+
// 允许外部传入色调映射模式,默认使用 THREE.NoToneMapping 还原真实材质与底图通透度,避免 ACESFilmic 压暗阴影与偏色
|
|
283
|
+
r.toneMapping = this.options.toneMapping !== undefined ? this.options.toneMapping : THREE.NoToneMapping;
|
|
284
|
+
r.toneMappingExposure = this.options.toneMappingExposure ?? 1.0;
|
|
214
285
|
return r;
|
|
215
286
|
}
|
|
216
287
|
/** 按 GL 能力开/关 DXT 压缩直传:两个 s3tc 扩展都要在(srgb 缺了贴图会发白失配) */
|
|
@@ -287,6 +358,11 @@ export class S3MViewer {
|
|
|
287
358
|
this._selectionEdges = null;
|
|
288
359
|
this._smidMeshes.clear();
|
|
289
360
|
this._meshHeight.clear();
|
|
361
|
+
// 场景已经清空,还挂着的动画通道(瓦片淡入 / 单体位移)不能再对着死对象写
|
|
362
|
+
this.animator.clear();
|
|
363
|
+
this.animator.unregisterPrefix('tile.');
|
|
364
|
+
this.animator.unregisterPrefix('feature.');
|
|
365
|
+
this._pathSpecs.clear();
|
|
290
366
|
this._heightMin = Infinity;
|
|
291
367
|
this._heightMax = -Infinity;
|
|
292
368
|
this.renderer?.dispose();
|
|
@@ -330,7 +406,14 @@ export class S3MViewer {
|
|
|
330
406
|
* 动态设置光照配置
|
|
331
407
|
*/
|
|
332
408
|
setLight(options) {
|
|
333
|
-
|
|
409
|
+
const normalized = { ...options };
|
|
410
|
+
if (normalized.mainColor !== undefined && normalized.keyLightColor === undefined) {
|
|
411
|
+
normalized.keyLightColor = normalized.mainColor;
|
|
412
|
+
}
|
|
413
|
+
if (normalized.mainIntensity !== undefined && normalized.keyLightIntensity === undefined) {
|
|
414
|
+
normalized.keyLightIntensity = normalized.mainIntensity;
|
|
415
|
+
}
|
|
416
|
+
Object.assign(this._lightOptions, normalized);
|
|
334
417
|
this._applyLightOptions();
|
|
335
418
|
this._updateLights();
|
|
336
419
|
this._needsRepaint = true;
|
|
@@ -342,6 +425,28 @@ export class S3MViewer {
|
|
|
342
425
|
getLight() {
|
|
343
426
|
return { ...this._lightOptions };
|
|
344
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* 动态设置色调映射模式与曝光度
|
|
430
|
+
*/
|
|
431
|
+
setToneMapping(toneMapping, exposure = 1.0) {
|
|
432
|
+
this.options.toneMapping = toneMapping;
|
|
433
|
+
this.options.toneMappingExposure = exposure;
|
|
434
|
+
if (this.renderer) {
|
|
435
|
+
this.renderer.toneMapping = toneMapping;
|
|
436
|
+
this.renderer.toneMappingExposure = exposure;
|
|
437
|
+
this._needsRepaint = true;
|
|
438
|
+
this.triggerRepaint();
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* 获取当前色调映射模式与曝光度
|
|
443
|
+
*/
|
|
444
|
+
getToneMapping() {
|
|
445
|
+
return {
|
|
446
|
+
toneMapping: this.renderer ? this.renderer.toneMapping : (this.options.toneMapping ?? THREE.NoToneMapping),
|
|
447
|
+
toneMappingExposure: this.renderer ? this.renderer.toneMappingExposure : (this.options.toneMappingExposure ?? 1.0),
|
|
448
|
+
};
|
|
449
|
+
}
|
|
345
450
|
/**
|
|
346
451
|
* 更新光照位置与方向:
|
|
347
452
|
* - 'headlamp': 随地图方位角 bearing 旋转,主光始终在镜头上方,受光面面向镜头;
|
|
@@ -504,6 +609,337 @@ export class S3MViewer {
|
|
|
504
609
|
setFeaturesStyle(smids, style) {
|
|
505
610
|
this._featureStyleManager.setFeaturesStyle(smids, style, this._meshesOf(smids));
|
|
506
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* 把单体平移到新位置(米,场景 ENU 坐标的增量)。
|
|
614
|
+
*
|
|
615
|
+
* 平移写进状态纹理第 2 行,所以拾取、选中描边、棱线都会自动跟随——
|
|
616
|
+
* 但**视锥剔除不会**:three 用的是几何原始包围球,移出包围球后整块瓦片会被剔掉。
|
|
617
|
+
* 所以这里顺手把「有位移」的网格关掉 frustumCulled,归零时再恢复。
|
|
618
|
+
*/
|
|
619
|
+
moveFeature(smid, translation, transition) {
|
|
620
|
+
const key = this._ensureFeatureChannel(smid, 'translation');
|
|
621
|
+
this.animator.to(key, translation, transition);
|
|
622
|
+
}
|
|
623
|
+
/** 让单体沿某个轴移动的便捷写法(单位米) */
|
|
624
|
+
moveFeatureBy(smid, delta, transition) {
|
|
625
|
+
const cur = this._featureStyleManager.getFeatureStyle(smid)?.translation ?? [0, 0, 0];
|
|
626
|
+
this.moveFeature(smid, [cur[0] + delta[0], cur[1] + delta[1], cur[2] + delta[2]], transition);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* 让单体进行刚体姿态旋转(在 GPU 顶点着色器中以 pivot 轴心进行 3x3 矩阵旋转变换)。
|
|
630
|
+
*
|
|
631
|
+
* @param smid 单体 SMID
|
|
632
|
+
* @param rotation 旋转弧度或姿态:数字(绕 Z 轴 yaw 弧度)| [rx, ry, rz] 欧拉角 | [x,y,z,w] 四元数 | Euler/Quaternion/Matrix
|
|
633
|
+
* @param pivot 旋转中心轴心点 [px, py, pz](局部坐标,默认 [0, 0, 0])
|
|
634
|
+
* @param transition 动画过渡参数
|
|
635
|
+
*/
|
|
636
|
+
rotateFeature(smid, rotation, pivot, transition) {
|
|
637
|
+
const cur = this._featureStyleManager.getFeatureStyle(smid) ?? {};
|
|
638
|
+
if (pivot) {
|
|
639
|
+
this._featureStyleManager.setFeatureStyle(smid, { ...cur, pivot }, this._meshesOf([smid]));
|
|
640
|
+
}
|
|
641
|
+
const targetEuler = extractEulerAngles(rotation);
|
|
642
|
+
const key = this._ensureFeatureChannel(smid, 'rotation');
|
|
643
|
+
this.animator.to(key, targetEuler, transition);
|
|
644
|
+
}
|
|
645
|
+
/** 让单体增量旋转的便捷写法 */
|
|
646
|
+
rotateFeatureBy(smid, deltaRotation, pivot, transition) {
|
|
647
|
+
const curEuler = extractEulerAngles(this._featureStyleManager.getFeatureStyle(smid)?.rotation);
|
|
648
|
+
const d = typeof deltaRotation === 'number' ? [0, 0, deltaRotation] : deltaRotation;
|
|
649
|
+
this.rotateFeature(smid, [curEuler[0] + d[0], curEuler[1] + d[1], curEuler[2] + d[2]], pivot, transition);
|
|
650
|
+
}
|
|
651
|
+
/**
|
|
652
|
+
* 双纹理 GPU 交叉淡化(跨状态 / 专题图平滑过渡)。
|
|
653
|
+
*
|
|
654
|
+
* 采用双纹理 (uBatchTex + uBatchTex2 + uBatchMix) 硬件混合,
|
|
655
|
+
* 即使几千上万栋建筑全量切换配色/高度/显隐/姿态,也无需在 CPU 逐单体创建通道,
|
|
656
|
+
* 由 GPU 顶点与片元着色器以 60FPS 零成本硬件插值完成过渡。
|
|
657
|
+
*/
|
|
658
|
+
crossFadeStyles(targetStyles, transition) {
|
|
659
|
+
const targetMap = targetStyles instanceof Map
|
|
660
|
+
? targetStyles
|
|
661
|
+
: new Map(Object.entries(targetStyles).map(([k, v]) => [Number(k), v]));
|
|
662
|
+
const meshes = this.getAllMeshes();
|
|
663
|
+
this._featureStyleManager.prepareCrossFade(targetMap, meshes);
|
|
664
|
+
const key = 'batch.mix';
|
|
665
|
+
if (!this.animator.has(key)) {
|
|
666
|
+
this.animator.register(key, {
|
|
667
|
+
dim: 1,
|
|
668
|
+
read: (out) => {
|
|
669
|
+
out[0] = this.uniforms.uBatchMix?.value ?? 0;
|
|
670
|
+
},
|
|
671
|
+
write: (v) => {
|
|
672
|
+
if (this.uniforms.uBatchMix) {
|
|
673
|
+
this.uniforms.uBatchMix.value = v[0];
|
|
674
|
+
}
|
|
675
|
+
this.triggerRepaint();
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
const duration = transition?.duration ?? 0;
|
|
680
|
+
const wantAnim = duration > 0 || !!transition?.spring;
|
|
681
|
+
if (!wantAnim) {
|
|
682
|
+
if (this.animator.has(key))
|
|
683
|
+
this.animator.cancel(key);
|
|
684
|
+
if (this.uniforms.uBatchMix)
|
|
685
|
+
this.uniforms.uBatchMix.value = 0;
|
|
686
|
+
this._featureStyleManager.commitCrossFade(targetMap, meshes);
|
|
687
|
+
this.triggerRepaint();
|
|
688
|
+
transition?.onComplete?.();
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (this.uniforms.uBatchMix)
|
|
692
|
+
this.uniforms.uBatchMix.value = 0;
|
|
693
|
+
this.animator.to(key, 1, {
|
|
694
|
+
...transition,
|
|
695
|
+
onComplete: () => {
|
|
696
|
+
if (this.uniforms.uBatchMix)
|
|
697
|
+
this.uniforms.uBatchMix.value = 0;
|
|
698
|
+
this._featureStyleManager.commitCrossFade(targetMap, meshes);
|
|
699
|
+
this.triggerRepaint();
|
|
700
|
+
transition?.onComplete?.();
|
|
701
|
+
},
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* 让单体沿折线运动。
|
|
706
|
+
*
|
|
707
|
+
* 路径先做**弧长参数化**(见 anim/Path.ts):缓动作用在「已走距离」上,而不是
|
|
708
|
+
* 顶点索引上,所以顶点疏密不均也不会忽快忽慢。
|
|
709
|
+
*
|
|
710
|
+
* 实现上没有引入新的 track 类型——把「进度 0→1」当成一条普通补间,通道的 write
|
|
711
|
+
* 把进度映射成弧长再查表得位置。这样打断、循环、缓动、预算全部复用现有调度器。
|
|
712
|
+
*
|
|
713
|
+
* 注意:不要和 `moveFeature` 同时驱动同一个单体,两者都写 `translation`。
|
|
714
|
+
*/
|
|
715
|
+
followPath(smid, points, opts = {}) {
|
|
716
|
+
const path = points instanceof Path ? points : new Path(points, { closed: opts.closed });
|
|
717
|
+
if (path.length <= 0)
|
|
718
|
+
return;
|
|
719
|
+
const offset = opts.offset ?? [0, 0, 0];
|
|
720
|
+
const duration = opts.duration ??
|
|
721
|
+
(opts.speed && opts.speed > 0 ? (path.length / opts.speed) * 1000 : 2000);
|
|
722
|
+
const key = `feature.${smid}.path`;
|
|
723
|
+
this._pathSpecs.set(key, { path, offset });
|
|
724
|
+
if (!this.animator.has(key)) {
|
|
725
|
+
this.animator.register(key, {
|
|
726
|
+
dim: 1,
|
|
727
|
+
// 每次都从起点开始:进度通道没有「当前值」的概念,循环时正好重置
|
|
728
|
+
read: (out) => {
|
|
729
|
+
out[0] = 0;
|
|
730
|
+
},
|
|
731
|
+
write: (v) => {
|
|
732
|
+
const spec = this._pathSpecs.get(key);
|
|
733
|
+
if (!spec)
|
|
734
|
+
return;
|
|
735
|
+
spec.path.sampleAt(v[0], this._pathScratch);
|
|
736
|
+
const meshes = this._meshesOf([smid]);
|
|
737
|
+
const cur = this._featureStyleManager.getFeatureStyle(smid) ?? {};
|
|
738
|
+
this._featureStyleManager.setFeatureStyle(smid, {
|
|
739
|
+
...cur,
|
|
740
|
+
translation: [
|
|
741
|
+
spec.offset[0] + this._pathScratch[0],
|
|
742
|
+
spec.offset[1] + this._pathScratch[1],
|
|
743
|
+
spec.offset[2] + this._pathScratch[2],
|
|
744
|
+
],
|
|
745
|
+
}, meshes);
|
|
746
|
+
// 轨迹可能跑到原始包围球外,关掉视锥剔除免得整块瓦片被剔掉
|
|
747
|
+
for (const mesh of meshes)
|
|
748
|
+
mesh.frustumCulled = false;
|
|
749
|
+
},
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
const run = () => {
|
|
753
|
+
this.animator.to(key, 1, {
|
|
754
|
+
duration,
|
|
755
|
+
easing: opts.easing ?? 'linear',
|
|
756
|
+
onComplete: () => {
|
|
757
|
+
if (opts.loop)
|
|
758
|
+
run();
|
|
759
|
+
else
|
|
760
|
+
opts.onComplete?.();
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
};
|
|
764
|
+
run();
|
|
765
|
+
}
|
|
766
|
+
/** 停下路径运动(停在当前位置,不回弹) */
|
|
767
|
+
stopPath(smid) {
|
|
768
|
+
const key = `feature.${smid}.path`;
|
|
769
|
+
this.animator.cancel(key);
|
|
770
|
+
this.animator.unregister(key);
|
|
771
|
+
this._pathSpecs.delete(key);
|
|
772
|
+
}
|
|
773
|
+
/**
|
|
774
|
+
* 单体样式过渡:颜色 / 抬高 / 透明度 / 平移可以任意组合。
|
|
775
|
+
*
|
|
776
|
+
* 各属性各走一条通道,互不覆盖——同一个单体上「一边抬高一边变色」是两条独立的
|
|
777
|
+
* track,写回时都是「读当前 → 改一个字段 → 写回」,不会互相冲掉。
|
|
778
|
+
* `hidden` 不可插值,直接生效。
|
|
779
|
+
*/
|
|
780
|
+
animateFeature(smid, style, transition) {
|
|
781
|
+
if (style.translation)
|
|
782
|
+
this.moveFeature(smid, style.translation, transition);
|
|
783
|
+
if (style.rotation != null)
|
|
784
|
+
this.rotateFeature(smid, style.rotation, style.pivot, transition);
|
|
785
|
+
if (style.elevation != null) {
|
|
786
|
+
this.animator.to(this._ensureFeatureChannel(smid, 'elevation'), style.elevation, transition);
|
|
787
|
+
}
|
|
788
|
+
if (style.opacity != null) {
|
|
789
|
+
this.animator.to(this._ensureFeatureChannel(smid, 'opacity'), style.opacity, transition);
|
|
790
|
+
}
|
|
791
|
+
if (style.color) {
|
|
792
|
+
this._colorScratch.set(style.color);
|
|
793
|
+
this.animator.to(this._ensureFeatureChannel(smid, 'color'), [this._colorScratch.r, this._colorScratch.g, this._colorScratch.b], transition);
|
|
794
|
+
}
|
|
795
|
+
if (style.hidden != null) {
|
|
796
|
+
const cur = this._featureStyleManager.getFeatureStyle(smid) ?? {};
|
|
797
|
+
this.setFeatureStyle(smid, { ...cur, hidden: style.hidden });
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* 批量单体样式过渡,支持 `stagger` 错峰。
|
|
802
|
+
*
|
|
803
|
+
* 错峰是这类系统性价比最高的选项:一万个单体错峰 8ms,总时长不变,但每帧只推进
|
|
804
|
+
* 一小批,把峰值摊平——比任何「每帧上限」都自然。
|
|
805
|
+
*/
|
|
806
|
+
animateFeatures(smids, style, transition) {
|
|
807
|
+
const stagger = transition?.stagger ?? 0;
|
|
808
|
+
if (stagger <= 0) {
|
|
809
|
+
for (const smid of smids)
|
|
810
|
+
this.animateFeature(smid, style, transition);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const baseDelay = transition?.delay ?? 0;
|
|
814
|
+
for (let i = 0; i < smids.length; i++) {
|
|
815
|
+
this.animateFeature(smids[i], style, { ...transition, delay: baseDelay + i * stagger });
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* 切换着色模式,用 fade-through 代替硬切。
|
|
820
|
+
*
|
|
821
|
+
* `uMode` 是 int,插不了值;能做的就是「压暗 → 换 → 亮回来」。
|
|
822
|
+
* 编排走 onComplete 回调而不是 Promise 链,这样换模式发生在同一帧的回调里,
|
|
823
|
+
* 不会多出一帧的空白。返回的 Promise 在整段编排结束后 resolve。
|
|
824
|
+
*/
|
|
825
|
+
setMode(mode, opts = {}) {
|
|
826
|
+
const d = Math.max(0, opts.duration ?? 300);
|
|
827
|
+
const dim = Math.min(1, Math.max(0.05, opts.dim ?? 0.35));
|
|
828
|
+
if (this.style.mode === mode)
|
|
829
|
+
return Promise.resolve();
|
|
830
|
+
if (d <= 0) {
|
|
831
|
+
this.setStyle({ mode });
|
|
832
|
+
return Promise.resolve();
|
|
833
|
+
}
|
|
834
|
+
const gen = ++this._modeGen;
|
|
835
|
+
return new Promise((resolve) => {
|
|
836
|
+
this.animator.to('style.fade', dim, {
|
|
837
|
+
duration: d,
|
|
838
|
+
easing: 'easeInOutQuad',
|
|
839
|
+
onComplete: () => {
|
|
840
|
+
// 中途又切了一次:这次编排作废,收尾交给新的那次
|
|
841
|
+
if (gen !== this._modeGen) {
|
|
842
|
+
resolve();
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
this.setStyle({ mode });
|
|
846
|
+
this.animator.to('style.fade', 1, {
|
|
847
|
+
duration: d,
|
|
848
|
+
easing: 'easeInOutQuad',
|
|
849
|
+
onComplete: resolve,
|
|
850
|
+
});
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* 按需注册「单体属性」通道。
|
|
857
|
+
*
|
|
858
|
+
* 通道按 smid 惰性创建:绝大多数场景只动几十个单体,没必要给全城几万个 smid
|
|
859
|
+
* 预先建通道。`removeFeatureStyle` / `clearFeatureStyles` 会顺手回收。
|
|
860
|
+
*
|
|
861
|
+
* 写回统一走 `setFeatureStyle`:它内部已经按 `_meshesOf([smid])` 收敛脏网格,
|
|
862
|
+
* 所以单个单体动画每帧只重烘「含这个单体」的那一两块,不会全场景重算。
|
|
863
|
+
*/
|
|
864
|
+
_ensureFeatureChannel(smid, prop) {
|
|
865
|
+
const key = `feature.${smid}.${prop}`;
|
|
866
|
+
if (this.animator.has(key))
|
|
867
|
+
return key;
|
|
868
|
+
const channel = prop === 'translation' || prop === 'color' || prop === 'rotation'
|
|
869
|
+
? {
|
|
870
|
+
dim: 3,
|
|
871
|
+
read: (out) => {
|
|
872
|
+
if (prop === 'translation') {
|
|
873
|
+
const t = this._featureStyleManager.getFeatureStyle(smid)?.translation;
|
|
874
|
+
out[0] = t?.[0] ?? 0;
|
|
875
|
+
out[1] = t?.[1] ?? 0;
|
|
876
|
+
out[2] = t?.[2] ?? 0;
|
|
877
|
+
}
|
|
878
|
+
else if (prop === 'rotation') {
|
|
879
|
+
const r = this._featureStyleManager.getFeatureStyle(smid)?.rotation;
|
|
880
|
+
const euler = extractEulerAngles(r);
|
|
881
|
+
out[0] = euler[0];
|
|
882
|
+
out[1] = euler[1];
|
|
883
|
+
out[2] = euler[2];
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
// 没有自定义色时以图层主色为起点,插值才连续
|
|
887
|
+
const hex = this._featureStyleManager.getFeatureStyle(smid)?.color ?? this.style.colorA;
|
|
888
|
+
this._colorScratch.set(hex);
|
|
889
|
+
out[0] = this._colorScratch.r;
|
|
890
|
+
out[1] = this._colorScratch.g;
|
|
891
|
+
out[2] = this._colorScratch.b;
|
|
892
|
+
}
|
|
893
|
+
},
|
|
894
|
+
write: (v) => {
|
|
895
|
+
const meshes = this._meshesOf([smid]);
|
|
896
|
+
const cur = this._featureStyleManager.getFeatureStyle(smid) ?? {};
|
|
897
|
+
if (prop === 'translation') {
|
|
898
|
+
this._featureStyleManager.setFeatureStyle(smid, { ...cur, translation: [v[0], v[1], v[2]] }, meshes);
|
|
899
|
+
// 有位移或旋转就关掉视锥剔除,否则移出原始包围球后整块瓦片会凭空消失
|
|
900
|
+
const transMoving = Math.abs(v[0]) > 1e-6 || Math.abs(v[1]) > 1e-6 || Math.abs(v[2]) > 1e-6;
|
|
901
|
+
const rotEuler = extractEulerAngles(cur.rotation);
|
|
902
|
+
const rotMoving = Math.abs(rotEuler[0]) > 1e-6 || Math.abs(rotEuler[1]) > 1e-6 || Math.abs(rotEuler[2]) > 1e-6;
|
|
903
|
+
for (const mesh of meshes)
|
|
904
|
+
mesh.frustumCulled = !(transMoving || rotMoving);
|
|
905
|
+
}
|
|
906
|
+
else if (prop === 'rotation') {
|
|
907
|
+
this._featureStyleManager.setFeatureStyle(smid, { ...cur, rotation: [v[0], v[1], v[2]] }, meshes);
|
|
908
|
+
const rotMoving = Math.abs(v[0]) > 1e-6 || Math.abs(v[1]) > 1e-6 || Math.abs(v[2]) > 1e-6;
|
|
909
|
+
const t = cur.translation ?? [0, 0, 0];
|
|
910
|
+
const transMoving = Math.abs(t[0]) > 1e-6 || Math.abs(t[1]) > 1e-6 || Math.abs(t[2]) > 1e-6;
|
|
911
|
+
for (const mesh of meshes)
|
|
912
|
+
mesh.frustumCulled = !(rotMoving || transMoving);
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
const c = this._colorScratch.setRGB(v[0], v[1], v[2]);
|
|
916
|
+
this._featureStyleManager.setFeatureStyle(smid, { ...cur, color: `#${c.getHexString()}` }, meshes);
|
|
917
|
+
}
|
|
918
|
+
},
|
|
919
|
+
}
|
|
920
|
+
: {
|
|
921
|
+
dim: 1,
|
|
922
|
+
read: (out) => {
|
|
923
|
+
const st = this._featureStyleManager.getFeatureStyle(smid);
|
|
924
|
+
out[0] = (prop === 'elevation' ? st?.elevation : st?.opacity) ?? (prop === 'opacity' ? 1 : 0);
|
|
925
|
+
},
|
|
926
|
+
write: (v) => {
|
|
927
|
+
const cur = this._featureStyleManager.getFeatureStyle(smid) ?? {};
|
|
928
|
+
const next = prop === 'elevation' ? { ...cur, elevation: v[0] } : { ...cur, opacity: v[0] };
|
|
929
|
+
this._featureStyleManager.setFeatureStyle(smid, next, this._meshesOf([smid]));
|
|
930
|
+
},
|
|
931
|
+
};
|
|
932
|
+
this.animator.register(key, channel);
|
|
933
|
+
return key;
|
|
934
|
+
}
|
|
935
|
+
/** 回收某个单体的动画通道(单体被删除 / 样式被清空时调用) */
|
|
936
|
+
_releaseFeatureChannels(smid) {
|
|
937
|
+
for (const prop of FEATURE_PROPS)
|
|
938
|
+
this.animator.unregister(`feature.${smid}.${prop}`);
|
|
939
|
+
const pathKey = `feature.${smid}.path`;
|
|
940
|
+
this.animator.unregister(pathKey);
|
|
941
|
+
this._pathSpecs.delete(pathKey);
|
|
942
|
+
}
|
|
507
943
|
/** 获取指定单体的自定义样式 */
|
|
508
944
|
getFeatureStyle(smid) {
|
|
509
945
|
return this._featureStyleManager.getFeatureStyle(smid);
|
|
@@ -514,10 +950,14 @@ export class S3MViewer {
|
|
|
514
950
|
}
|
|
515
951
|
/** 移除指定单体的自定义样式,恢复为图层全局样式 */
|
|
516
952
|
removeFeatureStyle(smid) {
|
|
953
|
+
this._releaseFeatureChannels(smid);
|
|
517
954
|
this._featureStyleManager.removeFeatureStyle(smid, this._meshesOf([smid]));
|
|
518
955
|
}
|
|
519
956
|
/** 清空所有单体的自定义样式 */
|
|
520
957
|
clearFeatureStyles() {
|
|
958
|
+
for (const smid of this._featureStyleManager.getFeatureStyles().keys()) {
|
|
959
|
+
this._releaseFeatureChannels(smid);
|
|
960
|
+
}
|
|
521
961
|
this._featureStyleManager.clearFeatureStyles();
|
|
522
962
|
}
|
|
523
963
|
/* ---------------------------------------------------------------- 按业务字段着色 */
|
|
@@ -704,6 +1144,7 @@ export class S3MViewer {
|
|
|
704
1144
|
// 数据自带的棱线包:只跟着 edges 开关显示,不参与拾取、不参与着色
|
|
705
1145
|
if (child.isLineSegments && child.userData.isEdgeLines) {
|
|
706
1146
|
child.visible = this._edgesEnabled;
|
|
1147
|
+
this._nativeEdgeLines.add(child);
|
|
707
1148
|
this._styleEdgeLine(child);
|
|
708
1149
|
continue;
|
|
709
1150
|
}
|
|
@@ -712,6 +1153,8 @@ export class S3MViewer {
|
|
|
712
1153
|
const mesh = child;
|
|
713
1154
|
if (!mesh.isMesh && !isPoints)
|
|
714
1155
|
continue;
|
|
1156
|
+
if (isPoints)
|
|
1157
|
+
this._pointClouds.add(child);
|
|
715
1158
|
const ud = mesh.userData;
|
|
716
1159
|
ud.pickId = this._allocPickId();
|
|
717
1160
|
ud.datasetId = entry.id;
|
|
@@ -738,12 +1181,13 @@ export class S3MViewer {
|
|
|
738
1181
|
const mat = styledMaterialOf(mesh);
|
|
739
1182
|
if (mat) {
|
|
740
1183
|
const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
|
|
741
|
-
mat.opacity
|
|
1184
|
+
// 不写 mat.opacity:全局透明度走共享 uOpacity,材质自身 opacity 保留模型自带 alpha
|
|
742
1185
|
mat.transparent = o < 0.999;
|
|
743
1186
|
mat.depthWrite = o >= 0.999;
|
|
744
1187
|
mat.flatShading = this.style.shading !== 'smooth';
|
|
745
1188
|
mat.needsUpdate = true;
|
|
746
1189
|
this._featureStyleManager.applyOverridesToMesh(mesh);
|
|
1190
|
+
this._startTileFade(mesh, mat);
|
|
747
1191
|
}
|
|
748
1192
|
}
|
|
749
1193
|
// 选中状态也要跟着继承:LOD 细化 / 瓦片重载会换掉 mesh,
|
|
@@ -751,6 +1195,53 @@ export class S3MViewer {
|
|
|
751
1195
|
this._replaySelectionOnto(mesh);
|
|
752
1196
|
}
|
|
753
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* 新瓦片淡入。
|
|
1200
|
+
*
|
|
1201
|
+
* 每块网格有自己的材质实例,所以给一个逐材质的 `uTileFade` 就够,不需要动共享
|
|
1202
|
+
* uniform、也不需要重建任何东西。淡入期间该网格进透明队列(混合 + 排序),
|
|
1203
|
+
* 淡完立刻把 `transparent` 还给图层状态,避免长期留在透明通道里。
|
|
1204
|
+
*
|
|
1205
|
+
* 通道 key 用自增序号而不是 pickId:pickId 会被回收复用,用它会串到新瓦片上。
|
|
1206
|
+
*/
|
|
1207
|
+
_startTileFade(mesh, mat) {
|
|
1208
|
+
const duration = this.style.fadeIn ?? 0;
|
|
1209
|
+
if (duration <= 0)
|
|
1210
|
+
return;
|
|
1211
|
+
const uFade = mat.userData.uTileFade;
|
|
1212
|
+
if (!uFade)
|
|
1213
|
+
return;
|
|
1214
|
+
uFade.value = 0;
|
|
1215
|
+
mat.transparent = true;
|
|
1216
|
+
// 淡入期间保持深度写入:否则能透过正在淡入的楼看到后面的东西,像在「漏」
|
|
1217
|
+
mat.depthWrite = true;
|
|
1218
|
+
const key = `tile.${this._tileFadeSeq++}.fade`;
|
|
1219
|
+
mesh.userData.tileFadeKey = key;
|
|
1220
|
+
this.animator.register(key, {
|
|
1221
|
+
dim: 1,
|
|
1222
|
+
read: (out) => {
|
|
1223
|
+
out[0] = uFade.value;
|
|
1224
|
+
},
|
|
1225
|
+
write: (v) => {
|
|
1226
|
+
uFade.value = v[0];
|
|
1227
|
+
},
|
|
1228
|
+
});
|
|
1229
|
+
this.animator.to(key, 1, {
|
|
1230
|
+
duration,
|
|
1231
|
+
easing: 'easeOutQuad',
|
|
1232
|
+
onComplete: () => {
|
|
1233
|
+
uFade.value = 1;
|
|
1234
|
+
mesh.userData.tileFadeKey = undefined;
|
|
1235
|
+
this.animator.unregister(key);
|
|
1236
|
+
// 淡入期间强制开了 transparent + depthWrite,结束时不能自己拍板关掉——
|
|
1237
|
+
// 图层可能本来就半透明、单体也可能还有淡化。交给淡化流程重新裁决一次。
|
|
1238
|
+
this._featureStyleManager.applyOverridesToMesh(mesh);
|
|
1239
|
+
if (!mat.transparent)
|
|
1240
|
+
mat.depthWrite = true;
|
|
1241
|
+
this._requestRepaint();
|
|
1242
|
+
},
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
754
1245
|
/**
|
|
755
1246
|
* 把「当前选中」重放到一块新登记的 mesh 上。
|
|
756
1247
|
*
|
|
@@ -819,6 +1310,16 @@ export class S3MViewer {
|
|
|
819
1310
|
_unregisterTile(node, entry) {
|
|
820
1311
|
node.group.traverse((child) => {
|
|
821
1312
|
const obj = child;
|
|
1313
|
+
if (child.isLineSegments && obj.userData?.isEdgeLines) {
|
|
1314
|
+
this._nativeEdgeLines.delete(child);
|
|
1315
|
+
}
|
|
1316
|
+
if (isPointCloudObject(child))
|
|
1317
|
+
this._pointClouds.delete(child);
|
|
1318
|
+
// 淡入还没结束就被淘汰:把通道收掉,否则会对着已释放的材质继续写
|
|
1319
|
+
if (obj.userData?.tileFadeKey) {
|
|
1320
|
+
this.animator.unregister(obj.userData.tileFadeKey);
|
|
1321
|
+
obj.userData.tileFadeKey = undefined;
|
|
1322
|
+
}
|
|
822
1323
|
if (obj.userData?.pickId != null) {
|
|
823
1324
|
this._pickables.delete(obj.userData.pickId);
|
|
824
1325
|
this._releasePickId(obj.userData.pickId);
|
|
@@ -889,12 +1390,160 @@ export class S3MViewer {
|
|
|
889
1390
|
}
|
|
890
1391
|
/* ---------------------------------------------------------------- 样式 */
|
|
891
1392
|
/** 动态设置图层整体透明度 (0.0 ~ 1.0) */
|
|
892
|
-
setOpacity(opacity) {
|
|
893
|
-
this.setStyle({ opacity });
|
|
1393
|
+
setOpacity(opacity, transition) {
|
|
1394
|
+
this.setStyle({ opacity }, transition);
|
|
894
1395
|
}
|
|
895
|
-
/**
|
|
896
|
-
|
|
897
|
-
|
|
1396
|
+
/**
|
|
1397
|
+
* 合并并应用样式。
|
|
1398
|
+
*
|
|
1399
|
+
* 只改 uniform 的项不会触发几何 / 着色器重建。带 `transition` 时,能插值的项
|
|
1400
|
+
* 交给动画调度器逐帧推进;不能插值的项(`mode` / `shading` / `edges`)走各自的
|
|
1401
|
+
* 编排逻辑(`edges` 由 setEdges 做淡入淡出,`mode` 建议配 fade-through)。
|
|
1402
|
+
*/
|
|
1403
|
+
setStyle(patch = {}, transition) {
|
|
1404
|
+
Object.assign(this._styleTarget, patch);
|
|
1405
|
+
const duration = transition?.duration ?? 0;
|
|
1406
|
+
const wantAnim = duration > 0 || !!transition?.spring;
|
|
1407
|
+
// 棱线开关是结构变更,不是插值:交给 setEdges 编排
|
|
1408
|
+
let rest = patch;
|
|
1409
|
+
if ('edges' in patch) {
|
|
1410
|
+
const { edges, ...others } = patch;
|
|
1411
|
+
rest = others;
|
|
1412
|
+
this.setEdges(!!edges, wantAnim ? transition : undefined);
|
|
1413
|
+
if (Object.keys(rest).length === 0) {
|
|
1414
|
+
this._requestRepaint();
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
const animated = [];
|
|
1419
|
+
const immediate = {};
|
|
1420
|
+
for (const key of Object.keys(rest)) {
|
|
1421
|
+
if (wantAnim && this.animator.has(`style.${String(key)}`))
|
|
1422
|
+
animated.push(String(key));
|
|
1423
|
+
else
|
|
1424
|
+
immediate[key] = rest[key];
|
|
1425
|
+
}
|
|
1426
|
+
if (Object.keys(immediate).length > 0) {
|
|
1427
|
+
Object.assign(this.style, immediate);
|
|
1428
|
+
this._applyStyleEffects(immediate);
|
|
1429
|
+
}
|
|
1430
|
+
for (const key of animated) {
|
|
1431
|
+
this.animator.to(`style.${key}`, this._animTargetOf(key, rest), {
|
|
1432
|
+
...transition,
|
|
1433
|
+
duration,
|
|
1434
|
+
});
|
|
1435
|
+
}
|
|
1436
|
+
this._requestRepaint();
|
|
1437
|
+
}
|
|
1438
|
+
getStyle() {
|
|
1439
|
+
return { ...this._styleTarget, modeLabel: STYLE_LABELS[this._styleTarget.mode] };
|
|
1440
|
+
}
|
|
1441
|
+
/** 把某个样式键的取值转成动画通道能吃的形状(标量 or 线性 RGB 三元组) */
|
|
1442
|
+
_animTargetOf(key, patch) {
|
|
1443
|
+
const value = patch[key];
|
|
1444
|
+
const dim = this._animDimOf(key);
|
|
1445
|
+
if (dim === 1)
|
|
1446
|
+
return Number(value);
|
|
1447
|
+
this._colorScratch.set(String(value));
|
|
1448
|
+
return [this._colorScratch.r, this._colorScratch.g, this._colorScratch.b];
|
|
1449
|
+
}
|
|
1450
|
+
/** 该样式键的动画维度:3 = 颜色,1 = 标量 */
|
|
1451
|
+
_animDimOf(key) {
|
|
1452
|
+
return STYLE_COLOR_KEYS.includes(key) ? 3 : 1;
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* 注册样式动画通道。
|
|
1456
|
+
*
|
|
1457
|
+
* 每个通道的 write 都是 O(1):只改 `this.style` 的一个字段 + 一次 uniform 赋值。
|
|
1458
|
+
* 这里不注册 `mode` / `shading` / `edges`——它们不可插值,见类文档。
|
|
1459
|
+
*/
|
|
1460
|
+
_registerAnimChannels() {
|
|
1461
|
+
for (const key of STYLE_SCALAR_KEYS) {
|
|
1462
|
+
this.animator.register(`style.${key}`, {
|
|
1463
|
+
dim: 1,
|
|
1464
|
+
read: (out) => {
|
|
1465
|
+
out[0] = Number(this.style[key] ?? 0);
|
|
1466
|
+
},
|
|
1467
|
+
write: (v) => {
|
|
1468
|
+
this.style[key] = v[0];
|
|
1469
|
+
this._onStyleChannelWrote(key);
|
|
1470
|
+
},
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
for (const key of STYLE_COLOR_KEYS) {
|
|
1474
|
+
this.animator.register(`style.${key}`, {
|
|
1475
|
+
dim: 3,
|
|
1476
|
+
read: (out) => {
|
|
1477
|
+
const c = this._uniformColorOf(key);
|
|
1478
|
+
out[0] = c.r;
|
|
1479
|
+
out[1] = c.g;
|
|
1480
|
+
out[2] = c.b;
|
|
1481
|
+
},
|
|
1482
|
+
write: (v) => {
|
|
1483
|
+
const c = this._uniformColorOf(key);
|
|
1484
|
+
// 直接写线性分量:shader 消费的就是线性值,端点严格吻合
|
|
1485
|
+
c.setRGB(v[0], v[1], v[2]);
|
|
1486
|
+
this.style[key] = `#${c.getHexString()}`;
|
|
1487
|
+
this._onStyleChannelWrote(key);
|
|
1488
|
+
},
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
// 棱线淡入淡出乘子:不属于 S3MStyle,单独一条通道
|
|
1492
|
+
this.animator.register('style.edgeFade', {
|
|
1493
|
+
dim: 1,
|
|
1494
|
+
read: (out) => {
|
|
1495
|
+
out[0] = this.uniforms.uEdgeFade?.value ?? 1;
|
|
1496
|
+
},
|
|
1497
|
+
write: (v) => {
|
|
1498
|
+
if (this.uniforms.uEdgeFade)
|
|
1499
|
+
this.uniforms.uEdgeFade.value = v[0];
|
|
1500
|
+
this._syncEdgeStyle();
|
|
1501
|
+
},
|
|
1502
|
+
});
|
|
1503
|
+
// 整体压暗乘子:setMode 的 fade-through 用它,也不是 S3MStyle 字段
|
|
1504
|
+
this.animator.register('style.fade', {
|
|
1505
|
+
dim: 1,
|
|
1506
|
+
read: (out) => {
|
|
1507
|
+
out[0] = this.uniforms.uFade?.value ?? 1;
|
|
1508
|
+
},
|
|
1509
|
+
write: (v) => {
|
|
1510
|
+
if (this.uniforms.uFade)
|
|
1511
|
+
this.uniforms.uFade.value = v[0];
|
|
1512
|
+
this._requestRepaint();
|
|
1513
|
+
},
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
/** 通道写完一个样式键后的最小副作用(全部 O(1)) */
|
|
1517
|
+
_onStyleChannelWrote(key) {
|
|
1518
|
+
if (key === 'opacity') {
|
|
1519
|
+
this._applyOpacity();
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
this._applyStyleUniforms();
|
|
1523
|
+
if (key === 'edgeColor' || key === 'edgeWidth' || key === 'edgeOpacity') {
|
|
1524
|
+
this._refreshEdgeLines();
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
/** 取某个颜色键对应的共享 uniform Color 对象 */
|
|
1528
|
+
_uniformColorOf(key) {
|
|
1529
|
+
const u = this.uniforms;
|
|
1530
|
+
switch (key) {
|
|
1531
|
+
case 'colorA':
|
|
1532
|
+
return u.uColorA.value;
|
|
1533
|
+
case 'colorB':
|
|
1534
|
+
return u.uColorB.value;
|
|
1535
|
+
case 'colorC':
|
|
1536
|
+
return u.uColorC.value;
|
|
1537
|
+
case 'selectColor':
|
|
1538
|
+
return u.uSelColor.value;
|
|
1539
|
+
case 'edgeColor':
|
|
1540
|
+
return this.edgeMaterial.uniforms.uColor.value;
|
|
1541
|
+
default:
|
|
1542
|
+
return u.uColorA.value;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
/** 应用一批样式键带来的副作用(uniform / 材质 / 棱线 / 选中态) */
|
|
1546
|
+
_applyStyleEffects(patch) {
|
|
898
1547
|
this._applyStyleUniforms();
|
|
899
1548
|
if ('opacity' in patch || 'depthWrite' in patch)
|
|
900
1549
|
this._applyOpacity();
|
|
@@ -903,13 +1552,13 @@ export class S3MViewer {
|
|
|
903
1552
|
this._applyPointCloudStyles();
|
|
904
1553
|
if ('shading' in patch)
|
|
905
1554
|
this._applyShading();
|
|
906
|
-
// 临界角变了要重建 CPU 棱线(EdgesGeometry
|
|
1555
|
+
// 临界角变了要重建 CPU 棱线(EdgesGeometry 的参数),开 / 关走 setEdges 整体刷新。
|
|
1556
|
+
// 必须先 _disposeEdges():setEdges 只挑「还没有棱线」的网格来建,
|
|
1557
|
+
// 不先拆掉的话已经建好的会被全部跳过,临界角改了等于没改。
|
|
907
1558
|
if ('edgeThreshold' in patch && this._edgesEnabled) {
|
|
1559
|
+
this._disposeEdges();
|
|
908
1560
|
this.setEdges(true);
|
|
909
1561
|
}
|
|
910
|
-
else if ('edges' in patch) {
|
|
911
|
-
this.setEdges(!!this.style.edges);
|
|
912
|
-
}
|
|
913
1562
|
else if ('edgeColor' in patch || 'edgeWidth' in patch || 'opacity' in patch || 'edgeOpacity' in patch) {
|
|
914
1563
|
this._refreshEdgeLines();
|
|
915
1564
|
}
|
|
@@ -920,10 +1569,6 @@ export class S3MViewer {
|
|
|
920
1569
|
else
|
|
921
1570
|
this._syncSelectionStyle();
|
|
922
1571
|
}
|
|
923
|
-
this._requestRepaint();
|
|
924
|
-
}
|
|
925
|
-
getStyle() {
|
|
926
|
-
return { ...this.style, modeLabel: STYLE_LABELS[this.style.mode] };
|
|
927
1572
|
}
|
|
928
1573
|
_applyStyleUniforms() {
|
|
929
1574
|
const u = this.uniforms;
|
|
@@ -959,6 +1604,9 @@ export class S3MViewer {
|
|
|
959
1604
|
if (u.uTexMix && s.texMix != null) {
|
|
960
1605
|
u.uTexMix.value = s.texMix;
|
|
961
1606
|
}
|
|
1607
|
+
if (u.uAmbientGain && s.ambientGain != null) {
|
|
1608
|
+
u.uAmbientGain.value = s.ambientGain;
|
|
1609
|
+
}
|
|
962
1610
|
if (this.outlineMaterial) {
|
|
963
1611
|
if (this.outlineMaterial.uniforms.uElevation && s.selectElevation != null) {
|
|
964
1612
|
this.outlineMaterial.uniforms.uElevation.value = s.selectElevation;
|
|
@@ -973,24 +1621,43 @@ export class S3MViewer {
|
|
|
973
1621
|
}
|
|
974
1622
|
}
|
|
975
1623
|
}
|
|
976
|
-
/**
|
|
1624
|
+
/**
|
|
1625
|
+
* 透明度:写共享 uniform,**不再逐 mesh 改材质**。
|
|
1626
|
+
*
|
|
1627
|
+
* 原来的写法每次都要遍历全部 pickables 改 `material.opacity` + `material.transparent`,
|
|
1628
|
+
* 是 O(meshes);而且 `opaque` 参与 three 的 program 缓存键,跨 0.999 会换一次 program。
|
|
1629
|
+
* 拖滑块时一秒钟几十次调用,几十万面的场景就是明显掉帧。
|
|
1630
|
+
*
|
|
1631
|
+
* 现在逐帧只写一个 float(`uOpacity`),混合开关只在**真正跨过阈值**时切一次。
|
|
1632
|
+
*/
|
|
977
1633
|
_applyOpacity() {
|
|
978
1634
|
const o = Math.min(1, Math.max(0.05, this.style.opacity ?? 1));
|
|
1635
|
+
if (this.uniforms.uOpacity)
|
|
1636
|
+
this.uniforms.uOpacity.value = o;
|
|
979
1637
|
const transparent = o < 0.999;
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1638
|
+
if (transparent !== this._opacityTransparent) {
|
|
1639
|
+
this._opacityTransparent = transparent;
|
|
1640
|
+
// 关键:若 depthWrite 开启(默认开启),即使半透明也保持深度写入,完美遮挡背面棱线,防止背面线条透出
|
|
1641
|
+
const keepDepth = this.style.depthWrite !== false;
|
|
1642
|
+
for (const p of this._pickables.values()) {
|
|
1643
|
+
if (isPointCloudObject(p.mesh))
|
|
1644
|
+
continue;
|
|
1645
|
+
const m = styledMaterialOf(p.mesh);
|
|
1646
|
+
if (!m)
|
|
1647
|
+
continue;
|
|
1648
|
+
// 有单体淡化 / 正在淡入的材质,transparent 由各自的流程管,这里不要抢
|
|
1649
|
+
if (m.userData.uBatchTexActive?.value)
|
|
1650
|
+
continue;
|
|
1651
|
+
if ((m.userData.uTileFade?.value ?? 1) < 1)
|
|
1652
|
+
continue;
|
|
1653
|
+
m.transparent = transparent;
|
|
1654
|
+
m.depthWrite = transparent ? keepDepth : true;
|
|
986
1655
|
}
|
|
987
|
-
const m = styledMaterialOf(p.mesh);
|
|
988
|
-
if (!m)
|
|
989
|
-
continue;
|
|
990
|
-
m.opacity = o;
|
|
991
|
-
m.transparent = transparent;
|
|
992
|
-
m.depthWrite = transparent ? keepDepth : true;
|
|
993
1656
|
}
|
|
1657
|
+
// 点云不走 StyleMaterial,自己有一套 uOpacity,逐帧刷的只有这几个对象
|
|
1658
|
+
for (const pc of this._pointClouds)
|
|
1659
|
+
this._applyPointCloudStyle(pc);
|
|
1660
|
+
this._syncEdgeStyle();
|
|
994
1661
|
this._requestRepaint();
|
|
995
1662
|
}
|
|
996
1663
|
/** 单个点云对象同步当前样式(着色模式 + 不透明度 + 选中高亮) */
|
|
@@ -999,10 +1666,8 @@ export class S3MViewer {
|
|
|
999
1666
|
}
|
|
1000
1667
|
/** 全部点云刷新:着色模式切换时用(uniform 是共享引用,改色不用走这里) */
|
|
1001
1668
|
_applyPointCloudStyles() {
|
|
1002
|
-
for (const
|
|
1003
|
-
|
|
1004
|
-
this._applyPointCloudStyle(p.mesh);
|
|
1005
|
-
}
|
|
1669
|
+
for (const pc of this._pointClouds)
|
|
1670
|
+
this._applyPointCloudStyle(pc);
|
|
1006
1671
|
this._requestRepaint();
|
|
1007
1672
|
}
|
|
1008
1673
|
/** 平面/平滑着色:FLAT_SHADED 是编译期宏,改它必须重编译 */
|
|
@@ -1124,10 +1789,16 @@ export class S3MViewer {
|
|
|
1124
1789
|
*
|
|
1125
1790
|
* 棱线本身用「屏幕空间等宽线」画(见 EdgeLines.ts):WebGL 的 lineWidth 基本无效,
|
|
1126
1791
|
* 只有摊成面向屏幕的四边形,`edgeWidth` 才能真正调粗。
|
|
1792
|
+
*
|
|
1793
|
+
* 带 `transition` 时做「淡入 / 淡出」,**不对几何做插值**(几何没法插值):
|
|
1794
|
+
* 开:立刻置位 + 开始分片建线,同时 `uEdgeFade` 0→1 —— 建好一块亮一块,视觉上是渐显;
|
|
1795
|
+
* 关:`uEdgeFade` 1→0,淡完才 dispose,避免棱线「啪」一下消失。
|
|
1127
1796
|
*/
|
|
1128
|
-
setEdges(enabled) {
|
|
1797
|
+
setEdges(enabled, transition) {
|
|
1798
|
+
const wantAnim = !!transition && ((transition.duration ?? 0) > 0 || !!transition.spring);
|
|
1129
1799
|
this._edgesEnabled = enabled;
|
|
1130
1800
|
this.style.edges = enabled;
|
|
1801
|
+
this._styleTarget.edges = enabled;
|
|
1131
1802
|
// 数据自带的棱线:直接切可见性
|
|
1132
1803
|
for (const entry of this.datasets.values()) {
|
|
1133
1804
|
entry.root.traverse((o) => {
|
|
@@ -1140,8 +1811,20 @@ export class S3MViewer {
|
|
|
1140
1811
|
}
|
|
1141
1812
|
this._syncEdgeStyle();
|
|
1142
1813
|
if (!enabled) {
|
|
1143
|
-
|
|
1144
|
-
this.
|
|
1814
|
+
if (wantAnim) {
|
|
1815
|
+
this.animator.to('style.edgeFade', 0, {
|
|
1816
|
+
...transition,
|
|
1817
|
+
onComplete: () => {
|
|
1818
|
+
this._disposeEdges();
|
|
1819
|
+
this._requestRepaint();
|
|
1820
|
+
transition?.onComplete?.();
|
|
1821
|
+
},
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
else {
|
|
1825
|
+
this._setEdgeFade(1);
|
|
1826
|
+
this._disposeEdges();
|
|
1827
|
+
}
|
|
1145
1828
|
this._requestRepaint();
|
|
1146
1829
|
return;
|
|
1147
1830
|
}
|
|
@@ -1163,6 +1846,25 @@ export class S3MViewer {
|
|
|
1163
1846
|
this._requestRepaint();
|
|
1164
1847
|
};
|
|
1165
1848
|
step();
|
|
1849
|
+
if (wantAnim) {
|
|
1850
|
+
// 从 0 起淡入:新建出来的棱线自动继承当前进度,不会半路冒出一条满亮度的线
|
|
1851
|
+
this._setEdgeFade(0);
|
|
1852
|
+
this.animator.to('style.edgeFade', 1, transition);
|
|
1853
|
+
}
|
|
1854
|
+
else {
|
|
1855
|
+
this._setEdgeFade(1);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
/** 直接写棱线淡入乘子(不走动画通道) */
|
|
1859
|
+
_setEdgeFade(v) {
|
|
1860
|
+
if (this.uniforms.uEdgeFade)
|
|
1861
|
+
this.uniforms.uEdgeFade.value = v;
|
|
1862
|
+
this._syncEdgeStyle();
|
|
1863
|
+
}
|
|
1864
|
+
/** 释放所有现算棱线几何 */
|
|
1865
|
+
_disposeEdges() {
|
|
1866
|
+
for (const p of this._pickables.values())
|
|
1867
|
+
this._removeEdges(p.mesh);
|
|
1166
1868
|
}
|
|
1167
1869
|
/**
|
|
1168
1870
|
* 挂载预制棱线缓存(Crease Cache)。
|
|
@@ -1209,19 +1911,27 @@ export class S3MViewer {
|
|
|
1209
1911
|
this._creaseLines = null;
|
|
1210
1912
|
this._requestRepaint();
|
|
1211
1913
|
}
|
|
1212
|
-
/**
|
|
1914
|
+
/**
|
|
1915
|
+
* 让全局棱线材质跟上 style.edgeColor / edgeWidth / edgeOpacity。
|
|
1916
|
+
*
|
|
1917
|
+
* 全程 O(1):只有一份共享 edgeMaterial(所有逐 mesh 棱线都用它)+ 一份预制缓存,
|
|
1918
|
+
* 所以棱线淡入淡出只要改一个 uniform,新建瓦片自动继承当前进度。
|
|
1919
|
+
*/
|
|
1213
1920
|
_syncEdgeStyle() {
|
|
1214
1921
|
const s = this.style;
|
|
1922
|
+
const fade = this.uniforms.uEdgeFade?.value ?? 1;
|
|
1923
|
+
const opacity = Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) *
|
|
1924
|
+
Math.min(1, Math.max(0.05, s.opacity ?? 1)) *
|
|
1925
|
+
fade;
|
|
1215
1926
|
const u = this.edgeMaterial.uniforms;
|
|
1216
1927
|
u.uColor.value.set(s.edgeColor);
|
|
1217
1928
|
u.uWidth.value = Math.max(0.1, s.edgeWidth ?? 1.5);
|
|
1218
|
-
u.uOpacity.value =
|
|
1219
|
-
Math.min(1, Math.max(0, s.edgeOpacity ?? 0.6)) * Math.min(1, Math.max(0.05, s.opacity ?? 1));
|
|
1929
|
+
u.uOpacity.value = opacity;
|
|
1220
1930
|
// 预制棱线缓存(LineBasicMaterial)同步颜色 / 不透明度
|
|
1221
1931
|
if (this._creaseLines) {
|
|
1222
1932
|
const m = this._creaseLines.material;
|
|
1223
1933
|
m.color.set(s.edgeColor);
|
|
1224
|
-
m.opacity =
|
|
1934
|
+
m.opacity = opacity;
|
|
1225
1935
|
}
|
|
1226
1936
|
}
|
|
1227
1937
|
/** 让数据自带的棱线跟着 style.edgeColor / opacity 走(这些是 LineBasicMaterial,改不了粗细) */
|
|
@@ -1231,20 +1941,28 @@ export class S3MViewer {
|
|
|
1231
1941
|
return;
|
|
1232
1942
|
mat.color.set(this.style.edgeColor);
|
|
1233
1943
|
const baseOpacity = this.style.edgeOpacity ?? 0.6;
|
|
1234
|
-
mat.opacity =
|
|
1944
|
+
mat.opacity =
|
|
1945
|
+
baseOpacity *
|
|
1946
|
+
Math.min(1, Math.max(0.05, this.style.opacity ?? 1)) *
|
|
1947
|
+
(this.uniforms.uEdgeFade?.value ?? 1);
|
|
1235
1948
|
mat.transparent = true;
|
|
1236
1949
|
mat.polygonOffset = true;
|
|
1237
1950
|
mat.polygonOffsetFactor = -1;
|
|
1238
1951
|
mat.polygonOffsetUnits = -1;
|
|
1239
1952
|
mat.needsUpdate = true;
|
|
1240
1953
|
}
|
|
1954
|
+
/**
|
|
1955
|
+
* 刷新所有棱线外观。
|
|
1956
|
+
*
|
|
1957
|
+
* 数据自带的棱线是逐对象的 LineBasicMaterial,改颜色只能一个个写;但原来每次都要
|
|
1958
|
+
* 遍历整个数据集树(几千个节点),颜色动画逐帧跑就是灾难。改成只刷注册时收集好的
|
|
1959
|
+
* 那批 LineSegments,复杂度降到 O(棱线条数)。
|
|
1960
|
+
*/
|
|
1241
1961
|
_refreshEdgeLines() {
|
|
1242
1962
|
this._syncEdgeStyle();
|
|
1243
|
-
for (const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
this._styleEdgeLine(o);
|
|
1247
|
-
});
|
|
1963
|
+
for (const lines of this._nativeEdgeLines) {
|
|
1964
|
+
if (lines.parent)
|
|
1965
|
+
this._styleEdgeLine(lines);
|
|
1248
1966
|
}
|
|
1249
1967
|
}
|
|
1250
1968
|
/** 地形 / 绿地 / 道路 / 地面铺装等不参与自动描边(数据自带棱线包另说) */
|
|
@@ -1293,7 +2011,22 @@ export class S3MViewer {
|
|
|
1293
2011
|
source.dispose();
|
|
1294
2012
|
if (!segments || segments.length < 6)
|
|
1295
2013
|
return;
|
|
1296
|
-
|
|
2014
|
+
// 3. 回填每段棱线所属的单体 batchId。
|
|
2015
|
+
// EdgesGeometry 只输出坐标不输出顶点号,只能拿端点坐标去查「位置 → batchId」表。
|
|
2016
|
+
// 没有这张表,单体一动棱线就留在原地——这正是「棱线跟随」的关键一环。
|
|
2017
|
+
const TOL = 0.005;
|
|
2018
|
+
const batchMap = buildPositionBatchMap(mesh.geometry, TOL);
|
|
2019
|
+
let batchIds;
|
|
2020
|
+
if (batchMap) {
|
|
2021
|
+
const segCount = segments.length / 6;
|
|
2022
|
+
batchIds = new Float32Array(segCount);
|
|
2023
|
+
for (let i = 0; i < segCount; i++) {
|
|
2024
|
+
const s = i * 6;
|
|
2025
|
+
batchIds[i] =
|
|
2026
|
+
batchMap.get(positionQuantKey(segments[s], segments[s + 1], segments[s + 2], 1 / TOL)) ?? -1;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
const lines = new THREE.Mesh(buildThickLineGeometry(segments, batchIds), this._createEdgeMaterial(mesh));
|
|
1297
2030
|
lines.renderOrder = 2;
|
|
1298
2031
|
// 屏幕空间外扩后可能超出原包围球,关掉剔除更稳
|
|
1299
2032
|
lines.frustumCulled = false;
|
|
@@ -1302,6 +2035,41 @@ export class S3MViewer {
|
|
|
1302
2035
|
ud.edges = lines;
|
|
1303
2036
|
mesh.add(lines); // 作为子节点,自动继承 mesh 的世界变换
|
|
1304
2037
|
}
|
|
2038
|
+
/**
|
|
2039
|
+
* 给一块网格造专属的棱线材质。
|
|
2040
|
+
*
|
|
2041
|
+
* 棱线材质必须逐网格一份,因为它要绑定**这个网格**的单体状态纹理;但外观
|
|
2042
|
+
* (颜色 / 粗细 / 透明度)必须全局统一——所以外观 uniform 直接复用
|
|
2043
|
+
* `_edgeStyleUniforms` 里的对象,`_syncEdgeStyle()` 改一次全部生效,
|
|
2044
|
+
* program 也因为着色器相同而只编译一次。
|
|
2045
|
+
*/
|
|
2046
|
+
_createEdgeMaterial(mesh) {
|
|
2047
|
+
const styled = styledMaterialOf(mesh);
|
|
2048
|
+
const batch = styled?.userData?.uBatchTex
|
|
2049
|
+
? {
|
|
2050
|
+
uBatchTex: styled.userData.uBatchTex,
|
|
2051
|
+
uBatchTex2: styled.userData.uBatchTex2,
|
|
2052
|
+
uBatchMix: this.uniforms.uBatchMix,
|
|
2053
|
+
uBatchTexWidth: styled.userData.uBatchTexWidth,
|
|
2054
|
+
uBatchTexActive: styled.userData.uBatchTexActive,
|
|
2055
|
+
}
|
|
2056
|
+
: undefined;
|
|
2057
|
+
return createThickLineMaterial(this.lineShared, {}, this._edgeStyleUniforms, batch);
|
|
2058
|
+
}
|
|
2059
|
+
/** 选中棱线的逐网格材质,道理同上 */
|
|
2060
|
+
_createSelectionEdgeMaterial(mesh) {
|
|
2061
|
+
const styled = styledMaterialOf(mesh);
|
|
2062
|
+
const batch = styled?.userData?.uBatchTex
|
|
2063
|
+
? {
|
|
2064
|
+
uBatchTex: styled.userData.uBatchTex,
|
|
2065
|
+
uBatchTex2: styled.userData.uBatchTex2,
|
|
2066
|
+
uBatchMix: this.uniforms.uBatchMix,
|
|
2067
|
+
uBatchTexWidth: styled.userData.uBatchTexWidth,
|
|
2068
|
+
uBatchTexActive: styled.userData.uBatchTexActive,
|
|
2069
|
+
}
|
|
2070
|
+
: undefined;
|
|
2071
|
+
return createThickLineMaterial(this.lineShared, {}, this._selEdgeStyleUniforms, batch);
|
|
2072
|
+
}
|
|
1305
2073
|
_removeEdges(mesh) {
|
|
1306
2074
|
const ud = mesh.userData;
|
|
1307
2075
|
const lines = ud.edges;
|
|
@@ -1309,6 +2077,14 @@ export class S3MViewer {
|
|
|
1309
2077
|
return;
|
|
1310
2078
|
mesh.remove(lines);
|
|
1311
2079
|
lines.geometry?.dispose();
|
|
2080
|
+
// 棱线材质现在是逐网格一份(要绑各自的 batch 纹理),得跟着释放
|
|
2081
|
+
const mat = lines.material;
|
|
2082
|
+
if (Array.isArray(mat)) {
|
|
2083
|
+
mat.forEach((m) => m?.dispose?.());
|
|
2084
|
+
}
|
|
2085
|
+
else {
|
|
2086
|
+
mat?.dispose?.();
|
|
2087
|
+
}
|
|
1312
2088
|
ud.edges = null;
|
|
1313
2089
|
}
|
|
1314
2090
|
/* ---------------------------------------------------------------- 拾取 */
|
|
@@ -1449,6 +2225,14 @@ export class S3MViewer {
|
|
|
1449
2225
|
const trans = off.translation;
|
|
1450
2226
|
const elev = off.elevation;
|
|
1451
2227
|
const finalPos = hitPoint.clone();
|
|
2228
|
+
if (off.rotation) {
|
|
2229
|
+
const rotMat = resolveRotationMatrix(off.rotation);
|
|
2230
|
+
if (rotMat) {
|
|
2231
|
+
const m3 = new THREE.Matrix3().set(rotMat[0], rotMat[1], rotMat[2], rotMat[3], rotMat[4], rotMat[5], rotMat[6], rotMat[7], rotMat[8]);
|
|
2232
|
+
const piv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
|
|
2233
|
+
finalPos.sub(piv).applyMatrix3(m3).add(piv);
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
1452
2236
|
if (trans)
|
|
1453
2237
|
finalPos.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
|
|
1454
2238
|
else if (elev)
|
|
@@ -1679,7 +2463,16 @@ export class S3MViewer {
|
|
|
1679
2463
|
const off = smid != null ? this._featureStyleManager.getFeatureEffectiveOffset(smid) : {};
|
|
1680
2464
|
const trans = off.translation;
|
|
1681
2465
|
const elev = off.elevation;
|
|
2466
|
+
const rotMat = off.rotation ? resolveRotationMatrix(off.rotation) : null;
|
|
2467
|
+
let rotM3 = null;
|
|
2468
|
+
let rotPiv = null;
|
|
2469
|
+
if (rotMat) {
|
|
2470
|
+
rotM3 = new THREE.Matrix3().set(rotMat[0], rotMat[1], rotMat[2], rotMat[3], rotMat[4], rotMat[5], rotMat[6], rotMat[7], rotMat[8]);
|
|
2471
|
+
rotPiv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
|
|
2472
|
+
}
|
|
1682
2473
|
for (const c of corners) {
|
|
2474
|
+
if (rotM3 && rotPiv)
|
|
2475
|
+
c.sub(rotPiv).applyMatrix3(rotM3).add(rotPiv);
|
|
1683
2476
|
if (trans)
|
|
1684
2477
|
c.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
|
|
1685
2478
|
else if (elev)
|
|
@@ -1830,24 +2623,34 @@ export class S3MViewer {
|
|
|
1830
2623
|
const ud = mesh.userData;
|
|
1831
2624
|
const geoRef = ud.geoRef;
|
|
1832
2625
|
let segments = [];
|
|
2626
|
+
// 这块网格上的棱线全部属于选中的那个单体,整批填同一个 batchId,
|
|
2627
|
+
// 顶点着色器才能从状态纹理里取到平移 / 拔高——选中跟随移动靠它。
|
|
2628
|
+
let batchId = -1;
|
|
1833
2629
|
if (mesh === result.mesh && result.instanceIndex != null) {
|
|
1834
2630
|
// 实例化几何没有逐单体的顶点区间,整个 base mesh 就是这一栋的外形
|
|
1835
2631
|
segments = [{ vertexOffset: 0, vertexCount: mesh.geometry.getAttribute('position')?.count ?? 0 }];
|
|
2632
|
+
const pickInfo = geoRef?.pickInfo;
|
|
2633
|
+
const entry = pickInfo?.[result.smid ?? ''] ??
|
|
2634
|
+
pickInfo?.[String(result.smid)];
|
|
2635
|
+
batchId = entry ? pickEntryId(entry) : -1;
|
|
1836
2636
|
}
|
|
1837
2637
|
else if (result.smid != null) {
|
|
1838
2638
|
const pickInfo = geoRef?.pickInfo;
|
|
1839
2639
|
const entry = pickInfo?.[result.smid] ??
|
|
1840
2640
|
pickInfo?.[String(result.smid)];
|
|
1841
2641
|
const bId = entry ? pickEntryId(entry) : -1;
|
|
2642
|
+
batchId = bId;
|
|
1842
2643
|
segments = entry?.segments ?? (bId >= 0 ? resolveFeature(geoRef, bId).segments : []);
|
|
1843
2644
|
}
|
|
1844
2645
|
else if (mesh === result.mesh) {
|
|
1845
2646
|
const localGeo = ud.geoByBatchId?.get(result.batchId) ?? geoRef;
|
|
1846
2647
|
segments = resolveFeature(localGeo, result.batchId).segments;
|
|
2648
|
+
batchId = result.batchId;
|
|
1847
2649
|
}
|
|
1848
2650
|
const pairs = buildFeatureEdgeSegments(mesh.geometry, segments);
|
|
1849
2651
|
if (pairs && pairs.length > 0) {
|
|
1850
|
-
const
|
|
2652
|
+
const batchIds = batchId >= 0 ? new Float32Array(pairs.length / 6).fill(batchId) : undefined;
|
|
2653
|
+
const lines = new THREE.Mesh(buildThickLineGeometry(pairs, batchIds), this._createSelectionEdgeMaterial(mesh));
|
|
1851
2654
|
lines.renderOrder = 4;
|
|
1852
2655
|
// 屏幕空间外扩后可能超出原包围球,关掉剔除更稳
|
|
1853
2656
|
lines.frustumCulled = false;
|
|
@@ -1912,7 +2715,10 @@ export class S3MViewer {
|
|
|
1912
2715
|
u.uColor.value.set(s.outlineColor ?? s.selectColor);
|
|
1913
2716
|
u.uWidth.value = Math.max(0.1, s.outlineWidthPx ?? 3);
|
|
1914
2717
|
u.uOpacity.value = Math.min(1, Math.max(0.05, s.opacity ?? 1));
|
|
1915
|
-
|
|
2718
|
+
// 只加全局 selectElevation:单体自己的拔高已经写在状态纹理第 2 行 z 里,
|
|
2719
|
+
// 顶点着色器采样时带出来了,这里再加一次就会双重拔高
|
|
2720
|
+
if (u.uElevation)
|
|
2721
|
+
u.uElevation.value = s.selectElevation ?? 0;
|
|
1916
2722
|
// 外扩描边是另一个材质实例,单体状态纹理的开关是建的时候抄过去的。
|
|
1917
2723
|
// 先选中、再抬高(或反过来)时模板会停留在旧值,这里补一次同步。
|
|
1918
2724
|
for (const outline of this._outlineMeshes) {
|
|
@@ -1998,6 +2804,8 @@ export class S3MViewer {
|
|
|
1998
2804
|
const gl = this.renderer.getContext();
|
|
1999
2805
|
if (gl && typeof gl.isContextLost === 'function' && gl.isContextLost())
|
|
2000
2806
|
return false;
|
|
2807
|
+
// 先推进动画,再刷脏标记,最后渲染 —— 保证本帧画出来的是最新值
|
|
2808
|
+
const animating = this.animator.tick();
|
|
2001
2809
|
// 脏标记正常由微任务刷掉;这里再兜一次底,保证「本帧画出来的」一定是最新样式
|
|
2002
2810
|
this.flushOverrides();
|
|
2003
2811
|
const canvas = this.canvas;
|
|
@@ -2049,8 +2857,12 @@ export class S3MViewer {
|
|
|
2049
2857
|
this._frame = 0;
|
|
2050
2858
|
this._fps.last = now;
|
|
2051
2859
|
}
|
|
2052
|
-
// 动态着色 / 瓦片加载中 /
|
|
2053
|
-
|
|
2860
|
+
// 动态着色 / 动画进行中 / 瓦片加载中 / 瓦片层级或可见性改变,都需要宿主继续驱动下一帧。
|
|
2861
|
+
// 动画结束那一帧必须精确回到 false,否则 MapLibre 会永远重绘(耗电、发热)。
|
|
2862
|
+
return (animating ||
|
|
2863
|
+
this.style.mode === STYLE.DYNAMIC ||
|
|
2864
|
+
this.stats.loading > 0 ||
|
|
2865
|
+
tilesetChanged);
|
|
2054
2866
|
}
|
|
2055
2867
|
/* ---------------------------------------------------------------- 只读属性 */
|
|
2056
2868
|
get fps() {
|