253 lines
8.9 KiB
JavaScript
253 lines
8.9 KiB
JavaScript
import fs from "node:fs/promises";
|
||
import path from "node:path";
|
||
import {
|
||
AlignmentType,
|
||
Document,
|
||
HeadingLevel,
|
||
ImageRun,
|
||
Packer,
|
||
Paragraph,
|
||
Table,
|
||
TableCell,
|
||
TableRow,
|
||
TextRun,
|
||
WidthType,
|
||
} from "docx";
|
||
|
||
const ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
|
||
const OUTPUT_DIR = path.join(ROOT, "output");
|
||
const reportJsonPath = path.join(OUTPUT_DIR, "toolpath-preview-cases.json");
|
||
const report = JSON.parse(await fs.readFile(reportJsonPath, "utf8"));
|
||
const generated = new Date(report.generatedAt || Date.now());
|
||
const ymd = generated.toISOString().slice(0, 10);
|
||
const docxPath = path.join(OUTPUT_DIR, `web-rtcp-5axis-toolpath-preview-report-${ymd}.docx`);
|
||
const counts = report.cases.reduce((acc, testCase) => {
|
||
acc[testCase.status] = (acc[testCase.status] || 0) + 1;
|
||
return acc;
|
||
}, {});
|
||
|
||
const children = [
|
||
new Paragraph({
|
||
text: "刀具预览与 G-code 执行刀具轨迹专项测试报告",
|
||
heading: HeadingLevel.TITLE,
|
||
alignment: AlignmentType.CENTER,
|
||
}),
|
||
centered(`测试对象:${report.targetUrl}`),
|
||
centered(`项目路径:${report.projectPath || "/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/index.html"}`),
|
||
centered(`生成时间:${formatDateTime(generated)}`),
|
||
blank(),
|
||
heading("1. 测试结论"),
|
||
para(`本次专项覆盖 6 个刀具预览/刀具轨迹场景:PASS ${counts.PASS || 0} 项,FAIL ${counts.FAIL || 0} 项。`),
|
||
para("已验证首屏预览、矩形 G-code、圆弧 G-code、清空预览、LinuxCNC vendored impeller 长路径、RTCP 运行态执行轨迹。截图中机床参考模型、TCP 球、刀轴线和刀路均有可见性证据。"),
|
||
para("发现 1 项关键问题:vendored impeller 程序在预览态为 RTCP on,但进入 G-code 运行态后 canvas dataset 中 RTCP 状态变为 off。"),
|
||
summaryTable(),
|
||
heading("2. 测试范围与判定口径"),
|
||
kvTable([
|
||
["预览对象", "五轴机床参考模型、工作台、XYZ 坐标轴、旋转轴、刀具/TCP 球、刀轴线"],
|
||
["轨迹对象", "G-code canonical motion 预览路径、rapid/feed/arc 分层、已执行轨迹、当前段高亮"],
|
||
["运行反馈", "LinuxCNC interpreter WASM、TP/runtime samples、task/HAL runtime feedback"],
|
||
["可见性口径", "canvas dataset ready + WebGL renderer + scene objects + pixel luminance/non-black ratio + 截图"],
|
||
["语义边界", "可视化层只消费 runtime/canonical motion/task-HAL feedback,不生成 G-code/CNC 语义"],
|
||
]),
|
||
heading("3. 场景结果总表"),
|
||
caseSummaryTable(report.cases),
|
||
heading("4. 失败/风险项"),
|
||
issueTable(report.cases.filter((testCase) => testCase.status !== "PASS")),
|
||
heading("5. 场景明细与截图"),
|
||
];
|
||
|
||
for (const testCase of report.cases) {
|
||
children.push(...(await caseBlock(testCase)));
|
||
}
|
||
|
||
children.push(
|
||
heading("6. 原始证据"),
|
||
kvTable([
|
||
["原始 JSON", reportJsonPath],
|
||
["截图数量", String(Object.keys(report.screenshots || {}).length)],
|
||
["Console error", String((report.consoleErrors || []).length)],
|
||
["Chrome", report.chromePath || "-"],
|
||
["Word 报告", docxPath],
|
||
["项目路径", report.projectPath || "-"],
|
||
]),
|
||
);
|
||
|
||
if ((report.consoleErrors || []).length > 0) {
|
||
children.push(
|
||
heading("7. Console 记录"),
|
||
...report.consoleErrors.map((entry) => para(entry)),
|
||
);
|
||
}
|
||
|
||
const doc = new Document({
|
||
sections: [{ properties: {}, children: children.flat() }],
|
||
});
|
||
|
||
await fs.writeFile(docxPath, await Packer.toBuffer(doc));
|
||
console.log(`toolpath_docx_report=${docxPath}`);
|
||
|
||
function heading(text) {
|
||
return new Paragraph({
|
||
text,
|
||
heading: HeadingLevel.HEADING_1,
|
||
spacing: { before: 240, after: 120 },
|
||
});
|
||
}
|
||
|
||
function centered(text) {
|
||
return new Paragraph({
|
||
alignment: AlignmentType.CENTER,
|
||
children: [new TextRun(String(text))],
|
||
});
|
||
}
|
||
|
||
function blank() {
|
||
return new Paragraph({ text: "" });
|
||
}
|
||
|
||
function para(text) {
|
||
return new Paragraph({
|
||
children: [new TextRun(String(text))],
|
||
spacing: { after: 100 },
|
||
});
|
||
}
|
||
|
||
function summaryTable() {
|
||
return new Table({
|
||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||
rows: [
|
||
new TableRow({
|
||
children: [
|
||
cell("场景总数", true),
|
||
cell(String(report.cases.length)),
|
||
cell("PASS", true),
|
||
cell(String(counts.PASS || 0)),
|
||
cell("FAIL", true),
|
||
cell(String(counts.FAIL || 0)),
|
||
],
|
||
}),
|
||
],
|
||
});
|
||
}
|
||
|
||
function kvTable(rows) {
|
||
return new Table({
|
||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||
rows: [
|
||
new TableRow({ children: [cell("项目", true), cell("内容", true)] }),
|
||
...rows.map(([key, value]) => new TableRow({ children: [cell(key), cell(value)] })),
|
||
],
|
||
});
|
||
}
|
||
|
||
function caseSummaryTable(cases) {
|
||
return new Table({
|
||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||
rows: [
|
||
new TableRow({
|
||
children: [cell("场景", true), cell("结果", true), cell("路径点", true), cell("执行点", true), cell("RTCP", true), cell("可见性", true)],
|
||
}),
|
||
...cases.map((testCase) => new TableRow({
|
||
children: [
|
||
cell(`${testCase.name}\n${testCase.summary}`),
|
||
cell(testCase.status),
|
||
cell(testCase.dataset.threePathPoints),
|
||
cell(testCase.dataset.threeExecutedPathPoints),
|
||
cell(testCase.dataset.threeRtcpState),
|
||
cell(`luma=${testCase.pixelStats?.averageLuminance ?? "-"}\nnonBlack=${testCase.pixelStats?.nonBlackRatio ?? "-"}`),
|
||
],
|
||
})),
|
||
],
|
||
});
|
||
}
|
||
|
||
function issueTable(failedCases) {
|
||
if (failedCases.length === 0) return para("未发现失败场景。");
|
||
return new Table({
|
||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||
rows: [
|
||
new TableRow({ children: [cell("场景", true), cell("失败检查", true), cell("影响", true), cell("建议", true)] }),
|
||
...failedCases.map((testCase) => new TableRow({
|
||
children: [
|
||
cell(testCase.name),
|
||
cell(testCase.checks.filter((item) => !item.pass).map((item) => `${item.name}: ${item.detail}`).join("\n")),
|
||
cell("G-code 运行态下 RTCP/TCP 轨迹状态与预览态不一致,可能误导操作者判断刀具姿态和 TCP 执行轨迹。"),
|
||
cell("检查 RUN/task-HAL status 合并逻辑,避免运行反馈将 RTCP/kinesType 从已加载程序的 TCP 状态回退到 identity/off。"),
|
||
],
|
||
})),
|
||
],
|
||
});
|
||
}
|
||
|
||
async function caseBlock(testCase) {
|
||
const screenshotPath = report.screenshots[testCase.name];
|
||
const blocks = [
|
||
new Paragraph({
|
||
text: `${testCase.name} - ${testCase.status}`,
|
||
heading: HeadingLevel.HEADING_2,
|
||
spacing: { before: 180, after: 120 },
|
||
}),
|
||
para(testCase.summary),
|
||
kvTable([
|
||
["activeProgram", testCase.state.activeProgram],
|
||
["programSource", testCase.state.programSource],
|
||
["programExecutionSourceMode", testCase.state.programExecutionSourceMode],
|
||
["runState", testCase.state.runState],
|
||
["rtcpState / kinsType", `${testCase.state.rtcpState} / ${testCase.state.kinsType}`],
|
||
["motion/sample index", `${testCase.state.programExecutionMotionIndex} / ${testCase.state.programExecutionSampleIndex}`],
|
||
["runtime feedback", testCase.state.programRuntimeFeedbackSource || "-"],
|
||
["path/executed/rapid/feed/arc", `${testCase.dataset.threePathPoints}/${testCase.dataset.threeExecutedPathPoints}/${testCase.dataset.threeRapidPathPoints}/${testCase.dataset.threeFeedPathPoints}/${testCase.dataset.threeArcPathPoints}`],
|
||
["renderer/model", `${testCase.dataset.threeRenderer} / ${testCase.dataset.threeMachineReferenceModel}`],
|
||
["pixel stats", `averageLuminance=${testCase.pixelStats?.averageLuminance ?? "-"}, nonBlackRatio=${testCase.pixelStats?.nonBlackRatio ?? "-"}`],
|
||
]),
|
||
new Paragraph({
|
||
text: "检查项",
|
||
heading: HeadingLevel.HEADING_3,
|
||
spacing: { before: 120, after: 80 },
|
||
}),
|
||
checksTable(testCase.checks),
|
||
];
|
||
if (screenshotPath) {
|
||
const image = await fs.readFile(screenshotPath);
|
||
blocks.push(
|
||
new Paragraph({
|
||
alignment: AlignmentType.CENTER,
|
||
children: [
|
||
new ImageRun({
|
||
data: image,
|
||
type: "png",
|
||
transformation: { width: 520, height: 390 },
|
||
}),
|
||
],
|
||
}),
|
||
para(`截图文件:${screenshotPath}`),
|
||
);
|
||
}
|
||
return blocks;
|
||
}
|
||
|
||
function checksTable(checks) {
|
||
return new Table({
|
||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||
rows: [
|
||
new TableRow({ children: [cell("检查", true), cell("结果", true), cell("证据", true)] }),
|
||
...checks.map((item) => new TableRow({
|
||
children: [cell(item.name), cell(item.pass ? "PASS" : "FAIL"), cell(item.detail)],
|
||
})),
|
||
],
|
||
});
|
||
}
|
||
|
||
function cell(text, bold = false) {
|
||
return new TableCell({
|
||
width: { size: 25, type: WidthType.PERCENTAGE },
|
||
children: String(text ?? "-").split("\n").map((line) => new Paragraph({
|
||
children: [new TextRun({ text: line, bold })],
|
||
})),
|
||
});
|
||
}
|
||
|
||
function formatDateTime(date) {
|
||
return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
|
||
}
|