s3m-three 0.3.8 → 0.3.10

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 (41) hide show
  1. package/dist/browser/s3m-three.iife.ext-three.js +322 -47
  2. package/dist/browser/s3m-three.iife.js +322 -47
  3. package/dist/core/types.d.ts +20 -0
  4. package/dist/core/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +5 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/render/EdgeLines.d.ts +18 -0
  10. package/dist/render/EdgeLines.d.ts.map +1 -1
  11. package/dist/render/EdgeLines.js +95 -13
  12. package/dist/render/EdgeLines.js.map +1 -1
  13. package/dist/render/FeatureStyleManager.d.ts +78 -0
  14. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  15. package/dist/render/FeatureStyleManager.js +407 -59
  16. package/dist/render/FeatureStyleManager.js.map +1 -1
  17. package/dist/render/GPUPathManager.d.ts +95 -0
  18. package/dist/render/GPUPathManager.d.ts.map +1 -0
  19. package/dist/render/GPUPathManager.js +265 -0
  20. package/dist/render/GPUPathManager.js.map +1 -0
  21. package/dist/render/OrthoProjection.d.ts +6 -0
  22. package/dist/render/OrthoProjection.d.ts.map +1 -1
  23. package/dist/render/OrthoProjection.js +48 -0
  24. package/dist/render/OrthoProjection.js.map +1 -1
  25. package/dist/render/S3MViewer.d.ts +66 -0
  26. package/dist/render/S3MViewer.d.ts.map +1 -1
  27. package/dist/render/S3MViewer.js +218 -6
  28. package/dist/render/S3MViewer.js.map +1 -1
  29. package/dist/render/StyleMaterial.d.ts +21 -0
  30. package/dist/render/StyleMaterial.d.ts.map +1 -1
  31. package/dist/render/StyleMaterial.js +256 -21
  32. package/dist/render/StyleMaterial.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/core/types.ts +10 -0
  35. package/src/index.ts +8 -0
  36. package/src/render/EdgeLines.ts +101 -13
  37. package/src/render/FeatureStyleManager.ts +524 -64
  38. package/src/render/GPUPathManager.ts +306 -0
  39. package/src/render/OrthoProjection.ts +52 -0
  40. package/src/render/S3MViewer.ts +272 -5
  41. package/src/render/StyleMaterial.ts +264 -21
@@ -41,6 +41,8 @@ import type { Channel, TransitionOptions } from '../anim/Animator.js';
41
41
  import { Path } from '../anim/Path.js';
42
42
  import type { EasingFn, EasingName } from '../anim/easing.js';
43
43
  import type { LineSharedUniforms, ThickLineMaterial, ThickLineStyleUniforms } from './EdgeLines.js';
44
+ import { GPUPathManager } from './GPUPathManager.js';
45
+ import type { GPUPathOptions } from './FeatureStyleManager.js';
44
46
  import {
45
47
  buildFeatureEdgeSegments,
46
48
  buildPositionBatchMap,
@@ -94,6 +96,8 @@ import {
94
96
  type FieldElevationOptions,
95
97
  type FieldElevationResult,
96
98
  type S3MFilter,
99
+ resolveRotationMatrix,
100
+ extractEulerAngles,
97
101
  } from './FeatureStyleManager.js';
98
102
 
99
103
  import {
@@ -304,7 +308,7 @@ const STYLE_SCALAR_KEYS = [
304
308
  const STYLE_COLOR_KEYS = ['colorA', 'colorB', 'colorC', 'selectColor', 'edgeColor'] as const;
305
309
 
306
310
  /** 可动画的单体属性 */
307
- const FEATURE_PROPS = ['translation', 'elevation', 'opacity', 'color'] as const;
311
+ const FEATURE_PROPS = ['translation', 'elevation', 'opacity', 'color', 'rotation'] as const;
308
312
  type FeatureProp = (typeof FEATURE_PROPS)[number];
309
313
 
310
314
  /** 单体沿折线运动的参数 */
@@ -368,6 +372,9 @@ export class S3MViewer {
368
372
  private readonly _datasetManager = new DatasetManager();
369
373
  /** 单体样式、专题着色与状态纹理管理器 */
370
374
  private readonly _featureStyleManager: FeatureStyleManager;
375
+ /** GPU 路径动画管理器 */
376
+ readonly gpuPathManager: GPUPathManager = new GPUPathManager();
377
+ private _customPathTime = false;
371
378
 
372
379
  /** 兼容 datasets 映射表暴露 */
373
380
  get datasets(): Map<string, S3MDatasetEntry> {
@@ -948,6 +955,111 @@ export class S3MViewer {
948
955
  this.moveFeature(smid, [cur[0] + delta[0], cur[1] + delta[1], cur[2] + delta[2]], transition);
949
956
  }
950
957
 
958
+ /**
959
+ * 让单体进行刚体姿态旋转(在 GPU 顶点着色器中以 pivot 轴心进行 3x3 矩阵旋转变换)。
960
+ *
961
+ * @param smid 单体 SMID
962
+ * @param rotation 旋转弧度或姿态:数字(绕 Z 轴 yaw 弧度)| [rx, ry, rz] 欧拉角 | [x,y,z,w] 四元数 | Euler/Quaternion/Matrix
963
+ * @param pivot 旋转中心轴心点 [px, py, pz](局部坐标,默认 [0, 0, 0])
964
+ * @param transition 动画过渡参数
965
+ */
966
+ rotateFeature(
967
+ smid: number,
968
+ rotation:
969
+ | number
970
+ | [number, number, number]
971
+ | [number, number, number, number]
972
+ | THREE.Euler
973
+ | THREE.Quaternion
974
+ | THREE.Matrix3
975
+ | THREE.Matrix4,
976
+ pivot?: [number, number, number],
977
+ transition?: TransitionOptions
978
+ ): void {
979
+ const cur = this._featureStyleManager.getFeatureStyle(smid) ?? {};
980
+ if (pivot) {
981
+ this._featureStyleManager.setFeatureStyle(smid, { ...cur, pivot }, this._meshesOf([smid]));
982
+ }
983
+ const targetEuler = extractEulerAngles(rotation);
984
+ const key = this._ensureFeatureChannel(smid, 'rotation');
985
+ this.animator.to(key, targetEuler, transition);
986
+ }
987
+
988
+ /** 让单体增量旋转的便捷写法 */
989
+ rotateFeatureBy(
990
+ smid: number,
991
+ deltaRotation: number | [number, number, number],
992
+ pivot?: [number, number, number],
993
+ transition?: TransitionOptions
994
+ ): void {
995
+ const curEuler = extractEulerAngles(this._featureStyleManager.getFeatureStyle(smid)?.rotation);
996
+ const d = typeof deltaRotation === 'number' ? [0, 0, deltaRotation] : deltaRotation;
997
+ this.rotateFeature(
998
+ smid,
999
+ [curEuler[0] + d[0], curEuler[1] + d[1], curEuler[2] + d[2]],
1000
+ pivot,
1001
+ transition
1002
+ );
1003
+ }
1004
+
1005
+ /**
1006
+ * 双纹理 GPU 交叉淡化(跨状态 / 专题图平滑过渡)。
1007
+ *
1008
+ * 采用双纹理 (uBatchTex + uBatchTex2 + uBatchMix) 硬件混合,
1009
+ * 即使几千上万栋建筑全量切换配色/高度/显隐/姿态,也无需在 CPU 逐单体创建通道,
1010
+ * 由 GPU 顶点与片元着色器以 60FPS 零成本硬件插值完成过渡。
1011
+ */
1012
+ crossFadeStyles(
1013
+ targetStyles: Map<number, FeatureStyle> | Record<string | number, FeatureStyle>,
1014
+ transition?: TransitionOptions
1015
+ ): void {
1016
+ const targetMap =
1017
+ targetStyles instanceof Map
1018
+ ? targetStyles
1019
+ : new Map(Object.entries(targetStyles).map(([k, v]) => [Number(k), v]));
1020
+
1021
+ const meshes = this.getAllMeshes();
1022
+ this._featureStyleManager.prepareCrossFade(targetMap, meshes);
1023
+
1024
+ const key = 'batch.mix';
1025
+ if (!this.animator.has(key)) {
1026
+ this.animator.register(key, {
1027
+ dim: 1,
1028
+ read: (out) => {
1029
+ out[0] = this.uniforms.uBatchMix?.value ?? 0;
1030
+ },
1031
+ write: (v) => {
1032
+ if (this.uniforms.uBatchMix) {
1033
+ this.uniforms.uBatchMix.value = v[0];
1034
+ }
1035
+ this.triggerRepaint();
1036
+ },
1037
+ });
1038
+ }
1039
+
1040
+ const duration = transition?.duration ?? 0;
1041
+ const wantAnim = duration > 0 || !!transition?.spring;
1042
+ if (!wantAnim) {
1043
+ if (this.animator.has(key)) this.animator.cancel(key);
1044
+ if (this.uniforms.uBatchMix) this.uniforms.uBatchMix.value = 0;
1045
+ this._featureStyleManager.commitCrossFade(targetMap, meshes);
1046
+ this.triggerRepaint();
1047
+ transition?.onComplete?.();
1048
+ return;
1049
+ }
1050
+
1051
+ if (this.uniforms.uBatchMix) this.uniforms.uBatchMix.value = 0;
1052
+ this.animator.to(key, 1, {
1053
+ ...transition,
1054
+ onComplete: () => {
1055
+ if (this.uniforms.uBatchMix) this.uniforms.uBatchMix.value = 0;
1056
+ this._featureStyleManager.commitCrossFade(targetMap, meshes);
1057
+ this.triggerRepaint();
1058
+ transition?.onComplete?.();
1059
+ },
1060
+ });
1061
+ }
1062
+
951
1063
  /**
952
1064
  * 让单体沿折线运动。
953
1065
  *
@@ -1025,6 +1137,96 @@ export class S3MViewer {
1025
1137
  this.animator.unregister(key);
1026
1138
  this._pathSpecs.delete(key);
1027
1139
  }
1140
+
1141
+ /**
1142
+ * 注册 GPU 路径到路径库
1143
+ * @param points 折线点数组或 Path 实例
1144
+ * @param opts 选项(可指定路径 ID 或闭合)
1145
+ * @returns 路径 ID
1146
+ */
1147
+ registerGPUPath(
1148
+ points: Path | ArrayLike<number> | Array<[number, number, number]>,
1149
+ opts: { id?: number; name?: string; closed?: boolean } = {}
1150
+ ): number {
1151
+ const id = this.gpuPathManager.registerPath(points, opts);
1152
+ this.gpuPathManager.updateUniforms(this.uniforms);
1153
+ return id;
1154
+ }
1155
+
1156
+ /**
1157
+ * 注销 GPU 路径
1158
+ */
1159
+ unregisterGPUPath(id: number): boolean {
1160
+ const res = this.gpuPathManager.unregisterPath(id);
1161
+ this.gpuPathManager.updateUniforms(this.uniforms);
1162
+ return res;
1163
+ }
1164
+
1165
+ /**
1166
+ * 获取 GPUPathManager 实例
1167
+ */
1168
+ getGPUPathManager(): GPUPathManager {
1169
+ return this.gpuPathManager;
1170
+ }
1171
+
1172
+ /**
1173
+ * 将单体绑定到 GPU 路径动画(智慧交通车流 / 大批量巡检无人机)
1174
+ * @param smid 单体 ID
1175
+ * @param path 路径 ID 或新的路径折线
1176
+ * @param options 动画配置(速度、相位、循环模式、自动朝向等)
1177
+ */
1178
+ bindGPUPath(
1179
+ smid: number,
1180
+ path: number | Path | ArrayLike<number> | Array<[number, number, number]>,
1181
+ options: Omit<GPUPathOptions, 'pathId'> = {}
1182
+ ): number {
1183
+ let pathId: number;
1184
+ if (typeof path === 'number') {
1185
+ pathId = path;
1186
+ } else {
1187
+ pathId = this.registerGPUPath(path);
1188
+ }
1189
+ const meshes = this._meshesOf([smid]);
1190
+ this._featureStyleManager.bindGPUPath(smid, pathId, options, meshes);
1191
+ for (const mesh of meshes) mesh.frustumCulled = false;
1192
+ this.gpuPathManager.updateUniforms(this.uniforms);
1193
+ return pathId;
1194
+ }
1195
+
1196
+ /**
1197
+ * 解绑单体的 GPU 路径动画
1198
+ */
1199
+ unbindGPUPath(smid: number): void {
1200
+ const meshes = this._meshesOf([smid]);
1201
+ this._featureStyleManager.unbindGPUPath(smid, meshes);
1202
+ this.gpuPathManager.updateUniforms(this.uniforms);
1203
+ }
1204
+
1205
+ /**
1206
+ * 手动设置单体在 GPU 路径上的归一化进度(0.0 ~ 1.0)
1207
+ */
1208
+ setGPUPathProgress(smid: number, progress: number): void {
1209
+ const meshes = this._meshesOf([smid]);
1210
+ this._featureStyleManager.setGPUPathProgress(smid, progress, meshes);
1211
+ for (const mesh of meshes) mesh.frustumCulled = false;
1212
+ }
1213
+
1214
+ /**
1215
+ * 设置全局 GPU 路径时间(秒)
1216
+ */
1217
+ setGPUPathTime(time: number, autoAdvance = false): void {
1218
+ if (this.uniforms.uPathTime) {
1219
+ this.uniforms.uPathTime.value = time;
1220
+ }
1221
+ this._customPathTime = !autoAdvance;
1222
+ }
1223
+
1224
+ /**
1225
+ * 获取全局 GPU 路径时间(秒)
1226
+ */
1227
+ getGPUPathTime(): number {
1228
+ return this.uniforms.uPathTime?.value ?? 0;
1229
+ }
1028
1230
  /**
1029
1231
  * 单体样式过渡:颜色 / 抬高 / 透明度 / 平移可以任意组合。
1030
1232
  *
@@ -1034,6 +1236,7 @@ export class S3MViewer {
1034
1236
  */
1035
1237
  animateFeature(smid: number, style: Partial<FeatureStyle>, transition?: TransitionOptions): void {
1036
1238
  if (style.translation) this.moveFeature(smid, style.translation, transition);
1239
+ if (style.rotation != null) this.rotateFeature(smid, style.rotation, style.pivot, transition);
1037
1240
  if (style.elevation != null) {
1038
1241
  this.animator.to(this._ensureFeatureChannel(smid, 'elevation'), style.elevation, transition);
1039
1242
  }
@@ -1127,7 +1330,7 @@ export class S3MViewer {
1127
1330
  const key = `feature.${smid}.${prop}`;
1128
1331
  if (this.animator.has(key)) return key;
1129
1332
 
1130
- const channel: Channel = prop === 'translation' || prop === 'color'
1333
+ const channel: Channel = prop === 'translation' || prop === 'color' || prop === 'rotation'
1131
1334
  ? {
1132
1335
  dim: 3,
1133
1336
  read: (out) => {
@@ -1136,6 +1339,12 @@ export class S3MViewer {
1136
1339
  out[0] = t?.[0] ?? 0;
1137
1340
  out[1] = t?.[1] ?? 0;
1138
1341
  out[2] = t?.[2] ?? 0;
1342
+ } else if (prop === 'rotation') {
1343
+ const r = this._featureStyleManager.getFeatureStyle(smid)?.rotation;
1344
+ const euler = extractEulerAngles(r);
1345
+ out[0] = euler[0];
1346
+ out[1] = euler[1];
1347
+ out[2] = euler[2];
1139
1348
  } else {
1140
1349
  // 没有自定义色时以图层主色为起点,插值才连续
1141
1350
  const hex = this._featureStyleManager.getFeatureStyle(smid)?.color ?? this.style.colorA;
@@ -1154,9 +1363,21 @@ export class S3MViewer {
1154
1363
  { ...cur, translation: [v[0], v[1], v[2]] },
1155
1364
  meshes
1156
1365
  );
1157
- // 有位移就关掉视锥剔除,否则移出原始包围球后整块瓦片会凭空消失
1158
- const moving = Math.abs(v[0]) > 1e-6 || Math.abs(v[1]) > 1e-6 || Math.abs(v[2]) > 1e-6;
1159
- for (const mesh of meshes) mesh.frustumCulled = !moving;
1366
+ // 有位移或旋转就关掉视锥剔除,否则移出原始包围球后整块瓦片会凭空消失
1367
+ const transMoving = Math.abs(v[0]) > 1e-6 || Math.abs(v[1]) > 1e-6 || Math.abs(v[2]) > 1e-6;
1368
+ const rotEuler = extractEulerAngles(cur.rotation);
1369
+ const rotMoving = Math.abs(rotEuler[0]) > 1e-6 || Math.abs(rotEuler[1]) > 1e-6 || Math.abs(rotEuler[2]) > 1e-6;
1370
+ for (const mesh of meshes) mesh.frustumCulled = !(transMoving || rotMoving);
1371
+ } else if (prop === 'rotation') {
1372
+ this._featureStyleManager.setFeatureStyle(
1373
+ smid,
1374
+ { ...cur, rotation: [v[0], v[1], v[2]] },
1375
+ meshes
1376
+ );
1377
+ const rotMoving = Math.abs(v[0]) > 1e-6 || Math.abs(v[1]) > 1e-6 || Math.abs(v[2]) > 1e-6;
1378
+ const t = cur.translation ?? [0, 0, 0];
1379
+ const transMoving = Math.abs(t[0]) > 1e-6 || Math.abs(t[1]) > 1e-6 || Math.abs(t[2]) > 1e-6;
1380
+ for (const mesh of meshes) mesh.frustumCulled = !(rotMoving || transMoving);
1160
1381
  } else {
1161
1382
  const c = this._colorScratch.setRGB(v[0], v[1], v[2]);
1162
1383
  this._featureStyleManager.setFeatureStyle(
@@ -2322,8 +2543,14 @@ export class S3MViewer {
2322
2543
  const batch = styled?.userData?.uBatchTex
2323
2544
  ? {
2324
2545
  uBatchTex: styled.userData.uBatchTex,
2546
+ uBatchTex2: styled.userData.uBatchTex2,
2547
+ uBatchMix: this.uniforms.uBatchMix,
2325
2548
  uBatchTexWidth: styled.userData.uBatchTexWidth,
2326
2549
  uBatchTexActive: styled.userData.uBatchTexActive,
2550
+ uPathTex: this.uniforms.uPathTex,
2551
+ uPathCount: this.uniforms.uPathCount,
2552
+ uPathTexActive: this.uniforms.uPathTexActive,
2553
+ uPathTime: this.uniforms.uPathTime,
2327
2554
  }
2328
2555
  : undefined;
2329
2556
  return createThickLineMaterial(this.lineShared, {}, this._edgeStyleUniforms, batch);
@@ -2335,8 +2562,14 @@ export class S3MViewer {
2335
2562
  const batch = styled?.userData?.uBatchTex
2336
2563
  ? {
2337
2564
  uBatchTex: styled.userData.uBatchTex,
2565
+ uBatchTex2: styled.userData.uBatchTex2,
2566
+ uBatchMix: this.uniforms.uBatchMix,
2338
2567
  uBatchTexWidth: styled.userData.uBatchTexWidth,
2339
2568
  uBatchTexActive: styled.userData.uBatchTexActive,
2569
+ uPathTex: this.uniforms.uPathTex,
2570
+ uPathCount: this.uniforms.uPathCount,
2571
+ uPathTexActive: this.uniforms.uPathTexActive,
2572
+ uPathTime: this.uniforms.uPathTime,
2340
2573
  }
2341
2574
  : undefined;
2342
2575
  return createThickLineMaterial(this.lineShared, {}, this._selEdgeStyleUniforms, batch);
@@ -2501,6 +2734,20 @@ export class S3MViewer {
2501
2734
  const trans = off.translation;
2502
2735
  const elev = off.elevation;
2503
2736
  const finalPos = hitPoint.clone();
2737
+
2738
+ if (off.rotation) {
2739
+ const rotMat = resolveRotationMatrix(off.rotation);
2740
+ if (rotMat) {
2741
+ const m3 = new THREE.Matrix3().set(
2742
+ rotMat[0], rotMat[1], rotMat[2],
2743
+ rotMat[3], rotMat[4], rotMat[5],
2744
+ rotMat[6], rotMat[7], rotMat[8]
2745
+ );
2746
+ const piv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
2747
+ finalPos.sub(piv).applyMatrix3(m3).add(piv);
2748
+ }
2749
+ }
2750
+
2504
2751
  if (trans) finalPos.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
2505
2752
  else if (elev) finalPos.z += elev;
2506
2753
 
@@ -2731,7 +2978,20 @@ export class S3MViewer {
2731
2978
  const trans = off.translation;
2732
2979
  const elev = off.elevation;
2733
2980
 
2981
+ const rotMat = off.rotation ? resolveRotationMatrix(off.rotation) : null;
2982
+ let rotM3: THREE.Matrix3 | null = null;
2983
+ let rotPiv: THREE.Vector3 | null = null;
2984
+ if (rotMat) {
2985
+ rotM3 = new THREE.Matrix3().set(
2986
+ rotMat[0], rotMat[1], rotMat[2],
2987
+ rotMat[3], rotMat[4], rotMat[5],
2988
+ rotMat[6], rotMat[7], rotMat[8]
2989
+ );
2990
+ rotPiv = off.pivot ? new THREE.Vector3(off.pivot[0], off.pivot[1], off.pivot[2]) : new THREE.Vector3(0, 0, 0);
2991
+ }
2992
+
2734
2993
  for (const c of corners) {
2994
+ if (rotM3 && rotPiv) c.sub(rotPiv).applyMatrix3(rotM3).add(rotPiv);
2735
2995
  if (trans) c.add(new THREE.Vector3(trans[0], trans[1], trans[2]));
2736
2996
  else if (elev) c.z += elev;
2737
2997
 
@@ -3127,6 +3387,12 @@ export class S3MViewer {
3127
3387
  this.uniforms.uTime.value += 0.016 * (this.style.speed ?? 1);
3128
3388
  }
3129
3389
 
3390
+ this.gpuPathManager.updateUniforms(this.uniforms);
3391
+ const gpuPathActive = (this.uniforms.uPathTexActive?.value ?? 0) > 0;
3392
+ if (gpuPathActive && !this._customPathTime && this.uniforms.uPathTime) {
3393
+ this.uniforms.uPathTime.value += 0.016;
3394
+ }
3395
+
3130
3396
  this._updateLights();
3131
3397
 
3132
3398
  // 等宽棱线按设备像素撑开,分辨率每帧同步一次
@@ -3149,6 +3415,7 @@ export class S3MViewer {
3149
3415
  return (
3150
3416
  animating ||
3151
3417
  this.style.mode === STYLE.DYNAMIC ||
3418
+ gpuPathActive ||
3152
3419
  this.stats.loading > 0 ||
3153
3420
  tilesetChanged
3154
3421
  );