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.
Files changed (67) hide show
  1. package/dist/anim/Animator.d.ts +111 -0
  2. package/dist/anim/Animator.d.ts.map +1 -0
  3. package/dist/anim/Animator.js +382 -0
  4. package/dist/anim/Animator.js.map +1 -0
  5. package/dist/anim/Path.d.ts +30 -0
  6. package/dist/anim/Path.d.ts.map +1 -0
  7. package/dist/anim/Path.js +100 -0
  8. package/dist/anim/Path.js.map +1 -0
  9. package/dist/anim/easing.d.ts +70 -0
  10. package/dist/anim/easing.d.ts.map +1 -0
  11. package/dist/anim/easing.js +170 -0
  12. package/dist/anim/easing.js.map +1 -0
  13. package/dist/anim/index.d.ts +6 -0
  14. package/dist/anim/index.d.ts.map +1 -0
  15. package/dist/anim/index.js +4 -0
  16. package/dist/anim/index.js.map +1 -0
  17. package/dist/browser/s3m-three.iife.ext-three.js +218 -39
  18. package/dist/browser/s3m-three.iife.js +218 -39
  19. package/dist/core/types.d.ts +20 -0
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +6 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/render/EdgeLines.d.ts +68 -14
  26. package/dist/render/EdgeLines.d.ts.map +1 -1
  27. package/dist/render/EdgeLines.js +125 -8
  28. package/dist/render/EdgeLines.js.map +1 -1
  29. package/dist/render/FeatureStyleManager.d.ts +62 -1
  30. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  31. package/dist/render/FeatureStyleManager.js +331 -60
  32. package/dist/render/FeatureStyleManager.js.map +1 -1
  33. package/dist/render/MergeVertices.d.ts +7 -0
  34. package/dist/render/MergeVertices.d.ts.map +1 -1
  35. package/dist/render/MergeVertices.js +10 -1
  36. package/dist/render/MergeVertices.js.map +1 -1
  37. package/dist/render/OrthoProjection.d.ts +6 -0
  38. package/dist/render/OrthoProjection.d.ts.map +1 -1
  39. package/dist/render/OrthoProjection.js +48 -0
  40. package/dist/render/OrthoProjection.js.map +1 -1
  41. package/dist/render/S3MViewer.d.ts +235 -9
  42. package/dist/render/S3MViewer.d.ts.map +1 -1
  43. package/dist/render/S3MViewer.js +880 -68
  44. package/dist/render/S3MViewer.js.map +1 -1
  45. package/dist/render/StyleMaterial.d.ts +28 -0
  46. package/dist/render/StyleMaterial.d.ts.map +1 -1
  47. package/dist/render/StyleMaterial.js +190 -21
  48. package/dist/render/StyleMaterial.js.map +1 -1
  49. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  50. package/dist/tileset/S3MTileset.js +3 -1
  51. package/dist/tileset/S3MTileset.js.map +1 -1
  52. package/docs/ANIMATION.md +534 -0
  53. package/docs/API.md +2 -0
  54. package/package.json +1 -1
  55. package/src/anim/Animator.ts +460 -0
  56. package/src/anim/Path.ts +107 -0
  57. package/src/anim/easing.ts +195 -0
  58. package/src/anim/index.ts +10 -0
  59. package/src/core/types.ts +10 -0
  60. package/src/index.ts +16 -0
  61. package/src/render/EdgeLines.ts +161 -15
  62. package/src/render/FeatureStyleManager.ts +414 -65
  63. package/src/render/MergeVertices.ts +11 -1
  64. package/src/render/OrthoProjection.ts +52 -0
  65. package/src/render/S3MViewer.ts +1012 -64
  66. package/src/render/StyleMaterial.ts +211 -22
  67. package/src/tileset/S3MTileset.ts +3 -1
@@ -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;
@@ -185,7 +344,15 @@ export class FeatureStyleManager {
185
344
  private readonly _triggerRepaint: () => void,
186
345
  private readonly _onStyleChanged: () => void,
187
346
  private readonly _getAllMeshes?: () => Iterable<THREE.Mesh>,
188
- private readonly _applyOverrideHook?: (mesh: THREE.Mesh) => void
347
+ private readonly _applyOverrideHook?: (mesh: THREE.Mesh) => void,
348
+ /**
349
+ * 图层自身是否处于半透明状态。
350
+ *
351
+ * 以前这里读 `material.opacity` 来判断「要不要把 transparent 还原」——自从全局
352
+ * 透明度改走共享 `uOpacity` 之后,`material.opacity` 不再等于图层透明度,
353
+ * 再读它就会在图层半透明时把材质错判成不透明、把整层透明效果抹掉。
354
+ */
355
+ private readonly _isLayerTransparent: () => boolean = () => false
189
356
  ) {}
190
357
 
191
358
  setFeatureStyle(smid: number, style: FeatureStyle, meshes?: Iterable<THREE.Mesh>): void {
@@ -208,11 +375,21 @@ export class FeatureStyleManager {
208
375
  return this.featureStyles.get(smid) ?? null;
209
376
  }
210
377
 
211
- 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
+ } {
212
384
  if (smid == null) return {};
213
385
  const st = this.featureStyles.get(smid);
214
386
  const elev = st?.elevation ?? this.thematicElevation.get(smid);
215
- return { translation: st?.translation, elevation: elev };
387
+ return {
388
+ translation: st?.translation,
389
+ elevation: elev,
390
+ rotation: st?.rotation,
391
+ pivot: st?.pivot,
392
+ };
216
393
  }
217
394
 
218
395
  getFeatureStyles(): Map<number, FeatureStyle> {
@@ -543,72 +720,48 @@ export class FeatureStyleManager {
543
720
  return { smids: 0, batches, conflicts, conflictingSmids: involved.size };
544
721
  }
545
722
 
546
- applyOverridesToMesh(mesh: THREE.Mesh): void {
547
- const mat = styledMaterialOf(mesh);
548
- if (!mat) return;
549
-
550
- const ud = mesh.userData as any;
551
- const pickInfo = ud.geoRef?.pickInfo;
552
-
553
- const hasStyles =
554
- this.featureStyles.size > 0 || this.thematicColors.size > 0 || this.thematicElevation.size > 0;
555
- const filter = this.filter;
556
- const hasFilter =
557
- this.fieldFilter != null ||
558
- (filter != null &&
559
- ((filter.include?.length ?? 0) > 0 ||
560
- (filter.exclude?.length ?? 0) > 0 ||
561
- filter.predicate != null));
562
-
563
- if (!hasStyles && !hasFilter) {
564
- if (mat.userData.uBatchTexActive) {
565
- mat.userData.uBatchTexActive.value = 0;
566
- }
567
- // 单体淡化清除后,把材质 transparent 还原成全局透明度语义
568
- if (mat.transparent && mat.opacity >= 0.999) mat.transparent = false;
569
- return;
570
- }
571
-
572
- if (!pickInfo || Object.keys(pickInfo).length === 0) {
573
- if (mat.userData.uBatchTexActive) {
574
- mat.userData.uBatchTexActive.value = 0;
575
- }
576
- if (mat.transparent && mat.opacity >= 0.999) mat.transparent = false;
577
- return;
578
- }
579
-
580
- let maxBatch = 0;
581
- for (const p of Object.values(pickInfo)) {
582
- const id = pickEntryId(p as any);
583
- if (id > maxBatch) maxBatch = id;
584
- }
585
- const width = Math.max(maxBatch + 1, 1);
586
-
587
- let record = this._meshBatchTextures.get(mesh);
588
- if (!record || record.width < width) {
589
- const data = new Float32Array(width * 2 * 4);
590
- const texture = new THREE.DataTexture(data, width, 2, THREE.RGBAFormat, THREE.FloatType);
591
- texture.minFilter = THREE.NearestFilter;
592
- texture.magFilter = THREE.NearestFilter;
593
- record = { texture, data, width };
594
- this._meshBatchTextures.set(mesh, record);
595
- mat.userData.uBatchTex.value = texture;
596
- mat.userData.uBatchTexWidth.value = width;
597
- }
598
-
599
- const { data, texture } = record;
600
- 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;
601
731
  for (let i = 0; i < w; i++) {
732
+ // Row 0: [R, G, B, elevation/hidden]
602
733
  data[i * 4 + 0] = -1;
603
734
  data[i * 4 + 1] = -1;
604
735
  data[i * 4 + 2] = -1;
605
736
  data[i * 4 + 3] = 0;
606
737
 
738
+ // Row 1: [translationX, translationY, translationZ, opacity]
607
739
  const r1 = (w + i) * 4;
608
740
  data[r1 + 0] = 0;
609
741
  data[r1 + 1] = 0;
610
742
  data[r1 + 2] = 0;
611
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;
612
765
  }
613
766
 
614
767
  let hasActive = false;
@@ -616,13 +769,17 @@ export class FeatureStyleManager {
616
769
  const colorOfBatch = new Map<number, string>();
617
770
  const elevOfBatch = new Map<number, number>();
618
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]>();
619
774
  const opacityOfBatch = new Map<number, number>();
620
775
  const ownersOfBatch = new Map<number, number>();
776
+ const filter = this.filter;
777
+ const ud = mesh.userData as any;
621
778
 
622
779
  for (const [smidStr, entry] of Object.entries(pickInfo)) {
623
780
  const smid = Number(smidStr);
624
781
  const bId = pickEntryId(entry as any);
625
- if (bId < 0 || bId >= record.width) continue;
782
+ if (bId < 0 || bId >= w) continue;
626
783
  ownersOfBatch.set(bId, (ownersOfBatch.get(bId) ?? 0) + 1);
627
784
 
628
785
  let hidden = false;
@@ -661,7 +818,7 @@ export class FeatureStyleManager {
661
818
  }
662
819
  }
663
820
 
664
- const st = this.featureStyles.get(smid);
821
+ const st = styleMap.get(smid);
665
822
  if (st?.hidden) {
666
823
  hiddenBatches.add(bId);
667
824
  continue;
@@ -686,6 +843,14 @@ export class FeatureStyleManager {
686
843
  if (st?.translation && !translationOfBatch.has(bId)) {
687
844
  translationOfBatch.set(bId, st.translation);
688
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
+ }
689
854
  }
690
855
 
691
856
  for (const bId of hiddenBatches) {
@@ -729,17 +894,113 @@ export class FeatureStyleManager {
729
894
  hasFade = true;
730
895
  }
731
896
 
732
- // 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
733
- if (hasFade && !mat.transparent) mat.transparent = true;
734
- else if (!hasFade && mat.transparent && mat.opacity >= 0.999) 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
+ }
735
927
 
736
928
  let conflicts = 0;
737
929
  for (const [bId, owners] of ownersOfBatch) {
738
930
  if (owners < 2) continue;
739
- 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;
740
932
  conflicts++;
741
933
  }
742
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
+
743
1004
  if (conflicts > 0 && !this._batchConflictWarned) {
744
1005
  this._batchConflictWarned = true;
745
1006
  console.warn(
@@ -756,6 +1017,93 @@ export class FeatureStyleManager {
756
1017
  }
757
1018
  }
758
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
+
759
1107
  markOverridesDirty(subset?: Iterable<THREE.Mesh>, allMeshes?: Iterable<THREE.Mesh>): void {
760
1108
  if (subset) {
761
1109
  for (const m of subset) this._markedMeshes.add(m);
@@ -775,6 +1123,7 @@ export class FeatureStyleManager {
775
1123
  const record = this._meshBatchTextures.get(mesh);
776
1124
  if (record) {
777
1125
  record.texture.dispose();
1126
+ record.texture2?.dispose();
778
1127
  this._meshBatchTextures.delete(mesh);
779
1128
  }
780
1129
  }
@@ -14,6 +14,16 @@
14
14
  */
15
15
  import * as THREE from 'three';
16
16
 
17
+ /**
18
+ * 位置量化 key:坐标落到 tolerance 网格上取整。
19
+ *
20
+ * 单独导出是因为棱线的「位置 → batchId」回填(见 EdgeLines.buildPositionBatchMap)
21
+ * 必须用同一套网格,否则焊过的顶点对不上号。
22
+ */
23
+ export function positionQuantKey(x: number, y: number, z: number, inv: number): string {
24
+ return `${Math.round(x * inv)},${Math.round(y * inv)},${Math.round(z * inv)}`;
25
+ }
26
+
17
27
  export function mergeVerticesByPosition(
18
28
  geometry: THREE.BufferGeometry,
19
29
  tolerance: number
@@ -31,7 +41,7 @@ export function mergeVerticesByPosition(
31
41
  const x = pos.getX(i);
32
42
  const y = pos.getY(i);
33
43
  const z = pos.getZ(i);
34
- const key = `${Math.round(x * inv)},${Math.round(y * inv)},${Math.round(z * inv)}`;
44
+ const key = positionQuantKey(x, y, z, inv);
35
45
  let id = map.get(key);
36
46
  if (id === undefined) {
37
47
  id = out.length / 3;
@@ -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
+ }