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.
Files changed (67) hide show
  1. package/dist/anim/Animator.d.ts +111 -0
  2. package/dist/anim/Animator.d.ts.map +1 -0
  3. package/dist/anim/Animator.js +382 -0
  4. package/dist/anim/Animator.js.map +1 -0
  5. package/dist/anim/Path.d.ts +30 -0
  6. package/dist/anim/Path.d.ts.map +1 -0
  7. package/dist/anim/Path.js +100 -0
  8. package/dist/anim/Path.js.map +1 -0
  9. package/dist/anim/easing.d.ts +70 -0
  10. package/dist/anim/easing.d.ts.map +1 -0
  11. package/dist/anim/easing.js +170 -0
  12. package/dist/anim/easing.js.map +1 -0
  13. package/dist/anim/index.d.ts +6 -0
  14. package/dist/anim/index.d.ts.map +1 -0
  15. package/dist/anim/index.js +4 -0
  16. package/dist/anim/index.js.map +1 -0
  17. package/dist/browser/s3m-three.iife.ext-three.js +218 -39
  18. package/dist/browser/s3m-three.iife.js +218 -39
  19. package/dist/core/types.d.ts +20 -0
  20. package/dist/core/types.d.ts.map +1 -1
  21. package/dist/index.d.ts +6 -2
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +3 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/render/EdgeLines.d.ts +68 -14
  26. package/dist/render/EdgeLines.d.ts.map +1 -1
  27. package/dist/render/EdgeLines.js +125 -8
  28. package/dist/render/EdgeLines.js.map +1 -1
  29. package/dist/render/FeatureStyleManager.d.ts +62 -1
  30. package/dist/render/FeatureStyleManager.d.ts.map +1 -1
  31. package/dist/render/FeatureStyleManager.js +331 -60
  32. package/dist/render/FeatureStyleManager.js.map +1 -1
  33. package/dist/render/MergeVertices.d.ts +7 -0
  34. package/dist/render/MergeVertices.d.ts.map +1 -1
  35. package/dist/render/MergeVertices.js +10 -1
  36. package/dist/render/MergeVertices.js.map +1 -1
  37. package/dist/render/OrthoProjection.d.ts +6 -0
  38. package/dist/render/OrthoProjection.d.ts.map +1 -1
  39. package/dist/render/OrthoProjection.js +48 -0
  40. package/dist/render/OrthoProjection.js.map +1 -1
  41. package/dist/render/S3MViewer.d.ts +235 -9
  42. package/dist/render/S3MViewer.d.ts.map +1 -1
  43. package/dist/render/S3MViewer.js +880 -68
  44. package/dist/render/S3MViewer.js.map +1 -1
  45. package/dist/render/StyleMaterial.d.ts +28 -0
  46. package/dist/render/StyleMaterial.d.ts.map +1 -1
  47. package/dist/render/StyleMaterial.js +190 -21
  48. package/dist/render/StyleMaterial.js.map +1 -1
  49. package/dist/tileset/S3MTileset.d.ts.map +1 -1
  50. package/dist/tileset/S3MTileset.js +3 -1
  51. package/dist/tileset/S3MTileset.js.map +1 -1
  52. package/docs/ANIMATION.md +534 -0
  53. package/docs/API.md +2 -0
  54. package/package.json +1 -1
  55. package/src/anim/Animator.ts +460 -0
  56. package/src/anim/Path.ts +107 -0
  57. package/src/anim/easing.ts +195 -0
  58. package/src/anim/index.ts +10 -0
  59. package/src/core/types.ts +10 -0
  60. package/src/index.ts +16 -0
  61. package/src/render/EdgeLines.ts +161 -15
  62. package/src/render/FeatureStyleManager.ts +414 -65
  63. package/src/render/MergeVertices.ts +11 -1
  64. package/src/render/OrthoProjection.ts +52 -0
  65. package/src/render/S3MViewer.ts +1012 -64
  66. package/src/render/StyleMaterial.ts +211 -22
  67. package/src/tileset/S3MTileset.ts +3 -1
@@ -0,0 +1,195 @@
1
+ /**
2
+ * 缓动曲线库
3
+ *
4
+ * 纯函数,不依赖 three / DOM,可以单独在 Worker 或 Node 里跑。
5
+ *
6
+ * 三类:
7
+ * 1. 命名曲线 —— CSS 那套 easeIn/Out/InOut × Quad…Bounce,覆盖 90% 场景
8
+ * 2. cubicBezier(x1,y1,x2,y2) —— 与 CSS `cubic-bezier()` 数值对齐,用来做产品级调校
9
+ * 3. steps(n, position) —— 离散跳变,做逐帧动画或「咔哒」感
10
+ *
11
+ * spring 不在这里:它没有「时长」概念,靠物理收敛,所以实现为 Animator 里的一种
12
+ * Track 类型,而不是 (t) => number 的曲线。
13
+ */
14
+
15
+ export type EasingFn = (t: number) => number;
16
+
17
+ /* ------------------------------------------------------------------ 基础 */
18
+
19
+ const c1 = 1.70158;
20
+ const c2 = c1 * 1.525;
21
+ const c3 = c1 + 1;
22
+ const c4 = (2 * Math.PI) / 3;
23
+ const c5 = (2 * Math.PI) / 4.5;
24
+ const n1 = 7.5625;
25
+ const d1 = 2.75;
26
+
27
+ function outBounce(t: number): number {
28
+ if (t < 1 / d1) return n1 * t * t;
29
+ if (t < 2 / d1) {
30
+ const x = t - 1.5 / d1;
31
+ return n1 * x * x + 0.75;
32
+ }
33
+ if (t < 2.5 / d1) {
34
+ const x = t - 2.25 / d1;
35
+ return n1 * x * x + 0.9375;
36
+ }
37
+ const x = t - 2.625 / d1;
38
+ return n1 * x * x + 0.984375;
39
+ }
40
+
41
+ function inBounce(t: number): number {
42
+ return 1 - outBounce(1 - t);
43
+ }
44
+
45
+ /* ------------------------------------------------------------------ 曲线表 */
46
+
47
+ const CURVES = {
48
+ linear: (t) => t,
49
+
50
+ easeInQuad: (t) => t * t,
51
+ easeOutQuad: (t) => 1 - (1 - t) * (1 - t),
52
+ easeInOutQuad: (t) => (t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2),
53
+
54
+ easeInCubic: (t) => t * t * t,
55
+ easeOutCubic: (t) => 1 - Math.pow(1 - t, 3),
56
+ easeInOutCubic: (t) => (t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2),
57
+
58
+ easeInQuart: (t) => t * t * t * t,
59
+ easeOutQuart: (t) => 1 - Math.pow(1 - t, 4),
60
+ easeInOutQuart: (t) => (t < 0.5 ? 8 * t * t * t * t : 1 - Math.pow(-2 * t + 2, 4) / 2),
61
+
62
+ easeInQuint: (t) => t * t * t * t * t,
63
+ easeOutQuint: (t) => 1 - Math.pow(1 - t, 5),
64
+ easeInOutQuint: (t) => (t < 0.5 ? 16 * t * t * t * t * t : 1 - Math.pow(-2 * t + 2, 5) / 2),
65
+
66
+ easeInSine: (t) => 1 - Math.cos((t * Math.PI) / 2),
67
+ easeOutSine: (t) => Math.sin((t * Math.PI) / 2),
68
+ easeInOutSine: (t) => -(Math.cos(Math.PI * t) - 1) / 2,
69
+
70
+ easeInExpo: (t) => (t === 0 ? 0 : Math.pow(2, 10 * t - 10)),
71
+ easeOutExpo: (t) => (t === 1 ? 1 : 1 - Math.pow(2, -10 * t)),
72
+ easeInOutExpo: (t) =>
73
+ t === 0 ? 0 : t === 1 ? 1 : t < 0.5 ? Math.pow(2, 20 * t - 10) / 2 : (2 - Math.pow(2, -20 * t + 10)) / 2,
74
+
75
+ easeInCirc: (t) => 1 - Math.sqrt(1 - t * t),
76
+ easeOutCirc: (t) => Math.sqrt(1 - Math.pow(t - 1, 2)),
77
+ easeInOutCirc: (t) =>
78
+ t < 0.5
79
+ ? (1 - Math.sqrt(1 - Math.pow(2 * t, 2))) / 2
80
+ : (Math.sqrt(1 - Math.pow(-2 * t + 2, 2)) + 1) / 2,
81
+
82
+ easeInBack: (t) => c3 * t * t * t - c1 * t * t,
83
+ easeOutBack: (t) => 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2),
84
+ easeInOutBack: (t) =>
85
+ t < 0.5
86
+ ? (Math.pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
87
+ : (Math.pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2,
88
+
89
+ easeInElastic: (t) =>
90
+ t === 0 ? 0 : t === 1 ? 1 : -Math.pow(2, 10 * t - 10) * Math.sin((t * 10 - 10.75) * c4),
91
+ easeOutElastic: (t) =>
92
+ t === 0 ? 0 : t === 1 ? 1 : Math.pow(2, -10 * t) * Math.sin((t * 10 - 0.75) * c4) + 1,
93
+ easeInOutElastic: (t) =>
94
+ t === 0
95
+ ? 0
96
+ : t === 1
97
+ ? 1
98
+ : t < 0.5
99
+ ? -(Math.pow(2, 20 * t - 10) * Math.sin((20 * t - 11.125) * c5)) / 2
100
+ : (Math.pow(2, -20 * t + 10) * Math.sin((20 * t - 11.125) * c5)) / 2 + 1,
101
+
102
+ easeInBounce: inBounce,
103
+ easeOutBounce: outBounce,
104
+ easeInOutBounce: (t) => (t < 0.5 ? (1 - inBounce(1 - 2 * t)) / 2 : (1 + inBounce(2 * t - 1)) / 2),
105
+ } satisfies Record<string, EasingFn>;
106
+
107
+ /** 全部内置曲线名(顺序稳定,方便做下拉框 / 遍历自检) */
108
+ export const EASING_NAMES = Object.keys(CURVES) as EasingName[];
109
+
110
+ export type EasingName = keyof typeof CURVES;
111
+
112
+ /** 按名字取曲线;名字不存在直接抛错,不做静默降级(拼错会变成「没动画」,很难查) */
113
+ export function easing(name: EasingName | EasingFn): EasingFn {
114
+ if (typeof name === 'function') return name;
115
+ const fn = (CURVES as Record<string, EasingFn>)[name];
116
+ if (!fn) throw new Error(`[s3m] 未知的缓动曲线: ${String(name)}`);
117
+ return fn;
118
+ }
119
+
120
+ /* ------------------------------------------------------------------ 贝塞尔 */
121
+
122
+ const NEWTON_ITERATIONS = 8;
123
+ const NEWTON_MIN_SLOPE = 0.001;
124
+ const SUBDIVISION_PRECISION = 1e-7;
125
+ const SUBDIVISION_MAX_ITERATIONS = 12;
126
+
127
+ const bezA = (a1: number, a2: number): number => 1 - 3 * a2 + 3 * a1;
128
+ const bezB = (a1: number, a2: number): number => 3 * a2 - 6 * a1;
129
+ const bezC = (a1: number): number => 3 * a1;
130
+
131
+ /** 三次贝塞尔在参数 t 处的值(x 或 y 用同一组公式,只是控制点不同) */
132
+ function bezierAt(t: number, a1: number, a2: number): number {
133
+ return ((bezA(a1, a2) * t + bezB(a1, a2)) * t + bezC(a1)) * t;
134
+ }
135
+
136
+ function bezierSlope(t: number, a1: number, a2: number): number {
137
+ return 3 * bezA(a1, a2) * t * t + 2 * bezB(a1, a2) * t + bezC(a1);
138
+ }
139
+
140
+ /**
141
+ * CSS `cubic-bezier(x1, y1, x2, y2)` 的等价实现。
142
+ *
143
+ * 与浏览器一致:先解 `x(t) = 输入进度` 得到参数 t,再取 `y(t)`。
144
+ * 用牛顿迭代,斜率过小时退化为二分——退化控制点(比如 (0,0,1,1))也稳定。
145
+ */
146
+ export function cubicBezier(x1: number, y1: number, x2: number, y2: number): EasingFn {
147
+ // 线性特例:控制点落在对角线上时不必迭代
148
+ if (x1 === y1 && x2 === y2) return (t) => t;
149
+
150
+ const solveT = (x: number): number => {
151
+ let t = x;
152
+ for (let i = 0; i < NEWTON_ITERATIONS; i++) {
153
+ const slope = bezierSlope(t, x1, x2);
154
+ if (Math.abs(slope) < NEWTON_MIN_SLOPE) break;
155
+ const err = bezierAt(t, x1, x2) - x;
156
+ if (Math.abs(err) < SUBDIVISION_PRECISION) return t;
157
+ t -= err / slope;
158
+ }
159
+ // 牛顿不收敛:二分兜底
160
+ let lo = 0;
161
+ let hi = 1;
162
+ t = x;
163
+ for (let i = 0; i < SUBDIVISION_MAX_ITERATIONS; i++) {
164
+ const v = bezierAt(t, x1, x2);
165
+ if (Math.abs(v - x) < SUBDIVISION_PRECISION) return t;
166
+ if (v < x) lo = t;
167
+ else hi = t;
168
+ t = (lo + hi) / 2;
169
+ }
170
+ return t;
171
+ };
172
+
173
+ return (t: number): number => {
174
+ if (t <= 0) return 0;
175
+ if (t >= 1) return 1;
176
+ return bezierAt(solveT(t), y1, y2);
177
+ };
178
+ }
179
+
180
+ /* ------------------------------------------------------------------ 离散 */
181
+
182
+ /**
183
+ * 阶梯曲线。
184
+ *
185
+ * @param count 档数
186
+ * @param position 'end' 表示「走完一档才跳」,'start' 表示「一进档就跳」
187
+ */
188
+ export function steps(count: number, position: 'start' | 'end' = 'end'): EasingFn {
189
+ const n = Math.max(1, Math.floor(count));
190
+ return (t: number): number => {
191
+ const clamped = Math.min(1, Math.max(0, t));
192
+ if (position === 'start') return Math.min(1, (Math.floor(clamped * n) + 1) / n);
193
+ return Math.min(1, Math.floor(clamped * n) / n);
194
+ };
195
+ }
@@ -0,0 +1,10 @@
1
+ export { Animator } from './Animator.js';
2
+ export type { Channel, SpringSpec, TransitionOptions } from './Animator.js';
3
+ export { Path } from './Path.js';
4
+ export {
5
+ easing,
6
+ cubicBezier,
7
+ steps,
8
+ EASING_NAMES,
9
+ } from './easing.js';
10
+ export type { EasingFn, EasingName } from './easing.js';
package/src/core/types.ts CHANGED
@@ -281,5 +281,15 @@ export interface StyleUniforms {
281
281
  uFilterHeightActive?: { value: number };
282
282
  uRoofThreshold?: { value: number };
283
283
  uTexMix?: { value: number };
284
+ /** 基础漫反射底色增益 / 直通自发光微调(0~1,默认 0.0) */
285
+ uAmbientGain?: { value: number };
286
+ /** 双纹理交叉淡化插值因子(0~1,0 = 状态 A,1 = 状态 B) */
287
+ uBatchMix?: { value: number };
288
+ /** 图层整体透明度(共享 uniform,透明度动画靠它做到 O(1)) */
289
+ uOpacity?: { value: number };
290
+ /** 棱线淡入淡出乘子(setEdges 过渡期间 0→1) */
291
+ uEdgeFade?: { value: number };
292
+ /** 整体压暗乘子(mode 切换的 fade-through) */
293
+ uFade?: { value: number };
284
294
  [key: string]: { value: unknown } | undefined;
285
295
  }
package/src/index.ts CHANGED
@@ -78,6 +78,11 @@ export {
78
78
  DatasetManager,
79
79
  FeatureStyleManager,
80
80
  } from './render/S3MViewer.js';
81
+ export {
82
+ resolveRotationMatrix,
83
+ extractEulerAngles,
84
+ } from './render/FeatureStyleManager.js';
85
+ export type { RotationMatrix9 } from './render/FeatureStyleManager.js';
81
86
  export type {
82
87
  DatasetAlignment,
83
88
  S3MDatasetOptions,
@@ -96,6 +101,7 @@ export type {
96
101
  GroundMode,
97
102
  GroundOptions,
98
103
  LightOptions,
104
+ PathFollowOptions,
99
105
  } from './render/S3MViewer.js';
100
106
 
101
107
  export { buildTileObjects } from './render/GeometryBuilder.js';
@@ -117,8 +123,18 @@ export type { StyleMode, SelectMode, StyledMaterial, StyledMaterialOptions, Outl
117
123
  export { Picker } from './render/Picker.js';
118
124
  export type { PickHit } from './render/Picker.js';
119
125
 
126
+ export { Animator, Path, easing, cubicBezier, steps, EASING_NAMES } from './anim/index.js';
127
+ export type {
128
+ Channel,
129
+ SpringSpec,
130
+ TransitionOptions,
131
+ EasingFn,
132
+ EasingName,
133
+ } from './anim/index.js';
134
+
120
135
  export {
121
136
  buildOrthoMVP,
137
+ buildBlendedMVP,
122
138
  extractProjectionMatrix,
123
139
  extractNearFar,
124
140
  extractFovRadians,
@@ -13,6 +13,7 @@
13
13
  * B 端算出来的法线正好和 A 端相反,反号一次才能拼成同一个四边形。
14
14
  */
15
15
  import * as THREE from 'three';
16
+ import { DUMMY_BATCH_TEX } from './StyleMaterial.js';
16
17
 
17
18
  /** 所有等宽线材质共享的分辨率 uniform(每帧更新一次即可) */
18
19
  export interface LineSharedUniforms {
@@ -20,14 +21,36 @@ export interface LineSharedUniforms {
20
21
  uDpr: { value: number };
21
22
  }
22
23
 
24
+ /**
25
+ * 等宽线的「外观」uniform(颜色 / 粗细 / 透明度 / 拔高)。
26
+ *
27
+ * 棱线材质是**逐网格一份**的(见 createThickLineMaterial 的 batchUniforms 参数),
28
+ * 但外观必须全局统一——所以这些 `{ value }` 对象由调用方持有并共享给每一份材质,
29
+ * 改一次全场景生效,棱线淡入淡出才能保持 O(1)。
30
+ */
31
+ export interface ThickLineStyleUniforms {
32
+ uColor: { value: THREE.Color };
33
+ uWidth: { value: number };
34
+ uOpacity: { value: number };
35
+ uElevation?: { value: number };
36
+ }
37
+
38
+ /** 等宽线的「单体状态」uniform:直接复用主材质 userData 里那几个对象,自动跟随 */
39
+ export interface ThickLineBatchUniforms {
40
+ uBatchTex: { value: THREE.Texture };
41
+ uBatchTex2?: { value: THREE.Texture };
42
+ uBatchMix?: { value: number };
43
+ uBatchTexWidth: { value: number };
44
+ uBatchTexActive: { value: number };
45
+ }
46
+
23
47
  export interface ThickLineMaterial extends THREE.ShaderMaterial {
24
- uniforms: LineSharedUniforms & {
25
- uColor: { value: THREE.Color };
26
- uOpacity: { value: number };
27
- uWidth: { value: number };
28
- uElevation: { value: number };
29
- [uniform: string]: THREE.IUniform;
30
- };
48
+ uniforms: LineSharedUniforms &
49
+ ThickLineStyleUniforms &
50
+ Required<ThickLineBatchUniforms> & {
51
+ uTranslation: { value: THREE.Vector3 };
52
+ [uniform: string]: THREE.IUniform;
53
+ };
31
54
  }
32
55
 
33
56
  export function createLineSharedUniforms(): LineSharedUniforms {
@@ -40,18 +63,66 @@ export function createLineSharedUniforms(): LineSharedUniforms {
40
63
  const VERT = /* glsl */ `
41
64
  attribute vec3 aOther;
42
65
  attribute float aSide;
66
+ attribute float aBatchId;
43
67
 
44
68
  uniform vec2 uResolution;
45
69
  uniform float uDpr;
46
70
  uniform float uWidth;
47
71
  uniform float uElevation;
48
72
  uniform vec3 uTranslation;
73
+ uniform sampler2D uBatchTex;
74
+ uniform sampler2D uBatchTex2;
75
+ uniform float uBatchMix;
76
+ uniform float uBatchTexWidth;
77
+ uniform int uBatchTexActive;
78
+
79
+ vec4 s3mSampleBatch( sampler2D texA, sampler2D texB, vec2 uv, float mixVal ) {
80
+ vec4 vA = texture2D( texA, uv );
81
+ if ( mixVal <= 0.0001 ) return vA;
82
+ vec4 vB = texture2D( texB, uv );
83
+ return mix( vA, vB, mixVal );
84
+ }
49
85
 
50
86
  void main() {
87
+ // 单体平移 / 旋转 / 拔高 / 隐藏:与主着色器同一份 5 行状态纹理、同一套变换口径。
88
+ // 不采样的话,单体一动或一转棱线就留在原地——这正是「棱线跟随」要解决的。
89
+ vec3 s3mA = position;
90
+ vec3 s3mB = aOther;
91
+ vec3 s3mCustomOffset = vec3( 0.0 );
92
+ mat3 s3mRot = mat3( 1.0 );
93
+ vec3 s3mPivot = vec3( 0.0 );
94
+
95
+ if ( uBatchTexActive > 0 && aBatchId >= 0.0 ) {
96
+ float s3mU = ( aBatchId + 0.5 ) / uBatchTexWidth;
97
+ vec4 bState = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.1 ), uBatchMix );
98
+ if ( bState.a < -0.5 ) {
99
+ // 该单体被隐藏:顶点着色器不能 discard,扔到裁剪空间外
100
+ gl_Position = vec4( 2.0, 2.0, 2.0, 1.0 );
101
+ return;
102
+ }
103
+ vec4 bOffset = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.3 ), uBatchMix );
104
+ s3mCustomOffset = bOffset.xyz;
105
+ if ( s3mCustomOffset.z == 0.0 && bState.a > 0.0 ) {
106
+ s3mCustomOffset.z = bState.a;
107
+ }
108
+ vec4 r2 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.5 ), uBatchMix );
109
+ vec4 r3 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.7 ), uBatchMix );
110
+ vec4 r4 = s3mSampleBatch( uBatchTex, uBatchTex2, vec2( s3mU, 0.9 ), uBatchMix );
111
+ s3mRot = mat3(
112
+ vec3( r2.x, r3.x, r4.x ),
113
+ vec3( r2.y, r3.y, r4.y ),
114
+ vec3( r2.z, r3.z, r4.z )
115
+ );
116
+ s3mPivot = vec3( r2.w, r3.w, r4.w );
117
+ }
118
+
119
+ s3mA = s3mRot * ( s3mA - s3mPivot ) + s3mPivot + s3mCustomOffset;
120
+ s3mB = s3mRot * ( s3mB - s3mPivot ) + s3mPivot + s3mCustomOffset;
121
+
51
122
  // 选中单体平移/拔高:与着色器里 transformed += uTranslation 保持同一口径(物体空间)
52
123
  vec3 offset = uTranslation + vec3( 0.0, 0.0, uElevation );
53
- vec4 clipA = projectionMatrix * modelViewMatrix * vec4( position + offset, 1.0 );
54
- vec4 clipB = projectionMatrix * modelViewMatrix * vec4( aOther + offset, 1.0 );
124
+ vec4 clipA = projectionMatrix * modelViewMatrix * vec4( s3mA + offset, 1.0 );
125
+ vec4 clipB = projectionMatrix * modelViewMatrix * vec4( s3mB + offset, 1.0 );
55
126
 
56
127
  if ( clipA.w <= 0.0 || clipB.w <= 0.0 ) {
57
128
  // 端点跑到相机后面,整段丢弃(裁到近平面的线段会有轻微截断,可接受)
@@ -87,19 +158,54 @@ const FRAG = /* glsl */ `
87
158
  }
88
159
  `;
89
160
 
161
+ /**
162
+ * 创建一份等宽线材质。
163
+ *
164
+ * @param styleUniforms 外观 uniform(颜色 / 粗细 / 透明度 / 拔高)。传入时**直接
165
+ * 复用这些 `{ value }` 对象**而不是新建——棱线材质逐网格一份,但外观必须全局统一,
166
+ * 共享对象才能让 `_syncEdgeStyle()` 的一次赋值作用到所有棱线上。
167
+ * @param batchUniforms 单体状态纹理 uniform。传入时同样直接复用主材质 userData 里
168
+ * 的那几个对象,主材质换纹理 / 开关 active 时棱线自动跟着走,零同步成本。
169
+ * 不传(或几何没有 aBatchId)时棱线不参与单体状态。
170
+ */
90
171
  export function createThickLineMaterial(
91
172
  shared: LineSharedUniforms,
92
- opts: { color?: THREE.ColorRepresentation; opacity?: number; width?: number; elevation?: number } = {}
173
+ opts: { color?: THREE.ColorRepresentation; opacity?: number; width?: number; elevation?: number } = {},
174
+ styleUniforms?: ThickLineStyleUniforms,
175
+ batchUniforms?: ThickLineBatchUniforms
93
176
  ): ThickLineMaterial {
177
+ const style: ThickLineStyleUniforms = styleUniforms ?? {
178
+ uColor: { value: new THREE.Color(opts.color ?? '#ffffff') },
179
+ uOpacity: { value: opts.opacity ?? 1 },
180
+ uWidth: { value: opts.width ?? 1.5 },
181
+ uElevation: { value: opts.elevation ?? 0 },
182
+ };
183
+ if (opts.color !== undefined && styleUniforms) style.uColor.value.set(opts.color);
184
+ if (opts.opacity !== undefined && styleUniforms) style.uOpacity.value = opts.opacity;
185
+ if (opts.width !== undefined && styleUniforms) style.uWidth.value = opts.width;
186
+
187
+ const batch: Required<ThickLineBatchUniforms> = {
188
+ uBatchTex: batchUniforms?.uBatchTex ?? { value: DUMMY_BATCH_TEX },
189
+ uBatchTex2: batchUniforms?.uBatchTex2 ?? { value: DUMMY_BATCH_TEX },
190
+ uBatchMix: batchUniforms?.uBatchMix ?? { value: 0 },
191
+ uBatchTexWidth: batchUniforms?.uBatchTexWidth ?? { value: 1 },
192
+ uBatchTexActive: batchUniforms?.uBatchTexActive ?? { value: 0 },
193
+ };
194
+
94
195
  const mat = new THREE.ShaderMaterial({
95
196
  uniforms: {
96
197
  uResolution: shared.uResolution,
97
198
  uDpr: shared.uDpr,
98
- uColor: { value: new THREE.Color(opts.color ?? '#ffffff') },
99
- uOpacity: { value: opts.opacity ?? 1 },
100
- uWidth: { value: opts.width ?? 1.5 },
101
- uElevation: { value: opts.elevation ?? 0 },
199
+ uColor: style.uColor,
200
+ uOpacity: style.uOpacity,
201
+ uWidth: style.uWidth,
202
+ uElevation: style.uElevation ?? { value: opts.elevation ?? 0 },
102
203
  uTranslation: { value: new THREE.Vector3(0, 0, 0) },
204
+ uBatchTex: batch.uBatchTex,
205
+ uBatchTex2: batch.uBatchTex2,
206
+ uBatchMix: batch.uBatchMix,
207
+ uBatchTexWidth: batch.uBatchTexWidth,
208
+ uBatchTexActive: batch.uBatchTexActive,
103
209
  },
104
210
  vertexShader: VERT,
105
211
  fragmentShader: FRAG,
@@ -117,14 +223,22 @@ export function createThickLineMaterial(
117
223
 
118
224
  /**
119
225
  * 线段对(每 6 个 float:x1 y1 z1 x2 y2 z2)→ 可绘制等宽线的几何体。
226
+ *
227
+ * @param batchIds 每段所属的单体 batchId(长度 = 段数)。给了它,顶点着色器才能
228
+ * 按单体从状态纹理取平移/拔高——棱线跟随移动靠的就是这个。不传全部填 -1,
229
+ * 着色器会跳过采样(等于不参与单体状态)。
120
230
  */
121
- export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.BufferGeometry {
231
+ export function buildThickLineGeometry(
232
+ segments: ArrayLike<number>,
233
+ batchIds?: ArrayLike<number>
234
+ ): THREE.BufferGeometry {
122
235
  const count = Math.floor(segments.length / 6);
123
236
  const vertexCount = count * 4;
124
237
 
125
238
  const position = new Float32Array(vertexCount * 3);
126
239
  const other = new Float32Array(vertexCount * 3);
127
240
  const side = new Float32Array(vertexCount);
241
+ const batch = new Float32Array(vertexCount);
128
242
  const index = vertexCount > 65535 ? new Uint32Array(count * 6) : new Uint16Array(count * 6);
129
243
 
130
244
  for (let i = 0; i < count; i++) {
@@ -135,6 +249,7 @@ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.Buffe
135
249
  const bx = segments[s + 3];
136
250
  const by = segments[s + 4];
137
251
  const bz = segments[s + 5];
252
+ const b = batchIds ? Number(batchIds[i] ?? -1) : -1;
138
253
 
139
254
  const v = i * 4;
140
255
  // A 端两枚
@@ -143,6 +258,7 @@ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.Buffe
143
258
  // B 端两枚(side 反号)
144
259
  setVertex(v + 2, bx, by, bz, ax, ay, az, 1);
145
260
  setVertex(v + 3, bx, by, bz, ax, ay, az, -1);
261
+ for (let k = 0; k < 4; k++) batch[v + k] = b;
146
262
 
147
263
  const o = i * 6;
148
264
  index[o] = v;
@@ -177,11 +293,41 @@ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.Buffe
177
293
  geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
178
294
  geometry.setAttribute('aOther', new THREE.BufferAttribute(other, 3));
179
295
  geometry.setAttribute('aSide', new THREE.BufferAttribute(side, 1));
296
+ geometry.setAttribute('aBatchId', new THREE.BufferAttribute(batch, 1));
180
297
  geometry.setIndex(new THREE.BufferAttribute(index, 1));
181
298
  geometry.computeBoundingSphere();
182
299
  return geometry;
183
300
  }
184
301
 
302
+ /**
303
+ * 从带 `aBatchId` 的几何里建「量化位置 → batchId」的查找表。
304
+ *
305
+ * `EdgesGeometry` 输出的线段只有坐标、没有顶点号,要知道每段棱线属于哪个单体,
306
+ * 只能拿端点坐标回来查。key 的量化方式必须和 `mergeVerticesByPosition` 完全一致,
307
+ * 否则焊过的顶点会对不上。返回 null 表示这个几何没有单体化信息。
308
+ */
309
+ export function buildPositionBatchMap(
310
+ geometry: THREE.BufferGeometry,
311
+ tolerance: number
312
+ ): Map<string, number> | null {
313
+ const pos = geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
314
+ const bid = geometry.getAttribute('aBatchId') as THREE.BufferAttribute | undefined;
315
+ if (!pos || !bid || pos.count === 0 || pos.count !== bid.count) return null;
316
+
317
+ const inv = 1 / Math.max(tolerance, 1e-9);
318
+ const map = new Map<string, number>();
319
+ for (let i = 0; i < pos.count; i++) {
320
+ const key = positionQuantKey(pos.getX(i), pos.getY(i), pos.getZ(i), inv);
321
+ if (!map.has(key)) map.set(key, bid.getX(i));
322
+ }
323
+ return map.size > 0 ? map : null;
324
+ }
325
+
326
+ /** 位置量化 key:与 mergeVerticesByPosition 同一套网格,保证焊过的顶点对得上 */
327
+ export function positionQuantKey(x: number, y: number, z: number, inv: number): string {
328
+ return `${Math.round(x * inv)},${Math.round(y * inv)},${Math.round(z * inv)}`;
329
+ }
330
+
185
331
  /* ------------------------------------------------------------------ 单体棱线提取 */
186
332
 
187
333
  export interface EdgeVertexRange {