feat: 新增3个思想实验室项目,完善私有剪贴板授权与上传功能,添加配套测试脚本
1. 新增怀旧弹珠台、人性与投资、穿越三体、四维空间四个思想实验室项目 2. 为私有剪贴板添加业务层授权校验,优化上传进度显示与鉴权失败处理 3. 新增多个调试与单元测试脚本,覆盖三体积分、弹珠台物理、集成测试等场景 4. 补全穿越三体项目的完整HTML页面资源
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
// ============================================================
|
||||
// dev_test_scripts/unit/test_retro_pinball_physics.js
|
||||
// 怀旧弹珠台 · 物理核心单元测试
|
||||
// 从 public/tools/thought_lab/labs/retro_pinball/index.html 中
|
||||
// 提取 ==PHYSICS_CORE_START== / ==PHYSICS_CORE_END== 之间的纯函数核心,
|
||||
// 在 node vm 中运行并断言物理正确性。
|
||||
// 运行:node dev_test_scripts/unit/test_retro_pinball_physics.js
|
||||
// ============================================================
|
||||
'use strict';
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
const HTML_PATH = path.join(__dirname, '..', '..', 'public', 'tools', 'thought_lab', 'labs', 'retro_pinball', 'index.html');
|
||||
|
||||
function loadCore() {
|
||||
const html = fs.readFileSync(HTML_PATH, 'utf-8');
|
||||
const s = html.indexOf('/* ==PHYSICS_CORE_START== */');
|
||||
const e = html.indexOf('/* ==PHYSICS_CORE_END== */');
|
||||
if (s < 0 || e < 0 || e <= s) throw new Error('未找到物理核心标记块');
|
||||
const code = html.slice(s, e);
|
||||
const sandbox = { Math, console };
|
||||
vm.createContext(sandbox);
|
||||
vm.runInContext(code, sandbox);
|
||||
if (!sandbox.PhysicsCore) throw new Error('PhysicsCore 未导出');
|
||||
return sandbox.PhysicsCore;
|
||||
}
|
||||
|
||||
const PC = loadCore();
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function check(name, cond, detail) {
|
||||
if (cond) { passed++; console.log(` [PASS] ${name}`); }
|
||||
else { failed++; console.error(` [FAIL] ${name}${detail ? ' -> ' + detail : ''}`); }
|
||||
}
|
||||
|
||||
const DT = 1 / 360;
|
||||
function baseParams(over) {
|
||||
return Object.assign({
|
||||
g: 2000, e: 0.55, mu: 0.12, airK: 0, rollR: 0,
|
||||
restV: 1, iterations: 8, slop: 0.3, correction: 0.8, maxV: 6000
|
||||
}, over || {});
|
||||
}
|
||||
const FLOOR = { type: 'seg', x1: -500, y1: 500, x2: 500, y2: 500 };
|
||||
|
||||
// ---------- 1. 恢复系数:回弹高度 ≈ e²·h ----------
|
||||
(function testRestitution() {
|
||||
console.log('1) 恢复系数回弹高度');
|
||||
const e = 0.5, h = 300;
|
||||
const p = baseParams({ e, mu: 0, restV: 1 });
|
||||
const b = PC.createBall(0, 500 - 11 - h, 11);
|
||||
const statics = [FLOOR];
|
||||
let bounced = false, apex = 1e9, done = false, t = 0;
|
||||
while (t < 3 && !done) {
|
||||
PC.stepWorld([b], statics, p, DT);
|
||||
t += DT;
|
||||
if (!bounced && b.vy < 0) bounced = true; // 回弹开始(y 向下,vy<0 为向上)
|
||||
if (bounced) {
|
||||
apex = Math.min(apex, b.y);
|
||||
if (b.vy > 0) done = true; // 到达顶点
|
||||
}
|
||||
}
|
||||
const rebound = (500 - 11) - apex;
|
||||
const expect = e * e * h;
|
||||
check(`回弹高度 ${rebound.toFixed(1)} ≈ e²h=${expect.toFixed(1)} (±10%)`, Math.abs(rebound - expect) <= expect * 0.1, `got=${rebound}`);
|
||||
})();
|
||||
|
||||
// ---------- 2. 能量不增 ----------
|
||||
(function testEnergy() {
|
||||
console.log('2) 机械能不自发增加');
|
||||
const p = baseParams({ e: 0.5, mu: 0 });
|
||||
const b = PC.createBall(0, 100, 11);
|
||||
const statics = [FLOOR];
|
||||
const E0 = 0.5 * b.m * (b.vx * b.vx + b.vy * b.vy) + b.m * p.g * (500 - b.y);
|
||||
let maxE = 0;
|
||||
for (let t = 0; t < 2; t += DT) {
|
||||
PC.stepWorld([b], statics, p, DT);
|
||||
const E = 0.5 * b.m * (b.vx * b.vx + b.vy * b.vy) + b.m * p.g * (500 - b.y);
|
||||
maxE = Math.max(maxE, E);
|
||||
}
|
||||
check(`maxE=${maxE.toFixed(0)} <= E0*1.02=${(E0 * 1.02).toFixed(0)}`, maxE <= E0 * 1.02);
|
||||
})();
|
||||
|
||||
// ---------- 3. 静止稳定:不沉陷、不抖动 ----------
|
||||
(function testRest() {
|
||||
console.log('3) 水平面静止稳定性');
|
||||
const p = baseParams({ e: 0.55, mu: 0.2 });
|
||||
const b = PC.createBall(0, 200, 11);
|
||||
const statics = [FLOOR];
|
||||
for (let t = 0; t < 3; t += DT) PC.stepWorld([b], statics, p, DT);
|
||||
const sp = Math.hypot(b.vx, b.vy);
|
||||
check(`静止速度 ${sp.toFixed(2)} < 20`, sp < 20);
|
||||
check(`不沉陷 y=${b.y.toFixed(2)} <= 489+1`, b.y <= 500 - 11 + 1);
|
||||
})();
|
||||
|
||||
// ---------- 4. 等质量弹性碰撞速度交换 ----------
|
||||
(function testMomentum() {
|
||||
console.log('4) 等质量 e=1 正碰速度交换');
|
||||
const p = baseParams({ g: 0, e: 1, mu: 0 });
|
||||
const a = PC.createBall(0, 0, 11); a.vx = 100;
|
||||
const b = PC.createBall(30, 0, 11);
|
||||
for (let t = 0; t < 0.5; t += DT) PC.stepWorld([a, b], [], p, DT);
|
||||
check(`a.vx=${a.vx.toFixed(1)}≈0`, Math.abs(a.vx) < 3, `got=${a.vx}`);
|
||||
check(`b.vx=${b.vx.toFixed(1)}≈100`, Math.abs(b.vx - 100) < 3, `got=${b.vx}`);
|
||||
})();
|
||||
|
||||
// ---------- 5. 高速不穿透 ----------
|
||||
(function testNoTunnel() {
|
||||
console.log('5) 3000px/s 不穿透薄壁');
|
||||
const p = baseParams({ g: 0, e: 0.5, mu: 0 });
|
||||
const wall = { type: 'seg', x1: 100, y1: -1000, x2: 100, y2: 1000 };
|
||||
const b = PC.createBall(0, 0, 11); b.vx = 3000;
|
||||
let maxX = -1e9;
|
||||
for (let t = 0; t < 0.5; t += DT) {
|
||||
PC.stepWorld([b], [wall], p, DT);
|
||||
maxX = Math.max(maxX, b.x);
|
||||
}
|
||||
check(`maxX=${maxX.toFixed(1)} <= 100`, maxX <= 100 + 0.5);
|
||||
})();
|
||||
|
||||
// ---------- 6. 摩擦减速 + 滚动建立 ----------
|
||||
(function testFrictionRoll() {
|
||||
console.log('6) 摩擦使滑动减速并建立滚动');
|
||||
const mk = (mu) => {
|
||||
const p = baseParams({ e: 0.3, mu, rollR: 0 });
|
||||
const b = PC.createBall(0, 500 - 11, 11); b.vx = 500;
|
||||
for (let t = 0; t < 0.5; t += DT) PC.stepWorld([b], [FLOOR], p, DT);
|
||||
return b;
|
||||
};
|
||||
const noF = mk(0), withF = mk(0.4);
|
||||
check(`无摩擦保持滑动 |vx|=${noF.vx.toFixed(0)} > 400`, noF.vx > 400);
|
||||
check(`有摩擦减速 vx=${withF.vx.toFixed(0)} < ${noF.vx.toFixed(0)}`, withF.vx < noF.vx - 50);
|
||||
check(`滚动建立 omega=${withF.omega.toFixed(1)} > 5`, withF.omega > 5);
|
||||
})();
|
||||
|
||||
// ---------- 7. 斜坡:无摩擦加速下滑 ----------
|
||||
(function testSlope() {
|
||||
console.log('7) 斜坡下滑加速度 ≈ g·sinθ');
|
||||
const theta = Math.PI / 6; // 30°
|
||||
const p = baseParams({ g: 2000, e: 0, mu: 0 });
|
||||
const slope = { type: 'seg', x1: 0, y1: 0, x2: 300, y2: 300 * Math.tan(theta) };
|
||||
const b = PC.createBall(50, 50 * Math.tan(theta) - 11, 11);
|
||||
const v0 = Math.hypot(b.vx, b.vy);
|
||||
for (let t = 0; t < 0.3; t += DT) PC.stepWorld([b], [slope], p, DT);
|
||||
const v1 = Math.hypot(b.vx, b.vy);
|
||||
const dv = v1 - v0;
|
||||
const expect = p.g * Math.sin(theta) * 0.3;
|
||||
check(`dv=${dv.toFixed(0)} ≈ g·sinθ·t=${expect.toFixed(0)} (±8%)`, Math.abs(dv - expect) <= expect * 0.08);
|
||||
})();
|
||||
|
||||
console.log(`\n结果: ${passed} passed, ${failed} failed`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
Reference in New Issue
Block a user