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
@@ -13,6 +13,113 @@
13
13
  */
14
14
  import * as THREE from 'three';
15
15
  import { resolveFeature, measureSegments, buildAttributes, pickEntryId, } from '../core/FeatureInfo.js';
16
+ /**
17
+ * 将各类旋转表示统一解析为 3x3 行优先矩阵数组
18
+ */
19
+ export function resolveRotationMatrix(rot) {
20
+ if (rot == null)
21
+ return null;
22
+ if (typeof rot === 'number') {
23
+ const c = Math.cos(rot);
24
+ const s = Math.sin(rot);
25
+ return [
26
+ c, -s, 0,
27
+ s, c, 0,
28
+ 0, 0, 1,
29
+ ];
30
+ }
31
+ if (Array.isArray(rot)) {
32
+ if (rot.length === 3) {
33
+ const euler = new THREE.Euler(rot[0], rot[1], rot[2]);
34
+ const m = new THREE.Matrix4().makeRotationFromEuler(euler);
35
+ const e = m.elements;
36
+ return [
37
+ e[0], e[4], e[8],
38
+ e[1], e[5], e[9],
39
+ e[2], e[6], e[10],
40
+ ];
41
+ }
42
+ if (rot.length === 4) {
43
+ const q = new THREE.Quaternion(rot[0], rot[1], rot[2], rot[3]);
44
+ const m = new THREE.Matrix4().makeRotationFromQuaternion(q);
45
+ const e = m.elements;
46
+ return [
47
+ e[0], e[4], e[8],
48
+ e[1], e[5], e[9],
49
+ e[2], e[6], e[10],
50
+ ];
51
+ }
52
+ }
53
+ if (rot instanceof THREE.Euler) {
54
+ const m = new THREE.Matrix4().makeRotationFromEuler(rot);
55
+ const e = m.elements;
56
+ return [
57
+ e[0], e[4], e[8],
58
+ e[1], e[5], e[9],
59
+ e[2], e[6], e[10],
60
+ ];
61
+ }
62
+ if (rot instanceof THREE.Quaternion) {
63
+ const m = new THREE.Matrix4().makeRotationFromQuaternion(rot);
64
+ const e = m.elements;
65
+ return [
66
+ e[0], e[4], e[8],
67
+ e[1], e[5], e[9],
68
+ e[2], e[6], e[10],
69
+ ];
70
+ }
71
+ if (rot instanceof THREE.Matrix3) {
72
+ const e = rot.elements;
73
+ return [
74
+ e[0], e[3], e[6],
75
+ e[1], e[4], e[7],
76
+ e[2], e[5], e[8],
77
+ ];
78
+ }
79
+ if (rot instanceof THREE.Matrix4) {
80
+ const e = rot.elements;
81
+ return [
82
+ e[0], e[4], e[8],
83
+ e[1], e[5], e[9],
84
+ e[2], e[6], e[10],
85
+ ];
86
+ }
87
+ return null;
88
+ }
89
+ /**
90
+ * 提取欧拉角 [rx, ry, rz](弧度)用于动画插值
91
+ */
92
+ export function extractEulerAngles(rot) {
93
+ if (rot == null)
94
+ return [0, 0, 0];
95
+ if (typeof rot === 'number')
96
+ return [0, 0, rot];
97
+ if (Array.isArray(rot)) {
98
+ if (rot.length === 3)
99
+ return [rot[0], rot[1], rot[2]];
100
+ if (rot.length === 4) {
101
+ const q = new THREE.Quaternion(rot[0], rot[1], rot[2], rot[3]);
102
+ const e = new THREE.Euler().setFromQuaternion(q);
103
+ return [e.x, e.y, e.z];
104
+ }
105
+ }
106
+ if (rot instanceof THREE.Euler)
107
+ return [rot.x, rot.y, rot.z];
108
+ if (rot instanceof THREE.Quaternion) {
109
+ const e = new THREE.Euler().setFromQuaternion(rot);
110
+ return [e.x, e.y, e.z];
111
+ }
112
+ if (rot instanceof THREE.Matrix3) {
113
+ const m = new THREE.Matrix4().set(rot.elements[0], rot.elements[3], rot.elements[6], 0, rot.elements[1], rot.elements[4], rot.elements[7], 0, rot.elements[2], rot.elements[5], rot.elements[8], 0, 0, 0, 0, 1);
114
+ const e = new THREE.Euler().setFromRotationMatrix(m);
115
+ return [e.x, e.y, e.z];
116
+ }
117
+ if (rot instanceof THREE.Matrix4) {
118
+ const e = new THREE.Euler().setFromRotationMatrix(rot);
119
+ return [e.x, e.y, e.z];
120
+ }
121
+ return [0, 0, 0];
122
+ }
16
123
  function styledMaterialOf(mesh) {
17
124
  const m = Array.isArray(mesh.material) ? mesh.material[0] : mesh.material;
18
125
  const styled = m;
@@ -115,7 +222,12 @@ export class FeatureStyleManager {
115
222
  return {};
116
223
  const st = this.featureStyles.get(smid);
117
224
  const elev = st?.elevation ?? this.thematicElevation.get(smid);
118
- return { translation: st?.translation, elevation: elev };
225
+ return {
226
+ translation: st?.translation,
227
+ elevation: elev,
228
+ rotation: st?.rotation,
229
+ pivot: st?.pivot,
230
+ };
119
231
  }
120
232
  getFeatureStyles() {
121
233
  return new Map(this.featureStyles);
@@ -423,78 +535,61 @@ export class FeatureStyleManager {
423
535
  }
424
536
  return { smids: 0, batches, conflicts, conflictingSmids: involved.size };
425
537
  }
426
- applyOverridesToMesh(mesh) {
427
- const mat = styledMaterialOf(mesh);
428
- if (!mat)
429
- return;
430
- const ud = mesh.userData;
431
- const pickInfo = ud.geoRef?.pickInfo;
432
- const hasStyles = this.featureStyles.size > 0 || this.thematicColors.size > 0 || this.thematicElevation.size > 0;
433
- const filter = this.filter;
434
- const hasFilter = this.fieldFilter != null ||
435
- (filter != null &&
436
- ((filter.include?.length ?? 0) > 0 ||
437
- (filter.exclude?.length ?? 0) > 0 ||
438
- filter.predicate != null));
439
- if (!hasStyles && !hasFilter) {
440
- if (mat.userData.uBatchTexActive) {
441
- mat.userData.uBatchTexActive.value = 0;
442
- }
443
- // 单体淡化清除后,把材质 transparent 还原成全局透明度语义
444
- if (mat.transparent && !this._isLayerTransparent())
445
- mat.transparent = false;
446
- return;
447
- }
448
- if (!pickInfo || Object.keys(pickInfo).length === 0) {
449
- if (mat.userData.uBatchTexActive) {
450
- mat.userData.uBatchTexActive.value = 0;
451
- }
452
- if (mat.transparent && !this._isLayerTransparent())
453
- mat.transparent = false;
454
- return;
455
- }
456
- let maxBatch = 0;
457
- for (const p of Object.values(pickInfo)) {
458
- const id = pickEntryId(p);
459
- if (id > maxBatch)
460
- maxBatch = id;
461
- }
462
- const width = Math.max(maxBatch + 1, 1);
463
- let record = this._meshBatchTextures.get(mesh);
464
- if (!record || record.width < width) {
465
- const data = new Float32Array(width * 2 * 4);
466
- const texture = new THREE.DataTexture(data, width, 2, THREE.RGBAFormat, THREE.FloatType);
467
- texture.minFilter = THREE.NearestFilter;
468
- texture.magFilter = THREE.NearestFilter;
469
- record = { texture, data, width };
470
- this._meshBatchTextures.set(mesh, record);
471
- mat.userData.uBatchTex.value = texture;
472
- mat.userData.uBatchTexWidth.value = width;
473
- }
474
- const { data, texture } = record;
475
- const w = record.width;
538
+ _bakeStylesIntoBuffer(data, width, pickInfo, mesh, styleMap) {
539
+ const w = width;
476
540
  for (let i = 0; i < w; i++) {
541
+ // Row 0: [R, G, B, elevation/hidden]
477
542
  data[i * 4 + 0] = -1;
478
543
  data[i * 4 + 1] = -1;
479
544
  data[i * 4 + 2] = -1;
480
545
  data[i * 4 + 3] = 0;
546
+ // Row 1: [translationX, translationY, translationZ, opacity]
481
547
  const r1 = (w + i) * 4;
482
548
  data[r1 + 0] = 0;
483
549
  data[r1 + 1] = 0;
484
550
  data[r1 + 2] = 0;
485
551
  data[r1 + 3] = 0;
552
+ // Row 2: [R00, R01, R02, pivot.x] (单位矩阵默认值)
553
+ const r2 = (w * 2 + i) * 4;
554
+ data[r2 + 0] = 1;
555
+ data[r2 + 1] = 0;
556
+ data[r2 + 2] = 0;
557
+ data[r2 + 3] = 0;
558
+ // Row 3: [R10, R11, R12, pivot.y]
559
+ const r3 = (w * 3 + i) * 4;
560
+ data[r3 + 0] = 0;
561
+ data[r3 + 1] = 1;
562
+ data[r3 + 2] = 0;
563
+ data[r3 + 3] = 0;
564
+ // Row 4: [R20, R21, R22, pivot.z]
565
+ const r4 = (w * 4 + i) * 4;
566
+ data[r4 + 0] = 0;
567
+ data[r4 + 1] = 0;
568
+ data[r4 + 2] = 1;
569
+ data[r4 + 3] = 0;
570
+ // Row 5: [pathId, speed, phase, modeFlag] (默认 -1 表示未绑定 GPU 路径)
571
+ const r5 = (w * 5 + i) * 4;
572
+ data[r5 + 0] = -1;
573
+ data[r5 + 1] = 0;
574
+ data[r5 + 2] = 0;
575
+ data[r5 + 3] = 0;
486
576
  }
487
577
  let hasActive = false;
488
578
  const hiddenBatches = new Set();
489
579
  const colorOfBatch = new Map();
490
580
  const elevOfBatch = new Map();
491
581
  const translationOfBatch = new Map();
582
+ const rotationOfBatch = new Map();
583
+ const pivotOfBatch = new Map();
584
+ const gpuPathOfBatch = new Map();
492
585
  const opacityOfBatch = new Map();
493
586
  const ownersOfBatch = new Map();
587
+ const filter = this.filter;
588
+ const ud = mesh.userData;
494
589
  for (const [smidStr, entry] of Object.entries(pickInfo)) {
495
590
  const smid = Number(smidStr);
496
591
  const bId = pickEntryId(entry);
497
- if (bId < 0 || bId >= record.width)
592
+ if (bId < 0 || bId >= w)
498
593
  continue;
499
594
  ownersOfBatch.set(bId, (ownersOfBatch.get(bId) ?? 0) + 1);
500
595
  let hidden = false;
@@ -530,7 +625,7 @@ export class FeatureStyleManager {
530
625
  continue;
531
626
  }
532
627
  }
533
- const st = this.featureStyles.get(smid);
628
+ const st = styleMap.get(smid);
534
629
  if (st?.hidden) {
535
630
  hiddenBatches.add(bId);
536
631
  continue;
@@ -554,6 +649,17 @@ export class FeatureStyleManager {
554
649
  if (st?.translation && !translationOfBatch.has(bId)) {
555
650
  translationOfBatch.set(bId, st.translation);
556
651
  }
652
+ if (st?.rotation != null && !rotationOfBatch.has(bId)) {
653
+ const rotMat = resolveRotationMatrix(st.rotation);
654
+ if (rotMat)
655
+ rotationOfBatch.set(bId, rotMat);
656
+ }
657
+ if (st?.pivot && !pivotOfBatch.has(bId)) {
658
+ pivotOfBatch.set(bId, st.pivot);
659
+ }
660
+ if (st?.gpuPath && !gpuPathOfBatch.has(bId)) {
661
+ gpuPathOfBatch.set(bId, st.gpuPath);
662
+ }
557
663
  }
558
664
  for (const bId of hiddenBatches) {
559
665
  data[bId * 4 + 3] = -1;
@@ -595,19 +701,126 @@ export class FeatureStyleManager {
595
701
  hasActive = true;
596
702
  hasFade = true;
597
703
  }
598
- // 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
599
- if (hasFade && !mat.transparent)
600
- mat.transparent = true;
601
- else if (!hasFade && mat.transparent && !this._isLayerTransparent())
602
- mat.transparent = false;
704
+ for (const [bId, rot] of rotationOfBatch) {
705
+ if (hiddenBatches.has(bId))
706
+ continue;
707
+ const piv = pivotOfBatch.get(bId) ?? [0, 0, 0];
708
+ const r2 = (w * 2 + bId) * 4;
709
+ data[r2 + 0] = rot[0];
710
+ data[r2 + 1] = rot[1];
711
+ data[r2 + 2] = rot[2];
712
+ data[r2 + 3] = piv[0];
713
+ const r3 = (w * 3 + bId) * 4;
714
+ data[r3 + 0] = rot[3];
715
+ data[r3 + 1] = rot[4];
716
+ data[r3 + 2] = rot[5];
717
+ data[r3 + 3] = piv[1];
718
+ const r4 = (w * 4 + bId) * 4;
719
+ data[r4 + 0] = rot[6];
720
+ data[r4 + 1] = rot[7];
721
+ data[r4 + 2] = rot[8];
722
+ data[r4 + 3] = piv[2];
723
+ hasActive = true;
724
+ }
725
+ for (const [bId, piv] of pivotOfBatch) {
726
+ if (rotationOfBatch.has(bId) || hiddenBatches.has(bId))
727
+ continue;
728
+ data[(w * 2 + bId) * 4 + 3] = piv[0];
729
+ data[(w * 3 + bId) * 4 + 3] = piv[1];
730
+ data[(w * 4 + bId) * 4 + 3] = piv[2];
731
+ hasActive = true;
732
+ }
733
+ // Row 5: 烘焙 GPU 路径动画参数 [pathId, speed, phase, modeFlag]
734
+ for (const [bId, gp] of gpuPathOfBatch) {
735
+ if (hiddenBatches.has(bId))
736
+ continue;
737
+ const r5 = (w * 5 + bId) * 4;
738
+ data[r5 + 0] = gp.pathId;
739
+ data[r5 + 1] = gp.speed ?? 0;
740
+ data[r5 + 2] = gp.phase ?? 0;
741
+ let mode = 1; // 1 = loop (默认)
742
+ if (gp.loop === 'clamp' || gp.loop === false) {
743
+ mode = 2;
744
+ }
745
+ else if (gp.loop === 'pingpong') {
746
+ mode = 3;
747
+ }
748
+ if (gp.autoHeading === false) {
749
+ mode += 3; // 4=loop+noHeading, 5=clamp+noHeading, 6=pingpong+noHeading
750
+ }
751
+ data[r5 + 3] = mode;
752
+ hasActive = true;
753
+ }
603
754
  let conflicts = 0;
604
755
  for (const [bId, owners] of ownersOfBatch) {
605
756
  if (owners < 2)
606
757
  continue;
607
- if (!hiddenBatches.has(bId) && !colorOfBatch.has(bId) && !elevOfBatch.has(bId))
758
+ if (!hiddenBatches.has(bId) &&
759
+ !colorOfBatch.has(bId) &&
760
+ !elevOfBatch.has(bId) &&
761
+ !rotationOfBatch.has(bId) &&
762
+ !gpuPathOfBatch.has(bId)) {
608
763
  continue;
764
+ }
609
765
  conflicts++;
610
766
  }
767
+ return { hasActive, hasFade, conflicts };
768
+ }
769
+ applyOverridesToMesh(mesh) {
770
+ const mat = styledMaterialOf(mesh);
771
+ if (!mat)
772
+ return;
773
+ const ud = mesh.userData;
774
+ const pickInfo = ud.geoRef?.pickInfo;
775
+ const hasStyles = this.featureStyles.size > 0 || this.thematicColors.size > 0 || this.thematicElevation.size > 0;
776
+ const filter = this.filter;
777
+ const hasFilter = this.fieldFilter != null ||
778
+ (filter != null &&
779
+ ((filter.include?.length ?? 0) > 0 ||
780
+ (filter.exclude?.length ?? 0) > 0 ||
781
+ filter.predicate != null));
782
+ if (!hasStyles && !hasFilter) {
783
+ if (mat.userData.uBatchTexActive) {
784
+ mat.userData.uBatchTexActive.value = 0;
785
+ }
786
+ // 单体淡化清除后,把材质 transparent 还原成全局透明度语义
787
+ if (mat.transparent && !this._isLayerTransparent())
788
+ mat.transparent = false;
789
+ return;
790
+ }
791
+ if (!pickInfo || Object.keys(pickInfo).length === 0) {
792
+ if (mat.userData.uBatchTexActive) {
793
+ mat.userData.uBatchTexActive.value = 0;
794
+ }
795
+ if (mat.transparent && !this._isLayerTransparent())
796
+ mat.transparent = false;
797
+ return;
798
+ }
799
+ let maxBatch = 0;
800
+ for (const p of Object.values(pickInfo)) {
801
+ const id = pickEntryId(p);
802
+ if (id > maxBatch)
803
+ maxBatch = id;
804
+ }
805
+ const width = Math.max(maxBatch + 1, 1);
806
+ let record = this._meshBatchTextures.get(mesh);
807
+ if (!record || record.width < width) {
808
+ const data = new Float32Array(width * 6 * 4);
809
+ const texture = new THREE.DataTexture(data, width, 6, THREE.RGBAFormat, THREE.FloatType);
810
+ texture.minFilter = THREE.NearestFilter;
811
+ texture.magFilter = THREE.NearestFilter;
812
+ record = { texture, data, width };
813
+ this._meshBatchTextures.set(mesh, record);
814
+ mat.userData.uBatchTex.value = texture;
815
+ mat.userData.uBatchTexWidth.value = width;
816
+ }
817
+ const { data, texture } = record;
818
+ const { hasActive, hasFade, conflicts } = this._bakeStylesIntoBuffer(data, record.width, pickInfo, mesh, this.featureStyles);
819
+ // 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
820
+ if (hasFade && !mat.transparent)
821
+ mat.transparent = true;
822
+ else if (!hasFade && mat.transparent && !this._isLayerTransparent())
823
+ mat.transparent = false;
611
824
  if (conflicts > 0 && !this._batchConflictWarned) {
612
825
  this._batchConflictWarned = true;
613
826
  console.warn(`[s3m] 有 ${conflicts} 个单体状态写不进独立的 batch(多个 SMID 共用同一个 batchId,\n` +
@@ -620,6 +833,90 @@ export class FeatureStyleManager {
620
833
  mat.userData.uBatchTexActive.value = hasActive ? 1 : 0;
621
834
  }
622
835
  }
836
+ /**
837
+ * 准备双纹理交叉淡化:烘焙目标样式到第二纹理 (uBatchTex2)
838
+ */
839
+ prepareCrossFade(targetStyles, meshes) {
840
+ for (const mesh of meshes) {
841
+ const mat = styledMaterialOf(mesh);
842
+ if (!mat)
843
+ continue;
844
+ const ud = mesh.userData;
845
+ const pickInfo = ud.geoRef?.pickInfo;
846
+ if (!pickInfo || Object.keys(pickInfo).length === 0)
847
+ continue;
848
+ let maxBatch = 0;
849
+ for (const p of Object.values(pickInfo)) {
850
+ const id = pickEntryId(p);
851
+ if (id > maxBatch)
852
+ maxBatch = id;
853
+ }
854
+ const width = Math.max(maxBatch + 1, 1);
855
+ let record = this._meshBatchTextures.get(mesh);
856
+ if (!record || record.width < width) {
857
+ const data = new Float32Array(width * 6 * 4);
858
+ const texture = new THREE.DataTexture(data, width, 6, THREE.RGBAFormat, THREE.FloatType);
859
+ texture.minFilter = THREE.NearestFilter;
860
+ texture.magFilter = THREE.NearestFilter;
861
+ record = { texture, data, width };
862
+ this._meshBatchTextures.set(mesh, record);
863
+ mat.userData.uBatchTex.value = texture;
864
+ mat.userData.uBatchTexWidth.value = width;
865
+ }
866
+ if (!record.texture2 || !record.data2 || record.data2.length < record.width * 6 * 4) {
867
+ const data2 = new Float32Array(record.width * 6 * 4);
868
+ const texture2 = new THREE.DataTexture(data2, record.width, 6, THREE.RGBAFormat, THREE.FloatType);
869
+ texture2.minFilter = THREE.NearestFilter;
870
+ texture2.magFilter = THREE.NearestFilter;
871
+ record.data2 = data2;
872
+ record.texture2 = texture2;
873
+ }
874
+ // 确保当前纹理包含当前状态
875
+ this._bakeStylesIntoBuffer(record.data, record.width, pickInfo, mesh, this.featureStyles);
876
+ record.texture.needsUpdate = true;
877
+ // 烘焙目标状态到 data2
878
+ const res2 = this._bakeStylesIntoBuffer(record.data2, record.width, pickInfo, mesh, targetStyles);
879
+ record.texture2.needsUpdate = true;
880
+ mat.userData.uBatchTex2.value = record.texture2;
881
+ mat.userData.uBatchTexActive.value = 1;
882
+ if (res2.hasFade && !mat.transparent)
883
+ mat.transparent = true;
884
+ }
885
+ }
886
+ /**
887
+ * 提交双纹理交叉淡化:将 targetStyles 回写到主状态,并将 data2 复制到 data
888
+ */
889
+ commitCrossFade(targetStyles, meshes) {
890
+ this.featureStyles.clear();
891
+ for (const [k, v] of targetStyles) {
892
+ this.featureStyles.set(k, v);
893
+ }
894
+ for (const mesh of meshes) {
895
+ const mat = styledMaterialOf(mesh);
896
+ if (!mat)
897
+ continue;
898
+ const record = this._meshBatchTextures.get(mesh);
899
+ if (!record || !record.data2 || !record.texture2)
900
+ continue;
901
+ record.data.set(record.data2);
902
+ record.texture.needsUpdate = true;
903
+ mat.userData.uBatchTex2.value = mat.userData.uBatchTex.value;
904
+ }
905
+ this._onStyleChanged();
906
+ this._triggerRepaint();
907
+ }
908
+ /**
909
+ * 取消交叉淡化并恢复单一纹理
910
+ */
911
+ cancelCrossFade(meshes) {
912
+ for (const mesh of meshes) {
913
+ const mat = styledMaterialOf(mesh);
914
+ if (!mat)
915
+ continue;
916
+ mat.userData.uBatchTex2.value = mat.userData.uBatchTex.value;
917
+ }
918
+ this._triggerRepaint();
919
+ }
623
920
  markOverridesDirty(subset, allMeshes) {
624
921
  if (subset) {
625
922
  for (const m of subset)
@@ -643,6 +940,7 @@ export class FeatureStyleManager {
643
940
  const record = this._meshBatchTextures.get(mesh);
644
941
  if (record) {
645
942
  record.texture.dispose();
943
+ record.texture2?.dispose();
646
944
  this._meshBatchTextures.delete(mesh);
647
945
  }
648
946
  }
@@ -667,5 +965,55 @@ export class FeatureStyleManager {
667
965
  this._markedMeshes.clear();
668
966
  this._triggerRepaint();
669
967
  }
968
+ /**
969
+ * 将单体绑定到 GPU 路径动画
970
+ * @param smid 单体 ID
971
+ * @param pathId GPUPathManager 中的路径索引
972
+ * @param options 速度、初始相位、循环模式与自动转向配置
973
+ * @param meshes 可选指定的网格集合,不传则全量生效
974
+ */
975
+ bindGPUPath(smid, pathId, options = {}, meshes) {
976
+ const cur = this.getFeatureStyle(smid) ?? {};
977
+ this.setFeatureStyle(smid, {
978
+ ...cur,
979
+ gpuPath: {
980
+ pathId,
981
+ speed: options.speed ?? 0,
982
+ phase: options.phase ?? 0,
983
+ loop: options.loop ?? 'loop',
984
+ autoHeading: options.autoHeading ?? true,
985
+ },
986
+ }, meshes);
987
+ }
988
+ /**
989
+ * 解绑单体的 GPU 路径动画
990
+ */
991
+ unbindGPUPath(smid, meshes) {
992
+ const cur = this.getFeatureStyle(smid);
993
+ if (!cur || !cur.gpuPath)
994
+ return;
995
+ const next = { ...cur };
996
+ delete next.gpuPath;
997
+ this.setFeatureStyle(smid, next, meshes);
998
+ }
999
+ /**
1000
+ * 直接设置单体在 GPU 路径上的归一化进度(0.0 ~ 1.0)
1001
+ * @param smid 单体 ID
1002
+ * @param progress 归一化进度 (0~1)
1003
+ * @param meshes 可选指定的网格集合
1004
+ */
1005
+ setGPUPathProgress(smid, progress, meshes) {
1006
+ const cur = this.getFeatureStyle(smid);
1007
+ if (!cur?.gpuPath)
1008
+ return;
1009
+ this.setFeatureStyle(smid, {
1010
+ ...cur,
1011
+ gpuPath: {
1012
+ ...cur.gpuPath,
1013
+ speed: 0,
1014
+ phase: Math.min(1.0, Math.max(0.0, progress)),
1015
+ },
1016
+ }, meshes);
1017
+ }
670
1018
  }
671
1019
  //# sourceMappingURL=FeatureStyleManager.js.map