s3m-three 0.3.7 → 0.3.8
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 +48 -10
- package/dist/browser/s3m-three.iife.js +48 -10
- package/dist/core/types.d.ts +16 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/render/EdgeLines.d.ts +62 -14
- package/dist/render/EdgeLines.d.ts.map +1 -1
- package/dist/render/EdgeLines.js +97 -8
- package/dist/render/EdgeLines.js.map +1 -1
- package/dist/render/FeatureStyleManager.d.ts +17 -1
- package/dist/render/FeatureStyleManager.d.ts.map +1 -1
- package/dist/render/FeatureStyleManager.js +14 -4
- 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/S3MViewer.d.ts +216 -9
- package/dist/render/S3MViewer.d.ts.map +1 -1
- package/dist/render/S3MViewer.js +763 -67
- package/dist/render/S3MViewer.js.map +1 -1
- package/dist/render/StyleMaterial.d.ts +19 -0
- package/dist/render/StyleMaterial.d.ts.map +1 -1
- package/dist/render/StyleMaterial.js +37 -0
- 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 +8 -0
- package/src/index.ts +10 -0
- package/src/render/EdgeLines.ts +131 -15
- package/src/render/FeatureStyleManager.ts +12 -4
- package/src/render/MergeVertices.ts +11 -1
- package/src/render/S3MViewer.ts +855 -64
- package/src/render/StyleMaterial.ts +54 -1
- 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,13 @@ 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
|
+
/** 图层整体透明度(共享 uniform,透明度动画靠它做到 O(1)) */
|
|
287
|
+
uOpacity?: { value: number };
|
|
288
|
+
/** 棱线淡入淡出乘子(setEdges 过渡期间 0→1) */
|
|
289
|
+
uEdgeFade?: { value: number };
|
|
290
|
+
/** 整体压暗乘子(mode 切换的 fade-through) */
|
|
291
|
+
uFade?: { value: number };
|
|
284
292
|
[key: string]: { value: unknown } | undefined;
|
|
285
293
|
}
|
package/src/index.ts
CHANGED
|
@@ -96,6 +96,7 @@ export type {
|
|
|
96
96
|
GroundMode,
|
|
97
97
|
GroundOptions,
|
|
98
98
|
LightOptions,
|
|
99
|
+
PathFollowOptions,
|
|
99
100
|
} from './render/S3MViewer.js';
|
|
100
101
|
|
|
101
102
|
export { buildTileObjects } from './render/GeometryBuilder.js';
|
|
@@ -117,6 +118,15 @@ export type { StyleMode, SelectMode, StyledMaterial, StyledMaterialOptions, Outl
|
|
|
117
118
|
export { Picker } from './render/Picker.js';
|
|
118
119
|
export type { PickHit } from './render/Picker.js';
|
|
119
120
|
|
|
121
|
+
export { Animator, Path, easing, cubicBezier, steps, EASING_NAMES } from './anim/index.js';
|
|
122
|
+
export type {
|
|
123
|
+
Channel,
|
|
124
|
+
SpringSpec,
|
|
125
|
+
TransitionOptions,
|
|
126
|
+
EasingFn,
|
|
127
|
+
EasingName,
|
|
128
|
+
} from './anim/index.js';
|
|
129
|
+
|
|
120
130
|
export {
|
|
121
131
|
buildOrthoMVP,
|
|
122
132
|
extractProjectionMatrix,
|
package/src/render/EdgeLines.ts
CHANGED
|
@@ -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,34 @@ 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
|
+
uBatchTexWidth: { value: number };
|
|
42
|
+
uBatchTexActive: { value: number };
|
|
43
|
+
}
|
|
44
|
+
|
|
23
45
|
export interface ThickLineMaterial extends THREE.ShaderMaterial {
|
|
24
|
-
uniforms: LineSharedUniforms &
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
};
|
|
46
|
+
uniforms: LineSharedUniforms &
|
|
47
|
+
ThickLineStyleUniforms &
|
|
48
|
+
Required<ThickLineBatchUniforms> & {
|
|
49
|
+
uTranslation: { value: THREE.Vector3 };
|
|
50
|
+
[uniform: string]: THREE.IUniform;
|
|
51
|
+
};
|
|
31
52
|
}
|
|
32
53
|
|
|
33
54
|
export function createLineSharedUniforms(): LineSharedUniforms {
|
|
@@ -40,18 +61,42 @@ export function createLineSharedUniforms(): LineSharedUniforms {
|
|
|
40
61
|
const VERT = /* glsl */ `
|
|
41
62
|
attribute vec3 aOther;
|
|
42
63
|
attribute float aSide;
|
|
64
|
+
attribute float aBatchId;
|
|
43
65
|
|
|
44
66
|
uniform vec2 uResolution;
|
|
45
67
|
uniform float uDpr;
|
|
46
68
|
uniform float uWidth;
|
|
47
69
|
uniform float uElevation;
|
|
48
70
|
uniform vec3 uTranslation;
|
|
71
|
+
uniform sampler2D uBatchTex;
|
|
72
|
+
uniform float uBatchTexWidth;
|
|
73
|
+
uniform int uBatchTexActive;
|
|
49
74
|
|
|
50
75
|
void main() {
|
|
76
|
+
// 单体平移 / 拔高 / 隐藏:与主着色器同一份状态纹理、同一套口径。
|
|
77
|
+
// 不采样的话,单体一动棱线就留在原地——这正是「棱线跟随」要解决的。
|
|
78
|
+
vec3 s3mA = position;
|
|
79
|
+
vec3 s3mB = aOther;
|
|
80
|
+
if ( uBatchTexActive > 0 && aBatchId >= 0.0 ) {
|
|
81
|
+
vec4 bState = texture2D( uBatchTex, vec2( ( aBatchId + 0.5 ) / uBatchTexWidth, 0.25 ) );
|
|
82
|
+
if ( bState.a < -0.5 ) {
|
|
83
|
+
// 该单体被隐藏:顶点着色器不能 discard,扔到裁剪空间外
|
|
84
|
+
gl_Position = vec4( 2.0, 2.0, 2.0, 1.0 );
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
vec4 bOffset = texture2D( uBatchTex, vec2( ( aBatchId + 0.5 ) / uBatchTexWidth, 0.75 ) );
|
|
88
|
+
vec3 s3mOffset = bOffset.xyz;
|
|
89
|
+
if ( s3mOffset.z == 0.0 && bState.a > 0.0 ) {
|
|
90
|
+
s3mOffset.z = bState.a;
|
|
91
|
+
}
|
|
92
|
+
s3mA += s3mOffset;
|
|
93
|
+
s3mB += s3mOffset;
|
|
94
|
+
}
|
|
95
|
+
|
|
51
96
|
// 选中单体平移/拔高:与着色器里 transformed += uTranslation 保持同一口径(物体空间)
|
|
52
97
|
vec3 offset = uTranslation + vec3( 0.0, 0.0, uElevation );
|
|
53
|
-
vec4 clipA = projectionMatrix * modelViewMatrix * vec4(
|
|
54
|
-
vec4 clipB = projectionMatrix * modelViewMatrix * vec4(
|
|
98
|
+
vec4 clipA = projectionMatrix * modelViewMatrix * vec4( s3mA + offset, 1.0 );
|
|
99
|
+
vec4 clipB = projectionMatrix * modelViewMatrix * vec4( s3mB + offset, 1.0 );
|
|
55
100
|
|
|
56
101
|
if ( clipA.w <= 0.0 || clipB.w <= 0.0 ) {
|
|
57
102
|
// 端点跑到相机后面,整段丢弃(裁到近平面的线段会有轻微截断,可接受)
|
|
@@ -87,19 +132,50 @@ const FRAG = /* glsl */ `
|
|
|
87
132
|
}
|
|
88
133
|
`;
|
|
89
134
|
|
|
135
|
+
/**
|
|
136
|
+
* 创建一份等宽线材质。
|
|
137
|
+
*
|
|
138
|
+
* @param styleUniforms 外观 uniform(颜色 / 粗细 / 透明度 / 拔高)。传入时**直接
|
|
139
|
+
* 复用这些 `{ value }` 对象**而不是新建——棱线材质逐网格一份,但外观必须全局统一,
|
|
140
|
+
* 共享对象才能让 `_syncEdgeStyle()` 的一次赋值作用到所有棱线上。
|
|
141
|
+
* @param batchUniforms 单体状态纹理 uniform。传入时同样直接复用主材质 userData 里
|
|
142
|
+
* 的那几个对象,主材质换纹理 / 开关 active 时棱线自动跟着走,零同步成本。
|
|
143
|
+
* 不传(或几何没有 aBatchId)时棱线不参与单体状态。
|
|
144
|
+
*/
|
|
90
145
|
export function createThickLineMaterial(
|
|
91
146
|
shared: LineSharedUniforms,
|
|
92
|
-
opts: { color?: THREE.ColorRepresentation; opacity?: number; width?: number; elevation?: number } = {}
|
|
147
|
+
opts: { color?: THREE.ColorRepresentation; opacity?: number; width?: number; elevation?: number } = {},
|
|
148
|
+
styleUniforms?: ThickLineStyleUniforms,
|
|
149
|
+
batchUniforms?: ThickLineBatchUniforms
|
|
93
150
|
): ThickLineMaterial {
|
|
151
|
+
const style: ThickLineStyleUniforms = styleUniforms ?? {
|
|
152
|
+
uColor: { value: new THREE.Color(opts.color ?? '#ffffff') },
|
|
153
|
+
uOpacity: { value: opts.opacity ?? 1 },
|
|
154
|
+
uWidth: { value: opts.width ?? 1.5 },
|
|
155
|
+
uElevation: { value: opts.elevation ?? 0 },
|
|
156
|
+
};
|
|
157
|
+
if (opts.color !== undefined && styleUniforms) style.uColor.value.set(opts.color);
|
|
158
|
+
if (opts.opacity !== undefined && styleUniforms) style.uOpacity.value = opts.opacity;
|
|
159
|
+
if (opts.width !== undefined && styleUniforms) style.uWidth.value = opts.width;
|
|
160
|
+
|
|
161
|
+
const batch: Required<ThickLineBatchUniforms> = batchUniforms ?? {
|
|
162
|
+
uBatchTex: { value: DUMMY_BATCH_TEX },
|
|
163
|
+
uBatchTexWidth: { value: 1 },
|
|
164
|
+
uBatchTexActive: { value: 0 },
|
|
165
|
+
};
|
|
166
|
+
|
|
94
167
|
const mat = new THREE.ShaderMaterial({
|
|
95
168
|
uniforms: {
|
|
96
169
|
uResolution: shared.uResolution,
|
|
97
170
|
uDpr: shared.uDpr,
|
|
98
|
-
uColor:
|
|
99
|
-
uOpacity:
|
|
100
|
-
uWidth:
|
|
101
|
-
uElevation: { value: opts.elevation ?? 0 },
|
|
171
|
+
uColor: style.uColor,
|
|
172
|
+
uOpacity: style.uOpacity,
|
|
173
|
+
uWidth: style.uWidth,
|
|
174
|
+
uElevation: style.uElevation ?? { value: opts.elevation ?? 0 },
|
|
102
175
|
uTranslation: { value: new THREE.Vector3(0, 0, 0) },
|
|
176
|
+
uBatchTex: batch.uBatchTex,
|
|
177
|
+
uBatchTexWidth: batch.uBatchTexWidth,
|
|
178
|
+
uBatchTexActive: batch.uBatchTexActive,
|
|
103
179
|
},
|
|
104
180
|
vertexShader: VERT,
|
|
105
181
|
fragmentShader: FRAG,
|
|
@@ -117,14 +193,22 @@ export function createThickLineMaterial(
|
|
|
117
193
|
|
|
118
194
|
/**
|
|
119
195
|
* 线段对(每 6 个 float:x1 y1 z1 x2 y2 z2)→ 可绘制等宽线的几何体。
|
|
196
|
+
*
|
|
197
|
+
* @param batchIds 每段所属的单体 batchId(长度 = 段数)。给了它,顶点着色器才能
|
|
198
|
+
* 按单体从状态纹理取平移/拔高——棱线跟随移动靠的就是这个。不传全部填 -1,
|
|
199
|
+
* 着色器会跳过采样(等于不参与单体状态)。
|
|
120
200
|
*/
|
|
121
|
-
export function buildThickLineGeometry(
|
|
201
|
+
export function buildThickLineGeometry(
|
|
202
|
+
segments: ArrayLike<number>,
|
|
203
|
+
batchIds?: ArrayLike<number>
|
|
204
|
+
): THREE.BufferGeometry {
|
|
122
205
|
const count = Math.floor(segments.length / 6);
|
|
123
206
|
const vertexCount = count * 4;
|
|
124
207
|
|
|
125
208
|
const position = new Float32Array(vertexCount * 3);
|
|
126
209
|
const other = new Float32Array(vertexCount * 3);
|
|
127
210
|
const side = new Float32Array(vertexCount);
|
|
211
|
+
const batch = new Float32Array(vertexCount);
|
|
128
212
|
const index = vertexCount > 65535 ? new Uint32Array(count * 6) : new Uint16Array(count * 6);
|
|
129
213
|
|
|
130
214
|
for (let i = 0; i < count; i++) {
|
|
@@ -135,6 +219,7 @@ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.Buffe
|
|
|
135
219
|
const bx = segments[s + 3];
|
|
136
220
|
const by = segments[s + 4];
|
|
137
221
|
const bz = segments[s + 5];
|
|
222
|
+
const b = batchIds ? Number(batchIds[i] ?? -1) : -1;
|
|
138
223
|
|
|
139
224
|
const v = i * 4;
|
|
140
225
|
// A 端两枚
|
|
@@ -143,6 +228,7 @@ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.Buffe
|
|
|
143
228
|
// B 端两枚(side 反号)
|
|
144
229
|
setVertex(v + 2, bx, by, bz, ax, ay, az, 1);
|
|
145
230
|
setVertex(v + 3, bx, by, bz, ax, ay, az, -1);
|
|
231
|
+
for (let k = 0; k < 4; k++) batch[v + k] = b;
|
|
146
232
|
|
|
147
233
|
const o = i * 6;
|
|
148
234
|
index[o] = v;
|
|
@@ -177,11 +263,41 @@ export function buildThickLineGeometry(segments: ArrayLike<number>): THREE.Buffe
|
|
|
177
263
|
geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
|
|
178
264
|
geometry.setAttribute('aOther', new THREE.BufferAttribute(other, 3));
|
|
179
265
|
geometry.setAttribute('aSide', new THREE.BufferAttribute(side, 1));
|
|
266
|
+
geometry.setAttribute('aBatchId', new THREE.BufferAttribute(batch, 1));
|
|
180
267
|
geometry.setIndex(new THREE.BufferAttribute(index, 1));
|
|
181
268
|
geometry.computeBoundingSphere();
|
|
182
269
|
return geometry;
|
|
183
270
|
}
|
|
184
271
|
|
|
272
|
+
/**
|
|
273
|
+
* 从带 `aBatchId` 的几何里建「量化位置 → batchId」的查找表。
|
|
274
|
+
*
|
|
275
|
+
* `EdgesGeometry` 输出的线段只有坐标、没有顶点号,要知道每段棱线属于哪个单体,
|
|
276
|
+
* 只能拿端点坐标回来查。key 的量化方式必须和 `mergeVerticesByPosition` 完全一致,
|
|
277
|
+
* 否则焊过的顶点会对不上。返回 null 表示这个几何没有单体化信息。
|
|
278
|
+
*/
|
|
279
|
+
export function buildPositionBatchMap(
|
|
280
|
+
geometry: THREE.BufferGeometry,
|
|
281
|
+
tolerance: number
|
|
282
|
+
): Map<string, number> | null {
|
|
283
|
+
const pos = geometry.getAttribute('position') as THREE.BufferAttribute | undefined;
|
|
284
|
+
const bid = geometry.getAttribute('aBatchId') as THREE.BufferAttribute | undefined;
|
|
285
|
+
if (!pos || !bid || pos.count === 0 || pos.count !== bid.count) return null;
|
|
286
|
+
|
|
287
|
+
const inv = 1 / Math.max(tolerance, 1e-9);
|
|
288
|
+
const map = new Map<string, number>();
|
|
289
|
+
for (let i = 0; i < pos.count; i++) {
|
|
290
|
+
const key = positionQuantKey(pos.getX(i), pos.getY(i), pos.getZ(i), inv);
|
|
291
|
+
if (!map.has(key)) map.set(key, bid.getX(i));
|
|
292
|
+
}
|
|
293
|
+
return map.size > 0 ? map : null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** 位置量化 key:与 mergeVerticesByPosition 同一套网格,保证焊过的顶点对得上 */
|
|
297
|
+
export function positionQuantKey(x: number, y: number, z: number, inv: number): string {
|
|
298
|
+
return `${Math.round(x * inv)},${Math.round(y * inv)},${Math.round(z * inv)}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
185
301
|
/* ------------------------------------------------------------------ 单体棱线提取 */
|
|
186
302
|
|
|
187
303
|
export interface EdgeVertexRange {
|
|
@@ -185,7 +185,15 @@ export class FeatureStyleManager {
|
|
|
185
185
|
private readonly _triggerRepaint: () => void,
|
|
186
186
|
private readonly _onStyleChanged: () => void,
|
|
187
187
|
private readonly _getAllMeshes?: () => Iterable<THREE.Mesh>,
|
|
188
|
-
private readonly _applyOverrideHook?: (mesh: THREE.Mesh) => void
|
|
188
|
+
private readonly _applyOverrideHook?: (mesh: THREE.Mesh) => void,
|
|
189
|
+
/**
|
|
190
|
+
* 图层自身是否处于半透明状态。
|
|
191
|
+
*
|
|
192
|
+
* 以前这里读 `material.opacity` 来判断「要不要把 transparent 还原」——自从全局
|
|
193
|
+
* 透明度改走共享 `uOpacity` 之后,`material.opacity` 不再等于图层透明度,
|
|
194
|
+
* 再读它就会在图层半透明时把材质错判成不透明、把整层透明效果抹掉。
|
|
195
|
+
*/
|
|
196
|
+
private readonly _isLayerTransparent: () => boolean = () => false
|
|
189
197
|
) {}
|
|
190
198
|
|
|
191
199
|
setFeatureStyle(smid: number, style: FeatureStyle, meshes?: Iterable<THREE.Mesh>): void {
|
|
@@ -565,7 +573,7 @@ export class FeatureStyleManager {
|
|
|
565
573
|
mat.userData.uBatchTexActive.value = 0;
|
|
566
574
|
}
|
|
567
575
|
// 单体淡化清除后,把材质 transparent 还原成全局透明度语义
|
|
568
|
-
if (mat.transparent &&
|
|
576
|
+
if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
|
|
569
577
|
return;
|
|
570
578
|
}
|
|
571
579
|
|
|
@@ -573,7 +581,7 @@ export class FeatureStyleManager {
|
|
|
573
581
|
if (mat.userData.uBatchTexActive) {
|
|
574
582
|
mat.userData.uBatchTexActive.value = 0;
|
|
575
583
|
}
|
|
576
|
-
if (mat.transparent &&
|
|
584
|
+
if (mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
|
|
577
585
|
return;
|
|
578
586
|
}
|
|
579
587
|
|
|
@@ -731,7 +739,7 @@ export class FeatureStyleManager {
|
|
|
731
739
|
|
|
732
740
|
// 有淡化单体时整块材质要开混合才有透明效果;清除后还原为全局透明度语义
|
|
733
741
|
if (hasFade && !mat.transparent) mat.transparent = true;
|
|
734
|
-
else if (!hasFade && mat.transparent &&
|
|
742
|
+
else if (!hasFade && mat.transparent && !this._isLayerTransparent()) mat.transparent = false;
|
|
735
743
|
|
|
736
744
|
let conflicts = 0;
|
|
737
745
|
for (const [bId, owners] of ownersOfBatch) {
|
|
@@ -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;
|