|
3 | 3 | * 使用时间插值偏差度量。 |
4 | 4 | */ |
5 | 5 | import { describe, expect, it } from 'bun:test' |
6 | | -import { Point } from '../src/utils' |
| 6 | +import { Point } from '../src/utils/physics' |
7 | 7 | import { |
8 | 8 | buildKeyTimes, |
9 | 9 | CompressedTrajectoryPoint, |
10 | 10 | simplifyTrajectoryDP, |
11 | 11 | simplifyTrajectoryGreedy, |
12 | 12 | simplifyTrajectoryRDP |
13 | 13 | } from '../src/utils/simplifyTrajectory' |
14 | | -import { maxLerpDeviation, sumSpans } from './simplifyHelpers' |
| 14 | +import { maxLerpDeviation, maxSpinDeviation, sumSpans } from './simplifyHelpers' |
15 | 15 |
|
16 | 16 | type SimplifyFn = ( |
17 | 17 | trajectory: Point[], |
18 | | - tolerance: number |
| 18 | + tolerance: number, |
| 19 | + rotationRadius?: number |
19 | 20 | ) => CompressedTrajectoryPoint[] |
20 | 21 |
|
21 | 22 | const algorithms: [string, SimplifyFn][] = [ |
@@ -191,6 +192,64 @@ describe('simplifyTrajectory algorithms', () => { |
191 | 192 | } |
192 | 193 | }) |
193 | 194 |
|
| 195 | +describe('rotation-aware trajectories', () => { |
| 196 | + // 位置匀速右移 + 自转匀速:位置与角度的插值都精确命中 |
| 197 | + const linearSpin: Point[] = Array.from({ length: 5 }, (_, i) => ({ |
| 198 | + x: i, |
| 199 | + y: 0, |
| 200 | + rotation: i * 0.5 |
| 201 | + })) |
| 202 | + |
| 203 | + for (const [name, simplify] of algorithms) { |
| 204 | + describe(name, () => { |
| 205 | + it('collapses uniform spin and drift to endpoints at tolerance 0', () => { |
| 206 | + expect(simplify(linearSpin, 0, 0.1)).toEqual([ |
| 207 | + { x: 0, y: 0, rotation: 0, span: 0 }, |
| 208 | + { x: 4, y: 0, rotation: 2, span: 4 } |
| 209 | + ]) |
| 210 | + }) |
| 211 | + |
| 212 | + it('keeps a keyframe where spin jumps far beyond the rotation error', () => { |
| 213 | + // 位置匀速但自转角在中点大跳变:丢弃中点的任何分段在该处的 |
| 214 | + // 角度插值误差都是跳变幅度本身,远超上限,违反误差上界承诺, |
| 215 | + // 因此中点必须保留为关键帧——结论只依赖承诺,与算法实现无关 |
| 216 | + const track: Point[] = [ |
| 217 | + { x: 0, y: 0, rotation: 0 }, |
| 218 | + { x: 1, y: 0, rotation: 10 }, |
| 219 | + { x: 2, y: 0, rotation: 0 } |
| 220 | + ] |
| 221 | + expect(simplify(track, 0, 0.1)).toEqual([ |
| 222 | + { x: 0, y: 0, rotation: 0, span: 0 }, |
| 223 | + { x: 1, y: 0, rotation: 10, span: 1 }, |
| 224 | + { x: 2, y: 0, rotation: 0, span: 1 } |
| 225 | + ]) |
| 226 | + }) |
| 227 | + |
| 228 | + it('ignores rotation when maxRotationError is 0', () => { |
| 229 | + // 上限 0 表示消费端不渲染旋转:只按位置度量,轨迹仍可并段 |
| 230 | + expect(simplify(linearSpin, 0)).toEqual([ |
| 231 | + { x: 0, y: 0, rotation: 0, span: 0 }, |
| 232 | + { x: 4, y: 0, rotation: 2, span: 4 } |
| 233 | + ]) |
| 234 | + }) |
| 235 | + |
| 236 | + it('bounds spin deviation on an accelerating spin', () => { |
| 237 | + const track: Point[] = Array.from({ length: 30 }, (_, i) => ({ |
| 238 | + x: 0, |
| 239 | + y: 0, |
| 240 | + rotation: 0.1 * i * i |
| 241 | + })) |
| 242 | + const maxRotationError = 0.4 |
| 243 | + const result = simplify(track, 0.4, maxRotationError) |
| 244 | + expect(sumSpans(result)).toBe(track.length - 1) |
| 245 | + expect(maxSpinDeviation(track, result)).toBeLessThanOrEqual( |
| 246 | + maxRotationError + 1e-9 |
| 247 | + ) |
| 248 | + }) |
| 249 | + }) |
| 250 | + } |
| 251 | +}) |
| 252 | + |
194 | 253 | describe('buildKeyTimes', () => { |
195 | 254 | it('stays strictly increasing for very long trajectories', () => { |
196 | 255 | // 2000 段等长 span:小数舍入可能把相邻值舍成相同(SMIL 要求严格递增) |
|
0 commit comments