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.
- package/dist/browser/s3m-three.iife.ext-three.js +322 -47
- package/dist/browser/s3m-three.iife.js +322 -47
- package/dist/core/types.d.ts +20 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/render/EdgeLines.d.ts +18 -0
- package/dist/render/EdgeLines.d.ts.map +1 -1
- package/dist/render/EdgeLines.js +95 -13
- package/dist/render/EdgeLines.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts +78 -0
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +407 -59
- package/dist/render/FeatureStyleManager.js.map +1 -1
- package/dist/render/GPUPathManager.d.ts +95 -0
- package/dist/render/GPUPathManager.d.ts.map +1 -0
- package/dist/render/GPUPathManager.js +265 -0
- package/dist/render/GPUPathManager.js.map +1 -0
- package/dist/render/OrthoProjection.d.ts +6 -0
- package/dist/render/OrthoProjection.d.ts.map +1 -1
- package/dist/render/OrthoProjection.js +48 -0
- package/dist/render/OrthoProjection.js.map +1 -1
- package/dist/render/S3MViewer.d.ts +66 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +218 -6
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +21 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +256 -21
- package/dist/render/StyleMaterial.js.map +1 -1
- package/package.json +1 -1
- package/src/core/types.ts +10 -0
- package/src/index.ts +8 -0
- package/src/render/EdgeLines.ts +101 -13
- package/src/render/FeatureStyleManager.ts +524 -64
- package/src/render/GPUPathManager.ts +306 -0
- package/src/render/OrthoProjection.ts +52 -0
- package/src/render/S3MViewer.ts +272 -5
- 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 {
|
|
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
|
-
|
|
427
|
-
const
|
|
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 >=
|
|
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 =
|
|
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
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
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) &&
|
|
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
|