s3m-three 0.3.8 → 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.
Files changed (36) hide show
  1. package/dist/browser/s3m-three.iife.ext-three.js +187 -46
  2. package/dist/browser/s3m-three.iife.js +187 -46
  3. package/dist/core/types.d.ts +4 -0
  4. package/dist/core/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/render/EdgeLines.d.ts +6 -0
  10. package/dist/render/EdgeLines.d.ts.map +1 -1
  11. package/dist/render/EdgeLines.js +41 -13
  12. package/dist/render/EdgeLines.js.map +1 -1
  13. package/dist/render/FeatureStyleManager.d.ts +45 -0
  14. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  15. package/dist/render/FeatureStyleManager.js +320 -59
  16. package/dist/render/FeatureStyleManager.js.map +1 -1
  17. package/dist/render/OrthoProjection.d.ts +6 -0
  18. package/dist/render/OrthoProjection.d.ts.map +1 -1
  19. package/dist/render/OrthoProjection.js +48 -0
  20. package/dist/render/OrthoProjection.js.map +1 -1
  21. package/dist/render/S3MViewer.d.ts +19 -0
  22. package/dist/render/S3MViewer.d.ts.map +1 -1
  23. package/dist/render/S3MViewer.js +122 -6
  24. package/dist/render/S3MViewer.js.map +1 -1
  25. package/dist/render/StyleMaterial.d.ts +9 -0
  26. package/dist/render/StyleMaterial.d.ts.map +1 -1
  27. package/dist/render/StyleMaterial.js +153 -21
  28. package/dist/render/StyleMaterial.js.map +1 -1
  29. package/package.json +1 -1
  30. package/src/core/types.ts +2 -0
  31. package/src/index.ts +6 -0
  32. package/src/render/EdgeLines.ts +43 -13
  33. package/src/render/FeatureStyleManager.ts +405 -64
  34. package/src/render/OrthoProjection.ts +52 -0
  35. package/src/render/S3MViewer.ts +162 -5
  36. package/src/render/StyleMaterial.ts +157 -21
@@ -32,6 +32,159 @@ export interface FeatureStyle {
32
32
  opacity?: number;
33
33
  /** 是否隐藏该单体 */
34
34
  hidden?: boolean;
35
+ /**
36
+ * 单体姿态旋转。
37
+ * 支持:
38
+ * 1. 纯数字:绕 Z 轴(朝天方向)旋转弧度(最常用的建筑朝向)
39
+ * 2. 数组 [rx, ry, rz]:欧拉角(弧度)
40
+ * 3. 数组 [x, y, z, w]:四元数
41
+ * 4. THREE.Euler | THREE.Quaternion | THREE.Matrix3 | THREE.Matrix4
42
+ */
43
+ rotation?:
44
+ | number
45
+ | [number, number, number]
46
+ | [number, number, number, number]
47
+ | THREE.Euler
48
+ | THREE.Quaternion
49
+ | THREE.Matrix3
50
+ | THREE.Matrix4;
51
+ /** 旋转中心轴心点 [px, py, pz](局部坐标,默认 [0, 0, 0]) */
52
+ pivot?: [number, number, number];
53
+ }
54
+
55
+ export type RotationMatrix9 = [
56
+ number, number, number, // Row 0: R00, R01, R02
57
+ number, number, number, // Row 1: R10, R11, R12
58
+ number, number, number // Row 2: R20, R21, R22
59
+ ];
60
+
61
+ /**
62
+ * 将各类旋转表示统一解析为 3x3 行优先矩阵数组
63
+ */
64
+ export function resolveRotationMatrix(
65
+ rot?:
66
+ | number
67
+ | [number, number, number]
68
+ | [number, number, number, number]
69
+ | THREE.Euler
70
+ | THREE.Quaternion
71
+ | THREE.Matrix3
72
+ | THREE.Matrix4
73
+ | null
74
+ ): RotationMatrix9 | null {
75
+ if (rot == null) return null;
76
+ if (typeof rot === 'number') {
77
+ const c = Math.cos(rot);
78
+ const s = Math.sin(rot);
79
+ return [
80
+ c, -s, 0,
81
+ s, c, 0,
82
+ 0, 0, 1,
83
+ ];
84
+ }
85
+ if (Array.isArray(rot)) {
86
+ if (rot.length === 3) {
87
+ const euler = new THREE.Euler(rot[0], rot[1], rot[2]);
88
+ const m = new THREE.Matrix4().makeRotationFromEuler(euler);
89
+ const e = m.elements;
90
+ return [
91
+ e[0], e[4], e[8],
92
+ e[1], e[5], e[9],
93
+ e[2], e[6], e[10],
94
+ ];
95
+ }
96
+ if (rot.length === 4) {
97
+ const q = new THREE.Quaternion(rot[0], rot[1], rot[2], rot[3]);
98
+ const m = new THREE.Matrix4().makeRotationFromQuaternion(q);
99
+ const e = m.elements;
100
+ return [
101
+ e[0], e[4], e[8],
102
+ e[1], e[5], e[9],
103
+ e[2], e[6], e[10],
104
+ ];
105
+ }
106
+ }
107
+ if (rot instanceof THREE.Euler) {
108
+ const m = new THREE.Matrix4().makeRotationFromEuler(rot);
109
+ const e = m.elements;
110
+ return [
111
+ e[0], e[4], e[8],
112
+ e[1], e[5], e[9],
113
+ e[2], e[6], e[10],
114
+ ];
115
+ }
116
+ if (rot instanceof THREE.Quaternion) {
117
+ const m = new THREE.Matrix4().makeRotationFromQuaternion(rot);
118
+ const e = m.elements;
119
+ return [
120
+ e[0], e[4], e[8],
121
+ e[1], e[5], e[9],
122
+ e[2], e[6], e[10],
123
+ ];
124
+ }
125
+ if (rot instanceof THREE.Matrix3) {
126
+ const e = rot.elements;
127
+ return [
128
+ e[0], e[3], e[6],
129
+ e[1], e[4], e[7],
130
+ e[2], e[5], e[8],
131
+ ];
132
+ }
133
+ if (rot instanceof THREE.Matrix4) {
134
+ const e = rot.elements;
135
+ return [
136
+ e[0], e[4], e[8],
137
+ e[1], e[5], e[9],
138
+ e[2], e[6], e[10],
139
+ ];
140
+ }
141
+ return null;
142
+ }
143
+
144
+ /**
145
+ * 提取欧拉角 [rx, ry, rz](弧度)用于动画插值
146
+ */
147
+ export function extractEulerAngles(
148
+ rot?:
149
+ | number
150
+ | [number, number, number]
151
+ | [number, number, number, number]
152
+ | THREE.Euler
153
+ | THREE.Quaternion
154
+ | THREE.Matrix3
155
+ | THREE.Matrix4
156
+ | null
157
+ ): [number, number, number] {
158
+ if (rot == null) return [0, 0, 0];
159
+ if (typeof rot === 'number') return [0, 0, rot];
160
+ if (Array.isArray(rot)) {
161
+ if (rot.length === 3) return [rot[0], rot[1], rot[2]];
162
+ if (rot.length === 4) {
163
+ const q = new THREE.Quaternion(rot[0], rot[1], rot[2], rot[3]);
164
+ const e = new THREE.Euler().setFromQuaternion(q);
165
+ return [e.x, e.y, e.z];
166
+ }
167
+ }
168
+ if (rot instanceof THREE.Euler) return [rot.x, rot.y, rot.z];
169
+ if (rot instanceof THREE.Quaternion) {
170
+ const e = new THREE.Euler().setFromQuaternion(rot);
171
+ return [e.x, e.y, e.z];
172
+ }
173
+ if (rot instanceof THREE.Matrix3) {
174
+ const m = new THREE.Matrix4().set(
175
+ rot.elements[0], rot.elements[3], rot.elements[6], 0,
176
+ rot.elements[1], rot.elements[4], rot.elements[7], 0,
177
+ rot.elements[2], rot.elements[5], rot.elements[8], 0,
178
+ 0, 0, 0, 1
179
+ );
180
+ const e = new THREE.Euler().setFromRotationMatrix(m);
181
+ return [e.x, e.y, e.z];
182
+ }
183
+ if (rot instanceof THREE.Matrix4) {
184
+ const e = new THREE.Euler().setFromRotationMatrix(rot);
185
+ return [e.x, e.y, e.z];
186
+ }
187
+ return [0, 0, 0];
35
188
  }
36
189
 
37
190
  export interface ColorByFieldOptions {
@@ -173,7 +326,13 @@ export class FeatureStyleManager {
173
326
 
174
327
  private readonly _meshBatchTextures = new WeakMap<
175
328
  THREE.Mesh,
176
- { texture: THREE.DataTexture; data: Float32Array; width: number }
329
+ {
330
+ texture: THREE.DataTexture;
331
+ data: Float32Array;
332
+ texture2?: THREE.DataTexture;
333
+ data2?: Float32Array;
334
+ width: number;
335
+ }
177
336
  >();
178
337
  private readonly _markedMeshes = new Set<THREE.Mesh>();
179
338
  private _dirtyAll = false;
@@ -216,11 +375,21 @@ export class FeatureStyleManager {
216
375
  return this.featureStyles.get(smid) ?? null;
217
376
  }
218
377
 
219
- getFeatureEffectiveOffset(smid: number | null): { translation?: [number, number, number]; elevation?: number } {
378
+ getFeatureEffectiveOffset(smid: number | null): {
379
+ translation?: [number, number, number];
380
+ elevation?: number;
381
+ rotation?: FeatureStyle['rotation'];
382
+ pivot?: [number, number, number];
383
+ } {
220
384
  if (smid == null) return {};
221
385
  const st = this.featureStyles.get(smid);
222
386
  const elev = st?.elevation ?? this.thematicElevation.get(smid);
223
- return { translation: st?.translation, elevation: elev };
387
+ return {
388
+ translation: st?.translation,
389
+ elevation: elev,
390
+ rotation: st?.rotation,
391
+ pivot: st?.pivot,
392
+ };
224
393
  }
225
394
 
226
395
  getFeatureStyles(): Map<number, FeatureStyle> {
@@ -551,72 +720,48 @@ export class FeatureStyleManager {
551
720
  return { smids: 0, batches, conflicts, conflictingSmids: involved.size };
552
721
  }
553
722
 
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;
723
+ private _bakeStylesIntoBuffer(
724
+ data: Float32Array,
725
+ width: number,
726
+ pickInfo: Record<string, unknown>,
727
+ mesh: THREE.Mesh,
728
+ styleMap: Map<number, FeatureStyle>
729
+ ): { hasActive: boolean; hasFade: boolean; conflicts: number } {
730
+ const w = width;
609
731
  for (let i = 0; i < w; i++) {
732
+ // Row 0: [R, G, B, elevation/hidden]
610
733
  data[i * 4 + 0] = -1;
611
734
  data[i * 4 + 1] = -1;
612
735
  data[i * 4 + 2] = -1;
613
736
  data[i * 4 + 3] = 0;
614
737
 
738
+ // Row 1: [translationX, translationY, translationZ, opacity]
615
739
  const r1 = (w + i) * 4;
616
740
  data[r1 + 0] = 0;
617
741
  data[r1 + 1] = 0;
618
742
  data[r1 + 2] = 0;
619
743
  data[r1 + 3] = 0;
744
+
745
+ // Row 2: [R00, R01, R02, pivot.x] (单位矩阵默认值)
746
+ const r2 = (w * 2 + i) * 4;
747
+ data[r2 + 0] = 1;
748
+ data[r2 + 1] = 0;
749
+ data[r2 + 2] = 0;
750
+ data[r2 + 3] = 0;
751
+
752
+ // Row 3: [R10, R11, R12, pivot.y]
753
+ const r3 = (w * 3 + i) * 4;
754
+ data[r3 + 0] = 0;
755
+ data[r3 + 1] = 1;
756
+ data[r3 + 2] = 0;
757
+ data[r3 + 3] = 0;
758
+
759
+ // Row 4: [R20, R21, R22, pivot.z]
760
+ const r4 = (w * 4 + i) * 4;
761
+ data[r4 + 0] = 0;
762
+ data[r4 + 1] = 0;
763
+ data[r4 + 2] = 1;
764
+ data[r4 + 3] = 0;
620
765
  }
621
766
 
622
767
  let hasActive = false;
@@ -624,13 +769,17 @@ export class FeatureStyleManager {
624
769
  const colorOfBatch = new Map<number, string>();
625
770
  const elevOfBatch = new Map<number, number>();
626
771
  const translationOfBatch = new Map<number, [number, number, number]>();
772
+ const rotationOfBatch = new Map<number, RotationMatrix9>();
773
+ const pivotOfBatch = new Map<number, [number, number, number]>();
627
774
  const opacityOfBatch = new Map<number, number>();
628
775
  const ownersOfBatch = new Map<number, number>();
776
+ const filter = this.filter;
777
+ const ud = mesh.userData as any;
629
778
 
630
779
  for (const [smidStr, entry] of Object.entries(pickInfo)) {
631
780
  const smid = Number(smidStr);
632
781
  const bId = pickEntryId(entry as any);
633
- if (bId < 0 || bId >= record.width) continue;
782
+ if (bId < 0 || bId >= w) continue;
634
783
  ownersOfBatch.set(bId, (ownersOfBatch.get(bId) ?? 0) + 1);
635
784
 
636
785
  let hidden = false;
@@ -669,7 +818,7 @@ export class FeatureStyleManager {
669
818
  }
670
819
  }
671
820
 
672
- const st = this.featureStyles.get(smid);
821
+ const st = styleMap.get(smid);
673
822
  if (st?.hidden) {
674
823
  hiddenBatches.add(bId);
675
824
  continue;
@@ -694,6 +843,14 @@ export class FeatureStyleManager {
694
843
  if (st?.translation && !translationOfBatch.has(bId)) {
695
844
  translationOfBatch.set(bId, st.translation);
696
845
  }
846
+
847
+ const rotMat = resolveRotationMatrix(st?.rotation);
848
+ if (rotMat && !rotationOfBatch.has(bId)) {
849
+ rotationOfBatch.set(bId, rotMat);
850
+ }
851
+ if (st?.pivot && !pivotOfBatch.has(bId)) {
852
+ pivotOfBatch.set(bId, st.pivot);
853
+ }
697
854
  }
698
855
 
699
856
  for (const bId of hiddenBatches) {
@@ -737,17 +894,113 @@ export class FeatureStyleManager {
737
894
  hasFade = true;
738
895
  }
739
896
 
740
- // 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
741
- if (hasFade && !mat.transparent) mat.transparent = true;
742
- else if (!hasFade && mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
897
+ for (const [bId, rot] of rotationOfBatch) {
898
+ if (hiddenBatches.has(bId)) continue;
899
+ const piv = pivotOfBatch.get(bId) ?? [0, 0, 0];
900
+ const r2 = (w * 2 + bId) * 4;
901
+ data[r2 + 0] = rot[0];
902
+ data[r2 + 1] = rot[1];
903
+ data[r2 + 2] = rot[2];
904
+ data[r2 + 3] = piv[0];
905
+
906
+ const r3 = (w * 3 + bId) * 4;
907
+ data[r3 + 0] = rot[3];
908
+ data[r3 + 1] = rot[4];
909
+ data[r3 + 2] = rot[5];
910
+ data[r3 + 3] = piv[1];
911
+
912
+ const r4 = (w * 4 + bId) * 4;
913
+ data[r4 + 0] = rot[6];
914
+ data[r4 + 1] = rot[7];
915
+ data[r4 + 2] = rot[8];
916
+ data[r4 + 3] = piv[2];
917
+ hasActive = true;
918
+ }
919
+
920
+ for (const [bId, piv] of pivotOfBatch) {
921
+ if (rotationOfBatch.has(bId) || hiddenBatches.has(bId)) continue;
922
+ data[(w * 2 + bId) * 4 + 3] = piv[0];
923
+ data[(w * 3 + bId) * 4 + 3] = piv[1];
924
+ data[(w * 4 + bId) * 4 + 3] = piv[2];
925
+ hasActive = true;
926
+ }
743
927
 
744
928
  let conflicts = 0;
745
929
  for (const [bId, owners] of ownersOfBatch) {
746
930
  if (owners < 2) continue;
747
- if (!hiddenBatches.has(bId) && !colorOfBatch.has(bId) && !elevOfBatch.has(bId)) continue;
931
+ if (!hiddenBatches.has(bId) && !colorOfBatch.has(bId) && !elevOfBatch.has(bId) && !rotationOfBatch.has(bId)) continue;
748
932
  conflicts++;
749
933
  }
750
934
 
935
+ return { hasActive, hasFade, conflicts };
936
+ }
937
+
938
+ applyOverridesToMesh(mesh: THREE.Mesh): void {
939
+ const mat = styledMaterialOf(mesh);
940
+ if (!mat) return;
941
+
942
+ const ud = mesh.userData as any;
943
+ const pickInfo = ud.geoRef?.pickInfo;
944
+
945
+ const hasStyles =
946
+ this.featureStyles.size > 0 || this.thematicColors.size > 0 || this.thematicElevation.size > 0;
947
+ const filter = this.filter;
948
+ const hasFilter =
949
+ this.fieldFilter != null ||
950
+ (filter != null &&
951
+ ((filter.include?.length ?? 0) > 0 ||
952
+ (filter.exclude?.length ?? 0) > 0 ||
953
+ filter.predicate != null));
954
+
955
+ if (!hasStyles && !hasFilter) {
956
+ if (mat.userData.uBatchTexActive) {
957
+ mat.userData.uBatchTexActive.value = 0;
958
+ }
959
+ // 单体淡化清除后,把材质 transparent 还原成全局透明度语义
960
+ if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
961
+ return;
962
+ }
963
+
964
+ if (!pickInfo || Object.keys(pickInfo).length === 0) {
965
+ if (mat.userData.uBatchTexActive) {
966
+ mat.userData.uBatchTexActive.value = 0;
967
+ }
968
+ if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
969
+ return;
970
+ }
971
+
972
+ let maxBatch = 0;
973
+ for (const p of Object.values(pickInfo)) {
974
+ const id = pickEntryId(p as any);
975
+ if (id > maxBatch) maxBatch = id;
976
+ }
977
+ const width = Math.max(maxBatch + 1, 1);
978
+
979
+ let record = this._meshBatchTextures.get(mesh);
980
+ if (!record || record.width < width) {
981
+ const data = new Float32Array(width * 5 * 4);
982
+ const texture = new THREE.DataTexture(data, width, 5, THREE.RGBAFormat, THREE.FloatType);
983
+ texture.minFilter = THREE.NearestFilter;
984
+ texture.magFilter = THREE.NearestFilter;
985
+ record = { texture, data, width };
986
+ this._meshBatchTextures.set(mesh, record);
987
+ mat.userData.uBatchTex.value = texture;
988
+ mat.userData.uBatchTexWidth.value = width;
989
+ }
990
+
991
+ const { data, texture } = record;
992
+ const { hasActive, hasFade, conflicts } = this._bakeStylesIntoBuffer(
993
+ data,
994
+ record.width,
995
+ pickInfo,
996
+ mesh,
997
+ this.featureStyles
998
+ );
999
+
1000
+ // 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
1001
+ if (hasFade && !mat.transparent) mat.transparent = true;
1002
+ else if (!hasFade && mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
1003
+
751
1004
  if (conflicts > 0 && !this._batchConflictWarned) {
752
1005
  this._batchConflictWarned = true;
753
1006
  console.warn(
@@ -764,6 +1017,93 @@ export class FeatureStyleManager {
764
1017
  }
765
1018
  }
766
1019
 
1020
+ /**
1021
+ * 准备双纹理交叉淡化:烘焙目标样式到第二纹理 (uBatchTex2)
1022
+ */
1023
+ prepareCrossFade(targetStyles: Map<number, FeatureStyle>, meshes: Iterable<THREE.Mesh>): void {
1024
+ for (const mesh of meshes) {
1025
+ const mat = styledMaterialOf(mesh);
1026
+ if (!mat) continue;
1027
+ const ud = mesh.userData as any;
1028
+ const pickInfo = ud.geoRef?.pickInfo;
1029
+ if (!pickInfo || Object.keys(pickInfo).length === 0) continue;
1030
+
1031
+ let maxBatch = 0;
1032
+ for (const p of Object.values(pickInfo)) {
1033
+ const id = pickEntryId(p as any);
1034
+ if (id > maxBatch) maxBatch = id;
1035
+ }
1036
+ const width = Math.max(maxBatch + 1, 1);
1037
+
1038
+ let record = this._meshBatchTextures.get(mesh);
1039
+ if (!record || record.width < width) {
1040
+ const data = new Float32Array(width * 5 * 4);
1041
+ const texture = new THREE.DataTexture(data, width, 5, THREE.RGBAFormat, THREE.FloatType);
1042
+ texture.minFilter = THREE.NearestFilter;
1043
+ texture.magFilter = THREE.NearestFilter;
1044
+ record = { texture, data, width };
1045
+ this._meshBatchTextures.set(mesh, record);
1046
+ mat.userData.uBatchTex.value = texture;
1047
+ mat.userData.uBatchTexWidth.value = width;
1048
+ }
1049
+
1050
+ if (!record.texture2 || !record.data2 || record.data2.length < record.width * 5 * 4) {
1051
+ const data2 = new Float32Array(record.width * 5 * 4);
1052
+ const texture2 = new THREE.DataTexture(data2, record.width, 5, THREE.RGBAFormat, THREE.FloatType);
1053
+ texture2.minFilter = THREE.NearestFilter;
1054
+ texture2.magFilter = THREE.NearestFilter;
1055
+ record.data2 = data2;
1056
+ record.texture2 = texture2;
1057
+ }
1058
+
1059
+ // 确保当前纹理包含当前状态
1060
+ this._bakeStylesIntoBuffer(record.data, record.width, pickInfo, mesh, this.featureStyles);
1061
+ record.texture.needsUpdate = true;
1062
+
1063
+ // 烘焙目标状态到 data2
1064
+ const res2 = this._bakeStylesIntoBuffer(record.data2, record.width, pickInfo, mesh, targetStyles);
1065
+ record.texture2.needsUpdate = true;
1066
+
1067
+ mat.userData.uBatchTex2.value = record.texture2;
1068
+ mat.userData.uBatchTexActive.value = 1;
1069
+ if (res2.hasFade && !mat.transparent) mat.transparent = true;
1070
+ }
1071
+ }
1072
+
1073
+ /**
1074
+ * 提交双纹理交叉淡化:将 targetStyles 回写到主状态,并将 data2 复制到 data
1075
+ */
1076
+ commitCrossFade(targetStyles: Map<number, FeatureStyle>, meshes: Iterable<THREE.Mesh>): void {
1077
+ this.featureStyles.clear();
1078
+ for (const [k, v] of targetStyles) {
1079
+ this.featureStyles.set(k, v);
1080
+ }
1081
+ for (const mesh of meshes) {
1082
+ const mat = styledMaterialOf(mesh);
1083
+ if (!mat) continue;
1084
+ const record = this._meshBatchTextures.get(mesh);
1085
+ if (!record || !record.data2 || !record.texture2) continue;
1086
+
1087
+ record.data.set(record.data2);
1088
+ record.texture.needsUpdate = true;
1089
+ mat.userData.uBatchTex2.value = mat.userData.uBatchTex.value;
1090
+ }
1091
+ this._onStyleChanged();
1092
+ this._triggerRepaint();
1093
+ }
1094
+
1095
+ /**
1096
+ * 取消交叉淡化并恢复单一纹理
1097
+ */
1098
+ cancelCrossFade(meshes: Iterable<THREE.Mesh>): void {
1099
+ for (const mesh of meshes) {
1100
+ const mat = styledMaterialOf(mesh);
1101
+ if (!mat) continue;
1102
+ mat.userData.uBatchTex2.value = mat.userData.uBatchTex.value;
1103
+ }
1104
+ this._triggerRepaint();
1105
+ }
1106
+
767
1107
  markOverridesDirty(subset?: Iterable<THREE.Mesh>, allMeshes?: Iterable<THREE.Mesh>): void {
768
1108
  if (subset) {
769
1109
  for (const m of subset) this._markedMeshes.add(m);
@@ -783,6 +1123,7 @@ export class FeatureStyleManager {
783
1123
  const record = this._meshBatchTextures.get(mesh);
784
1124
  if (record) {
785
1125
  record.texture.dispose();
1126
+ record.texture2?.dispose();
786
1127
  this._meshBatchTextures.delete(mesh);
787
1128
  }
788
1129
  }
@@ -187,3 +187,55 @@ export function buildOrthoMVP(input: OrthoInput, params: OrthoParams): OrthoMVP
187
187
  ortho.updateProjectionMatrix();
188
188
  return { matrix: ortho.projectionMatrix.clone().multiply(view), view };
189
189
  }
190
+
191
+ /**
192
+ * 平滑过渡正交与透视 MVP:
193
+ * t = 0 时纯透视,t = 1 时纯正交,0 < t < 1 时在投影矩阵元素级平滑线性插值。
194
+ * 因为半高以目标焦平面距离为基准,目标平面上的几何在过渡期间零位移零跳变。
195
+ */
196
+ export function buildBlendedMVP(input: OrthoInput, params: OrthoParams, t: number): OrthoMVP | null {
197
+ const p = input.projectionMatrix;
198
+ if (!p || p.length < 16) return null;
199
+
200
+ const pMat = new THREE.Matrix4().fromArray(Array.from(p));
201
+ const mvp = new THREE.Matrix4().fromArray(Array.from(input.mvp));
202
+ if (pMat.determinant() === 0) return null;
203
+ if (!matchesFov(pMat, params.fov)) return null;
204
+
205
+ const view = pMat.clone().invert().multiply(mvp);
206
+ if (!closeTo(pMat.clone().multiply(view), Array.from(input.mvp))) return null;
207
+ if (!isAffine(view)) return null;
208
+
209
+ const clampedT = Math.max(0, Math.min(1, t));
210
+ if (clampedT <= 0) {
211
+ return { matrix: mvp, view };
212
+ }
213
+
214
+ const { distance, fov, aspect } = params;
215
+ if (!(distance > 0) || !(fov > 0) || !(aspect > 0)) return null;
216
+
217
+ const halfH = distance * Math.tan(fov / 2);
218
+ const halfW = halfH * aspect;
219
+
220
+ let near = input.nearZ != null && input.nearZ > 0 ? input.nearZ : 1;
221
+ let far = input.farZ != null && input.farZ > near ? input.farZ : distance * 4 + 1000;
222
+ if (!(far > near)) far = near + 1000;
223
+
224
+ const ortho = new THREE.OrthographicCamera(-halfW, halfW, halfH, -halfH, near, far);
225
+ ortho.updateProjectionMatrix();
226
+
227
+ if (clampedT >= 1) {
228
+ return { matrix: ortho.projectionMatrix.clone().multiply(view), view };
229
+ }
230
+
231
+ // 广义齐次投影矩阵元素插值:P(t) = (1 - t) * P_persp + t * P_ortho
232
+ const pBlend = new THREE.Matrix4();
233
+ const ePersp = pMat.elements;
234
+ const eOrtho = ortho.projectionMatrix.elements;
235
+ const eBlend = pBlend.elements;
236
+ for (let i = 0; i < 16; i++) {
237
+ eBlend[i] = (1 - clampedT) * ePersp[i] + clampedT * eOrtho[i];
238
+ }
239
+
240
+ return { matrix: pBlend.multiply(view), view };
241
+ }