Files

144 lines
5.0 KiB
JavaScript

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,
),
);