45bfae6e94
Initial import of all source code, config, and README assets: the packages workspace (cli, core, server, web, docs, landing, skills), build scripts, tooling config, and CI workflows. Includes the data-layout revision made on this branch: the local data root defaults to ~/.penguin/data (PENGUIN_HOME still overrides; the installer keeps its binaries in ~/.penguin), and every Agent lives under <project>/agents/<agent>/ — path helpers, the three agent-enumeration scans, the system prompt, built-in Skills, tests and docs all follow the new layout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018ihk8iQuo3kv2aPjAYEPuR
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { SPRING_DEFAULT, SPRING_MOMENTUM, isSettled, stepSpring } from "../src/lib/spring";
|
|
import type { SpringConfig } from "../src/lib/spring";
|
|
|
|
/** Runs a number of frames at 16ms per frame, returning the final state and path. */
|
|
function run(from: number, target: number, config: SpringConfig, frames = 240) {
|
|
let x = from;
|
|
let v = 0;
|
|
const path: number[] = [];
|
|
for (let i = 0; i < frames; i++) {
|
|
[x, v] = stepSpring(x, v, target, config, 16);
|
|
path.push(x);
|
|
}
|
|
return { x, v, path };
|
|
}
|
|
|
|
describe("stepSpring", () => {
|
|
it("收敛到目标", () => {
|
|
const { x, v } = run(600, 0, SPRING_DEFAULT);
|
|
expect(Math.abs(x)).toBeLessThan(0.5);
|
|
expect(Math.abs(v)).toBeLessThan(5);
|
|
});
|
|
|
|
it("临界阻尼(damping=1)从静止出发不过冲", () => {
|
|
const { path } = run(600, 0, SPRING_DEFAULT);
|
|
for (const p of path) expect(p).toBeGreaterThanOrEqual(-0.5);
|
|
});
|
|
|
|
it("欠阻尼(damping=0.8)会过冲后回摆", () => {
|
|
const { path } = run(600, 0, SPRING_MOMENTUM);
|
|
expect(Math.min(...path)).toBeLessThan(-1);
|
|
});
|
|
|
|
it("初速度朝反方向时先随速度走再回归(速度交接语义)", () => {
|
|
let x = 100;
|
|
let v = 800; // moving away from target 0
|
|
[x, v] = stepSpring(x, v, 0, SPRING_MOMENTUM, 16);
|
|
expect(x).toBeGreaterThan(100);
|
|
for (let i = 0; i < 240; i++) [x, v] = stepSpring(x, v, 0, SPRING_MOMENTUM, 16);
|
|
expect(Math.abs(x)).toBeLessThan(0.5);
|
|
});
|
|
|
|
it("超大帧间隔(后台标签页恢复)不数值爆炸", () => {
|
|
let x = 600;
|
|
let v = 0;
|
|
for (let i = 0; i < 60; i++) [x, v] = stepSpring(x, v, 0, SPRING_MOMENTUM, 500);
|
|
expect(Number.isFinite(x)).toBe(true);
|
|
expect(Number.isFinite(v)).toBe(true);
|
|
expect(Math.abs(x)).toBeLessThan(1);
|
|
});
|
|
});
|
|
|
|
describe("isSettled", () => {
|
|
it("位置与速度双阈值", () => {
|
|
expect(isSettled(0.4, 4, 0)).toBe(true);
|
|
expect(isSettled(1, 0, 0)).toBe(false);
|
|
expect(isSettled(0, 10, 0)).toBe(false);
|
|
});
|
|
});
|