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
@@ -21,6 +21,19 @@ import {
21
21
  import type { AttributeQuery, AttributeQueryOptions, AttributeRow } from '../core/AttributeQuery.js';
22
22
  import type { StyledMaterial } from './StyleMaterial.js';
23
23
 
24
+ export interface GPUPathOptions {
25
+ /** 绑定的路径索引 ID(0, 1, 2...) */
26
+ pathId: number;
27
+ /** 进度的运动速率(归一化进度/秒,例如 0.05 即 20 秒一圈;为 0 则靠 progress/phase 驱动) */
28
+ speed?: number;
29
+ /** 初始相位 / 进度偏移(0.0 ~ 1.0) */
30
+ phase?: number;
31
+ /** 循环模式:'loop'(循环,默认) | 'pingpong'(往返) | 'clamp'(单次到达终点停止) */
32
+ loop?: 'loop' | 'pingpong' | 'clamp' | boolean;
33
+ /** 是否根据路径切线自动旋转朝向(偏航角 yaw,默认 true) */
34
+ autoHeading?: boolean;
35
+ }
36
+
24
37
  export interface FeatureStyle {
25
38
  /** 自定义单体颜色 */
26
39
  color?: string;
@@ -32,6 +45,161 @@ export interface FeatureStyle {
32
45
  opacity?: number;
33
46
  /** 是否隐藏该单体 */
34
47
  hidden?: boolean;
48
+ /**
49
+ * 单体姿态旋转。
50
+ * 支持:
51
+ * 1. 纯数字:绕 Z 轴(朝天方向)旋转弧度(最常用的建筑朝向)
52
+ * 2. 数组 [rx, ry, rz]:欧拉角(弧度)
53
+ * 3. 数组 [x, y, z, w]:四元数
54
+ * 4. THREE.Euler | THREE.Quaternion | THREE.Matrix3 | THREE.Matrix4
55
+ */
56
+ rotation?:
57
+ | number
58
+ | [number, number, number]
59
+ | [number, number, number, number]
60
+ | THREE.Euler
61
+ | THREE.Quaternion
62
+ | THREE.Matrix3
63
+ | THREE.Matrix4;
64
+ /** 旋转中心轴心点 [px, py, pz](局部坐标,默认 [0, 0, 0]) */
65
+ pivot?: [number, number, number];
66
+ /** GPU 路径动画配置 */
67
+ gpuPath?: GPUPathOptions;
68
+ }
69
+
70
+ export type RotationMatrix9 = [
71
+ number, number, number, // Row 0: R00, R01, R02
72
+ number, number, number, // Row 1: R10, R11, R12
73
+ number, number, number // Row 2: R20, R21, R22
74
+ ];
75
+
76
+ /**
77
+ * 将各类旋转表示统一解析为 3x3 行优先矩阵数组
78
+ */
79
+ export function resolveRotationMatrix(
80
+ rot?:
81
+ | number
82
+ | [number, number, number]
83
+ | [number, number, number, number]
84
+ | THREE.Euler
85
+ | THREE.Quaternion
86
+ | THREE.Matrix3
87
+ | THREE.Matrix4
88
+ | null
89
+ ): RotationMatrix9 | null {
90
+ if (rot == null) return null;
91
+ if (typeof rot === 'number') {
92
+ const c = Math.cos(rot);
93
+ const s = Math.sin(rot);
94
+ return [
95
+ c, -s, 0,
96
+ s, c, 0,
97
+ 0, 0, 1,
98
+ ];
99
+ }
100
+ if (Array.isArray(rot)) {
101
+ if (rot.length === 3) {
102
+ const euler = new THREE.Euler(rot[0], rot[1], rot[2]);
103
+ const m = new THREE.Matrix4().makeRotationFromEuler(euler);
104
+ const e = m.elements;
105
+ return [
106
+ e[0], e[4], e[8],
107
+ e[1], e[5], e[9],
108
+ e[2], e[6], e[10],
109
+ ];
110
+ }
111
+ if (rot.length === 4) {
112
+ const q = new THREE.Quaternion(rot[0], rot[1], rot[2], rot[3]);
113
+ const m = new THREE.Matrix4().makeRotationFromQuaternion(q);
114
+ const e = m.elements;
115
+ return [
116
+ e[0], e[4], e[8],
117
+ e[1], e[5], e[9],
118
+ e[2], e[6], e[10],
119
+ ];
120
+ }
121
+ }
122
+ if (rot instanceof THREE.Euler) {
123
+ const m = new THREE.Matrix4().makeRotationFromEuler(rot);
124
+ const e = m.elements;
125
+ return [
126
+ e[0], e[4], e[8],
127
+ e[1], e[5], e[9],
128
+ e[2], e[6], e[10],
129
+ ];
130
+ }
131
+ if (rot instanceof THREE.Quaternion) {
132
+ const m = new THREE.Matrix4().makeRotationFromQuaternion(rot);
133
+ const e = m.elements;
134
+ return [
135
+ e[0], e[4], e[8],
136
+ e[1], e[5], e[9],
137
+ e[2], e[6], e[10],
138
+ ];
139
+ }
140
+ if (rot instanceof THREE.Matrix3) {
141
+ const e = rot.elements;
142
+ return [
143
+ e[0], e[3], e[6],
144
+ e[1], e[4], e[7],
145
+ e[2], e[5], e[8],
146
+ ];
147
+ }
148
+ if (rot instanceof THREE.Matrix4) {
149
+ const e = rot.elements;
150
+ return [
151
+ e[0], e[4], e[8],
152
+ e[1], e[5], e[9],
153
+ e[2], e[6], e[10],
154
+ ];
155
+ }
156
+ return null;
157
+ }
158
+
159
+ /**
160
+ * 提取欧拉角 [rx, ry, rz](弧度)用于动画插值
161
+ */
162
+ export function extractEulerAngles(
163
+ rot?:
164
+ | number
165
+ | [number, number, number]
166
+ | [number, number, number, number]
167
+ | THREE.Euler
168
+ | THREE.Quaternion
169
+ | THREE.Matrix3
170
+ | THREE.Matrix4
171
+ | null
172
+ ): [number, number, number] {
173
+ if (rot == null) return [0, 0, 0];
174
+ if (typeof rot === 'number') return [0, 0, rot];
175
+ if (Array.isArray(rot)) {
176
+ if (rot.length === 3) return [rot[0], rot[1], rot[2]];
177
+ if (rot.length === 4) {
178
+ const q = new THREE.Quaternion(rot[0], rot[1], rot[2], rot[3]);
179
+ const e = new THREE.Euler().setFromQuaternion(q);
180
+ return [e.x, e.y, e.z];
181
+ }
182
+ }
183
+ if (rot instanceof THREE.Euler) return [rot.x, rot.y, rot.z];
184
+ if (rot instanceof THREE.Quaternion) {
185
+ const e = new THREE.Euler().setFromQuaternion(rot);
186
+ return [e.x, e.y, e.z];
187
+ }
188
+ if (rot instanceof THREE.Matrix3) {
189
+ const m = new THREE.Matrix4().set(
190
+ rot.elements[0], rot.elements[3], rot.elements[6], 0,
191
+ rot.elements[1], rot.elements[4], rot.elements[7], 0,
192
+ rot.elements[2], rot.elements[5], rot.elements[8], 0,
193
+ 0, 0, 0, 1
194
+ );
195
+ const e = new THREE.Euler().setFromRotationMatrix(m);
196
+ return [e.x, e.y, e.z];
197
+ }
198
+ if (rot instanceof THREE.Matrix4) {
199
+ const e = new THREE.Euler().setFromRotationMatrix(rot);
200
+ return [e.x, e.y, e.z];
201
+ }
202
+ return [0, 0, 0];
35
203
  }
36
204
 
37
205
  export interface ColorByFieldOptions {
@@ -173,7 +341,13 @@ export class FeatureStyleManager {
173
341
 
174
342
  private readonly _meshBatchTextures = new WeakMap<
175
343
  THREE.Mesh,
176
- { texture: THREE.DataTexture; data: Float32Array; width: number }
344
+ {
345
+ texture: THREE.DataTexture;
346
+ data: Float32Array;
347
+ texture2?: THREE.DataTexture;
348
+ data2?: Float32Array;
349
+ width: number;
350
+ }
177
351
  >();
178
352
  private readonly _markedMeshes = new Set<THREE.Mesh>();
179
353
  private _dirtyAll = false;
@@ -216,11 +390,21 @@ export class FeatureStyleManager {
216
390
  return this.featureStyles.get(smid) ?? null;
217
391
  }
218
392
 
219
- getFeatureEffectiveOffset(smid: number | null): { translation?: [number, number, number]; elevation?: number } {
393
+ getFeatureEffectiveOffset(smid: number | null): {
394
+ translation?: [number, number, number];
395
+ elevation?: number;
396
+ rotation?: FeatureStyle['rotation'];
397
+ pivot?: [number, number, number];
398
+ } {
220
399
  if (smid == null) return {};
221
400
  const st = this.featureStyles.get(smid);
222
401
  const elev = st?.elevation ?? this.thematicElevation.get(smid);
223
- return { translation: st?.translation, elevation: elev };
402
+ return {
403
+ translation: st?.translation,
404
+ elevation: elev,
405
+ rotation: st?.rotation,
406
+ pivot: st?.pivot,
407
+ };
224
408
  }
225
409
 
226
410
  getFeatureStyles(): Map<number, FeatureStyle> {
@@ -551,72 +735,55 @@ export class FeatureStyleManager {
551
735
  return { smids: 0, batches, conflicts, conflictingSmids: involved.size };
552
736
  }
553
737
 
554
- applyOverridesToMesh(mesh: THREE.Mesh): void {
555
- const mat = styledMaterialOf(mesh);
556
- if (!mat) return;
557
-
558
- const ud = mesh.userData as any;
559
- const pickInfo = ud.geoRef?.pickInfo;
560
-
561
- const hasStyles =
562
- this.featureStyles.size > 0 || this.thematicColors.size > 0 || this.thematicElevation.size > 0;
563
- const filter = this.filter;
564
- const hasFilter =
565
- this.fieldFilter != null ||
566
- (filter != null &&
567
- ((filter.include?.length ?? 0) > 0 ||
568
- (filter.exclude?.length ?? 0) > 0 ||
569
- filter.predicate != null));
570
-
571
- if (!hasStyles && !hasFilter) {
572
- if (mat.userData.uBatchTexActive) {
573
- mat.userData.uBatchTexActive.value = 0;
574
- }
575
- // 单体淡化清除后,把材质 transparent 还原成全局透明度语义
576
- if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
577
- return;
578
- }
579
-
580
- if (!pickInfo || Object.keys(pickInfo).length === 0) {
581
- if (mat.userData.uBatchTexActive) {
582
- mat.userData.uBatchTexActive.value = 0;
583
- }
584
- if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
585
- return;
586
- }
587
-
588
- let maxBatch = 0;
589
- for (const p of Object.values(pickInfo)) {
590
- const id = pickEntryId(p as any);
591
- if (id > maxBatch) maxBatch = id;
592
- }
593
- const width = Math.max(maxBatch + 1, 1);
594
-
595
- let record = this._meshBatchTextures.get(mesh);
596
- if (!record || record.width < width) {
597
- const data = new Float32Array(width * 2 * 4);
598
- const texture = new THREE.DataTexture(data, width, 2, THREE.RGBAFormat, THREE.FloatType);
599
- texture.minFilter = THREE.NearestFilter;
600
- texture.magFilter = THREE.NearestFilter;
601
- record = { texture, data, width };
602
- this._meshBatchTextures.set(mesh, record);
603
- mat.userData.uBatchTex.value = texture;
604
- mat.userData.uBatchTexWidth.value = width;
605
- }
606
-
607
- const { data, texture } = record;
608
- const w = record.width;
738
+ private _bakeStylesIntoBuffer(
739
+ data: Float32Array,
740
+ width: number,
741
+ pickInfo: Record<string, unknown>,
742
+ mesh: THREE.Mesh,
743
+ styleMap: Map<number, FeatureStyle>
744
+ ): { hasActive: boolean; hasFade: boolean; conflicts: number } {
745
+ const w = width;
609
746
  for (let i = 0; i < w; i++) {
747
+ // Row 0: [R, G, B, elevation/hidden]
610
748
  data[i * 4 + 0] = -1;
611
749
  data[i * 4 + 1] = -1;
612
750
  data[i * 4 + 2] = -1;
613
751
  data[i * 4 + 3] = 0;
614
752
 
753
+ // Row 1: [translationX, translationY, translationZ, opacity]
615
754
  const r1 = (w + i) * 4;
616
755
  data[r1 + 0] = 0;
617
756
  data[r1 + 1] = 0;
618
757
  data[r1 + 2] = 0;
619
758
  data[r1 + 3] = 0;
759
+
760
+ // Row 2: [R00, R01, R02, pivot.x] (单位矩阵默认值)
761
+ const r2 = (w * 2 + i) * 4;
762
+ data[r2 + 0] = 1;
763
+ data[r2 + 1] = 0;
764
+ data[r2 + 2] = 0;
765
+ data[r2 + 3] = 0;
766
+
767
+ // Row 3: [R10, R11, R12, pivot.y]
768
+ const r3 = (w * 3 + i) * 4;
769
+ data[r3 + 0] = 0;
770
+ data[r3 + 1] = 1;
771
+ data[r3 + 2] = 0;
772
+ data[r3 + 3] = 0;
773
+
774
+ // Row 4: [R20, R21, R22, pivot.z]
775
+ const r4 = (w * 4 + i) * 4;
776
+ data[r4 + 0] = 0;
777
+ data[r4 + 1] = 0;
778
+ data[r4 + 2] = 1;
779
+ data[r4 + 3] = 0;
780
+
781
+ // Row 5: [pathId, speed, phase, modeFlag] (默认 -1 表示未绑定 GPU 路径)
782
+ const r5 = (w * 5 + i) * 4;
783
+ data[r5 + 0] = -1;
784
+ data[r5 + 1] = 0;
785
+ data[r5 + 2] = 0;
786
+ data[r5 + 3] = 0;
620
787
  }
621
788
 
622
789
  let hasActive = false;
@@ -624,13 +791,18 @@ export class FeatureStyleManager {
624
791
  const colorOfBatch = new Map<number, string>();
625
792
  const elevOfBatch = new Map<number, number>();
626
793
  const translationOfBatch = new Map<number, [number, number, number]>();
794
+ const rotationOfBatch = new Map<number, RotationMatrix9>();
795
+ const pivotOfBatch = new Map<number, [number, number, number]>();
796
+ const gpuPathOfBatch = new Map<number, GPUPathOptions>();
627
797
  const opacityOfBatch = new Map<number, number>();
628
798
  const ownersOfBatch = new Map<number, number>();
799
+ const filter = this.filter;
800
+ const ud = mesh.userData as any;
629
801
 
630
802
  for (const [smidStr, entry] of Object.entries(pickInfo)) {
631
803
  const smid = Number(smidStr);
632
804
  const bId = pickEntryId(entry as any);
633
- if (bId < 0 || bId >= record.width) continue;
805
+ if (bId < 0 || bId >= w) continue;
634
806
  ownersOfBatch.set(bId, (ownersOfBatch.get(bId) ?? 0) + 1);
635
807
 
636
808
  let hidden = false;
@@ -669,7 +841,7 @@ export class FeatureStyleManager {
669
841
  }
670
842
  }
671
843
 
672
- const st = this.featureStyles.get(smid);
844
+ const st = styleMap.get(smid);
673
845
  if (st?.hidden) {
674
846
  hiddenBatches.add(bId);
675
847
  continue;
@@ -694,6 +866,17 @@ export class FeatureStyleManager {
694
866
  if (st?.translation && !translationOfBatch.has(bId)) {
695
867
  translationOfBatch.set(bId, st.translation);
696
868
  }
869
+
870
+ if (st?.rotation != null && !rotationOfBatch.has(bId)) {
871
+ const rotMat = resolveRotationMatrix(st.rotation);
872
+ if (rotMat) rotationOfBatch.set(bId, rotMat);
873
+ }
874
+ if (st?.pivot && !pivotOfBatch.has(bId)) {
875
+ pivotOfBatch.set(bId, st.pivot);
876
+ }
877
+ if (st?.gpuPath && !gpuPathOfBatch.has(bId)) {
878
+ gpuPathOfBatch.set(bId, st.gpuPath);
879
+ }
697
880
  }
698
881
 
699
882
  for (const bId of hiddenBatches) {
@@ -737,17 +920,142 @@ export class FeatureStyleManager {
737
920
  hasFade = true;
738
921
  }
739
922
 
740
- // 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
741
- if (hasFade && !mat.transparent) mat.transparent = true;
742
- else if (!hasFade && mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
923
+ for (const [bId, rot] of rotationOfBatch) {
924
+ if (hiddenBatches.has(bId)) continue;
925
+ const piv = pivotOfBatch.get(bId) ?? [0, 0, 0];
926
+ const r2 = (w * 2 + bId) * 4;
927
+ data[r2 + 0] = rot[0];
928
+ data[r2 + 1] = rot[1];
929
+ data[r2 + 2] = rot[2];
930
+ data[r2 + 3] = piv[0];
931
+
932
+ const r3 = (w * 3 + bId) * 4;
933
+ data[r3 + 0] = rot[3];
934
+ data[r3 + 1] = rot[4];
935
+ data[r3 + 2] = rot[5];
936
+ data[r3 + 3] = piv[1];
937
+
938
+ const r4 = (w * 4 + bId) * 4;
939
+ data[r4 + 0] = rot[6];
940
+ data[r4 + 1] = rot[7];
941
+ data[r4 + 2] = rot[8];
942
+ data[r4 + 3] = piv[2];
943
+ hasActive = true;
944
+ }
945
+
946
+ for (const [bId, piv] of pivotOfBatch) {
947
+ if (rotationOfBatch.has(bId) || hiddenBatches.has(bId)) continue;
948
+ data[(w * 2 + bId) * 4 + 3] = piv[0];
949
+ data[(w * 3 + bId) * 4 + 3] = piv[1];
950
+ data[(w * 4 + bId) * 4 + 3] = piv[2];
951
+ hasActive = true;
952
+ }
953
+
954
+ // Row 5: 烘焙 GPU 路径动画参数 [pathId, speed, phase, modeFlag]
955
+ for (const [bId, gp] of gpuPathOfBatch) {
956
+ if (hiddenBatches.has(bId)) continue;
957
+ const r5 = (w * 5 + bId) * 4;
958
+ data[r5 + 0] = gp.pathId;
959
+ data[r5 + 1] = gp.speed ?? 0;
960
+ data[r5 + 2] = gp.phase ?? 0;
961
+
962
+ let mode = 1; // 1 = loop (默认)
963
+ if (gp.loop === 'clamp' || gp.loop === false) {
964
+ mode = 2;
965
+ } else if (gp.loop === 'pingpong') {
966
+ mode = 3;
967
+ }
968
+ if (gp.autoHeading === false) {
969
+ mode += 3; // 4=loop+noHeading, 5=clamp+noHeading, 6=pingpong+noHeading
970
+ }
971
+ data[r5 + 3] = mode;
972
+ hasActive = true;
973
+ }
743
974
 
744
975
  let conflicts = 0;
745
976
  for (const [bId, owners] of ownersOfBatch) {
746
977
  if (owners < 2) continue;
747
- if (!hiddenBatches.has(bId) && !colorOfBatch.has(bId) && !elevOfBatch.has(bId)) continue;
978
+ if (
979
+ !hiddenBatches.has(bId) &&
980
+ !colorOfBatch.has(bId) &&
981
+ !elevOfBatch.has(bId) &&
982
+ !rotationOfBatch.has(bId) &&
983
+ !gpuPathOfBatch.has(bId)
984
+ ) {
985
+ continue;
986
+ }
748
987
  conflicts++;
749
988
  }
750
989
 
990
+ return { hasActive, hasFade, conflicts };
991
+ }
992
+
993
+ applyOverridesToMesh(mesh: THREE.Mesh): void {
994
+ const mat = styledMaterialOf(mesh);
995
+ if (!mat) return;
996
+
997
+ const ud = mesh.userData as any;
998
+ const pickInfo = ud.geoRef?.pickInfo;
999
+
1000
+ const hasStyles =
1001
+ this.featureStyles.size > 0 || this.thematicColors.size > 0 || this.thematicElevation.size > 0;
1002
+ const filter = this.filter;
1003
+ const hasFilter =
1004
+ this.fieldFilter != null ||
1005
+ (filter != null &&
1006
+ ((filter.include?.length ?? 0) > 0 ||
1007
+ (filter.exclude?.length ?? 0) > 0 ||
1008
+ filter.predicate != null));
1009
+
1010
+ if (!hasStyles && !hasFilter) {
1011
+ if (mat.userData.uBatchTexActive) {
1012
+ mat.userData.uBatchTexActive.value = 0;
1013
+ }
1014
+ // 单体淡化清除后,把材质 transparent 还原成全局透明度语义
1015
+ if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
1016
+ return;
1017
+ }
1018
+
1019
+ if (!pickInfo || Object.keys(pickInfo).length === 0) {
1020
+ if (mat.userData.uBatchTexActive) {
1021
+ mat.userData.uBatchTexActive.value = 0;
1022
+ }
1023
+ if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
1024
+ return;
1025
+ }
1026
+
1027
+ let maxBatch = 0;
1028
+ for (const p of Object.values(pickInfo)) {
1029
+ const id = pickEntryId(p as any);
1030
+ if (id > maxBatch) maxBatch = id;
1031
+ }
1032
+ const width = Math.max(maxBatch + 1, 1);
1033
+
1034
+ let record = this._meshBatchTextures.get(mesh);
1035
+ if (!record || record.width < width) {
1036
+ const data = new Float32Array(width * 6 * 4);
1037
+ const texture = new THREE.DataTexture(data, width, 6, THREE.RGBAFormat, THREE.FloatType);
1038
+ texture.minFilter = THREE.NearestFilter;
1039
+ texture.magFilter = THREE.NearestFilter;
1040
+ record = { texture, data, width };
1041
+ this._meshBatchTextures.set(mesh, record);
1042
+ mat.userData.uBatchTex.value = texture;
1043
+ mat.userData.uBatchTexWidth.value = width;
1044
+ }
1045
+
1046
+ const { data, texture } = record;
1047
+ const { hasActive, hasFade, conflicts } = this._bakeStylesIntoBuffer(
1048
+ data,
1049
+ record.width,
1050
+ pickInfo,
1051
+ mesh,
1052
+ this.featureStyles
1053
+ );
1054
+
1055
+ // 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
1056
+ if (hasFade && !mat.transparent) mat.transparent = true;
1057
+ else if (!hasFade && mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
1058
+
751
1059
  if (conflicts > 0 && !this._batchConflictWarned) {
752
1060
  this._batchConflictWarned = true;
753
1061
  console.warn(
@@ -764,6 +1072,93 @@ export class FeatureStyleManager {
764
1072
  }
765
1073
  }
766
1074
 
1075
+ /**
1076
+ * 准备双纹理交叉淡化:烘焙目标样式到第二纹理 (uBatchTex2)
1077
+ */
1078
+ prepareCrossFade(targetStyles: Map<number, FeatureStyle>, meshes: Iterable<THREE.Mesh>): void {
1079
+ for (const mesh of meshes) {
1080
+ const mat = styledMaterialOf(mesh);
1081
+ if (!mat) continue;
1082
+ const ud = mesh.userData as any;
1083
+ const pickInfo = ud.geoRef?.pickInfo;
1084
+ if (!pickInfo || Object.keys(pickInfo).length === 0) continue;
1085
+
1086
+ let maxBatch = 0;
1087
+ for (const p of Object.values(pickInfo)) {
1088
+ const id = pickEntryId(p as any);
1089
+ if (id > maxBatch) maxBatch = id;
1090
+ }
1091
+ const width = Math.max(maxBatch + 1, 1);
1092
+
1093
+ let record = this._meshBatchTextures.get(mesh);
1094
+ if (!record || record.width < width) {
1095
+ const data = new Float32Array(width * 6 * 4);
1096
+ const texture = new THREE.DataTexture(data, width, 6, THREE.RGBAFormat, THREE.FloatType);
1097
+ texture.minFilter = THREE.NearestFilter;
1098
+ texture.magFilter = THREE.NearestFilter;
1099
+ record = { texture, data, width };
1100
+ this._meshBatchTextures.set(mesh, record);
1101
+ mat.userData.uBatchTex.value = texture;
1102
+ mat.userData.uBatchTexWidth.value = width;
1103
+ }
1104
+
1105
+ if (!record.texture2 || !record.data2 || record.data2.length < record.width * 6 * 4) {
1106
+ const data2 = new Float32Array(record.width * 6 * 4);
1107
+ const texture2 = new THREE.DataTexture(data2, record.width, 6, THREE.RGBAFormat, THREE.FloatType);
1108
+ texture2.minFilter = THREE.NearestFilter;
1109
+ texture2.magFilter = THREE.NearestFilter;
1110
+ record.data2 = data2;
1111
+ record.texture2 = texture2;
1112
+ }
1113
+
1114
+ // 确保当前纹理包含当前状态
1115
+ this._bakeStylesIntoBuffer(record.data, record.width, pickInfo, mesh, this.featureStyles);
1116
+ record.texture.needsUpdate = true;
1117
+
1118
+ // 烘焙目标状态到 data2
1119
+ const res2 = this._bakeStylesIntoBuffer(record.data2, record.width, pickInfo, mesh, targetStyles);
1120
+ record.texture2.needsUpdate = true;
1121
+
1122
+ mat.userData.uBatchTex2.value = record.texture2;
1123
+ mat.userData.uBatchTexActive.value = 1;
1124
+ if (res2.hasFade && !mat.transparent) mat.transparent = true;
1125
+ }
1126
+ }
1127
+
1128
+ /**
1129
+ * 提交双纹理交叉淡化:将 targetStyles 回写到主状态,并将 data2 复制到 data
1130
+ */
1131
+ commitCrossFade(targetStyles: Map<number, FeatureStyle>, meshes: Iterable<THREE.Mesh>): void {
1132
+ this.featureStyles.clear();
1133
+ for (const [k, v] of targetStyles) {
1134
+ this.featureStyles.set(k, v);
1135
+ }
1136
+ for (const mesh of meshes) {
1137
+ const mat = styledMaterialOf(mesh);
1138
+ if (!mat) continue;
1139
+ const record = this._meshBatchTextures.get(mesh);
1140
+ if (!record || !record.data2 || !record.texture2) continue;
1141
+
1142
+ record.data.set(record.data2);
1143
+ record.texture.needsUpdate = true;
1144
+ mat.userData.uBatchTex2.value = mat.userData.uBatchTex.value;
1145
+ }
1146
+ this._onStyleChanged();
1147
+ this._triggerRepaint();
1148
+ }
1149
+
1150
+ /**
1151
+ * 取消交叉淡化并恢复单一纹理
1152
+ */
1153
+ cancelCrossFade(meshes: Iterable<THREE.Mesh>): void {
1154
+ for (const mesh of meshes) {
1155
+ const mat = styledMaterialOf(mesh);
1156
+ if (!mat) continue;
1157
+ mat.userData.uBatchTex2.value = mat.userData.uBatchTex.value;
1158
+ }
1159
+ this._triggerRepaint();
1160
+ }
1161
+
767
1162
  markOverridesDirty(subset?: Iterable<THREE.Mesh>, allMeshes?: Iterable<THREE.Mesh>): void {
768
1163
  if (subset) {
769
1164
  for (const m of subset) this._markedMeshes.add(m);
@@ -783,6 +1178,7 @@ export class FeatureStyleManager {
783
1178
  const record = this._meshBatchTextures.get(mesh);
784
1179
  if (record) {
785
1180
  record.texture.dispose();
1181
+ record.texture2?.dispose();
786
1182
  this._meshBatchTextures.delete(mesh);
787
1183
  }
788
1184
  }
@@ -807,4 +1203,68 @@ export class FeatureStyleManager {
807
1203
  this._markedMeshes.clear();
808
1204
  this._triggerRepaint();
809
1205
  }
1206
+
1207
+ /**
1208
+ * 将单体绑定到 GPU 路径动画
1209
+ * @param smid 单体 ID
1210
+ * @param pathId GPUPathManager 中的路径索引
1211
+ * @param options 速度、初始相位、循环模式与自动转向配置
1212
+ * @param meshes 可选指定的网格集合,不传则全量生效
1213
+ */
1214
+ bindGPUPath(
1215
+ smid: number,
1216
+ pathId: number,
1217
+ options: Omit<GPUPathOptions, 'pathId'> = {},
1218
+ meshes?: Iterable<THREE.Mesh>
1219
+ ): void {
1220
+ const cur = this.getFeatureStyle(smid) ?? {};
1221
+ this.setFeatureStyle(
1222
+ smid,
1223
+ {
1224
+ ...cur,
1225
+ gpuPath: {
1226
+ pathId,
1227
+ speed: options.speed ?? 0,
1228
+ phase: options.phase ?? 0,
1229
+ loop: options.loop ?? 'loop',
1230
+ autoHeading: options.autoHeading ?? true,
1231
+ },
1232
+ },
1233
+ meshes
1234
+ );
1235
+ }
1236
+
1237
+ /**
1238
+ * 解绑单体的 GPU 路径动画
1239
+ */
1240
+ unbindGPUPath(smid: number, meshes?: Iterable<THREE.Mesh>): void {
1241
+ const cur = this.getFeatureStyle(smid);
1242
+ if (!cur || !cur.gpuPath) return;
1243
+ const next = { ...cur };
1244
+ delete next.gpuPath;
1245
+ this.setFeatureStyle(smid, next, meshes);
1246
+ }
1247
+
1248
+ /**
1249
+ * 直接设置单体在 GPU 路径上的归一化进度(0.0 ~ 1.0)
1250
+ * @param smid 单体 ID
1251
+ * @param progress 归一化进度 (0~1)
1252
+ * @param meshes 可选指定的网格集合
1253
+ */
1254
+ setGPUPathProgress(smid: number, progress: number, meshes?: Iterable<THREE.Mesh>): void {
1255
+ const cur = this.getFeatureStyle(smid);
1256
+ if (!cur?.gpuPath) return;
1257
+ this.setFeatureStyle(
1258
+ smid,
1259
+ {
1260
+ ...cur,
1261
+ gpuPath: {
1262
+ ...cur.gpuPath,
1263
+ speed: 0,
1264
+ phase: Math.min(1.0, Math.max(0.0, progress)),
1265
+ },
1266
+ },
1267
+ meshes
1268
+ );
1269
+ }
810
1270
  }