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.
- package/dist/browser/s3m-three.iife.ext-three.js +187 -46
- package/dist/browser/s3m-three.iife.js +187 -46
- package/dist/core/types.d.ts +4 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/render/EdgeLines.d.ts +6 -0
- package/dist/render/EdgeLines.d.ts.map +1 -1
- package/dist/render/EdgeLines.js +41 -13
- package/dist/render/EdgeLines.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts +45 -0
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +320 -59
- package/dist/render/FeatureStyleManager.js.map +1 -1
- 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 +19 -0
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +122 -6
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +9 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +153 -21
- package/dist/render/StyleMaterial.js.map +1 -1
- package/package.json +1 -1
- package/src/core/types.ts +2 -0
- package/src/index.ts +6 -0
- package/src/render/EdgeLines.ts +43 -13
- package/src/render/FeatureStyleManager.ts +405 -64
- package/src/render/OrthoProjection.ts +52 -0
- package/src/render/S3MViewer.ts +162 -5
- 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
|
-
{
|
|
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): {
|
|
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 {
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
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 >=
|
|
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 =
|
|
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
|
-
|
|
742
|
-
|
|
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
|
+
}
|