s3m-three 0.3.8 → 0.3.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/browser/s3m-three.iife.ext-three.js +322 -47
  2. package/dist/browser/s3m-three.iife.js +322 -47
  3. package/dist/core/types.d.ts +20 -0
  4. package/dist/core/types.d.ts.map +1 -1
  5. package/dist/index.d.ts +5 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/render/EdgeLines.d.ts +18 -0
  10. package/dist/render/EdgeLines.d.ts.map +1 -1
  11. package/dist/render/EdgeLines.js +95 -13
  12. package/dist/render/EdgeLines.js.map +1 -1
  13. package/dist/render/FeatureStyleManager.d.ts +78 -0
  14. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  15. package/dist/render/FeatureStyleManager.js +407 -59
  16. package/dist/render/FeatureStyleManager.js.map +1 -1
  17. package/dist/render/GPUPathManager.d.ts +95 -0
  18. package/dist/render/GPUPathManager.d.ts.map +1 -0
  19. package/dist/render/GPUPathManager.js +265 -0
  20. package/dist/render/GPUPathManager.js.map +1 -0
  21. package/dist/render/OrthoProjection.d.ts +6 -0
  22. package/dist/render/OrthoProjection.d.ts.map +1 -1
  23. package/dist/render/OrthoProjection.js +48 -0
  24. package/dist/render/OrthoProjection.js.map +1 -1
  25. package/dist/render/S3MViewer.d.ts +66 -0
  26. package/dist/render/S3MViewer.d.ts.map +1 -1
  27. package/dist/render/S3MViewer.js +218 -6
  28. package/dist/render/S3MViewer.js.map +1 -1
  29. package/dist/render/StyleMaterial.d.ts +21 -0
  30. package/dist/render/StyleMaterial.d.ts.map +1 -1
  31. package/dist/render/StyleMaterial.js +256 -21
  32. package/dist/render/StyleMaterial.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/core/types.ts +10 -0
  35. package/src/index.ts +8 -0
  36. package/src/render/EdgeLines.ts +101 -13
  37. package/src/render/FeatureStyleManager.ts +524 -64
  38. package/src/render/GPUPathManager.ts +306 -0
  39. package/src/render/OrthoProjection.ts +52 -0
  40. package/src/render/S3MViewer.ts +272 -5
  41. package/src/render/StyleMaterial.ts +264 -21
@@ -0,0 +1,306 @@
1
+ /**
2
+ * GPU 路径动画管理器
3
+ *
4
+ * 负责:
5
+ * 1. 注册与管理多条三维离散路径(Path)
6
+ * 2. 将空间折线等弧长采样并烘焙为 Float32Array 格式的 2D 数据纹理 (uPathTex):
7
+ * - Width: SAMPLES_PER_PATH (默认 256)
8
+ * - Height: max(pathCount, 1)
9
+ * - Format: RGBAFormat, FloatType
10
+ * - 像素通道:
11
+ * - R: X 坐标 (米)
12
+ * - G: Y 坐标 (米)
13
+ * - B: Z 坐标 (米)
14
+ * - A: 切线偏航角 yaw (弧度, atan2(dy, dx), 经过连续角度展开 unwrap)
15
+ * 3. 硬件双线性插值 (LinearFilter) 支持:
16
+ * GPU 在顶点着色器中按归一化进度 t (0~1) 采样,硬件自动插值获得平滑轨迹与朝向
17
+ */
18
+ import * as THREE from 'three';
19
+ import { Path } from '../anim/Path.js';
20
+
21
+ export const SAMPLES_PER_PATH = 256;
22
+
23
+ export interface GPUPathInfo {
24
+ id: number;
25
+ name?: string;
26
+ path: Path;
27
+ }
28
+
29
+ export class GPUPathManager {
30
+ private _paths: Map<number, GPUPathInfo> = new Map();
31
+ private _nextId = 0;
32
+ private _texture: THREE.DataTexture | null = null;
33
+ private _data: Float32Array | null = null;
34
+ private _dirty = false;
35
+
36
+ /** 默认空纹理,防止 WebGL 未绑定报错 */
37
+ static readonly DUMMY_PATH_TEX: THREE.DataTexture = (() => {
38
+ const data = new Float32Array(SAMPLES_PER_PATH * 1 * 4);
39
+ const tex = new THREE.DataTexture(data, SAMPLES_PER_PATH, 1, THREE.RGBAFormat, THREE.FloatType);
40
+ tex.minFilter = THREE.LinearFilter;
41
+ tex.magFilter = THREE.LinearFilter;
42
+ tex.wrapS = THREE.ClampToEdgeWrapping;
43
+ tex.wrapT = THREE.ClampToEdgeWrapping;
44
+ tex.needsUpdate = true;
45
+ return tex;
46
+ })();
47
+
48
+ constructor() {}
49
+
50
+ /**
51
+ * 注册一条路径到 GPU 路径库
52
+ * @param pointsOrPath 折线点数组或现有的 Path 实例
53
+ * @param opts 注册选项(可指定固定 id 或 name)
54
+ * @returns 分配的路径 ID (从 0 开始的连续或离散索引)
55
+ */
56
+ registerPath(
57
+ pointsOrPath: Path | ArrayLike<number> | Array<[number, number, number]>,
58
+ opts: { id?: number; name?: string; closed?: boolean } = {}
59
+ ): number {
60
+ const path =
61
+ pointsOrPath instanceof Path
62
+ ? pointsOrPath
63
+ : new Path(pointsOrPath, { closed: opts.closed });
64
+
65
+ let id = opts.id;
66
+ if (id == null) {
67
+ id = this._nextId++;
68
+ while (this._paths.has(id)) {
69
+ id = this._nextId++;
70
+ }
71
+ } else {
72
+ if (id >= this._nextId) {
73
+ this._nextId = id + 1;
74
+ }
75
+ }
76
+
77
+ this._paths.set(id, { id, name: opts.name, path });
78
+ this._dirty = true;
79
+ return id;
80
+ }
81
+
82
+ /**
83
+ * 注销一条路径
84
+ */
85
+ unregisterPath(id: number): boolean {
86
+ const removed = this._paths.delete(id);
87
+ if (removed) {
88
+ this._dirty = true;
89
+ }
90
+ return removed;
91
+ }
92
+
93
+ /**
94
+ * 获取指定 ID 的路径对象
95
+ */
96
+ getPath(id: number): Path | undefined {
97
+ return this._paths.get(id)?.path;
98
+ }
99
+
100
+ /**
101
+ * 是否存在指定 ID 的路径
102
+ */
103
+ hasPath(id: number): boolean {
104
+ return this._paths.has(id);
105
+ }
106
+
107
+ /**
108
+ * 获取所有已注册的路径 ID
109
+ */
110
+ getPathIds(): number[] {
111
+ return Array.from(this._paths.keys()).sort((a, b) => a - b);
112
+ }
113
+
114
+ /**
115
+ * 清空所有已注册的路径
116
+ */
117
+ clear(): void {
118
+ if (this._paths.size > 0) {
119
+ this._paths.clear();
120
+ this._nextId = 0;
121
+ this._dirty = true;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * 当前注册的有效路径数量
127
+ */
128
+ get count(): number {
129
+ return this._paths.size;
130
+ }
131
+
132
+ /**
133
+ * 获取 GPU 路径数据纹理,若有变动会自动触发重新烘焙
134
+ */
135
+ getTexture(): THREE.DataTexture {
136
+ if (this._dirty || !this._texture) {
137
+ this._bake();
138
+ }
139
+ return this._texture ?? GPUPathManager.DUMMY_PATH_TEX;
140
+ }
141
+
142
+ /**
143
+ * 将当前路径纹理与 uniform 状态同步到材质 uniform 字典
144
+ */
145
+ updateUniforms(uniforms: Record<string, any>): void {
146
+ const tex = this.getTexture();
147
+ const active = this._paths.size > 0 ? 1 : 0;
148
+ const maxId = this._getMaxPathId();
149
+ const height = Math.max(maxId + 1, 1);
150
+
151
+ if (uniforms.uPathTex) uniforms.uPathTex.value = tex;
152
+ if (uniforms.uPathCount) uniforms.uPathCount.value = height;
153
+ if (uniforms.uPathTexActive) uniforms.uPathTexActive.value = active;
154
+ }
155
+
156
+ /**
157
+ * 烘焙路径数据到纹理
158
+ */
159
+ private _bake(): void {
160
+ this._dirty = false;
161
+ if (this._paths.size === 0) {
162
+ if (this._texture && this._texture !== GPUPathManager.DUMMY_PATH_TEX) {
163
+ this._texture.dispose();
164
+ }
165
+ this._texture = GPUPathManager.DUMMY_PATH_TEX;
166
+ this._data = null;
167
+ return;
168
+ }
169
+
170
+ const maxId = this._getMaxPathId();
171
+ const height = Math.max(maxId + 1, 1);
172
+ const totalPixels = SAMPLES_PER_PATH * height;
173
+ const bufferLength = totalPixels * 4;
174
+
175
+ if (!this._data || this._data.length !== bufferLength) {
176
+ if (this._texture && this._texture !== GPUPathManager.DUMMY_PATH_TEX) {
177
+ this._texture.dispose();
178
+ }
179
+ this._data = new Float32Array(bufferLength);
180
+ this._texture = new THREE.DataTexture(
181
+ this._data,
182
+ SAMPLES_PER_PATH,
183
+ height,
184
+ THREE.RGBAFormat,
185
+ THREE.FloatType
186
+ );
187
+ this._texture.minFilter = THREE.LinearFilter;
188
+ this._texture.magFilter = THREE.LinearFilter;
189
+ this._texture.wrapS = THREE.ClampToEdgeWrapping;
190
+ this._texture.wrapT = THREE.ClampToEdgeWrapping;
191
+ } else {
192
+ this._data.fill(0);
193
+ }
194
+
195
+ const scratchA = [0, 0, 0];
196
+ const scratchB = [0, 0, 0];
197
+ const N = SAMPLES_PER_PATH;
198
+
199
+ for (const [id, info] of this._paths) {
200
+ if (id < 0 || id >= height) continue;
201
+ const path = info.path;
202
+ const rowOffset = id * N * 4;
203
+
204
+ // 提取 N 个等弧长采样点位置与航向切线角
205
+ let lastYaw = 0;
206
+ for (let i = 0; i < N; i++) {
207
+ const t = i / (N - 1);
208
+ path.sampleAt(t, scratchA);
209
+
210
+ // 计算切线方向:取一个极小的前向步长
211
+ let yaw = lastYaw;
212
+ if (path.length > 1e-4) {
213
+ const deltaT = Math.min(1.0, t + 1e-3);
214
+ path.sampleAt(deltaT, scratchB);
215
+ let dx = scratchB[0] - scratchA[0];
216
+ let dy = scratchB[1] - scratchA[1];
217
+ if (dx * dx + dy * dy < 1e-8 && t > 0) {
218
+ // 前向位移过小则尝试后向差分
219
+ const prevT = Math.max(0.0, t - 1e-3);
220
+ path.sampleAt(prevT, scratchB);
221
+ dx = scratchA[0] - scratchB[0];
222
+ dy = scratchA[1] - scratchB[1];
223
+ }
224
+ if (dx * dx + dy * dy >= 1e-8) {
225
+ yaw = Math.atan2(dy, dx);
226
+ }
227
+ }
228
+
229
+ // 连续角度展开 (Angle Unwrap):避免在 ±PI 处发生 360° 线性插值回旋
230
+ if (i > 0) {
231
+ let diff = yaw - lastYaw;
232
+ while (diff > Math.PI) {
233
+ yaw -= 2 * Math.PI;
234
+ diff = yaw - lastYaw;
235
+ }
236
+ while (diff < -Math.PI) {
237
+ yaw += 2 * Math.PI;
238
+ diff = yaw - lastYaw;
239
+ }
240
+ }
241
+ lastYaw = yaw;
242
+
243
+ const pIdx = rowOffset + i * 4;
244
+ this._data[pIdx + 0] = scratchA[0];
245
+ this._data[pIdx + 1] = scratchA[1];
246
+ this._data[pIdx + 2] = scratchA[2];
247
+ this._data[pIdx + 3] = yaw;
248
+ }
249
+ }
250
+
251
+ if (this._texture) {
252
+ this._texture.needsUpdate = true;
253
+ }
254
+ }
255
+
256
+ private _getMaxPathId(): number {
257
+ let max = 0;
258
+ for (const id of this._paths.keys()) {
259
+ if (id > max) max = id;
260
+ }
261
+ return max;
262
+ }
263
+
264
+ /**
265
+ * 纯数学模拟采样烘焙纹理(用于单测验证硬件线性插值精度)
266
+ */
267
+ sampleBaked(pathId: number, t: number): { position: [number, number, number]; yaw: number } {
268
+ if (!this._data || !this._paths.has(pathId)) {
269
+ return { position: [0, 0, 0], yaw: 0 };
270
+ }
271
+ const maxId = this._getMaxPathId();
272
+ if (pathId < 0 || pathId > maxId) {
273
+ return { position: [0, 0, 0], yaw: 0 };
274
+ }
275
+
276
+ const N = SAMPLES_PER_PATH;
277
+ const clampedT = Math.min(1.0, Math.max(0.0, t));
278
+ const sampleCoord = clampedT * (N - 1);
279
+ const i0 = Math.floor(sampleCoord);
280
+ const i1 = Math.min(N - 1, i0 + 1);
281
+ const frac = sampleCoord - i0;
282
+
283
+ const rowOffset = pathId * N * 4;
284
+ const idx0 = rowOffset + i0 * 4;
285
+ const idx1 = rowOffset + i1 * 4;
286
+
287
+ const x = this._data[idx0 + 0] * (1 - frac) + this._data[idx1 + 0] * frac;
288
+ const y = this._data[idx0 + 1] * (1 - frac) + this._data[idx1 + 1] * frac;
289
+ const z = this._data[idx0 + 2] * (1 - frac) + this._data[idx1 + 2] * frac;
290
+ const yaw = this._data[idx0 + 3] * (1 - frac) + this._data[idx1 + 3] * frac;
291
+
292
+ return { position: [x, y, z], yaw };
293
+ }
294
+
295
+ /**
296
+ * 释放纹理与显存
297
+ */
298
+ dispose(): void {
299
+ if (this._texture && this._texture !== GPUPathManager.DUMMY_PATH_TEX) {
300
+ this._texture.dispose();
301
+ this._texture = null;
302
+ }
303
+ this._data = null;
304
+ this._paths.clear();
305
+ }
306
+ }
@@ -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
+ }