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.
- package/dist/anim/Animator.d.ts +111 -0
- package/dist/anim/Animator.d.ts.map +1 -0
- package/dist/anim/Animator.js +382 -0
- package/dist/anim/Animator.js.map +1 -0
- package/dist/anim/Path.d.ts +30 -0
- package/dist/anim/Path.d.ts.map +1 -0
- package/dist/anim/Path.js +100 -0
- package/dist/anim/Path.js.map +1 -0
- package/dist/anim/easing.d.ts +70 -0
- package/dist/anim/easing.d.ts.map +1 -0
- package/dist/anim/easing.js +170 -0
- package/dist/anim/easing.js.map +1 -0
- package/dist/anim/index.d.ts +6 -0
- package/dist/anim/index.d.ts.map +1 -0
- package/dist/anim/index.js +4 -0
- package/dist/anim/index.js.map +1 -0
- package/dist/browser/s3m-three.iife.ext-three.js +218 -39
- package/dist/browser/s3m-three.iife.js +218 -39
- package/dist/core/types.d.ts +20 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +6 -2
- 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 +68 -14
- package/dist/render/EdgeLines.d.ts.map +1 -1
- package/dist/render/EdgeLines.js +125 -8
- package/dist/render/EdgeLines.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts +62 -1
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +331 -60
- package/dist/render/FeatureStyleManager.js.map +1 -1
- package/dist/render/MergeVertices.d.ts +7 -0
- package/dist/render/MergeVertices.d.ts.map +1 -1
- package/dist/render/MergeVertices.js +10 -1
- package/dist/render/MergeVertices.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 +235 -9
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +880 -68
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +28 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +190 -21
- package/dist/render/StyleMaterial.js.map +1 -1
- package/dist/tileset/S3MTileset.d.ts.map +1 -1
- package/dist/tileset/S3MTileset.js +3 -1
- package/dist/tileset/S3MTileset.js.map +1 -1
- package/docs/ANIMATION.md +534 -0
- package/docs/API.md +2 -0
- package/package.json +1 -1
- package/src/anim/Animator.ts +460 -0
- package/src/anim/Path.ts +107 -0
- package/src/anim/easing.ts +195 -0
- package/src/anim/index.ts +10 -0
- package/src/core/types.ts +10 -0
- package/src/index.ts +16 -0
- package/src/render/EdgeLines.ts +161 -15
- package/src/render/FeatureStyleManager.ts +414 -65
- package/src/render/MergeVertices.ts +11 -1
- package/src/render/OrthoProjection.ts +52 -0
- package/src/render/S3MViewer.ts +1012 -64
- package/src/render/StyleMaterial.ts +211 -22
- 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
|
-
{
|
|
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): {
|
|
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 {
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
const
|
|
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 >=
|
|
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 =
|
|
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
|
-
|
|
734
|
-
|
|
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 =
|
|
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
|
+
}
|