Initial commit: RHINE LAB · ANALYSIS OS:三维界面与动效实验(本仓库不含个人归档索引数据)
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import * as THREE from "three";
|
||||
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";
|
||||
import {
|
||||
records,
|
||||
archiveColumns,
|
||||
columnFiles,
|
||||
fileLocation,
|
||||
fileAtSlot,
|
||||
} from "../src/data.ts";
|
||||
import {
|
||||
cinematicField,
|
||||
columnStrength,
|
||||
INSPECTION_LIFT,
|
||||
returnStep,
|
||||
damp,
|
||||
} from "../src/motion.ts";
|
||||
|
||||
// 条数不再有上限(校验器只要求每列 ≥1、总数 ≥5),因此这里不写死 40 / 8,
|
||||
// 而是按数据推导并检查真正的契约:每列非空、列内槽位互不冲突。
|
||||
assert.ok(records.length >= 5, "Archive must carry at least five documents");
|
||||
const slots = new Set();
|
||||
const perColumn = [];
|
||||
let maxPerColumn = 0;
|
||||
for (let lane = 0; lane < archiveColumns.length; lane++) {
|
||||
const files = columnFiles(lane);
|
||||
assert.ok(files.length >= 1, "Every column has at least one readable file");
|
||||
perColumn.push(files.length);
|
||||
maxPerColumn = Math.max(maxPerColumn, files.length);
|
||||
const laneSlots = new Set();
|
||||
for (const index of files) {
|
||||
const location = fileLocation(index);
|
||||
assert.equal(location.lane, lane);
|
||||
assert.equal(fileAtSlot(location.slot), index);
|
||||
assert.ok(location.row >= 0 && location.row < 32);
|
||||
// slot = lane * 32 + (row % 32):同一列里超过 32 条才会开始复用槽位,
|
||||
// 届时 fileAtSlot(截断语义)本就不是用来寻址这些条目的,所以只断言列内不冲突。
|
||||
assert.ok(
|
||||
!laneSlots.has(location.slot) || files.length > 32,
|
||||
`Slots within one column must stay distinct (lane ${lane})`,
|
||||
);
|
||||
laneSlots.add(location.slot);
|
||||
slots.add(location.slot);
|
||||
const record = records[index];
|
||||
// 归档内容取代原 demo 之后,字数与条数不再固定:
|
||||
// 契约以 scripts/archive-content.mjs 的校验器为准——摘要非空、研究记录至少一条。
|
||||
// 这里保留一个极低的下限,只防止空壳内容混入。
|
||||
assert.ok(
|
||||
record.abstract.trim().length > 20,
|
||||
`Record abstract must carry real text (got ${record.abstract.trim().length} chars)`,
|
||||
);
|
||||
assert.ok(
|
||||
record.findings.length >= 1 &&
|
||||
record.findings.every((line) => String(line).trim().length > 0),
|
||||
"Every record carries at least one non-empty research note",
|
||||
);
|
||||
// 本地归档条目的参考链接是 file:(浏览器不允许网页跳转,仅供溯源);
|
||||
// 公开设定类条目仍用 https:。
|
||||
assert.ok(
|
||||
["https:", "file:"].includes(new URL(record.source).protocol),
|
||||
`Record source must be https: or file: (got ${new URL(record.source).protocol})`,
|
||||
);
|
||||
}
|
||||
}
|
||||
// 每列不超过 32 条时,所有条目的槽位应当恰好铺满、互不重叠。
|
||||
if (maxPerColumn <= 32) {
|
||||
assert.equal(slots.size, records.length, "No two documents occupy the same slot");
|
||||
}
|
||||
const crests = Array.from({ length: 5 }, (_, lane) =>
|
||||
Math.max(
|
||||
...Array.from({ length: 32 }, (_, row) => cinematicField(row, lane, 25.4)),
|
||||
),
|
||||
);
|
||||
assert.ok(
|
||||
Math.max(...crests) - Math.min(...crests) < 1e-9,
|
||||
"Frame 760 crests share the same height",
|
||||
);
|
||||
assert.ok(columnStrength(0, 2) >= 0.25, "Other columns retain a visible wave");
|
||||
assert.ok(columnStrength(2, 2) > columnStrength(1, 2));
|
||||
let maxDelta = 0;
|
||||
for (let f = 750; f < 786; f++) {
|
||||
for (let row = 0; row < 32; row++)
|
||||
for (let lane = 0; lane < 5; lane++) {
|
||||
maxDelta = Math.max(
|
||||
maxDelta,
|
||||
Math.abs(
|
||||
cinematicField(row, lane, (f + 1) / 25 - 5) -
|
||||
cinematicField(row, lane, f / 25 - 5),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
assert.ok(
|
||||
maxDelta < 0.65,
|
||||
"The equal-crest to selected-column handoff is continuous",
|
||||
);
|
||||
|
||||
// Validate against the delivered Blender model, not a duplicate nominal box.
|
||||
const bytes = await readFile(
|
||||
new URL("../public/assets/archive-cassette.glb", import.meta.url),
|
||||
);
|
||||
const gltf = await new GLTFLoader().parseAsync(
|
||||
bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
|
||||
"",
|
||||
);
|
||||
const box = new THREE.Box3().setFromObject(gltf.scene);
|
||||
const height = box.max.y - box.min.y;
|
||||
assert.ok(
|
||||
INSPECTION_LIFT - height > 0.25,
|
||||
"Inspection clears the adjacent card while staying near the array",
|
||||
);
|
||||
assert.ok(INSPECTION_LIFT <= 4.1, "Inspection lift remains modest");
|
||||
let angle = 0.8,
|
||||
elapsed = 0;
|
||||
while (angle !== 0 && elapsed < 2) {
|
||||
angle = returnStep(angle, 1 / 60);
|
||||
elapsed += 1 / 60;
|
||||
}
|
||||
assert.equal(angle, 0, "Alignment finishes exactly before insertion");
|
||||
assert.ok(elapsed > 0.5 && elapsed < 1.2);
|
||||
const coarse = { value: 2, velocity: 0 },
|
||||
fine = { ...coarse };
|
||||
for (let i = 0; i < 30; i++) damp(coarse, 3, 4, 1 / 30);
|
||||
for (let i = 0; i < 120; i++) damp(fine, 3, 4, 1 / 120);
|
||||
assert.ok(Math.abs(coarse.value - fine.value) < 1e-9);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
documents: records.length,
|
||||
perColumn,
|
||||
maxPerColumn,
|
||||
crestsAt760: crests,
|
||||
maxFrameDelta: maxDelta,
|
||||
modelHeight: height,
|
||||
inspectionLift: INSPECTION_LIFT,
|
||||
alignmentSeconds: elapsed,
|
||||
checks: "passed",
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
Reference in New Issue
Block a user