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
|
@@ -21,6 +21,19 @@ import {
|
|
|
21
21
|
import type { AttributeQuery, AttributeQueryOptions, AttributeRow } from '../core/AttributeQuery.js';
|
|
22
22
|
import type { StyledMaterial } from './StyleMaterial.js';
|
|
23
23
|
|
|
24
|
+
export interface GPUPathOptions {
|
|
25
|
+
/** 绑定的路径索引 ID(0, 1, 2...) */
|
|
26
|
+
pathId: number;
|
|
27
|
+
/** 进度的运动速率(归一化进度/秒,例如 0.05 即 20 秒一圈;为 0 则靠 progress/phase 驱动) */
|
|
28
|
+
speed?: number;
|
|
29
|
+
/** 初始相位 / 进度偏移(0.0 ~ 1.0) */
|
|
30
|
+
phase?: number;
|
|
31
|
+
/** 循环模式:'loop'(循环,默认) | 'pingpong'(往返) | 'clamp'(单次到达终点停止) */
|
|
32
|
+
loop?: 'loop' | 'pingpong' | 'clamp' | boolean;
|
|
33
|
+
/** 是否根据路径切线自动旋转朝向(偏航角 yaw,默认 true) */
|
|
34
|
+
autoHeading?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
export interface FeatureStyle {
|
|
25
38
|
/** 自定义单体颜色 */
|
|
26
39
|
color?: string;
|
|
@@ -32,6 +45,161 @@ export interface FeatureStyle {
|
|
|
32
45
|
opacity?: number;
|
|
33
46
|
/** 是否隐藏该单体 */
|
|
34
47
|
hidden?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* 单体姿态旋转。
|
|
50
|
+
* 支持:
|
|
51
|
+
* 1. 纯数字:绕 Z 轴(朝天方向)旋转弧度(最常用的建筑朝向)
|
|
52
|
+
* 2. 数组 [rx, ry, rz]:欧拉角(弧度)
|
|
53
|
+
* 3. 数组 [x, y, z, w]:四元数
|
|
54
|
+
* 4. THREE.Euler | THREE.Quaternion | THREE.Matrix3 | THREE.Matrix4
|
|
55
|
+
*/
|
|
56
|
+
rotation?:
|
|
57
|
+
| number
|
|
58
|
+
| [number, number, number]
|
|
59
|
+
| [number, number, number, number]
|
|
60
|
+
| THREE.Euler
|
|
61
|
+
| THREE.Quaternion
|
|
62
|
+
| THREE.Matrix3
|
|
63
|
+
| THREE.Matrix4;
|
|
64
|
+
/** 旋转中心轴心点 [px, py, pz](局部坐标,默认 [0, 0, 0]) */
|
|
65
|
+
pivot?: [number, number, number];
|
|
66
|
+
/** GPU 路径动画配置 */
|
|
67
|
+
gpuPath?: GPUPathOptions;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type RotationMatrix9 = [
|
|
71
|
+
number, number, number, // Row 0: R00, R01, R02
|
|
72
|
+
number, number, number, // Row 1: R10, R11, R12
|
|
73
|
+
number, number, number // Row 2: R20, R21, R22
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 将各类旋转表示统一解析为 3x3 行优先矩阵数组
|
|
78
|
+
*/
|
|
79
|
+
export function resolveRotationMatrix(
|
|
80
|
+
rot?:
|
|
81
|
+
| number
|
|
82
|
+
| [number, number, number]
|
|
83
|
+
| [number, number, number, number]
|
|
84
|
+
| THREE.Euler
|
|
85
|
+
| THREE.Quaternion
|
|
86
|
+
| THREE.Matrix3
|
|
87
|
+
| THREE.Matrix4
|
|
88
|
+
| null
|
|
89
|
+
): RotationMatrix9 | null {
|
|
90
|
+
if (rot == null) return null;
|
|
91
|
+
if (typeof rot === 'number') {
|
|
92
|
+
const c = Math.cos(rot);
|
|
93
|
+
const s = Math.sin(rot);
|
|
94
|
+
return [
|
|
95
|
+
c, -s, 0,
|
|
96
|
+
s, c, 0,
|
|
97
|
+
0, 0, 1,
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
if (Array.isArray(rot)) {
|
|
101
|
+
if (rot.length === 3) {
|
|
102
|
+
const euler = new THREE.Euler(rot[0], rot[1], rot[2]);
|
|
103
|
+
const m = new THREE.Matrix4().makeRotationFromEuler(euler);
|
|
104
|
+
const e = m.elements;
|
|
105
|
+
return [
|
|
106
|
+
e[0], e[4], e[8],
|
|
107
|
+
e[1], e[5], e[9],
|
|
108
|
+
e[2], e[6], e[10],
|
|
109
|
+
];
|
|
110
|
+
}
|
|
111
|
+
if (rot.length === 4) {
|
|
112
|
+
const q = new THREE.Quaternion(rot[0], rot[1], rot[2], rot[3]);
|
|
113
|
+
const m = new THREE.Matrix4().makeRotationFromQuaternion(q);
|
|
114
|
+
const e = m.elements;
|
|
115
|
+
return [
|
|
116
|
+
e[0], e[4], e[8],
|
|
117
|
+
e[1], e[5], e[9],
|
|
118
|
+
e[2], e[6], e[10],
|
|
119
|
+
];
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (rot instanceof THREE.Euler) {
|
|
123
|
+
const m = new THREE.Matrix4().makeRotationFromEuler(rot);
|
|
124
|
+
const e = m.elements;
|
|
125
|
+
return [
|
|
126
|
+
e[0], e[4], e[8],
|
|
127
|
+
e[1], e[5], e[9],
|
|
128
|
+
e[2], e[6], e[10],
|
|
129
|
+
];
|
|
130
|
+
}
|
|
131
|
+
if (rot instanceof THREE.Quaternion) {
|
|
132
|
+
const m = new THREE.Matrix4().makeRotationFromQuaternion(rot);
|
|
133
|
+
const e = m.elements;
|
|
134
|
+
return [
|
|
135
|
+
e[0], e[4], e[8],
|
|
136
|
+
e[1], e[5], e[9],
|
|
137
|
+
e[2], e[6], e[10],
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
if (rot instanceof THREE.Matrix3) {
|
|
141
|
+
const e = rot.elements;
|
|
142
|
+
return [
|
|
143
|
+
e[0], e[3], e[6],
|
|
144
|
+
e[1], e[4], e[7],
|
|
145
|
+
e[2], e[5], e[8],
|
|
146
|
+
];
|
|
147
|
+
}
|
|
148
|
+
if (rot instanceof THREE.Matrix4) {
|
|
149
|
+
const e = rot.elements;
|
|
150
|
+
return [
|
|
151
|
+
e[0], e[4], e[8],
|
|
152
|
+
e[1], e[5], e[9],
|
|
153
|
+
e[2], e[6], e[10],
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 提取欧拉角 [rx, ry, rz](弧度)用于动画插值
|
|
161
|
+
*/
|
|
162
|
+
export function extractEulerAngles(
|
|
163
|
+
rot?:
|
|
164
|
+
| number
|
|
165
|
+
| [number, number, number]
|
|
166
|
+
| [number, number, number, number]
|
|
167
|
+
| THREE.Euler
|
|
168
|
+
| THREE.Quaternion
|
|
169
|
+
| THREE.Matrix3
|
|
170
|
+
| THREE.Matrix4
|
|
171
|
+
| null
|
|
172
|
+
): [number, number, number] {
|
|
173
|
+
if (rot == null) return [0, 0, 0];
|
|
174
|
+
if (typeof rot === 'number') return [0, 0, rot];
|
|
175
|
+
if (Array.isArray(rot)) {
|
|
176
|
+
if (rot.length === 3) return [rot[0], rot[1], rot[2]];
|
|
177
|
+
if (rot.length === 4) {
|
|
178
|
+
const q = new THREE.Quaternion(rot[0], rot[1], rot[2], rot[3]);
|
|
179
|
+
const e = new THREE.Euler().setFromQuaternion(q);
|
|
180
|
+
return [e.x, e.y, e.z];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (rot instanceof THREE.Euler) return [rot.x, rot.y, rot.z];
|
|
184
|
+
if (rot instanceof THREE.Quaternion) {
|
|
185
|
+
const e = new THREE.Euler().setFromQuaternion(rot);
|
|
186
|
+
return [e.x, e.y, e.z];
|
|
187
|
+
}
|
|
188
|
+
if (rot instanceof THREE.Matrix3) {
|
|
189
|
+
const m = new THREE.Matrix4().set(
|
|
190
|
+
rot.elements[0], rot.elements[3], rot.elements[6], 0,
|
|
191
|
+
rot.elements[1], rot.elements[4], rot.elements[7], 0,
|
|
192
|
+
rot.elements[2], rot.elements[5], rot.elements[8], 0,
|
|
193
|
+
0, 0, 0, 1
|
|
194
|
+
);
|
|
195
|
+
const e = new THREE.Euler().setFromRotationMatrix(m);
|
|
196
|
+
return [e.x, e.y, e.z];
|
|
197
|
+
}
|
|
198
|
+
if (rot instanceof THREE.Matrix4) {
|
|
199
|
+
const e = new THREE.Euler().setFromRotationMatrix(rot);
|
|
200
|
+
return [e.x, e.y, e.z];
|
|
201
|
+
}
|
|
202
|
+
return [0, 0, 0];
|
|
35
203
|
}
|
|
36
204
|
|
|
37
205
|
export interface ColorByFieldOptions {
|
|
@@ -173,7 +341,13 @@ export class FeatureStyleManager {
|
|
|
173
341
|
|
|
174
342
|
private readonly _meshBatchTextures = new WeakMap<
|
|
175
343
|
THREE.Mesh,
|
|
176
|
-
{
|
|
344
|
+
{
|
|
345
|
+
texture: THREE.DataTexture;
|
|
346
|
+
data: Float32Array;
|
|
347
|
+
texture2?: THREE.DataTexture;
|
|
348
|
+
data2?: Float32Array;
|
|
349
|
+
width: number;
|
|
350
|
+
}
|
|
177
351
|
>();
|
|
178
352
|
private readonly _markedMeshes = new Set<THREE.Mesh>();
|
|
179
353
|
private _dirtyAll = false;
|
|
@@ -216,11 +390,21 @@ export class FeatureStyleManager {
|
|
|
216
390
|
return this.featureStyles.get(smid) ?? null;
|
|
217
391
|
}
|
|
218
392
|
|
|
219
|
-
getFeatureEffectiveOffset(smid: number | null): {
|
|
393
|
+
getFeatureEffectiveOffset(smid: number | null): {
|
|
394
|
+
translation?: [number, number, number];
|
|
395
|
+
elevation?: number;
|
|
396
|
+
rotation?: FeatureStyle['rotation'];
|
|
397
|
+
pivot?: [number, number, number];
|
|
398
|
+
} {
|
|
220
399
|
if (smid == null) return {};
|
|
221
400
|
const st = this.featureStyles.get(smid);
|
|
222
401
|
const elev = st?.elevation ?? this.thematicElevation.get(smid);
|
|
223
|
-
return {
|
|
402
|
+
return {
|
|
403
|
+
translation: st?.translation,
|
|
404
|
+
elevation: elev,
|
|
405
|
+
rotation: st?.rotation,
|
|
406
|
+
pivot: st?.pivot,
|
|
407
|
+
};
|
|
224
408
|
}
|
|
225
409
|
|
|
226
410
|
getFeatureStyles(): Map<number, FeatureStyle> {
|
|
@@ -551,72 +735,55 @@ export class FeatureStyleManager {
|
|
|
551
735
|
return { smids: 0, batches, conflicts, conflictingSmids: involved.size };
|
|
552
736
|
}
|
|
553
737
|
|
|
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;
|
|
738
|
+
private _bakeStylesIntoBuffer(
|
|
739
|
+
data: Float32Array,
|
|
740
|
+
width: number,
|
|
741
|
+
pickInfo: Record<string, unknown>,
|
|
742
|
+
mesh: THREE.Mesh,
|
|
743
|
+
styleMap: Map<number, FeatureStyle>
|
|
744
|
+
): { hasActive: boolean; hasFade: boolean; conflicts: number } {
|
|
745
|
+
const w = width;
|
|
609
746
|
for (let i = 0; i < w; i++) {
|
|
747
|
+
// Row 0: [R, G, B, elevation/hidden]
|
|
610
748
|
data[i * 4 + 0] = -1;
|
|
611
749
|
data[i * 4 + 1] = -1;
|
|
612
750
|
data[i * 4 + 2] = -1;
|
|
613
751
|
data[i * 4 + 3] = 0;
|
|
614
752
|
|
|
753
|
+
// Row 1: [translationX, translationY, translationZ, opacity]
|
|
615
754
|
const r1 = (w + i) * 4;
|
|
616
755
|
data[r1 + 0] = 0;
|
|
617
756
|
data[r1 + 1] = 0;
|
|
618
757
|
data[r1 + 2] = 0;
|
|
619
758
|
data[r1 + 3] = 0;
|
|
759
|
+
|
|
760
|
+
// Row 2: [R00, R01, R02, pivot.x] (单位矩阵默认值)
|
|
761
|
+
const r2 = (w * 2 + i) * 4;
|
|
762
|
+
data[r2 + 0] = 1;
|
|
763
|
+
data[r2 + 1] = 0;
|
|
764
|
+
data[r2 + 2] = 0;
|
|
765
|
+
data[r2 + 3] = 0;
|
|
766
|
+
|
|
767
|
+
// Row 3: [R10, R11, R12, pivot.y]
|
|
768
|
+
const r3 = (w * 3 + i) * 4;
|
|
769
|
+
data[r3 + 0] = 0;
|
|
770
|
+
data[r3 + 1] = 1;
|
|
771
|
+
data[r3 + 2] = 0;
|
|
772
|
+
data[r3 + 3] = 0;
|
|
773
|
+
|
|
774
|
+
// Row 4: [R20, R21, R22, pivot.z]
|
|
775
|
+
const r4 = (w * 4 + i) * 4;
|
|
776
|
+
data[r4 + 0] = 0;
|
|
777
|
+
data[r4 + 1] = 0;
|
|
778
|
+
data[r4 + 2] = 1;
|
|
779
|
+
data[r4 + 3] = 0;
|
|
780
|
+
|
|
781
|
+
// Row 5: [pathId, speed, phase, modeFlag] (默认 -1 表示未绑定 GPU 路径)
|
|
782
|
+
const r5 = (w * 5 + i) * 4;
|
|
783
|
+
data[r5 + 0] = -1;
|
|
784
|
+
data[r5 + 1] = 0;
|
|
785
|
+
data[r5 + 2] = 0;
|
|
786
|
+
data[r5 + 3] = 0;
|
|
620
787
|
}
|
|
621
788
|
|
|
622
789
|
let hasActive = false;
|
|
@@ -624,13 +791,18 @@ export class FeatureStyleManager {
|
|
|
624
791
|
const colorOfBatch = new Map<number, string>();
|
|
625
792
|
const elevOfBatch = new Map<number, number>();
|
|
626
793
|
const translationOfBatch = new Map<number, [number, number, number]>();
|
|
794
|
+
const rotationOfBatch = new Map<number, RotationMatrix9>();
|
|
795
|
+
const pivotOfBatch = new Map<number, [number, number, number]>();
|
|
796
|
+
const gpuPathOfBatch = new Map<number, GPUPathOptions>();
|
|
627
797
|
const opacityOfBatch = new Map<number, number>();
|
|
628
798
|
const ownersOfBatch = new Map<number, number>();
|
|
799
|
+
const filter = this.filter;
|
|
800
|
+
const ud = mesh.userData as any;
|
|
629
801
|
|
|
630
802
|
for (const [smidStr, entry] of Object.entries(pickInfo)) {
|
|
631
803
|
const smid = Number(smidStr);
|
|
632
804
|
const bId = pickEntryId(entry as any);
|
|
633
|
-
if (bId < 0 || bId >=
|
|
805
|
+
if (bId < 0 || bId >= w) continue;
|
|
634
806
|
ownersOfBatch.set(bId, (ownersOfBatch.get(bId) ?? 0) + 1);
|
|
635
807
|
|
|
636
808
|
let hidden = false;
|
|
@@ -669,7 +841,7 @@ export class FeatureStyleManager {
|
|
|
669
841
|
}
|
|
670
842
|
}
|
|
671
843
|
|
|
672
|
-
const st =
|
|
844
|
+
const st = styleMap.get(smid);
|
|
673
845
|
if (st?.hidden) {
|
|
674
846
|
hiddenBatches.add(bId);
|
|
675
847
|
continue;
|
|
@@ -694,6 +866,17 @@ export class FeatureStyleManager {
|
|
|
694
866
|
if (st?.translation && !translationOfBatch.has(bId)) {
|
|
695
867
|
translationOfBatch.set(bId, st.translation);
|
|
696
868
|
}
|
|
869
|
+
|
|
870
|
+
if (st?.rotation != null && !rotationOfBatch.has(bId)) {
|
|
871
|
+
const rotMat = resolveRotationMatrix(st.rotation);
|
|
872
|
+
if (rotMat) rotationOfBatch.set(bId, rotMat);
|
|
873
|
+
}
|
|
874
|
+
if (st?.pivot && !pivotOfBatch.has(bId)) {
|
|
875
|
+
pivotOfBatch.set(bId, st.pivot);
|
|
876
|
+
}
|
|
877
|
+
if (st?.gpuPath && !gpuPathOfBatch.has(bId)) {
|
|
878
|
+
gpuPathOfBatch.set(bId, st.gpuPath);
|
|
879
|
+
}
|
|
697
880
|
}
|
|
698
881
|
|
|
699
882
|
for (const bId of hiddenBatches) {
|
|
@@ -737,17 +920,142 @@ export class FeatureStyleManager {
|
|
|
737
920
|
hasFade = true;
|
|
738
921
|
}
|
|
739
922
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
923
|
+
for (const [bId, rot] of rotationOfBatch) {
|
|
924
|
+
if (hiddenBatches.has(bId)) continue;
|
|
925
|
+
const piv = pivotOfBatch.get(bId) ?? [0, 0, 0];
|
|
926
|
+
const r2 = (w * 2 + bId) * 4;
|
|
927
|
+
data[r2 + 0] = rot[0];
|
|
928
|
+
data[r2 + 1] = rot[1];
|
|
929
|
+
data[r2 + 2] = rot[2];
|
|
930
|
+
data[r2 + 3] = piv[0];
|
|
931
|
+
|
|
932
|
+
const r3 = (w * 3 + bId) * 4;
|
|
933
|
+
data[r3 + 0] = rot[3];
|
|
934
|
+
data[r3 + 1] = rot[4];
|
|
935
|
+
data[r3 + 2] = rot[5];
|
|
936
|
+
data[r3 + 3] = piv[1];
|
|
937
|
+
|
|
938
|
+
const r4 = (w * 4 + bId) * 4;
|
|
939
|
+
data[r4 + 0] = rot[6];
|
|
940
|
+
data[r4 + 1] = rot[7];
|
|
941
|
+
data[r4 + 2] = rot[8];
|
|
942
|
+
data[r4 + 3] = piv[2];
|
|
943
|
+
hasActive = true;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
for (const [bId, piv] of pivotOfBatch) {
|
|
947
|
+
if (rotationOfBatch.has(bId) || hiddenBatches.has(bId)) continue;
|
|
948
|
+
data[(w * 2 + bId) * 4 + 3] = piv[0];
|
|
949
|
+
data[(w * 3 + bId) * 4 + 3] = piv[1];
|
|
950
|
+
data[(w * 4 + bId) * 4 + 3] = piv[2];
|
|
951
|
+
hasActive = true;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Row 5: 烘焙 GPU 路径动画参数 [pathId, speed, phase, modeFlag]
|
|
955
|
+
for (const [bId, gp] of gpuPathOfBatch) {
|
|
956
|
+
if (hiddenBatches.has(bId)) continue;
|
|
957
|
+
const r5 = (w * 5 + bId) * 4;
|
|
958
|
+
data[r5 + 0] = gp.pathId;
|
|
959
|
+
data[r5 + 1] = gp.speed ?? 0;
|
|
960
|
+
data[r5 + 2] = gp.phase ?? 0;
|
|
961
|
+
|
|
962
|
+
let mode = 1; // 1 = loop (默认)
|
|
963
|
+
if (gp.loop === 'clamp' || gp.loop === false) {
|
|
964
|
+
mode = 2;
|
|
965
|
+
} else if (gp.loop === 'pingpong') {
|
|
966
|
+
mode = 3;
|
|
967
|
+
}
|
|
968
|
+
if (gp.autoHeading === false) {
|
|
969
|
+
mode += 3; // 4=loop+noHeading, 5=clamp+noHeading, 6=pingpong+noHeading
|
|
970
|
+
}
|
|
971
|
+
data[r5 + 3] = mode;
|
|
972
|
+
hasActive = true;
|
|
973
|
+
}
|
|
743
974
|
|
|
744
975
|
let conflicts = 0;
|
|
745
976
|
for (const [bId, owners] of ownersOfBatch) {
|
|
746
977
|
if (owners < 2) continue;
|
|
747
|
-
if (
|
|
978
|
+
if (
|
|
979
|
+
!hiddenBatches.has(bId) &&
|
|
980
|
+
!colorOfBatch.has(bId) &&
|
|
981
|
+
!elevOfBatch.has(bId) &&
|
|
982
|
+
!rotationOfBatch.has(bId) &&
|
|
983
|
+
!gpuPathOfBatch.has(bId)
|
|
984
|
+
) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
748
987
|
conflicts++;
|
|
749
988
|
}
|
|
750
989
|
|
|
990
|
+
return { hasActive, hasFade, conflicts };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
applyOverridesToMesh(mesh: THREE.Mesh): void {
|
|
994
|
+
const mat = styledMaterialOf(mesh);
|
|
995
|
+
if (!mat) return;
|
|
996
|
+
|
|
997
|
+
const ud = mesh.userData as any;
|
|
998
|
+
const pickInfo = ud.geoRef?.pickInfo;
|
|
999
|
+
|
|
1000
|
+
const hasStyles =
|
|
1001
|
+
this.featureStyles.size > 0 || this.thematicColors.size > 0 || this.thematicElevation.size > 0;
|
|
1002
|
+
const filter = this.filter;
|
|
1003
|
+
const hasFilter =
|
|
1004
|
+
this.fieldFilter != null ||
|
|
1005
|
+
(filter != null &&
|
|
1006
|
+
((filter.include?.length ?? 0) > 0 ||
|
|
1007
|
+
(filter.exclude?.length ?? 0) > 0 ||
|
|
1008
|
+
filter.predicate != null));
|
|
1009
|
+
|
|
1010
|
+
if (!hasStyles && !hasFilter) {
|
|
1011
|
+
if (mat.userData.uBatchTexActive) {
|
|
1012
|
+
mat.userData.uBatchTexActive.value = 0;
|
|
1013
|
+
}
|
|
1014
|
+
// 单体淡化清除后,把材质 transparent 还原成全局透明度语义
|
|
1015
|
+
if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
if (!pickInfo || Object.keys(pickInfo).length === 0) {
|
|
1020
|
+
if (mat.userData.uBatchTexActive) {
|
|
1021
|
+
mat.userData.uBatchTexActive.value = 0;
|
|
1022
|
+
}
|
|
1023
|
+
if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
let maxBatch = 0;
|
|
1028
|
+
for (const p of Object.values(pickInfo)) {
|
|
1029
|
+
const id = pickEntryId(p as any);
|
|
1030
|
+
if (id > maxBatch) maxBatch = id;
|
|
1031
|
+
}
|
|
1032
|
+
const width = Math.max(maxBatch + 1, 1);
|
|
1033
|
+
|
|
1034
|
+
let record = this._meshBatchTextures.get(mesh);
|
|
1035
|
+
if (!record || record.width < width) {
|
|
1036
|
+
const data = new Float32Array(width * 6 * 4);
|
|
1037
|
+
const texture = new THREE.DataTexture(data, width, 6, THREE.RGBAFormat, THREE.FloatType);
|
|
1038
|
+
texture.minFilter = THREE.NearestFilter;
|
|
1039
|
+
texture.magFilter = THREE.NearestFilter;
|
|
1040
|
+
record = { texture, data, width };
|
|
1041
|
+
this._meshBatchTextures.set(mesh, record);
|
|
1042
|
+
mat.userData.uBatchTex.value = texture;
|
|
1043
|
+
mat.userData.uBatchTexWidth.value = width;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const { data, texture } = record;
|
|
1047
|
+
const { hasActive, hasFade, conflicts } = this._bakeStylesIntoBuffer(
|
|
1048
|
+
data,
|
|
1049
|
+
record.width,
|
|
1050
|
+
pickInfo,
|
|
1051
|
+
mesh,
|
|
1052
|
+
this.featureStyles
|
|
1053
|
+
);
|
|
1054
|
+
|
|
1055
|
+
// 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
|
|
1056
|
+
if (hasFade && !mat.transparent) mat.transparent = true;
|
|
1057
|
+
else if (!hasFade && mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
|
|
1058
|
+
|
|
751
1059
|
if (conflicts > 0 && !this._batchConflictWarned) {
|
|
752
1060
|
this._batchConflictWarned = true;
|
|
753
1061
|
console.warn(
|
|
@@ -764,6 +1072,93 @@ export class FeatureStyleManager {
|
|
|
764
1072
|
}
|
|
765
1073
|
}
|
|
766
1074
|
|
|
1075
|
+
/**
|
|
1076
|
+
* 准备双纹理交叉淡化:烘焙目标样式到第二纹理 (uBatchTex2)
|
|
1077
|
+
*/
|
|
1078
|
+
prepareCrossFade(targetStyles: Map<number, FeatureStyle>, meshes: Iterable<THREE.Mesh>): void {
|
|
1079
|
+
for (const mesh of meshes) {
|
|
1080
|
+
const mat = styledMaterialOf(mesh);
|
|
1081
|
+
if (!mat) continue;
|
|
1082
|
+
const ud = mesh.userData as any;
|
|
1083
|
+
const pickInfo = ud.geoRef?.pickInfo;
|
|
1084
|
+
if (!pickInfo || Object.keys(pickInfo).length === 0) continue;
|
|
1085
|
+
|
|
1086
|
+
let maxBatch = 0;
|
|
1087
|
+
for (const p of Object.values(pickInfo)) {
|
|
1088
|
+
const id = pickEntryId(p as any);
|
|
1089
|
+
if (id > maxBatch) maxBatch = id;
|
|
1090
|
+
}
|
|
1091
|
+
const width = Math.max(maxBatch + 1, 1);
|
|
1092
|
+
|
|
1093
|
+
let record = this._meshBatchTextures.get(mesh);
|
|
1094
|
+
if (!record || record.width < width) {
|
|
1095
|
+
const data = new Float32Array(width * 6 * 4);
|
|
1096
|
+
const texture = new THREE.DataTexture(data, width, 6, THREE.RGBAFormat, THREE.FloatType);
|
|
1097
|
+
texture.minFilter = THREE.NearestFilter;
|
|
1098
|
+
texture.magFilter = THREE.NearestFilter;
|
|
1099
|
+
record = { texture, data, width };
|
|
1100
|
+
this._meshBatchTextures.set(mesh, record);
|
|
1101
|
+
mat.userData.uBatchTex.value = texture;
|
|
1102
|
+
mat.userData.uBatchTexWidth.value = width;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
if (!record.texture2 || !record.data2 || record.data2.length < record.width * 6 * 4) {
|
|
1106
|
+
const data2 = new Float32Array(record.width * 6 * 4);
|
|
1107
|
+
const texture2 = new THREE.DataTexture(data2, record.width, 6, THREE.RGBAFormat, THREE.FloatType);
|
|
1108
|
+
texture2.minFilter = THREE.NearestFilter;
|
|
1109
|
+
texture2.magFilter = THREE.NearestFilter;
|
|
1110
|
+
record.data2 = data2;
|
|
1111
|
+
record.texture2 = texture2;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// 确保当前纹理包含当前状态
|
|
1115
|
+
this._bakeStylesIntoBuffer(record.data, record.width, pickInfo, mesh, this.featureStyles);
|
|
1116
|
+
record.texture.needsUpdate = true;
|
|
1117
|
+
|
|
1118
|
+
// 烘焙目标状态到 data2
|
|
1119
|
+
const res2 = this._bakeStylesIntoBuffer(record.data2, record.width, pickInfo, mesh, targetStyles);
|
|
1120
|
+
record.texture2.needsUpdate = true;
|
|
1121
|
+
|
|
1122
|
+
mat.userData.uBatchTex2.value = record.texture2;
|
|
1123
|
+
mat.userData.uBatchTexActive.value = 1;
|
|
1124
|
+
if (res2.hasFade && !mat.transparent) mat.transparent = true;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* 提交双纹理交叉淡化:将 targetStyles 回写到主状态,并将 data2 复制到 data
|
|
1130
|
+
*/
|
|
1131
|
+
commitCrossFade(targetStyles: Map<number, FeatureStyle>, meshes: Iterable<THREE.Mesh>): void {
|
|
1132
|
+
this.featureStyles.clear();
|
|
1133
|
+
for (const [k, v] of targetStyles) {
|
|
1134
|
+
this.featureStyles.set(k, v);
|
|
1135
|
+
}
|
|
1136
|
+
for (const mesh of meshes) {
|
|
1137
|
+
const mat = styledMaterialOf(mesh);
|
|
1138
|
+
if (!mat) continue;
|
|
1139
|
+
const record = this._meshBatchTextures.get(mesh);
|
|
1140
|
+
if (!record || !record.data2 || !record.texture2) continue;
|
|
1141
|
+
|
|
1142
|
+
record.data.set(record.data2);
|
|
1143
|
+
record.texture.needsUpdate = true;
|
|
1144
|
+
mat.userData.uBatchTex2.value = mat.userData.uBatchTex.value;
|
|
1145
|
+
}
|
|
1146
|
+
this._onStyleChanged();
|
|
1147
|
+
this._triggerRepaint();
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* 取消交叉淡化并恢复单一纹理
|
|
1152
|
+
*/
|
|
1153
|
+
cancelCrossFade(meshes: Iterable<THREE.Mesh>): void {
|
|
1154
|
+
for (const mesh of meshes) {
|
|
1155
|
+
const mat = styledMaterialOf(mesh);
|
|
1156
|
+
if (!mat) continue;
|
|
1157
|
+
mat.userData.uBatchTex2.value = mat.userData.uBatchTex.value;
|
|
1158
|
+
}
|
|
1159
|
+
this._triggerRepaint();
|
|
1160
|
+
}
|
|
1161
|
+
|
|
767
1162
|
markOverridesDirty(subset?: Iterable<THREE.Mesh>, allMeshes?: Iterable<THREE.Mesh>): void {
|
|
768
1163
|
if (subset) {
|
|
769
1164
|
for (const m of subset) this._markedMeshes.add(m);
|
|
@@ -783,6 +1178,7 @@ export class FeatureStyleManager {
|
|
|
783
1178
|
const record = this._meshBatchTextures.get(mesh);
|
|
784
1179
|
if (record) {
|
|
785
1180
|
record.texture.dispose();
|
|
1181
|
+
record.texture2?.dispose();
|
|
786
1182
|
this._meshBatchTextures.delete(mesh);
|
|
787
1183
|
}
|
|
788
1184
|
}
|
|
@@ -807,4 +1203,68 @@ export class FeatureStyleManager {
|
|
|
807
1203
|
this._markedMeshes.clear();
|
|
808
1204
|
this._triggerRepaint();
|
|
809
1205
|
}
|
|
1206
|
+
|
|
1207
|
+
/**
|
|
1208
|
+
* 将单体绑定到 GPU 路径动画
|
|
1209
|
+
* @param smid 单体 ID
|
|
1210
|
+
* @param pathId GPUPathManager 中的路径索引
|
|
1211
|
+
* @param options 速度、初始相位、循环模式与自动转向配置
|
|
1212
|
+
* @param meshes 可选指定的网格集合,不传则全量生效
|
|
1213
|
+
*/
|
|
1214
|
+
bindGPUPath(
|
|
1215
|
+
smid: number,
|
|
1216
|
+
pathId: number,
|
|
1217
|
+
options: Omit<GPUPathOptions, 'pathId'> = {},
|
|
1218
|
+
meshes?: Iterable<THREE.Mesh>
|
|
1219
|
+
): void {
|
|
1220
|
+
const cur = this.getFeatureStyle(smid) ?? {};
|
|
1221
|
+
this.setFeatureStyle(
|
|
1222
|
+
smid,
|
|
1223
|
+
{
|
|
1224
|
+
...cur,
|
|
1225
|
+
gpuPath: {
|
|
1226
|
+
pathId,
|
|
1227
|
+
speed: options.speed ?? 0,
|
|
1228
|
+
phase: options.phase ?? 0,
|
|
1229
|
+
loop: options.loop ?? 'loop',
|
|
1230
|
+
autoHeading: options.autoHeading ?? true,
|
|
1231
|
+
},
|
|
1232
|
+
},
|
|
1233
|
+
meshes
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
/**
|
|
1238
|
+
* 解绑单体的 GPU 路径动画
|
|
1239
|
+
*/
|
|
1240
|
+
unbindGPUPath(smid: number, meshes?: Iterable<THREE.Mesh>): void {
|
|
1241
|
+
const cur = this.getFeatureStyle(smid);
|
|
1242
|
+
if (!cur || !cur.gpuPath) return;
|
|
1243
|
+
const next = { ...cur };
|
|
1244
|
+
delete next.gpuPath;
|
|
1245
|
+
this.setFeatureStyle(smid, next, meshes);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* 直接设置单体在 GPU 路径上的归一化进度(0.0 ~ 1.0)
|
|
1250
|
+
* @param smid 单体 ID
|
|
1251
|
+
* @param progress 归一化进度 (0~1)
|
|
1252
|
+
* @param meshes 可选指定的网格集合
|
|
1253
|
+
*/
|
|
1254
|
+
setGPUPathProgress(smid: number, progress: number, meshes?: Iterable<THREE.Mesh>): void {
|
|
1255
|
+
const cur = this.getFeatureStyle(smid);
|
|
1256
|
+
if (!cur?.gpuPath) return;
|
|
1257
|
+
this.setFeatureStyle(
|
|
1258
|
+
smid,
|
|
1259
|
+
{
|
|
1260
|
+
...cur,
|
|
1261
|
+
gpuPath: {
|
|
1262
|
+
...cur.gpuPath,
|
|
1263
|
+
speed: 0,
|
|
1264
|
+
phase: Math.min(1.0, Math.max(0.0, progress)),
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
meshes
|
|
1268
|
+
);
|
|
1269
|
+
}
|
|
810
1270
|
}
|