Add RTCP simulation QA updates
1
.gitignore
vendored
@@ -4,3 +4,4 @@
|
||||
/text.txt
|
||||
/test-results/
|
||||
/linuxcnc/
|
||||
node_modules/
|
||||
|
||||
356
qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
Normal file
@@ -0,0 +1,356 @@
|
||||
import fs from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { PNG } from "pngjs";
|
||||
|
||||
const REPO_ROOT = path.resolve("/home/meswork/cnc_wams");
|
||||
const QA_ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
|
||||
const OUTPUT_DIR = path.join(QA_ROOT, "output");
|
||||
const SCREENSHOT_DIR = path.join(QA_ROOT, "screenshots", "toolpath-preview-cases");
|
||||
const CHROME_PATH = process.env.CHROME_PATH || "/usr/bin/google-chrome";
|
||||
const FIXTURE_URL = "/web-rtcp-5axis-sim-plan/app/index.html";
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
|
||||
const MIME_TYPES = {
|
||||
".css": "text/css; charset=utf-8",
|
||||
".html": "text/html; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".mjs": "text/javascript; charset=utf-8",
|
||||
".svg": "image/svg+xml",
|
||||
".wasm": "application/wasm",
|
||||
".xml": "application/xml; charset=utf-8",
|
||||
};
|
||||
|
||||
function contentTypeFor(filePath) {
|
||||
return MIME_TYPES[path.extname(filePath).toLowerCase()] || "application/octet-stream";
|
||||
}
|
||||
|
||||
function createStaticServer(rootDir) {
|
||||
return http.createServer(async (request, response) => {
|
||||
try {
|
||||
const requestPath = decodeURIComponent(new URL(request.url || "/", "http://127.0.0.1").pathname);
|
||||
const relativePath = requestPath === "/" ? "/index.html" : requestPath;
|
||||
const targetPath = path.resolve(rootDir, `.${relativePath}`);
|
||||
if (!targetPath.startsWith(rootDir)) {
|
||||
response.writeHead(403);
|
||||
response.end("forbidden");
|
||||
return;
|
||||
}
|
||||
let stat = await fs.stat(targetPath).catch(() => null);
|
||||
let filePath = targetPath;
|
||||
if (stat?.isDirectory()) {
|
||||
filePath = path.join(targetPath, "index.html");
|
||||
stat = await fs.stat(filePath).catch(() => null);
|
||||
}
|
||||
if (!stat?.isFile()) {
|
||||
response.writeHead(404);
|
||||
response.end("not found");
|
||||
return;
|
||||
}
|
||||
const body = await fs.readFile(filePath);
|
||||
response.writeHead(200, {
|
||||
"Content-Type": contentTypeFor(filePath),
|
||||
"Content-Length": String(body.byteLength),
|
||||
"Cache-Control": "no-store",
|
||||
});
|
||||
response.end(body);
|
||||
} catch (error) {
|
||||
response.writeHead(500);
|
||||
response.end(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const server = createStaticServer(REPO_ROOT);
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") {
|
||||
throw new Error("failed to start static server");
|
||||
}
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: CHROME_PATH,
|
||||
defaultViewport: { width: 1600, height: 1200, deviceScaleFactor: 1 },
|
||||
args: [
|
||||
"--disable-gpu",
|
||||
"--enable-webgl",
|
||||
"--use-angle=swiftshader",
|
||||
"--enable-unsafe-swiftshader",
|
||||
"--no-sandbox",
|
||||
],
|
||||
});
|
||||
|
||||
const page = await browser.newPage();
|
||||
const consoleErrors = [];
|
||||
page.on("console", (msg) => {
|
||||
if (msg.type() === "error") {
|
||||
consoleErrors.push(msg.text());
|
||||
}
|
||||
});
|
||||
page.on("pageerror", (error) => {
|
||||
consoleErrors.push(error.message);
|
||||
});
|
||||
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: `${baseUrl}${FIXTURE_URL}`,
|
||||
projectPath: path.join(REPO_ROOT, "web-rtcp-5axis-sim-plan", "app", "index.html"),
|
||||
chromePath: CHROME_PATH,
|
||||
screenshots: {},
|
||||
cases: [],
|
||||
consoleErrors,
|
||||
};
|
||||
|
||||
async function wait(ms) {
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function waitForState(predicate, timeoutMs = 20000, label = "state condition") {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const snapshot = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
|
||||
if (predicate(snapshot)) return snapshot;
|
||||
await wait(100);
|
||||
}
|
||||
throw new Error(`timeout waiting for ${label}`);
|
||||
}
|
||||
|
||||
async function getCanvasDataset() {
|
||||
return page.$eval("[data-five-axis-canvas]", (canvas) => ({ ...canvas.dataset }));
|
||||
}
|
||||
|
||||
async function waitForCanvasReady(timeoutMs = 20000) {
|
||||
await page.waitForFunction(() => {
|
||||
const canvas = document.querySelector("[data-five-axis-canvas]");
|
||||
return canvas?.dataset?.threeReady === "true"
|
||||
&& canvas?.dataset?.threePreviewScope === "machine-reference-and-toolpath";
|
||||
}, { timeout: timeoutMs });
|
||||
}
|
||||
|
||||
async function captureCase(name) {
|
||||
const screenshotPath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
||||
const canvas = await page.$("[data-five-axis-canvas]");
|
||||
if (!canvas) {
|
||||
throw new Error(`missing canvas for ${name}`);
|
||||
}
|
||||
await canvas.screenshot({ path: screenshotPath });
|
||||
report.screenshots[name] = screenshotPath;
|
||||
return screenshotPath;
|
||||
}
|
||||
|
||||
async function recordCase(name, summary, checks = []) {
|
||||
const dataset = await getCanvasDataset();
|
||||
const state = await page.evaluate(() => JSON.parse(JSON.stringify(window.webRtcp5AxisSimulation.getState())));
|
||||
const screenshotPath = report.screenshots[name];
|
||||
const pixelStats = screenshotPath ? await analyzePng(screenshotPath) : null;
|
||||
report.cases.push({
|
||||
name,
|
||||
summary,
|
||||
status: checks.every((check) => check.pass) ? "PASS" : "FAIL",
|
||||
checks,
|
||||
pixelStats,
|
||||
dataset,
|
||||
state: {
|
||||
activeProgram: state.activeProgram,
|
||||
activeLine: state.activeLine,
|
||||
programSource: state.programSource,
|
||||
programExecutionSourceMode: state.programExecutionSourceMode,
|
||||
programExecutionSummary: state.programExecution?.summary || null,
|
||||
runState: state.runState,
|
||||
rtcpState: state.rtcpState,
|
||||
kinsType: state.kinsType,
|
||||
activeLine: state.activeLine,
|
||||
programExecutionMotionIndex: state.programExecutionMotionIndex,
|
||||
programExecutionSampleIndex: state.programExecutionSampleIndex,
|
||||
programRuntimeFeedbackSource: state.programRuntimeFeedback?.sourceMode || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function previewChecks(dataset, {
|
||||
requirePathPoints = true,
|
||||
requireExecutedPath = false,
|
||||
requireArcPoints = false,
|
||||
requireRapidFeed = false,
|
||||
expectPathPoints = null,
|
||||
expectRtcp = null,
|
||||
} = {}) {
|
||||
const checks = [
|
||||
check("canvas ready", dataset.threeReady === "true", `threeReady=${dataset.threeReady}`),
|
||||
check("WebGL renderer", dataset.threeRenderer === "webgl", `renderer=${dataset.threeRenderer}`),
|
||||
check("机床参考模型", dataset.threePreviewScope === "machine-reference-and-toolpath" && dataset.threeMachineReferenceModel === "webgl-five-axis-reference", `scope=${dataset.threePreviewScope}, model=${dataset.threeMachineReferenceModel}`),
|
||||
check("TCP 球标记", dataset.threeTcpMarker === "sphere" && dataset.threeToolExecutionMarker === "true", `tcp=${dataset.threeTcpMarker}, marker=${dataset.threeToolExecutionMarker}`),
|
||||
check("刀轴线", dataset.threeToolAxisMarker === "line", `toolAxisMarker=${dataset.threeToolAxisMarker}`),
|
||||
check("场景对象数量", Number(dataset.threeSceneObjects || 0) >= 12, `sceneObjects=${dataset.threeSceneObjects}`),
|
||||
check("无 G-code 语义生成", dataset.threeNoGcodeSemanticsGeneration === "ok", `semanticGuard=${dataset.threeNoGcodeSemanticsGeneration}`),
|
||||
];
|
||||
if (requirePathPoints) {
|
||||
checks.push(check("刀路预览点", Number(dataset.threePathPoints || 0) >= 1, `pathPoints=${dataset.threePathPoints}`));
|
||||
}
|
||||
if (requireExecutedPath) {
|
||||
checks.push(check("执行轨迹点", Number(dataset.threeExecutedPathPoints || 0) >= 1, `executed=${dataset.threeExecutedPathPoints}`));
|
||||
}
|
||||
if (requireArcPoints) {
|
||||
checks.push(check("圆弧轨迹点", Number(dataset.threeArcPathPoints || 0) >= 1, `arc=${dataset.threeArcPathPoints}`));
|
||||
}
|
||||
if (requireRapidFeed) {
|
||||
checks.push(check("rapid/feed 区分", Number(dataset.threeRapidPathPoints || 0) >= 1 && Number(dataset.threeFeedPathPoints || 0) >= 1, `rapid=${dataset.threeRapidPathPoints}, feed=${dataset.threeFeedPathPoints}`));
|
||||
}
|
||||
if (expectPathPoints !== null) {
|
||||
checks.push(check("路径点期望", Number(dataset.threePathPoints || 0) === expectPathPoints, `pathPoints=${dataset.threePathPoints}, expected=${expectPathPoints}`));
|
||||
}
|
||||
if (expectRtcp !== null) {
|
||||
checks.push(check("RTCP 状态", dataset.threeRtcpState === expectRtcp, `rtcp=${dataset.threeRtcpState}`));
|
||||
}
|
||||
return checks;
|
||||
}
|
||||
|
||||
function check(name, pass, detail) {
|
||||
return { name, pass: Boolean(pass), detail };
|
||||
}
|
||||
|
||||
function assertChecks(caseName, checks) {
|
||||
const failed = checks.filter((item) => !item.pass);
|
||||
if (failed.length > 0) {
|
||||
console.warn(`${caseName} failed: ${failed.map((item) => `${item.name} (${item.detail})`).join("; ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await page.goto(`${baseUrl}${FIXTURE_URL}`, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
|
||||
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 });
|
||||
await waitForState((state) => state.kinematicsRuntimeReadiness?.loaded === true, 20000, "kinematics runtime ready");
|
||||
await waitForCanvasReady();
|
||||
await wait(1200);
|
||||
|
||||
let dataset = await getCanvasDataset();
|
||||
let checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true });
|
||||
assertChecks("01-home-toolpath", checks);
|
||||
await captureCase("01-home-toolpath");
|
||||
await recordCase("01-home-toolpath", "默认首屏预览:机床参考模型、TCP 球、刀轴线、fixture 路径和执行轨迹可见", checks);
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.webRtcp5AxisSimulation.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "operator-demo.ngc",
|
||||
content: [
|
||||
"G90 G17",
|
||||
"G0 X0 Y0 Z0",
|
||||
"G1 X10 F100",
|
||||
"G1 Y10",
|
||||
"G1 X0",
|
||||
"G1 Y0",
|
||||
"G0 Z5",
|
||||
"M5",
|
||||
"M2",
|
||||
].join("\n"),
|
||||
});
|
||||
});
|
||||
await waitForState((state) => state.programExecutionSourceMode === "linuxcnc-interpreter-wasm" && state.activeProgram === "operator-demo.ngc", 20000, "operator demo loaded");
|
||||
await waitForCanvasReady();
|
||||
await wait(500);
|
||||
dataset = await getCanvasDataset();
|
||||
checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true, requireRapidFeed: true });
|
||||
assertChecks("02-operator-demo-toolpath", checks);
|
||||
await captureCase("02-operator-demo-toolpath");
|
||||
await recordCase("02-operator-demo-toolpath", "本地矩形 G-code:验证 LinuxCNC interpreter canonical motion、rapid/feed 区分、执行轨迹和 TCP 标记", checks);
|
||||
|
||||
await page.evaluate(() => {
|
||||
window.webRtcp5AxisSimulation.dispatch({
|
||||
type: "LOAD_PROGRAM",
|
||||
filename: "operator-arc-demo.ngc",
|
||||
content: [
|
||||
"G90 G17",
|
||||
"G0 X1 Y0 Z0",
|
||||
"G2 X0 Y1 I-1 J0 F60",
|
||||
"M2",
|
||||
].join("\n"),
|
||||
});
|
||||
});
|
||||
await waitForState((state) => state.activeProgram === "operator-arc-demo.ngc" && state.programExecution?.summary?.motionTypes?.includes("ARC_FEED"), 20000, "arc demo loaded");
|
||||
await waitForCanvasReady();
|
||||
await wait(500);
|
||||
dataset = await getCanvasDataset();
|
||||
checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true, requireArcPoints: true });
|
||||
assertChecks("03-arc-demo-toolpath", checks);
|
||||
await captureCase("03-arc-demo-toolpath");
|
||||
await recordCase("03-arc-demo-toolpath", "圆弧 G-code:验证 ARC_FEED 进入弧线轨迹图层并保留执行轨迹", checks);
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="clear-preview"]').click());
|
||||
await waitForCanvasReady();
|
||||
await wait(500);
|
||||
dataset = await getCanvasDataset();
|
||||
checks = previewChecks(dataset, { requirePathPoints: false, expectPathPoints: 0 });
|
||||
assertChecks("04-clear-preview-reference", checks);
|
||||
await captureCase("04-clear-preview-reference");
|
||||
await recordCase("04-clear-preview-reference", "清空刀路后:路径点为 0,但机床参考模型、TCP 球和刀轴线仍应可见", checks);
|
||||
|
||||
await waitForState((state) => state.machineFileStaging?.status === "staged" && (state.machineFileStaging?.gcodeSources?.length || 0) >= 4, 25000, "machine files staged");
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc");
|
||||
await waitForState((state) => state.activeProgram?.endsWith("impeller-7bl-xyzac.ngc"), 25000, "vendored impeller loaded");
|
||||
await waitForCanvasReady();
|
||||
await wait(1200);
|
||||
dataset = await getCanvasDataset();
|
||||
checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true, requireRapidFeed: true, expectRtcp: "on" });
|
||||
assertChecks("05-vendored-impeller-toolpath", checks);
|
||||
await captureCase("05-vendored-impeller-toolpath");
|
||||
await recordCase("05-vendored-impeller-toolpath", "LinuxCNC vendored 五轴 impeller 程序:验证长路径、switchkins/RTCP 状态、rapid/feed 图层和 TCP 执行轨迹", checks);
|
||||
|
||||
await page.evaluate(() => document.querySelector('[data-action="power"]').click());
|
||||
await waitForState((state) => state.machine.taskState === "on", 10000, "machine powered on");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-manual"]').click());
|
||||
await waitForState((state) => state.machine.mode === "manual", 10000, "manual mode selected");
|
||||
await page.evaluate(() => document.querySelector('[data-action="HOME"]').click());
|
||||
await waitForState((state) => state.machine.allHomed === true, 10000, "machine homed");
|
||||
await page.evaluate(() => document.querySelector('[data-action="mode-auto"]').click());
|
||||
await waitForState((state) => state.machine.mode === "auto", 10000, "auto mode selected");
|
||||
await page.evaluate(() => document.querySelector('[data-action="kins-tcp"]').click());
|
||||
await waitForState((state) => state.rtcpState === "on" && state.kinsType === "tcp-xyzac", 10000, "RTCP enabled");
|
||||
await page.evaluate(() => document.querySelector('[data-action="RUN"]').click());
|
||||
await waitForState((state) => state.runState === "running" && state.programRuntimeFeedback?.sourceMode === "linuxcnc-task-motion-hal-wasm", 15000, "program running");
|
||||
await waitForCanvasReady();
|
||||
await wait(600);
|
||||
dataset = await getCanvasDataset();
|
||||
checks = previewChecks(dataset, { requirePathPoints: true, requireExecutedPath: true, requireRapidFeed: true, expectRtcp: "on" });
|
||||
assertChecks("06-running-rtcp-toolpath", checks);
|
||||
await captureCase("06-running-rtcp-toolpath");
|
||||
await recordCase("06-running-rtcp-toolpath", "G-code 运行态:验证 task/HAL runtime feedback 驱动执行轨迹、当前段高亮、TCP 球和刀轴线跟随", checks);
|
||||
|
||||
report.consoleErrors = consoleErrors;
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "toolpath-preview-cases.json"),
|
||||
`${JSON.stringify(report, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
} finally {
|
||||
await page.close().catch(() => {});
|
||||
await browser.close().catch(() => {});
|
||||
await new Promise((resolve) => server.close(resolve));
|
||||
}
|
||||
|
||||
async function analyzePng(filePath) {
|
||||
const buffer = await fs.readFile(filePath);
|
||||
const png = PNG.sync.read(buffer);
|
||||
const { width, height, data } = png;
|
||||
let luminanceSum = 0;
|
||||
let nonBlack = 0;
|
||||
for (let index = 0; index < data.length; index += 4) {
|
||||
const luminance = data[index] * 0.2126 + data[index + 1] * 0.7152 + data[index + 2] * 0.0722;
|
||||
luminanceSum += luminance;
|
||||
if (luminance > 8) nonBlack += 1;
|
||||
}
|
||||
const total = width * height;
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
averageLuminance: Number((luminanceSum / total).toFixed(2)),
|
||||
nonBlackRatio: Number((nonBlack / total).toFixed(4)),
|
||||
};
|
||||
}
|
||||
7
qa/web-rtcp-5axis-site-test/fixtures/test-program.ngc
Normal file
@@ -0,0 +1,7 @@
|
||||
%
|
||||
G90 G17 G21
|
||||
G0 X0 Y0 Z5
|
||||
G1 Z-1 F200
|
||||
G1 X10 Y10 F300
|
||||
M30
|
||||
%
|
||||
260
qa/web-rtcp-5axis-site-test/generate-docx-report.mjs
Normal file
@@ -0,0 +1,260 @@
|
||||
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 SCREENSHOT_DIR = path.join(ROOT, "screenshots");
|
||||
const reportPath = path.join(OUTPUT_DIR, "site-test-report.json");
|
||||
const rawReport = JSON.parse(await fs.readFile(reportPath, "utf8"));
|
||||
|
||||
const generated = new Date(rawReport.generatedAt || Date.now());
|
||||
const ymd = generated.toISOString().slice(0, 10);
|
||||
const docxPath = path.join(OUTPUT_DIR, `web-rtcp-5axis-site-test-report-${ymd}.docx`);
|
||||
const counts = countByStatus(rawReport.findings || []);
|
||||
const total = rawReport.findings?.length || 0;
|
||||
const nonPassFindings = (rawReport.findings || []).filter((finding) => finding.status !== "PASS");
|
||||
const screenshotEntries = Object.entries(rawReport.screenshots || {}).sort(([a], [b]) => a.localeCompare(b));
|
||||
|
||||
const environmentRows = [
|
||||
["测试目标", rawReport.targetUrl || "https://82.156.24.101:8092/"],
|
||||
["测试日期", ymd],
|
||||
["测试方式", "Google Chrome 真实浏览器自动化测试 + DOM/runtime state 校验 + 截图留证"],
|
||||
["浏览器执行路径", rawReport.chromePath || "/usr/bin/google-chrome"],
|
||||
["报告数据", reportPath],
|
||||
["截图目录", SCREENSHOT_DIR],
|
||||
];
|
||||
|
||||
const scopeRows = [
|
||||
["界面结构", "标题栏、预览区、DRO、G-code 区、右侧模式栏、信息区、override、主轴/冷却、底部控制栏"],
|
||||
["机床/模式", "POWER、E-STOP、RESET、AUTO、MANUAL/JOG、MDI、HOME、JOG"],
|
||||
["五轴/RTCP", "IDENTITY/TCP、MDI M428/M429、RTCP/kinematics 诊断"],
|
||||
["程序工作流", "Stage LinuxCNC 源程序、加载 vendored 程序、打开本地 G-code、Run/Pause/Resume/Step/Stop/Reload"],
|
||||
["操作控制", "Rapid Override、Feed Override、Spindle Override、Flood/Mist、预览视角、全屏"],
|
||||
["会话与诊断", "Save Session、Restore Session、Audit Full Boundary、Profile 切换"],
|
||||
];
|
||||
|
||||
const children = [
|
||||
new Paragraph({
|
||||
text: "Web RTCP 5 Axis Simulation 功能测试报告",
|
||||
heading: HeadingLevel.TITLE,
|
||||
alignment: AlignmentType.CENTER,
|
||||
}),
|
||||
centered(`测试对象:${rawReport.targetUrl || "https://82.156.24.101:8092/"}`),
|
||||
centered(`生成时间:${formatDateTime(generated)}`),
|
||||
blank(),
|
||||
heading("1. 结论摘要"),
|
||||
para(`本次共测试 ${total} 个功能点:PASS ${counts.PASS || 0} 项,FAIL ${counts.FAIL || 0} 项,WARN ${counts.WARN || 0} 项。`),
|
||||
para(summaryText(counts)),
|
||||
summaryTable(total, counts),
|
||||
heading("2. 测试环境"),
|
||||
kvTable(environmentRows),
|
||||
heading("3. 测试范围"),
|
||||
kvTable(scopeRows),
|
||||
heading("4. 问题清单"),
|
||||
nonPassFindings.length > 0
|
||||
? findingTable(nonPassFindings)
|
||||
: para("未发现 FAIL/WARN 项。"),
|
||||
heading("5. 详细测试结果"),
|
||||
resultTable(rawReport.findings || []),
|
||||
heading("6. 截图证据"),
|
||||
];
|
||||
|
||||
for (const [name, filePath] of screenshotEntries) {
|
||||
children.push(...(await imageBlock(name, filePath)));
|
||||
}
|
||||
|
||||
children.push(
|
||||
heading("7. 运行日志摘要"),
|
||||
kvTable([
|
||||
["Console error", String((rawReport.consoleLogs || []).filter((log) => log.type === "error").length)],
|
||||
["Page error", String((rawReport.pageErrors || []).length)],
|
||||
["Request failure", String((rawReport.requestFailures || []).length)],
|
||||
["首屏截图平均亮度", String(rawReport.screenshotAnalysis?.averageLuminance ?? "-")],
|
||||
["首屏非黑像素比例", String(rawReport.screenshotAnalysis?.nonBlackRatio ?? "-")],
|
||||
["最终 RTCP 边界", rawReport.finalSummary?.frameBoundary || "-"],
|
||||
["最终 Task/HAL", rawReport.finalSummary?.taskHal || "-"],
|
||||
]),
|
||||
heading("8. 原始证据文件"),
|
||||
para(`原始 JSON:${reportPath}`),
|
||||
para(`Word 报告:${docxPath}`),
|
||||
);
|
||||
|
||||
const doc = new Document({
|
||||
sections: [{ properties: {}, children: children.flat() }],
|
||||
});
|
||||
|
||||
await fs.writeFile(docxPath, await Packer.toBuffer(doc));
|
||||
console.log(`docx_report=${docxPath}`);
|
||||
|
||||
function countByStatus(findings) {
|
||||
return findings.reduce((acc, finding) => {
|
||||
acc[finding.status] = (acc[finding.status] || 0) + 1;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function summaryText(statusCounts) {
|
||||
if ((statusCounts.FAIL || 0) > 0) {
|
||||
return "主流程可执行,但仍存在需修复的问题;重点集中在预览 ready 诊断、首屏 LinuxCNC kinematics 自动挂接、HOME 后 JOG 坐标连续性、MDI M428 和会话恢复验证。";
|
||||
}
|
||||
if ((statusCounts.WARN || 0) > 0) {
|
||||
return "主流程通过,存在需要后续确认的 WARN 项。";
|
||||
}
|
||||
return "全部测试项通过。";
|
||||
}
|
||||
|
||||
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(text)],
|
||||
});
|
||||
}
|
||||
|
||||
function blank() {
|
||||
return new Paragraph({ text: "" });
|
||||
}
|
||||
|
||||
function para(text) {
|
||||
return new Paragraph({
|
||||
children: [new TextRun(String(text))],
|
||||
spacing: { after: 100 },
|
||||
});
|
||||
}
|
||||
|
||||
function summaryTable(summaryTotal, statusCounts) {
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [
|
||||
cell("总项数", true),
|
||||
cell(String(summaryTotal)),
|
||||
cell("PASS", true),
|
||||
cell(String(statusCounts.PASS || 0)),
|
||||
cell("FAIL", true),
|
||||
cell(String(statusCounts.FAIL || 0)),
|
||||
cell("WARN", true),
|
||||
cell(String(statusCounts.WARN || 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 findingTable(findings) {
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [cell("结果", true), cell("功能点", true), cell("实际表现/证据", true), cell("备注", true)],
|
||||
}),
|
||||
...findings.map((finding) => new TableRow({
|
||||
children: [
|
||||
cell(finding.status),
|
||||
cell(`${finding.key}\n${finding.title}`),
|
||||
cell(finding.actual),
|
||||
cell(finding.detail || finding.expectation || "-"),
|
||||
],
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
function resultTable(findings) {
|
||||
return new Table({
|
||||
width: { size: 100, type: WidthType.PERCENTAGE },
|
||||
rows: [
|
||||
new TableRow({
|
||||
children: [cell("序号", true), cell("功能点", true), cell("预期", true), cell("实际", true), cell("结果", true)],
|
||||
}),
|
||||
...findings.map((finding, index) => new TableRow({
|
||||
children: [
|
||||
cell(String(index + 1)),
|
||||
cell(`${finding.key}\n${finding.title}`),
|
||||
cell(finding.expectation),
|
||||
cell(finding.actual),
|
||||
cell(finding.status),
|
||||
],
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
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 })],
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
async function imageBlock(name, filePath) {
|
||||
const image = await fs.readFile(filePath);
|
||||
return [
|
||||
new Paragraph({
|
||||
text: screenshotTitle(name),
|
||||
heading: HeadingLevel.HEADING_2,
|
||||
spacing: { before: 180, after: 120 },
|
||||
}),
|
||||
new Paragraph({
|
||||
alignment: AlignmentType.CENTER,
|
||||
children: [
|
||||
new ImageRun({
|
||||
data: image,
|
||||
type: "png",
|
||||
transformation: { width: 520, height: 390 },
|
||||
}),
|
||||
],
|
||||
}),
|
||||
para(`${screenshotTitle(name)};文件:${filePath}`),
|
||||
];
|
||||
}
|
||||
|
||||
function screenshotTitle(name) {
|
||||
const titles = {
|
||||
"01-home": "图 1 首屏界面",
|
||||
"02-profile-xyzbc": "图 2 Profile 切换到 xyzbc-trt",
|
||||
"03-vendored-program-loaded": "图 3 LinuxCNC 五轴源程序加载",
|
||||
"04-run-state": "图 4 程序运行状态",
|
||||
"05-local-program-opened": "图 5 本地 G-code 文件导入",
|
||||
"06-after-audit": "图 6 Audit Full Boundary 后界面",
|
||||
"07-final": "图 7 急停/复位后最终界面",
|
||||
};
|
||||
return titles[name] || name;
|
||||
}
|
||||
|
||||
function formatDateTime(date) {
|
||||
return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, " UTC");
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
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");
|
||||
}
|
||||
4349
qa/web-rtcp-5axis-site-test/output/site-test-report.json
Normal file
881
qa/web-rtcp-5axis-site-test/output/toolpath-preview-cases.json
Normal file
@@ -0,0 +1,881 @@
|
||||
{
|
||||
"generatedAt": "2026-06-22T13:12:11.146Z",
|
||||
"targetUrl": "http://127.0.0.1:43627/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"projectPath": "/home/meswork/cnc_wams/web-rtcp-5axis-sim-plan/app/index.html",
|
||||
"chromePath": "/usr/bin/google-chrome",
|
||||
"screenshots": {
|
||||
"01-home-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/01-home-toolpath.png",
|
||||
"02-operator-demo-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/02-operator-demo-toolpath.png",
|
||||
"03-arc-demo-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/03-arc-demo-toolpath.png",
|
||||
"04-clear-preview-reference": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/04-clear-preview-reference.png",
|
||||
"05-vendored-impeller-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/05-vendored-impeller-toolpath.png",
|
||||
"06-running-rtcp-toolpath": "/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/06-running-rtcp-toolpath.png"
|
||||
},
|
||||
"cases": [
|
||||
{
|
||||
"name": "01-home-toolpath",
|
||||
"summary": "默认首屏预览:机床参考模型、TCP 球、刀轴线、fixture 路径和执行轨迹可见",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=2"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
}
|
||||
],
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 62.52,
|
||||
"nonBlackRatio": 0.7491
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "2",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "2",
|
||||
"threeFeedPathPoints": "0",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "2",
|
||||
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/boat-xyzac.ngc",
|
||||
"activeLine": 1,
|
||||
"programSource": "linuxcnc-vendored-5axis-gcode",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
"ready": true,
|
||||
"programLineCount": 1868,
|
||||
"canonicalEventCount": 29,
|
||||
"motionEventCount": 2,
|
||||
"motionTypes": [
|
||||
"STRAIGHT_TRAVERSE"
|
||||
],
|
||||
"finalAxes": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 5,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"w": 0
|
||||
},
|
||||
"switchkinsEventCount": 2,
|
||||
"switchkinsCodes": [
|
||||
"M428",
|
||||
"M429"
|
||||
],
|
||||
"switchkinsRemapBoundary": "linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
|
||||
"remapRuntimeReady": false,
|
||||
"plannerRuntimeReady": true,
|
||||
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
"machineFileExecutionReady": false,
|
||||
"fullLinuxCncProgramExecutionReady": false
|
||||
},
|
||||
"runState": "idle",
|
||||
"rtcpState": "on",
|
||||
"kinsType": "tcp-xyzac",
|
||||
"programExecutionMotionIndex": 0,
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-task-motion-hal-wasm"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "02-operator-demo-toolpath",
|
||||
"summary": "本地矩形 G-code:验证 LinuxCNC interpreter canonical motion、rapid/feed 区分、执行轨迹和 TCP 标记",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=6"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "rapid/feed 区分",
|
||||
"pass": true,
|
||||
"detail": "rapid=2, feed=4"
|
||||
}
|
||||
],
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 59.4,
|
||||
"nonBlackRatio": 0.7092
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "6",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":0,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "2",
|
||||
"threeFeedPathPoints": "4",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "2",
|
||||
"threeCurrentSegmentType": "STRAIGHT_FEED"
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "operator-demo.ngc",
|
||||
"activeLine": 2,
|
||||
"programSource": "operator-file",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
"ready": true,
|
||||
"programLineCount": 9,
|
||||
"canonicalEventCount": 37,
|
||||
"motionEventCount": 6,
|
||||
"motionTypes": [
|
||||
"STRAIGHT_TRAVERSE",
|
||||
"STRAIGHT_FEED"
|
||||
],
|
||||
"finalAxes": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 5,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"w": 0
|
||||
},
|
||||
"switchkinsEventCount": 0,
|
||||
"switchkinsCodes": [],
|
||||
"switchkinsRemapBoundary": null,
|
||||
"remapRuntimeReady": false,
|
||||
"plannerRuntimeReady": true,
|
||||
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
"machineFileExecutionReady": false,
|
||||
"fullLinuxCncProgramExecutionReady": false
|
||||
},
|
||||
"runState": "idle",
|
||||
"rtcpState": "on",
|
||||
"kinsType": "tcp-xyzac",
|
||||
"programExecutionMotionIndex": 0,
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "03-arc-demo-toolpath",
|
||||
"summary": "圆弧 G-code:验证 ARC_FEED 进入弧线轨迹图层并保留执行轨迹",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=2"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "圆弧轨迹点",
|
||||
"pass": true,
|
||||
"detail": "arc=1"
|
||||
}
|
||||
],
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 65.26,
|
||||
"nonBlackRatio": 0.7885
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "2",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":1,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "1",
|
||||
"threeFeedPathPoints": "0",
|
||||
"threeArcPathPoints": "1",
|
||||
"threeCurrentSegmentPoints": "1",
|
||||
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "operator-arc-demo.ngc",
|
||||
"activeLine": 2,
|
||||
"programSource": "operator-file",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
"ready": true,
|
||||
"programLineCount": 4,
|
||||
"canonicalEventCount": 19,
|
||||
"motionEventCount": 2,
|
||||
"motionTypes": [
|
||||
"STRAIGHT_TRAVERSE",
|
||||
"ARC_FEED"
|
||||
],
|
||||
"finalAxes": {
|
||||
"x": 0,
|
||||
"y": 1,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"w": 0,
|
||||
"arc": {
|
||||
"plane": 170,
|
||||
"firstAxis": "x",
|
||||
"secondAxis": "y",
|
||||
"thirdAxis": "z",
|
||||
"firstEnd": 0,
|
||||
"secondEnd": 1,
|
||||
"centerFirst": 0,
|
||||
"centerSecond": 0,
|
||||
"rotation": -1,
|
||||
"axisEndPoint": 0
|
||||
}
|
||||
},
|
||||
"switchkinsEventCount": 0,
|
||||
"switchkinsCodes": [],
|
||||
"switchkinsRemapBoundary": null,
|
||||
"remapRuntimeReady": false,
|
||||
"plannerRuntimeReady": true,
|
||||
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
"machineFileExecutionReady": false,
|
||||
"fullLinuxCncProgramExecutionReady": false
|
||||
},
|
||||
"runState": "idle",
|
||||
"rtcpState": "on",
|
||||
"kinsType": "tcp-xyzac",
|
||||
"programExecutionMotionIndex": 0,
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "04-clear-preview-reference",
|
||||
"summary": "清空刀路后:路径点为 0,但机床参考模型、TCP 球和刀轴线仍应可见",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "路径点期望",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=0, expected=0"
|
||||
}
|
||||
],
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 64.72,
|
||||
"nonBlackRatio": 0.7874
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "0",
|
||||
"threeExecutedPathPoints": "0",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolAxis": "{\"x\":0,\"y\":0,\"z\":1}",
|
||||
"threeTcpPose": "{\"x\":1,\"y\":0,\"z\":0,\"a\":0,\"c\":0}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threeCurrentSegmentHighlight": "pending",
|
||||
"threeRapidFeedVisualDistinction": "pending",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "0",
|
||||
"threeFeedPathPoints": "0",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "0",
|
||||
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "operator-arc-demo.ngc",
|
||||
"activeLine": 2,
|
||||
"programSource": "operator-file",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
"ready": true,
|
||||
"programLineCount": 4,
|
||||
"canonicalEventCount": 19,
|
||||
"motionEventCount": 2,
|
||||
"motionTypes": [
|
||||
"STRAIGHT_TRAVERSE",
|
||||
"ARC_FEED"
|
||||
],
|
||||
"finalAxes": {
|
||||
"x": 0,
|
||||
"y": 1,
|
||||
"z": 0,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"w": 0,
|
||||
"arc": {
|
||||
"plane": 170,
|
||||
"firstAxis": "x",
|
||||
"secondAxis": "y",
|
||||
"thirdAxis": "z",
|
||||
"firstEnd": 0,
|
||||
"secondEnd": 1,
|
||||
"centerFirst": 0,
|
||||
"centerSecond": 0,
|
||||
"rotation": -1,
|
||||
"axisEndPoint": 0
|
||||
}
|
||||
},
|
||||
"switchkinsEventCount": 0,
|
||||
"switchkinsCodes": [],
|
||||
"switchkinsRemapBoundary": null,
|
||||
"remapRuntimeReady": false,
|
||||
"plannerRuntimeReady": true,
|
||||
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
"machineFileExecutionReady": false,
|
||||
"fullLinuxCncProgramExecutionReady": false
|
||||
},
|
||||
"runState": "idle",
|
||||
"rtcpState": "on",
|
||||
"kinsType": "tcp-xyzac",
|
||||
"programExecutionMotionIndex": 0,
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "05-vendored-impeller-toolpath",
|
||||
"summary": "LinuxCNC vendored 五轴 impeller 程序:验证长路径、switchkins/RTCP 状态、rapid/feed 图层和 TCP 执行轨迹",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=1498"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "rapid/feed 区分",
|
||||
"pass": true,
|
||||
"detail": "rapid=186, feed=1436"
|
||||
},
|
||||
{
|
||||
"name": "RTCP 状态",
|
||||
"pass": true,
|
||||
"detail": "rtcp=on"
|
||||
}
|
||||
],
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 35.56,
|
||||
"nonBlackRatio": 0.4577
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0,\"y\":0,\"z\":0.35}",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":36.474,\"y\":-22.486,\"z\":-13.749,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "186",
|
||||
"threeFeedPathPoints": "1436",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "1",
|
||||
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
"activeLine": 8,
|
||||
"programSource": "linuxcnc-vendored-5axis-gcode",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
"ready": true,
|
||||
"programLineCount": 4507,
|
||||
"canonicalEventCount": 22313,
|
||||
"motionEventCount": 4492,
|
||||
"motionTypes": [
|
||||
"STRAIGHT_TRAVERSE",
|
||||
"STRAIGHT_FEED"
|
||||
],
|
||||
"finalAxes": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 40,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"w": 0
|
||||
},
|
||||
"switchkinsEventCount": 2,
|
||||
"switchkinsCodes": [
|
||||
"M428",
|
||||
"M429"
|
||||
],
|
||||
"switchkinsRemapBoundary": "linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
|
||||
"remapRuntimeReady": false,
|
||||
"plannerRuntimeReady": true,
|
||||
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
"machineFileExecutionReady": false,
|
||||
"fullLinuxCncProgramExecutionReady": false
|
||||
},
|
||||
"runState": "idle",
|
||||
"rtcpState": "on",
|
||||
"kinsType": "tcp-xyzac",
|
||||
"programExecutionMotionIndex": 0,
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-tp-runtime-sample"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "06-running-rtcp-toolpath",
|
||||
"summary": "G-code 运行态:验证 task/HAL runtime feedback 驱动执行轨迹、当前段高亮、TCP 球和刀轴线跟随",
|
||||
"status": "PASS",
|
||||
"checks": [
|
||||
{
|
||||
"name": "canvas ready",
|
||||
"pass": true,
|
||||
"detail": "threeReady=true"
|
||||
},
|
||||
{
|
||||
"name": "WebGL renderer",
|
||||
"pass": true,
|
||||
"detail": "renderer=webgl"
|
||||
},
|
||||
{
|
||||
"name": "机床参考模型",
|
||||
"pass": true,
|
||||
"detail": "scope=machine-reference-and-toolpath, model=webgl-five-axis-reference"
|
||||
},
|
||||
{
|
||||
"name": "TCP 球标记",
|
||||
"pass": true,
|
||||
"detail": "tcp=sphere, marker=true"
|
||||
},
|
||||
{
|
||||
"name": "刀轴线",
|
||||
"pass": true,
|
||||
"detail": "toolAxisMarker=line"
|
||||
},
|
||||
{
|
||||
"name": "场景对象数量",
|
||||
"pass": true,
|
||||
"detail": "sceneObjects=20"
|
||||
},
|
||||
{
|
||||
"name": "无 G-code 语义生成",
|
||||
"pass": true,
|
||||
"detail": "semanticGuard=ok"
|
||||
},
|
||||
{
|
||||
"name": "刀路预览点",
|
||||
"pass": true,
|
||||
"detail": "pathPoints=1498"
|
||||
},
|
||||
{
|
||||
"name": "执行轨迹点",
|
||||
"pass": true,
|
||||
"detail": "executed=1"
|
||||
},
|
||||
{
|
||||
"name": "rapid/feed 区分",
|
||||
"pass": true,
|
||||
"detail": "rapid=186, feed=1436"
|
||||
},
|
||||
{
|
||||
"name": "RTCP 状态",
|
||||
"pass": true,
|
||||
"detail": "rtcp=on"
|
||||
}
|
||||
],
|
||||
"pixelStats": {
|
||||
"width": 803,
|
||||
"height": 874,
|
||||
"averageLuminance": 36.05,
|
||||
"nonBlackRatio": 0.4607
|
||||
},
|
||||
"dataset": {
|
||||
"fiveAxisCanvas": "true",
|
||||
"engine": "three.js r183",
|
||||
"threeReady": "true",
|
||||
"threeRevision": "183",
|
||||
"threePathPoints": "1498",
|
||||
"threeExecutedPathPoints": "1",
|
||||
"threeSceneObjects": "20",
|
||||
"threeToolhead": "{\"x\":0.26,\"y\":-0.458,\"z\":1.485}",
|
||||
"threeToolAxis": "{\"x\":0.558,\"y\":0.769,\"z\":0.312}",
|
||||
"threeTcpPose": "{\"x\":7.417,\"y\":-13.098,\"z\":28.366,\"a\":-71.841,\"c\":-35.93}",
|
||||
"threeRtcpState": "on",
|
||||
"threeSelectedView": "iso",
|
||||
"threeFrameApi": "web-rtcp-5axis-motion-frame",
|
||||
"threeRenderer": "webgl",
|
||||
"threeSceneMode": "program-preview-and-tool-execution",
|
||||
"threePreviewScope": "machine-reference-and-toolpath",
|
||||
"threeMachineReferenceModel": "webgl-five-axis-reference",
|
||||
"threeCameraControls": "orbit-pan-zoom",
|
||||
"threeProgramPreviewSource": "linuxcnc-interpreter-wasm",
|
||||
"threeToolExecutionMarker": "true",
|
||||
"threeTcpMarker": "sphere",
|
||||
"threeToolAxisMarker": "line",
|
||||
"threeToolpathPreviewSource": "linuxcnc_interpreter_canonical_motion",
|
||||
"threeToolExecutionTraceSource": "linuxcnc_tp_samples_or_task_motion_hal_feedback",
|
||||
"threePathFitBounds": "ok",
|
||||
"threeCurrentSegmentHighlight": "ok",
|
||||
"threeRapidFeedVisualDistinction": "ok",
|
||||
"threeNoGcodeSemanticsGeneration": "ok",
|
||||
"threeRapidPathPoints": "186",
|
||||
"threeFeedPathPoints": "1436",
|
||||
"threeArcPathPoints": "0",
|
||||
"threeCurrentSegmentPoints": "1",
|
||||
"threeCurrentSegmentType": "STRAIGHT_TRAVERSE"
|
||||
},
|
||||
"state": {
|
||||
"activeProgram": "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/impeller-7bl-xyzac.ngc",
|
||||
"activeLine": 5,
|
||||
"programSource": "linuxcnc-vendored-5axis-gcode",
|
||||
"programExecutionSourceMode": "linuxcnc-interpreter-wasm",
|
||||
"programExecutionSummary": {
|
||||
"ready": true,
|
||||
"programLineCount": 4507,
|
||||
"canonicalEventCount": 22313,
|
||||
"motionEventCount": 4492,
|
||||
"motionTypes": [
|
||||
"STRAIGHT_TRAVERSE",
|
||||
"STRAIGHT_FEED"
|
||||
],
|
||||
"finalAxes": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"z": 40,
|
||||
"a": 0,
|
||||
"b": 0,
|
||||
"c": 0,
|
||||
"u": 0,
|
||||
"v": 0,
|
||||
"w": 0
|
||||
},
|
||||
"switchkinsEventCount": 2,
|
||||
"switchkinsCodes": [
|
||||
"M428",
|
||||
"M429"
|
||||
],
|
||||
"switchkinsRemapBoundary": "linuxcnc_switchkins_remap_mcode_preserved_web_runtime_applied",
|
||||
"remapRuntimeReady": false,
|
||||
"plannerRuntimeReady": true,
|
||||
"plannerSemanticBoundary": "linuxcnc_tp_queue_runtime_timing_from_canonical_motion",
|
||||
"machineFileExecutionReady": false,
|
||||
"fullLinuxCncProgramExecutionReady": false
|
||||
},
|
||||
"runState": "running",
|
||||
"rtcpState": "on",
|
||||
"kinsType": "tcp-xyzac",
|
||||
"programExecutionMotionIndex": 0,
|
||||
"programExecutionSampleIndex": 0,
|
||||
"programRuntimeFeedbackSource": "linuxcnc-task-motion-hal-wasm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"consoleErrors": [
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)",
|
||||
"Failed to load resource: the server responded with a status of 404 (Not Found)"
|
||||
]
|
||||
}
|
||||
516
qa/web-rtcp-5axis-site-test/package-lock.json
generated
Normal file
@@ -0,0 +1,516 @@
|
||||
{
|
||||
"name": "web-rtcp-5axis-site-test",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "web-rtcp-5axis-site-test",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"docx": "^9.7.1",
|
||||
"pngjs": "^7.0.0",
|
||||
"puppeteer-core": "^25.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.0.4.tgz",
|
||||
"integrity": "sha512-HGM8iAmGTf+Y7t0373szVbTmt3d7vPkYL/1bpOkOFO0YUYLgSeuYBCzESklogNPvOBnZ/MRD5f07OkpqH1trtA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"modern-tar": "^0.7.6",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"browsers": "lib/main-cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"proxy-agent": ">=8.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"proxy-agent": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.9.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.4.tgz",
|
||||
"integrity": "sha512-dszCsrKb5U7ZsVZBWiHFklTloVl0mSEnWH/iZXfZUlI4rzCUnsvGmgqfuVRHL54ugE7/wRuxEIXRa2iMZ+BG6g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-16.0.1.tgz",
|
||||
"integrity": "sha512-J63PGu/9PpeCwLIcKYyzWP6yaVL5pxuBc0shlYCYM8BaAkmlwiQboXO1iNbOgSDbVklEyYFfNEcHD8oOAWacUA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.19.0 <22.0.0 || >=22.12.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"devtools-protocol": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1624250",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1624250.tgz",
|
||||
"integrity": "sha512-YFAat/lOiIk0ARmBweG+ygrEcbZrq5B9urRyUoeQKp53MlidHXE2TmTbxKcaXoQj7u/aX+jebDO4BW55rs0WwA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/docx": {
|
||||
"version": "9.7.1",
|
||||
"resolved": "https://registry.npmjs.org/docx/-/docx-9.7.1.tgz",
|
||||
"integrity": "sha512-ilXFf9Moz47ABjFpDiA5s1w9lpb4EFSp7+5iiJSbfyYDM+bpZdAgLlSr7fW4aXhVe/E+F6QCv0EvRVFEd5CsWg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^25.2.3",
|
||||
"hash.js": "^1.1.7",
|
||||
"jszip": "^3.10.1",
|
||||
"nanoid": "^5.1.3",
|
||||
"xml": "^1.0.1",
|
||||
"xml-js": "^1.6.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/hash.js": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
|
||||
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.3",
|
||||
"minimalistic-assert": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isarray": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jszip": {
|
||||
"version": "3.10.1",
|
||||
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
|
||||
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
|
||||
"license": "(MIT OR GPL-3.0-or-later)",
|
||||
"dependencies": {
|
||||
"lie": "~3.3.0",
|
||||
"pako": "~1.0.2",
|
||||
"readable-stream": "~2.3.6",
|
||||
"setimmediate": "^1.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/lie": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
|
||||
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"immediate": "~3.0.5"
|
||||
}
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
|
||||
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/mitt": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
|
||||
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/modern-tar": {
|
||||
"version": "0.7.6",
|
||||
"resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.7.6.tgz",
|
||||
"integrity": "sha512-sweCIVXzx1aIGTCdzcMlSZt1h8k5Tmk08VNAuRk3IU28XamGiOH5ypi11g6De2CH7PhYqSSnGy2A/EFhbWnVKg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "5.1.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.15.tgz",
|
||||
"integrity": "sha512-kBg3RpGtIe+RpTbyXwoI6pk5yD7KUiI3sygUqgeBMRst42KmhB4RZC7eiO9Wa1HIpaCCtpE2DJ6OI4Wi5ebwFw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || >=20"
|
||||
}
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
|
||||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "25.1.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.1.0.tgz",
|
||||
"integrity": "sha512-jKzy5y4WG6uNuFbTWgW1D7mqoT9o0nllc/6a1DGF775T1mPmgw3scdFEtEq67yVFikavQmbYq6NLfbTfxHSlqQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "3.0.4",
|
||||
"chromium-bidi": "16.0.1",
|
||||
"devtools-protocol": "0.0.1624250",
|
||||
"typed-query-selector": "^2.12.2",
|
||||
"webdriver-bidi-protocol": "0.4.2",
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "2.3.8",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
|
||||
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sax": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz",
|
||||
"integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/typed-query-selector": {
|
||||
"version": "2.12.2",
|
||||
"resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
|
||||
"integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.24.6",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/webdriver-bidi-protocol": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
|
||||
"integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
|
||||
"integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/xml-js": {
|
||||
"version": "1.6.11",
|
||||
"resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz",
|
||||
"integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sax": "^1.2.4"
|
||||
},
|
||||
"bin": {
|
||||
"xml-js": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
|
||||
"integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.25.76",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
qa/web-rtcp-5axis-site-test/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "web-rtcp-5axis-site-test",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"docx": "^9.7.1",
|
||||
"pngjs": "^7.0.0",
|
||||
"puppeteer-core": "^25.1.0"
|
||||
}
|
||||
}
|
||||
752
qa/web-rtcp-5axis-site-test/run-site-test.mjs
Normal file
@@ -0,0 +1,752 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import puppeteer from "puppeteer-core";
|
||||
import { PNG } from "pngjs";
|
||||
|
||||
const ROOT = path.resolve("/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test");
|
||||
const OUTPUT_DIR = path.join(ROOT, "output");
|
||||
const SCREENSHOT_DIR = path.join(ROOT, "screenshots");
|
||||
const URL = "https://82.156.24.101:8092/";
|
||||
const CHROME_PATH = "/usr/bin/google-chrome";
|
||||
|
||||
const localProgramPath = path.join(ROOT, "fixtures", "test-program.ngc");
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
await fs.mkdir(SCREENSHOT_DIR, { recursive: true });
|
||||
await fs.mkdir(path.dirname(localProgramPath), { recursive: true });
|
||||
|
||||
const findings = [];
|
||||
const consoleLogs = [];
|
||||
const pageErrors = [];
|
||||
const requestFailures = [];
|
||||
|
||||
const browser = await puppeteer.launch({
|
||||
headless: true,
|
||||
executablePath: CHROME_PATH,
|
||||
defaultViewport: { width: 1600, height: 1200, deviceScaleFactor: 1 },
|
||||
ignoreHTTPSErrors: true,
|
||||
args: [
|
||||
"--ignore-certificate-errors",
|
||||
"--disable-gpu",
|
||||
"--enable-webgl",
|
||||
"--use-angle=swiftshader",
|
||||
"--enable-unsafe-swiftshader",
|
||||
"--no-sandbox",
|
||||
],
|
||||
});
|
||||
|
||||
let page;
|
||||
|
||||
try {
|
||||
page = await browser.newPage();
|
||||
page.on("console", async (msg) => {
|
||||
let text = msg.text();
|
||||
if (msg.type() === "error" && msg.args().length > 0) {
|
||||
try {
|
||||
const values = await Promise.all(msg.args().map((arg) => arg.jsonValue().catch(() => null)));
|
||||
const serialized = values.filter((value) => value !== null);
|
||||
if (serialized.length > 0) {
|
||||
text = `${text} ${JSON.stringify(serialized)}`;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
consoleLogs.push({
|
||||
type: msg.type(),
|
||||
text,
|
||||
location: msg.location(),
|
||||
});
|
||||
});
|
||||
page.on("pageerror", (error) => {
|
||||
pageErrors.push({
|
||||
message: error.message,
|
||||
stack: error.stack || "",
|
||||
});
|
||||
});
|
||||
page.on("requestfailed", (request) => {
|
||||
requestFailures.push({
|
||||
url: request.url(),
|
||||
method: request.method(),
|
||||
errorText: request.failure()?.errorText || "unknown",
|
||||
resourceType: request.resourceType(),
|
||||
});
|
||||
});
|
||||
|
||||
await fs.writeFile(localProgramPath, [
|
||||
"%",
|
||||
"G90 G17 G21",
|
||||
"G0 X0 Y0 Z5",
|
||||
"G1 Z-1 F200",
|
||||
"G1 X10 Y10 F300",
|
||||
"M30",
|
||||
"%",
|
||||
"",
|
||||
].join("\n"), "utf8");
|
||||
|
||||
await page.goto(URL, { waitUntil: "networkidle2", timeout: 60000 });
|
||||
await page.waitForSelector('[data-shell="gmoccapy-5axis"]', { timeout: 15000 });
|
||||
await page.waitForFunction(() => Boolean(window.webRtcp5AxisSimulation?.getState), { timeout: 15000 });
|
||||
await page.waitForFunction(() => {
|
||||
const state = window.webRtcp5AxisSimulation?.getState?.();
|
||||
return Boolean(state?.iniConfigReadiness?.loaded);
|
||||
}, { timeout: 20000 }).catch(() => {});
|
||||
await waitForAppIdle(20000);
|
||||
await waitForState(
|
||||
(state) => state.taskHalRuntimeReadiness?.halSyncReady === true,
|
||||
20000,
|
||||
"task/HAL runtime readiness",
|
||||
).catch(() => null);
|
||||
|
||||
const screenshots = {};
|
||||
|
||||
async function capture(name, clipSelector = null) {
|
||||
const targetPath = path.join(SCREENSHOT_DIR, `${name}.png`);
|
||||
if (clipSelector) {
|
||||
const element = await page.$(clipSelector);
|
||||
if (element) {
|
||||
await element.screenshot({ path: targetPath });
|
||||
} else {
|
||||
await page.screenshot({ path: targetPath, fullPage: true });
|
||||
}
|
||||
} else {
|
||||
await page.screenshot({ path: targetPath, fullPage: true });
|
||||
}
|
||||
screenshots[name] = targetPath;
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
async function getState() {
|
||||
return page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
return JSON.parse(JSON.stringify(state));
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForState(predicate, timeoutMs = 10000, label = "state condition") {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
const snapshot = await getState();
|
||||
if (predicate(snapshot)) return snapshot;
|
||||
await sleep(100);
|
||||
}
|
||||
throw new Error(`Timed out waiting for ${label}`);
|
||||
}
|
||||
|
||||
async function waitForAppIdle(timeoutMs = 8000) {
|
||||
await waitForState(
|
||||
(state) => !state.taskHalExecutionPending && !state.interpreterExecutionPending,
|
||||
timeoutMs,
|
||||
"app idle",
|
||||
).catch(() => null);
|
||||
}
|
||||
|
||||
async function getSummary() {
|
||||
return page.evaluate(() => {
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
const regions = window.webRtcp5AxisSimulation.getRegions?.() || null;
|
||||
const machineSummary = document.querySelector('[data-machine-state="summary"]')?.textContent?.trim() || "";
|
||||
const frameBoundary = document.querySelector('[data-rtcp-diagnostic="boundary"]')?.textContent?.trim() || "";
|
||||
const boundaryReady = document.querySelector('[data-linuxcnc-boundary="readiness"]')?.textContent?.trim() || "";
|
||||
const taskHal = document.querySelector('[data-task-hal-runtime="readiness"]')?.textContent?.trim() || "";
|
||||
const machineFiles = document.querySelector('[data-machine-file-staging="status"]')?.textContent?.trim() || "";
|
||||
const activeLine = document.querySelector('[data-active-program-line]')?.textContent?.trim() || "";
|
||||
const linuxCncSourceStatus = document.querySelector('[data-linuxcnc-gcode-source="status"]')?.textContent?.trim() || "";
|
||||
const canvas = document.querySelector("[data-five-axis-canvas]");
|
||||
return {
|
||||
regions,
|
||||
machineSummary,
|
||||
frameBoundary,
|
||||
boundaryReady,
|
||||
taskHal,
|
||||
machineFiles,
|
||||
activeLine,
|
||||
linuxCncSourceStatus,
|
||||
canvas: canvas ? { ...canvas.dataset } : null,
|
||||
state,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function recordResult(key, title, expectation, actual, status, detail = "") {
|
||||
findings.push({
|
||||
key,
|
||||
title,
|
||||
expectation,
|
||||
actual,
|
||||
status,
|
||||
detail,
|
||||
});
|
||||
}
|
||||
|
||||
async function clickAndWait(selector, waitMs = 600) {
|
||||
await page.click(selector);
|
||||
await sleep(waitMs);
|
||||
await waitForAppIdle();
|
||||
}
|
||||
|
||||
async function setInputValue(selector, value) {
|
||||
await page.$eval(selector, (el, nextValue) => {
|
||||
el.value = nextValue;
|
||||
el.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
el.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}, value);
|
||||
await sleep(400);
|
||||
}
|
||||
|
||||
function classify(condition, passText, failText) {
|
||||
return condition ? { status: "PASS", text: passText } : { status: "FAIL", text: failText };
|
||||
}
|
||||
|
||||
function classifyWarn(condition, passText, warnText) {
|
||||
return condition ? { status: "PASS", text: passText } : { status: "WARN", text: warnText };
|
||||
}
|
||||
|
||||
const initial = await getSummary();
|
||||
await capture("01-home");
|
||||
|
||||
const regionsPass = [
|
||||
"titlebar",
|
||||
"preview",
|
||||
"dro",
|
||||
"gcode",
|
||||
"status-sidebar",
|
||||
"info-tabs",
|
||||
"override",
|
||||
"spindle-coolant",
|
||||
"bottom-controls",
|
||||
].every((region) => initial.regions?.[region] === true);
|
||||
const regionsStatus = classify(regionsPass, "9个主区域全部渲染", "存在主区域未渲染");
|
||||
await recordResult(
|
||||
"layout-regions",
|
||||
"主界面九大区域渲染",
|
||||
"titlebar/preview/dro/gcode/sidebar/info/override/spindle/bottom 全部存在",
|
||||
regionsStatus.text,
|
||||
regionsStatus.status,
|
||||
);
|
||||
|
||||
const canvasFallback = initial.canvas?.threeRenderer === "2d-fallback";
|
||||
const canvasReady = initial.canvas?.threeReady === "true";
|
||||
const canvasStatus = canvasReady
|
||||
? (canvasFallback
|
||||
? { status: "WARN", text: `预览可用,但当前测试环境使用 ${initial.canvas.threeRenderer},原因:${initial.canvas.threeFallbackReason || "-"}` }
|
||||
: { status: "PASS", text: `预览已启用 ${initial.canvas.threeRenderer}` })
|
||||
: { status: "FAIL", text: "预览画布未进入 ready 状态" };
|
||||
await recordResult(
|
||||
"preview-canvas",
|
||||
"预览画布初始化",
|
||||
"预览区域可渲染机床与路径",
|
||||
canvasStatus.text,
|
||||
canvasStatus.status,
|
||||
initial.canvas ? JSON.stringify(initial.canvas) : "missing canvas dataset",
|
||||
);
|
||||
|
||||
const initialBoundaryStatus = classify(
|
||||
initial.frameBoundary.includes("linuxcnc_kinematics_wasm_c_abi"),
|
||||
`RTCP 帧边界已接入 LinuxCNC 运动学:${initial.frameBoundary}`,
|
||||
`RTCP 帧仍处于降级边界:${initial.frameBoundary}`,
|
||||
);
|
||||
await recordResult(
|
||||
"initial-boundary",
|
||||
"首屏 RTCP/运动学边界",
|
||||
"首屏应优先使用 LinuxCNC/WASM 运动学边界",
|
||||
initialBoundaryStatus.text,
|
||||
initialBoundaryStatus.status,
|
||||
);
|
||||
|
||||
const initialTaskHalStatus = classifyWarn(
|
||||
!/pending|blocked/i.test(initial.taskHal),
|
||||
`Task/HAL 已就绪:${initial.taskHal}`,
|
||||
`Task/HAL 未完成就绪:${initial.taskHal}`,
|
||||
);
|
||||
await recordResult(
|
||||
"initial-task-hal",
|
||||
"首屏 Task/HAL 运行态",
|
||||
"Task/HAL readiness 应就绪",
|
||||
initialTaskHalStatus.text,
|
||||
initialTaskHalStatus.status,
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="power"]');
|
||||
await waitForState((nextState) => nextState.machine.powerOn === true, 8000, "machine power on").catch(() => null);
|
||||
let state = await getState();
|
||||
await recordResult(
|
||||
"power-on",
|
||||
"POWER 上电",
|
||||
"点击 POWER 后 machine.powerOn=true,taskState=on",
|
||||
`powerOn=${state.machine.powerOn}, taskState=${state.machine.taskState}, runState=${state.runState}`,
|
||||
state.machine.powerOn && state.machine.taskState === "on" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mode-jog"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "manual", 8000, "JOG/manual mode").catch(() => null);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mode-jog",
|
||||
"JOG 模式切换",
|
||||
"点击 JOG 后 mode 归一到 manual",
|
||||
`mode=${state.machine.mode}`,
|
||||
state.machine.mode === "manual" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="HOME"]');
|
||||
await waitForState((nextState) => nextState.machine.allHomed === true, 8000, "machine homed").catch(() => null);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"home",
|
||||
"HOME 回参考点",
|
||||
"点击 HOME 后 allHomed=true,runState=idle",
|
||||
`allHomed=${state.machine.allHomed}, runState=${state.runState}`,
|
||||
state.machine.allHomed && state.runState === "idle" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeJogX = state.axisPose.x;
|
||||
await clickAndWait('[data-action="JOG_X_POS"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"jog-x-plus",
|
||||
"JOG X+",
|
||||
"点击 X+ 后 X 坐标增加",
|
||||
`before=${beforeJogX}, after=${state.axisPose.x}`,
|
||||
state.axisPose.x > beforeJogX ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeJogY = state.axisPose.y;
|
||||
await clickAndWait('[data-action="JOG_Y_NEG"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"jog-y-minus",
|
||||
"JOG Y-",
|
||||
"点击 Y- 后 Y 坐标减小",
|
||||
`before=${beforeJogY}, after=${state.axisPose.y}`,
|
||||
state.axisPose.y < beforeJogY ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mode-auto"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "auto", 8000, "AUTO mode").catch(() => null);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mode-auto",
|
||||
"AUTO 模式切换",
|
||||
"点击 AUTO 后 machine.mode=auto",
|
||||
`mode=${state.machine.mode}`,
|
||||
state.machine.mode === "auto" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mode-mdi"]');
|
||||
await waitForState((nextState) => nextState.machine.mode === "mdi", 8000, "MDI mode").catch(() => null);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mode-mdi",
|
||||
"MDI 模式切换",
|
||||
"点击 MDI 后 machine.mode=mdi",
|
||||
`mode=${state.machine.mode}`,
|
||||
state.machine.mode === "mdi" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await setInputValue('[data-action="mdi-command"]', "M428");
|
||||
await clickAndWait('[data-action="mdi-submit"]', 800);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mdi-m428",
|
||||
"MDI 执行 M428",
|
||||
"执行 M428 后 RTCP 打开,kinsType 切到 tcp-*",
|
||||
`rtcp=${state.rtcpState}, kinsType=${state.kinsType}, message=${state.operatorMessage}`,
|
||||
state.rtcpState === "on" && String(state.kinsType).startsWith("tcp-") ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="mdi-history"][data-command="M429"]', 800);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"mdi-m429",
|
||||
"MDI 快捷执行 M429",
|
||||
"执行 M429 后 RTCP 关闭,kinsType=identity",
|
||||
`rtcp=${state.rtcpState}, kinsType=${state.kinsType}, message=${state.operatorMessage}`,
|
||||
state.rtcpState === "off" && state.kinsType === "identity" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="kins-tcp"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"sidebar-tcp",
|
||||
"侧栏 TCP 按钮",
|
||||
"点击 TCP 后 RTCP 打开",
|
||||
`rtcp=${state.rtcpState}, kinsType=${state.kinsType}`,
|
||||
state.rtcpState === "on" && String(state.kinsType).startsWith("tcp-") ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="kins-identity"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"sidebar-identity",
|
||||
"侧栏 IDENTITY 按钮",
|
||||
"点击 IDENTITY 后 RTCP 关闭",
|
||||
`rtcp=${state.rtcpState}, kinsType=${state.kinsType}`,
|
||||
state.rtcpState === "off" && state.kinsType === "identity" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeRapid = state.feed.rapidOverride;
|
||||
await clickAndWait('[data-action="rapid-override-up"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"rapid-override",
|
||||
"Rapid Override 调整",
|
||||
"点击 + 后 rapidOverride 增加",
|
||||
`before=${beforeRapid}, after=${state.feed.rapidOverride}`,
|
||||
state.feed.rapidOverride > beforeRapid ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeFeed = state.feed.feedOverride;
|
||||
await clickAndWait('[data-action="feed-override-down"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"feed-override",
|
||||
"Feed Override 调整",
|
||||
"点击 - 后 feedOverride 减少",
|
||||
`before=${beforeFeed}, after=${state.feed.feedOverride}`,
|
||||
state.feed.feedOverride < beforeFeed ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeSpindle = state.spindle.override;
|
||||
await clickAndWait('[data-action="spindle-override-up"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"spindle-override",
|
||||
"Spindle Override 调整",
|
||||
"点击 + 后 spindle.override 增加",
|
||||
`before=${beforeSpindle}, after=${state.spindle.override}`,
|
||||
state.spindle.override > beforeSpindle ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const floodBefore = state.coolant.flood;
|
||||
await clickAndWait('[data-action="toggle-flood"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"coolant-flood",
|
||||
"Flood 冷却开关",
|
||||
"点击 Flood 后 flood 状态切换",
|
||||
`before=${floodBefore}, after=${state.coolant.flood}`,
|
||||
state.coolant.flood !== floodBefore ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const mistBefore = state.coolant.mist;
|
||||
await clickAndWait('[data-action="toggle-mist"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"coolant-mist",
|
||||
"Mist 冷却开关",
|
||||
"点击 Mist 后 mist 状态切换",
|
||||
`before=${mistBefore}, after=${state.coolant.mist}`,
|
||||
state.coolant.mist !== mistBefore ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="view-x"]');
|
||||
state = await getState();
|
||||
const viewXPass = state.preview.selectedView === "x";
|
||||
await recordResult(
|
||||
"view-x",
|
||||
"预览视角 X",
|
||||
"点击 X 后 selectedView=x",
|
||||
`selectedView=${state.preview.selectedView}`,
|
||||
viewXPass ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="view-y"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"view-y",
|
||||
"预览视角 Y",
|
||||
"点击 Y 后 selectedView=y",
|
||||
`selectedView=${state.preview.selectedView}`,
|
||||
state.preview.selectedView === "y" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="clear-preview"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"clear-preview",
|
||||
"Clear Preview",
|
||||
"点击 Clear 后 pathPoints=0",
|
||||
`pathPoints=${state.preview.pathPoints}`,
|
||||
state.preview.pathPoints === 0 ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="reset-view"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"reset-view",
|
||||
"Fit/Reset View",
|
||||
"点击 Fit 后 selectedView=iso",
|
||||
`selectedView=${state.preview.selectedView}`,
|
||||
state.preview.selectedView === "iso" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="FULL"]');
|
||||
state = await getState();
|
||||
const fullOn = state.preview.fullscreen === true;
|
||||
await clickAndWait('[data-action="FULL"]');
|
||||
const stateAfterFullOff = await getState();
|
||||
await recordResult(
|
||||
"fullscreen-toggle",
|
||||
"Full 全屏切换",
|
||||
"连续点击两次 Full 后 fullscreen true 再 false",
|
||||
`first=${state.preview.fullscreen}, second=${stateAfterFullOff.preview.fullscreen}`,
|
||||
fullOn && stateAfterFullOff.preview.fullscreen === false ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await page.select('[data-action="select-profile"]', "xyzbc-trt");
|
||||
await sleep(2000);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"profile-switch",
|
||||
"Profile 切换到 xyzbc-trt",
|
||||
"切换后 machineProfile=xyzbc-trt,INI 重新加载",
|
||||
`machineProfile=${state.machineProfile}, iniLoaded=${state.iniConfigReadiness.loaded}, iniPath=${state.iniConfigReadiness.path}`,
|
||||
state.machineProfile === "xyzbc-trt" && state.iniConfigReadiness.loaded ? "PASS" : "FAIL",
|
||||
);
|
||||
await capture("02-profile-xyzbc");
|
||||
|
||||
await page.select('[data-action="select-profile"]', "xyzac-trt");
|
||||
await sleep(2000);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"profile-switch-back",
|
||||
"Profile 切回 xyzac-trt",
|
||||
"切回后 machineProfile=xyzac-trt",
|
||||
`machineProfile=${state.machineProfile}, iniLoaded=${state.iniConfigReadiness.loaded}`,
|
||||
state.machineProfile === "xyzac-trt" && state.iniConfigReadiness.loaded ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="stage-linuxcnc-sources"]', 5000);
|
||||
let summary = await getSummary();
|
||||
const stagedCount = summary.state.machineFileStaging?.gcodeSources?.length || 0;
|
||||
const stagingStatus = classifyWarn(
|
||||
stagedCount > 0 && summary.state.machineFileStaging.status === "staged",
|
||||
`已 staged ${stagedCount} 个 LinuxCNC 五轴 G-code 源文件`,
|
||||
`staging 未完成:status=${summary.state.machineFileStaging.status}, lastError=${summary.state.machineFileStaging.lastError || "-"}`,
|
||||
);
|
||||
await recordResult(
|
||||
"stage-linuxcnc-sources",
|
||||
"Stage LinuxCNC 5-axis 源程序",
|
||||
"点击 Stage 后应完成 machine files staging 并出现可选 G-code",
|
||||
stagingStatus.text,
|
||||
stagingStatus.status,
|
||||
);
|
||||
|
||||
if (stagedCount > 0) {
|
||||
const sourceRel = summary.state.machineFileStaging.gcodeSources[0].sourceRel;
|
||||
await page.select('[data-action="select-linuxcnc-gcode-source"]', sourceRel);
|
||||
await sleep(4000);
|
||||
summary = await getSummary();
|
||||
const loadedVendored = summary.state.programSource === "linuxcnc-vendored-5axis-gcode";
|
||||
await recordResult(
|
||||
"load-vendored-program",
|
||||
"加载 LinuxCNC 五轴源程序",
|
||||
"选择源程序后 programSource=linuxcnc-vendored-5axis-gcode",
|
||||
`programSource=${summary.state.programSource}, activeProgram=${summary.state.activeProgram}, sourceRel=${summary.state.programSourceRel || "-"}`,
|
||||
loadedVendored ? "PASS" : "FAIL",
|
||||
);
|
||||
await capture("03-vendored-program-loaded");
|
||||
} else {
|
||||
await recordResult(
|
||||
"load-vendored-program",
|
||||
"加载 LinuxCNC 五轴源程序",
|
||||
"应可加载 staged 后的 vendored 程序",
|
||||
"因 staging 未完成,本项无法继续",
|
||||
"FAIL",
|
||||
);
|
||||
}
|
||||
|
||||
state = await getState();
|
||||
if (!state.machine.powerOn) {
|
||||
await clickAndWait('[data-action="power"]');
|
||||
await waitForState((nextState) => nextState.machine.powerOn === true, 8000, "machine power on before run").catch(() => null);
|
||||
}
|
||||
if (!state.machine.allHomed) {
|
||||
await clickAndWait('[data-action="mode-jog"]');
|
||||
await clickAndWait('[data-action="HOME"]');
|
||||
await waitForState((nextState) => nextState.machine.allHomed === true, 8000, "machine homed before run").catch(() => null);
|
||||
}
|
||||
await clickAndWait('[data-action="mode-auto"]');
|
||||
await clickAndWait('[data-action="RUN"]', 2500);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"run-program",
|
||||
"Run 程序",
|
||||
"点击 Run 后程序进入 running/complete,活动行或运行反馈推进",
|
||||
`runState=${state.runState}, activeLine=${state.activeLine}, source=${state.programExecutionSourceMode}, feedback=${state.programRuntimeFeedback?.apiName || "-"}`,
|
||||
["running", "complete"].includes(state.runState) || Number(state.activeLine) !== 501 ? "PASS" : "FAIL",
|
||||
);
|
||||
await capture("04-run-state");
|
||||
|
||||
await clickAndWait('[data-action="PAUSE"]', 1200);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"pause-program",
|
||||
"Pause 程序",
|
||||
"点击 Pause 后 runState=paused",
|
||||
`runState=${state.runState}, interpState=${state.machine.interpState}`,
|
||||
state.runState === "paused" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="RESUME"]', 1200);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"resume-program",
|
||||
"Resume 程序",
|
||||
"点击 Resume 后 runState 返回 running/idle",
|
||||
`runState=${state.runState}, interpState=${state.machine.interpState}`,
|
||||
["running", "idle", "complete"].includes(state.runState) ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const beforeStepLine = state.activeLine;
|
||||
await clickAndWait('[data-action="STEP"]', 1500);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"step-program",
|
||||
"Step 单步执行",
|
||||
"点击 Step 后 runState=stepping,activeLine 前进或保持受控",
|
||||
`runState=${state.runState}, activeLine=${state.activeLine}`,
|
||||
state.runState === "stepping" || state.activeLine !== beforeStepLine ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="STOP"]', 1200);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"stop-program",
|
||||
"Stop 停止程序",
|
||||
"点击 Stop 后 runState=stopped",
|
||||
`runState=${state.runState}, interpState=${state.machine.interpState}`,
|
||||
state.runState === "stopped" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="RELOAD"]', 1200);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"reload-program",
|
||||
"Reload 程序",
|
||||
"点击 Reload 后 runState=idle,程序回到起始状态",
|
||||
`runState=${state.runState}, activeLine=${state.activeLine}`,
|
||||
state.runState === "idle" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const fileInput = await page.$('[data-action="OPEN_FILE"]');
|
||||
await fileInput.uploadFile(localProgramPath);
|
||||
await sleep(3000);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"open-local-program",
|
||||
"Open 本地 G-code 文件",
|
||||
"上传本地文件后 activeProgram 为上传文件,programSource=upload",
|
||||
`activeProgram=${state.activeProgram}, programSource=${state.programSource}, lineCount=${state.lineCount}`,
|
||||
state.activeProgram.endsWith("test-program.ngc") && ["upload", "operator-file"].includes(state.programSource) ? "PASS" : "FAIL",
|
||||
);
|
||||
await capture("05-local-program-opened");
|
||||
|
||||
await clickAndWait('[data-action="SAVE_SESSION"]', 2500);
|
||||
state = await getState();
|
||||
const saveSessionStatus = classifyWarn(
|
||||
state.sessionPersistence.status === "saved",
|
||||
`会话已保存:${state.sessionPersistence.path} (${state.sessionPersistence.storageMode})`,
|
||||
`会话保存未成功:status=${state.sessionPersistence.status}, error=${state.sessionPersistence.lastError || "-"}`,
|
||||
);
|
||||
await recordResult(
|
||||
"save-session",
|
||||
"Save Session",
|
||||
"点击 Save Session 后会话状态应为 saved",
|
||||
saveSessionStatus.text,
|
||||
saveSessionStatus.status,
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="JOG_X_POS"]');
|
||||
const modifiedState = await getState();
|
||||
await clickAndWait('[data-action="RESTORE_SESSION"]', 2500);
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"restore-session",
|
||||
"Restore Session",
|
||||
"点击 Restore Session 后会话恢复到最近保存快照",
|
||||
`modifiedX=${modifiedState.axisPose.x}, restoredX=${state.axisPose.x}, status=${state.sessionPersistence.status}`,
|
||||
state.sessionPersistence.status === "restored" && state.axisPose.x !== modifiedState.axisPose.x ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="AUDIT_FULL_BOUNDARY"]', 6000);
|
||||
summary = await getSummary();
|
||||
const auditStatus = classifyWarn(
|
||||
!summary.state.interpreterExecutionPending,
|
||||
`Audit 执行完成,fullBoundary=${summary.state.fullExecutionBoundary?.fullLinuxCncProgramExecutionReady}`,
|
||||
"Audit 触发后仍在 pending 或未返回结果",
|
||||
);
|
||||
await recordResult(
|
||||
"audit-full-boundary",
|
||||
"Audit Full Boundary",
|
||||
"点击 Audit 后应触发五轴 machine-file 运行审计并刷新边界状态",
|
||||
`${auditStatus.text}; boundaryStatus=${summary.state.fullExecutionBoundary?.boundaryStatus || "-"}; machineRun=${summary.state.machineFileExecution?.summary?.machineFileExecutionReady ?? "-"}`,
|
||||
auditStatus.status,
|
||||
);
|
||||
await capture("06-after-audit");
|
||||
|
||||
await clickAndWait('[data-action="estop"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"estop",
|
||||
"E-STOP 急停",
|
||||
"点击 E-STOP 后 estopActive=true,runState=estopped",
|
||||
`estopActive=${state.machine.estopActive}, runState=${state.runState}, powerOn=${state.machine.powerOn}`,
|
||||
state.machine.estopActive && state.runState === "estopped" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
await clickAndWait('[data-action="reset"]');
|
||||
state = await getState();
|
||||
await recordResult(
|
||||
"reset",
|
||||
"RESET 复位",
|
||||
"点击 RESET 后 estopActive=false,powerOn=false,taskState=estop-reset",
|
||||
`estopActive=${state.machine.estopActive}, powerOn=${state.machine.powerOn}, taskState=${state.machine.taskState}`,
|
||||
!state.machine.estopActive && !state.machine.powerOn && state.machine.taskState === "estop-reset" ? "PASS" : "FAIL",
|
||||
);
|
||||
|
||||
const finalSummary = await getSummary();
|
||||
await capture("07-final");
|
||||
|
||||
const screenshotAnalysis = await analyzeScreenshot(screenshots["01-home"]);
|
||||
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
targetUrl: URL,
|
||||
chromePath: CHROME_PATH,
|
||||
screenshots,
|
||||
screenshotAnalysis,
|
||||
finalSummary,
|
||||
findings,
|
||||
consoleLogs,
|
||||
pageErrors,
|
||||
requestFailures,
|
||||
};
|
||||
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, "site-test-report.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
async function analyzeScreenshot(filePath) {
|
||||
if (!filePath) return null;
|
||||
const buffer = await fs.readFile(filePath);
|
||||
const png = PNG.sync.read(buffer);
|
||||
const { width, height, data } = png;
|
||||
let sum = 0;
|
||||
let nonBlackPixels = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
const r = data[i];
|
||||
const g = data[i + 1];
|
||||
const b = data[i + 2];
|
||||
const luminance = (r + g + b) / 3;
|
||||
sum += luminance;
|
||||
if (luminance > 8) nonBlackPixels += 1;
|
||||
}
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
averageLuminance: Number((sum / (width * height)).toFixed(2)),
|
||||
nonBlackRatio: Number((nonBlackPixels / (width * height)).toFixed(4)),
|
||||
};
|
||||
}
|
||||
BIN
qa/web-rtcp-5axis-site-test/screenshots/01-home.png
Normal file
|
After Width: | Height: | Size: 170 KiB |
BIN
qa/web-rtcp-5axis-site-test/screenshots/02-profile-xyzbc.png
Normal file
|
After Width: | Height: | Size: 151 KiB |
|
After Width: | Height: | Size: 154 KiB |
BIN
qa/web-rtcp-5axis-site-test/screenshots/04-run-state.png
Normal file
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 146 KiB |
BIN
qa/web-rtcp-5axis-site-test/screenshots/06-after-audit.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
BIN
qa/web-rtcp-5axis-site-test/screenshots/07-final.png
Normal file
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 115 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 132 KiB |
@@ -327,7 +327,7 @@ int hal_pin_s64_newf(hal_pin_dir_t dir, hal_s64_t **data_ptr_addr, int comp_id,
|
||||
return rc;
|
||||
}
|
||||
|
||||
int hal_param_float_newf(hal_pin_dir_t, hal_float_t *data_addr, int, const char *, ...)
|
||||
int hal_param_float_newf(hal_param_dir_t, hal_float_t *data_addr, int, const char *, ...)
|
||||
{
|
||||
if (!data_addr) {
|
||||
return -1;
|
||||
|
||||
@@ -80,7 +80,7 @@ build_kinematics_module() {
|
||||
-s ALLOW_MEMORY_GROWTH=1 \
|
||||
-s NO_EXIT_RUNTIME=1 \
|
||||
-s EXPORTED_FUNCTIONS='["_malloc","_free","_lckins_init","_lckins_exit","_lckins_type","_lckins_switchable","_lckins_switch","_lckins_forward","_lckins_inverse","_lckins_run_probe","_lckins_free_string"]' \
|
||||
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString"]'
|
||||
-s EXPORTED_RUNTIME_METHODS='["UTF8ToString","HEAPF64","HEAPU32"]'
|
||||
}
|
||||
|
||||
TRT_COMMON_SOURCES=(
|
||||
|
||||
@@ -25,6 +25,12 @@ let attachedKinematicsProfile = store.getState().machineProfile;
|
||||
let attachedIniProfile = store.getState().machineProfile;
|
||||
let attachedMachineFileProfile = store.getState().machineProfile;
|
||||
let machineFileSeedPromise = machineFileSeedReady;
|
||||
const autoLoadedProgramProfiles = new Set();
|
||||
|
||||
Promise.allSettled([interpreterRuntimeReady, machineFileSeedReady]).then(() => {
|
||||
ensureDefaultLinuxCncProgramPreview(store).catch(() => {});
|
||||
});
|
||||
|
||||
store.subscribe((state) => {
|
||||
if (state.machineProfile !== attachedIniProfile) {
|
||||
attachedIniProfile = state.machineProfile;
|
||||
@@ -32,12 +38,20 @@ store.subscribe((state) => {
|
||||
}
|
||||
if (state.machineProfile !== attachedKinematicsProfile) {
|
||||
attachedKinematicsProfile = state.machineProfile;
|
||||
attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile).catch(() => {});
|
||||
const profileId = state.machineProfile;
|
||||
attachDefaultKinematicsRuntime(store, state.profile.kinematicsModuleId || state.machineProfile)
|
||||
.then(() => store.refreshKinematicsFrame({
|
||||
operatorMessage: `LinuxCNC kinematics ${profileId} profile frame refreshed`,
|
||||
}))
|
||||
.catch(() => {});
|
||||
}
|
||||
if (state.machineProfile !== attachedMachineFileProfile) {
|
||||
attachedMachineFileProfile = state.machineProfile;
|
||||
machineFileSeedPromise = ensureMachineFilesForProfile(store);
|
||||
window.webRtcp5AxisSimulation.machineFileSeedReady = machineFileSeedPromise;
|
||||
machineFileSeedPromise.finally(() => {
|
||||
ensureDefaultLinuxCncProgramPreview(store).catch(() => {});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -86,6 +100,58 @@ async function ensureMachineFilesForProfile(store) {
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureDefaultLinuxCncProgramPreview(store) {
|
||||
const state = store.getState();
|
||||
if (!state.interpreterRuntime?.loaded) return null;
|
||||
if (state.machineFileStaging?.status !== "staged" || !state.machineFileStaging?.gcodeSources?.length) return null;
|
||||
|
||||
const profileId = state.machineProfile;
|
||||
if (autoLoadedProgramProfiles.has(profileId)) return state.programExecution;
|
||||
|
||||
const defaultSource = selectDefaultLinuxCncSource(state);
|
||||
if (!defaultSource) return null;
|
||||
|
||||
autoLoadedProgramProfiles.add(profileId);
|
||||
store.dispatch({ type: "LOAD_LINUXCNC_GCODE_SOURCE", sourceRel: defaultSource.sourceRel });
|
||||
await waitForStore(
|
||||
store,
|
||||
(nextState) => !nextState.interpreterExecutionPending && nextState.programExecution?.sourceMode === "linuxcnc-interpreter-wasm",
|
||||
15000,
|
||||
).catch((error) => {
|
||||
autoLoadedProgramProfiles.delete(profileId);
|
||||
throw error;
|
||||
});
|
||||
return store.getState().programExecution;
|
||||
}
|
||||
|
||||
function selectDefaultLinuxCncSource(state) {
|
||||
const sources = state.machineFileStaging?.gcodeSources || [];
|
||||
const preferredFilename = `${state.machineProfile}_switchkins.ngc`;
|
||||
return sources.find((source) => source.filename === preferredFilename)
|
||||
|| sources.find((source) => source.filename.includes(state.machineProfile))
|
||||
|| sources[0]
|
||||
|| null;
|
||||
}
|
||||
|
||||
function waitForStore(store, predicate, timeoutMs = 10000) {
|
||||
const initialState = store.getState();
|
||||
if (predicate(initialState)) {
|
||||
return Promise.resolve(initialState);
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
unsubscribe();
|
||||
reject(new Error(`Timed out after ${timeoutMs}ms waiting for store state`));
|
||||
}, timeoutMs);
|
||||
const unsubscribe = store.subscribe((state) => {
|
||||
if (!predicate(state)) return;
|
||||
window.clearTimeout(timeoutId);
|
||||
unsubscribe();
|
||||
resolve(state);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function attachDefaultKinematicsRuntime(store, moduleId = "xyzac-trt") {
|
||||
const sdkModuleUrls = [
|
||||
new URL("../../../wasm-port/runtime/sdk/src/linuxcnc-kinematics.js", import.meta.url).href,
|
||||
|
||||
@@ -193,11 +193,16 @@ export function normalizeTaskHalStatus(status = {}) {
|
||||
b: Number(axis.b ?? halPins["axis.4.pos-cmd"]?.value ?? 0),
|
||||
c: Number(axis.c ?? halPins["axis.5.pos-cmd"]?.value ?? 0),
|
||||
},
|
||||
axisPoseFrame: isJogMotion(motion) ? "task-local" : "work",
|
||||
currentVelocity: Number(motion.currentVel || motion.currentVelocity || 0) * 60,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isJogMotion(motion = {}) {
|
||||
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
|
||||
}
|
||||
|
||||
function sessionFileDescriptor(file) {
|
||||
return {
|
||||
sourceRel: file.sourceRel,
|
||||
|
||||
@@ -82,6 +82,7 @@ const initialState = {
|
||||
},
|
||||
sourceMode: "fixture-ui-only",
|
||||
frameSourceMode: "fixture-ui-only",
|
||||
desiredFrameSourceMode: "fixture-ui-only",
|
||||
machine: {
|
||||
powerOn: false,
|
||||
estopActive: false,
|
||||
@@ -142,6 +143,7 @@ const initialState = {
|
||||
taskHalExecutionPending: false,
|
||||
taskHalExecutionSequence: 0,
|
||||
taskHalFallbackReason: null,
|
||||
pendingJogCommand: null,
|
||||
interpreterExecutionPending: false,
|
||||
interpreterExecutionSequence: 0,
|
||||
machineFileExecution: null,
|
||||
@@ -230,7 +232,7 @@ export function createSimulationStore(seed = {}) {
|
||||
activeLine: seed.activeLine || initialState.activeLine,
|
||||
kinsType: seedKinsType,
|
||||
rtcpEnabled: seedRtcpState === "on" || seedKinsType === "tcp-xyzac",
|
||||
sourceMode: seed.sourceMode || seed.frameSourceMode || initialState.sourceMode,
|
||||
sourceMode: seed.desiredFrameSourceMode || seed.sourceMode || seed.frameSourceMode || initialState.sourceMode,
|
||||
});
|
||||
let state = {
|
||||
...initialState,
|
||||
@@ -268,6 +270,7 @@ export function createSimulationStore(seed = {}) {
|
||||
...next,
|
||||
sourceMode: frame.sourceMode,
|
||||
frameSourceMode: frame.sourceMode,
|
||||
desiredFrameSourceMode: next.desiredFrameSourceMode || frame.sourceMode,
|
||||
axisPose: frame.axisPose,
|
||||
jointPose: frame.jointPose,
|
||||
tcpPose: frame.tcpPose,
|
||||
@@ -321,8 +324,7 @@ export function createSimulationStore(seed = {}) {
|
||||
kinematicsExecutionContext: runtime?.executionContext || (runtime?.loaded ? "direct" : "none"),
|
||||
linuxCncBoundaryAdapter: adapter,
|
||||
linuxCncBoundaryReadiness: createLinuxCncBoundaryReadiness(adapter),
|
||||
sourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
|
||||
frameSourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
|
||||
desiredFrameSourceMode: runtime?.loaded ? "source-derived-kinematics-wasm" : "fixture-ui-only",
|
||||
operatorMessage: runtime?.loaded
|
||||
? `LinuxCNC kinematics ${runtime.moduleId} ready`
|
||||
: "LinuxCNC kinematics runtime missing",
|
||||
@@ -803,6 +805,7 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({
|
||||
taskHalFallbackReason: action.error,
|
||||
taskHalExecutionPending: false,
|
||||
pendingJogCommand: null,
|
||||
operatorMessage: `task/HAL fallback: ${action.error}`,
|
||||
});
|
||||
break;
|
||||
@@ -833,6 +836,7 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({
|
||||
sourceMode: action.sourceMode,
|
||||
frameSourceMode: action.sourceMode,
|
||||
desiredFrameSourceMode: action.sourceMode,
|
||||
operatorMessage: `frame source ${action.sourceMode}`,
|
||||
});
|
||||
break;
|
||||
@@ -971,6 +975,13 @@ export function createSimulationStore(seed = {}) {
|
||||
const direction = Number(action.direction || 1);
|
||||
const increment = Number(action.increment || state.machine.jogIncrement);
|
||||
if (state.taskHalRuntime?.loaded) {
|
||||
const pendingJogCommand = {
|
||||
axis,
|
||||
direction,
|
||||
increment,
|
||||
basePose: { ...state.axisPose },
|
||||
createdAtLine: state.activeLine,
|
||||
};
|
||||
runTaskHalCommandSequence([
|
||||
{
|
||||
type: "EMC_JOG_INCR",
|
||||
@@ -978,7 +989,10 @@ export function createSimulationStore(seed = {}) {
|
||||
distance: direction * increment,
|
||||
velocity: Number(action.velocity || 60),
|
||||
},
|
||||
], { operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}` }).catch(() => {});
|
||||
], {
|
||||
pendingJogCommand,
|
||||
operatorMessage: `task/HAL jog ${axis.toUpperCase()} ${direction > 0 ? "+" : "-"}${increment}`,
|
||||
}).catch(() => {});
|
||||
break;
|
||||
}
|
||||
setState({
|
||||
@@ -1371,6 +1385,9 @@ export function createSimulationStore(seed = {}) {
|
||||
};
|
||||
|
||||
const refreshAsyncKinematicsFrame = async ({ operatorMessage = state.operatorMessage } = {}) => {
|
||||
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") {
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
@@ -1381,44 +1398,58 @@ export function createSimulationStore(seed = {}) {
|
||||
asyncFrameRefreshSequence: sequence,
|
||||
};
|
||||
notify();
|
||||
await switchKinematicsRuntimeForState(state);
|
||||
const frameSource = await state.kinematicsRuntime.frameForJoints(
|
||||
jointsFromAxisPose(state.axisPose, state.profile),
|
||||
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
|
||||
);
|
||||
if (state.asyncFrameRefreshSequence !== sequence) {
|
||||
try {
|
||||
await switchKinematicsRuntimeForState(state);
|
||||
const frameSource = await state.kinematicsRuntime.frameForJoints(
|
||||
jointsFromAxisPose(state.axisPose, state.profile),
|
||||
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
|
||||
);
|
||||
if (state.asyncFrameRefreshSequence !== sequence) {
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: state.axisPose,
|
||||
activeLine: state.activeLine,
|
||||
kinsType: state.kinsType,
|
||||
rtcpEnabled: state.rtcpState === "on" || state.kinsType.startsWith("tcp-"),
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
profile: state.profile,
|
||||
linuxCncKinematicsResult: frameSource,
|
||||
});
|
||||
const nextState = {
|
||||
...state,
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
frameSourceMode: "source-derived-kinematics-wasm",
|
||||
desiredFrameSourceMode: "source-derived-kinematics-wasm",
|
||||
axisPose: frame.axisPose,
|
||||
jointPose: frame.jointPose,
|
||||
tcpPose: frame.tcpPose,
|
||||
toolAxisVector: frame.toolAxisVector,
|
||||
rtcpState: frame.rtcpState,
|
||||
rtcpFrame: frame,
|
||||
lastKinematicsResult: frameSource,
|
||||
dro: buildDroFromFrame(frame, state.programRuntimeFeedback),
|
||||
asyncFrameRefreshPending: false,
|
||||
operatorMessage,
|
||||
};
|
||||
state = {
|
||||
...nextState,
|
||||
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(nextState),
|
||||
};
|
||||
notify();
|
||||
return frame;
|
||||
} catch (error) {
|
||||
if (state.asyncFrameRefreshSequence !== sequence) {
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
state = {
|
||||
...state,
|
||||
asyncFrameRefreshPending: false,
|
||||
operatorMessage: `LinuxCNC kinematics refresh failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
};
|
||||
notify();
|
||||
return state.rtcpFrame;
|
||||
}
|
||||
const frame = buildRtcpFrame({
|
||||
axisPose: state.axisPose,
|
||||
activeLine: state.activeLine,
|
||||
kinsType: state.kinsType,
|
||||
rtcpEnabled: state.rtcpState === "on" || state.kinsType.startsWith("tcp-"),
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
profile: state.profile,
|
||||
linuxCncKinematicsResult: frameSource,
|
||||
});
|
||||
const nextState = {
|
||||
...state,
|
||||
sourceMode: frame.sourceMode,
|
||||
frameSourceMode: frame.sourceMode,
|
||||
axisPose: frame.axisPose,
|
||||
jointPose: frame.jointPose,
|
||||
tcpPose: frame.tcpPose,
|
||||
toolAxisVector: frame.toolAxisVector,
|
||||
rtcpState: frame.rtcpState,
|
||||
rtcpFrame: frame,
|
||||
lastKinematicsResult: frameSource,
|
||||
dro: buildDroFromFrame(frame, state.programRuntimeFeedback),
|
||||
asyncFrameRefreshPending: false,
|
||||
operatorMessage,
|
||||
};
|
||||
state = {
|
||||
...nextState,
|
||||
fullExecutionBoundary: createFullLinuxCncExecutionBoundary(nextState),
|
||||
};
|
||||
notify();
|
||||
return frame;
|
||||
};
|
||||
|
||||
const saveSession = async (options = {}) => {
|
||||
@@ -1543,6 +1574,7 @@ export function createSimulationStore(seed = {}) {
|
||||
taskPeriodNs = 10000000,
|
||||
servoPeriodNs = 1000000,
|
||||
operatorMessage = "task/HAL command complete",
|
||||
pendingJogCommand = null,
|
||||
} = {}) => {
|
||||
if (!state.taskHalRuntime?.loaded) {
|
||||
throw new Error("LinuxCNC task/HAL runtime not attached");
|
||||
@@ -1551,6 +1583,7 @@ export function createSimulationStore(seed = {}) {
|
||||
setState({
|
||||
taskHalExecutionPending: true,
|
||||
taskHalExecutionSequence: sequence,
|
||||
pendingJogCommand,
|
||||
operatorMessage: "LinuxCNC task/HAL command running",
|
||||
});
|
||||
try {
|
||||
@@ -1614,13 +1647,21 @@ export function createSimulationStore(seed = {}) {
|
||||
};
|
||||
|
||||
const scheduleAsyncKinematicsRefresh = () => {
|
||||
if (state.asyncFrameRefreshPending) return null;
|
||||
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") return null;
|
||||
if (!state.kinematicsRuntime?.loaded || !isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null;
|
||||
if (state.frameSourceMode !== "source-derived-kinematics-wasm") return null;
|
||||
const frame = state.rtcpFrame;
|
||||
if (
|
||||
frame?.sourceMode === "source-derived-kinematics-wasm" &&
|
||||
frame.readiness?.linuxCncKinematicsReady === true &&
|
||||
frame.activeLine === state.activeLine
|
||||
frame.activeLine === state.activeLine &&
|
||||
frame.kinsType === state.kinsType &&
|
||||
frame.axisPose?.x === state.axisPose.x &&
|
||||
frame.axisPose?.y === state.axisPose.y &&
|
||||
frame.axisPose?.z === state.axisPose.z &&
|
||||
frame.axisPose?.a === state.axisPose.a &&
|
||||
frame.axisPose?.b === state.axisPose.b &&
|
||||
frame.axisPose?.c === state.axisPose.c
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -1645,10 +1686,9 @@ export function createSimulationStore(seed = {}) {
|
||||
}
|
||||
|
||||
function buildFrameForState(state, patch = {}) {
|
||||
const requestedSourceMode = state.frameSourceMode || state.sourceMode;
|
||||
const requestedSourceMode = state.desiredFrameSourceMode || state.frameSourceMode || state.sourceMode;
|
||||
let linuxCncKinematicsResult = patch.lastKinematicsResult || null;
|
||||
let sourceMode = requestedSourceMode;
|
||||
let operatorMessage = state.operatorMessage;
|
||||
|
||||
if (requestedSourceMode === "source-derived-kinematics-wasm") {
|
||||
if (state.kinematicsRuntime?.loaded && !isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
|
||||
@@ -1657,10 +1697,12 @@ function buildFrameForState(state, patch = {}) {
|
||||
jointsFromAxisPose(state.axisPose, state.profile),
|
||||
{ jointCount: state.kinematicsRuntime.jointCount || 5 },
|
||||
);
|
||||
} else if (state.kinematicsRuntime?.loaded && isAsyncKinematicsRuntime(state.kinematicsRuntime)) {
|
||||
sourceMode = "fixture-ui-only";
|
||||
linuxCncKinematicsResult = null;
|
||||
} else {
|
||||
sourceMode = "fixture-ui-only";
|
||||
linuxCncKinematicsResult = null;
|
||||
operatorMessage = "LinuxCNC kinematics runtime missing; using fixture frame";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1674,10 +1716,6 @@ function buildFrameForState(state, patch = {}) {
|
||||
linuxCncKinematicsResult,
|
||||
});
|
||||
|
||||
if (operatorMessage !== state.operatorMessage) {
|
||||
state.operatorMessage = operatorMessage;
|
||||
}
|
||||
|
||||
return {
|
||||
frame,
|
||||
lastKinematicsResult: linuxCncKinematicsResult,
|
||||
@@ -1743,11 +1781,8 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
const taskMode = normalizeLinuxCncTaskMode(ui.taskMode || task.mode || state.machine.mode);
|
||||
const interpState = normalizeTaskHalInterpState(ui.interpState || task.interpState);
|
||||
const activeLine = state.programStartLine + Math.max(Number(ui.activeLine || 1) - 1, 0);
|
||||
const kinsType = kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
|
||||
const axisPose = clampAxisPoseToProfile({
|
||||
...state.axisPose,
|
||||
...ui.axisPose,
|
||||
}, state.profile);
|
||||
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
|
||||
const axisPose = resolveTaskHalAxisPose(state, status);
|
||||
const currentVelocity = Number.isFinite(ui.currentVelocity) && ui.currentVelocity > 0
|
||||
? ui.currentVelocity
|
||||
: state.feed.currentVelocity;
|
||||
@@ -1772,11 +1807,14 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
taskHalStatus: status,
|
||||
taskHalExecutionPending: false,
|
||||
taskHalFallbackReason: null,
|
||||
pendingJogCommand: null,
|
||||
activeLine,
|
||||
axisPose,
|
||||
kinsType,
|
||||
rtcpState: rtcpStateFromKinsType(kinsType),
|
||||
programExecutionSourceMode: "linuxcnc-task-motion-hal-wasm",
|
||||
programExecutionSourceMode: state.programExecution
|
||||
? state.programExecutionSourceMode
|
||||
: "linuxcnc-task-motion-hal-wasm",
|
||||
machine: {
|
||||
...state.machine,
|
||||
powerOn: taskState === "on",
|
||||
@@ -1797,6 +1835,93 @@ function applyTaskHalStatusPatch(state, status, operatorMessage) {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveTaskHalKinsType(state, status, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const numeric = Number(ui.switchkinsType);
|
||||
if (Number.isFinite(numeric) && numeric !== 0) {
|
||||
return kinsTypeFromSwitchkinsTypeValue(state, numeric);
|
||||
}
|
||||
|
||||
const programKinsType = kinsTypeFromProgramActiveLine(state, activeLine);
|
||||
if (programKinsType) {
|
||||
return programKinsType;
|
||||
}
|
||||
|
||||
return kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
|
||||
}
|
||||
|
||||
function kinsTypeFromProgramActiveLine(state, activeLine) {
|
||||
const motion = programMotionAtOrBeforeLine(state, activeLine)
|
||||
|| state.programExecution?.motion?.[clampMotionIndex(state, state.programExecutionMotionIndex)];
|
||||
return kinsTypeFromProgramMotion(state, motion);
|
||||
}
|
||||
|
||||
function programMotionAtOrBeforeLine(state, activeLine) {
|
||||
const motion = state.programExecution?.motion;
|
||||
if (!Array.isArray(motion) || motion.length === 0) return null;
|
||||
const line = Number(activeLine);
|
||||
if (!Number.isFinite(line)) return null;
|
||||
let candidate = null;
|
||||
for (const item of motion) {
|
||||
const itemLine = Number(item?.line);
|
||||
if (!Number.isFinite(itemLine)) continue;
|
||||
if (itemLine > line) break;
|
||||
candidate = item;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function resolveTaskHalAxisPose(state, status) {
|
||||
const ui = status?.ui || {};
|
||||
const axisPose = ui.axisPose;
|
||||
if (!axisPose || typeof axisPose !== "object") {
|
||||
return state.axisPose;
|
||||
}
|
||||
|
||||
if (ui.axisPoseFrame === "work") {
|
||||
return clampAxisPoseToProfile({ ...state.axisPose, ...axisPose }, state.profile);
|
||||
}
|
||||
|
||||
if (ui.axisPoseDelta && typeof ui.axisPoseDelta === "object") {
|
||||
return addAxisDelta(state.axisPose, ui.axisPoseDelta, state.profile);
|
||||
}
|
||||
|
||||
if (state.pendingJogCommand && isJogStatus(status)) {
|
||||
const { axis, direction, increment, basePose } = state.pendingJogCommand;
|
||||
return clampAxisPoseToProfile({
|
||||
...basePose,
|
||||
[axis]: Number(basePose?.[axis] || 0) + direction * increment,
|
||||
}, state.profile);
|
||||
}
|
||||
|
||||
if (!ui.axisPoseFrame && wouldResetNonZeroPoseToLocalZero(state.axisPose, axisPose)) {
|
||||
return state.axisPose;
|
||||
}
|
||||
|
||||
return clampAxisPoseToProfile({ ...state.axisPose, ...axisPose }, state.profile);
|
||||
}
|
||||
|
||||
function addAxisDelta(axisPose, delta, profile) {
|
||||
const next = { ...axisPose };
|
||||
for (const axis of ["x", "y", "z", "a", "b", "c"]) {
|
||||
if (!Number.isFinite(Number(delta[axis]))) continue;
|
||||
next[axis] = Number(next[axis] || 0) + Number(delta[axis]);
|
||||
}
|
||||
return clampAxisPoseToProfile(next, profile);
|
||||
}
|
||||
|
||||
function isJogStatus(status) {
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
return Number(motion.motionType) === 3 || Number(motion.teleopMode) === 1 || motion.teleopMode === true;
|
||||
}
|
||||
|
||||
function wouldResetNonZeroPoseToLocalZero(currentPose = {}, nextPose = {}) {
|
||||
const axes = ["x", "y", "z", "a", "b", "c"];
|
||||
const currentHasNonZero = axes.some((axis) => Math.abs(Number(currentPose[axis] || 0)) > 0.001);
|
||||
const nextIsNearZero = axes.every((axis) => Math.abs(Number(nextPose[axis] || 0)) <= 0.001);
|
||||
return currentHasNonZero && nextIsNearZero;
|
||||
}
|
||||
|
||||
function createTaskHalRuntimeFeedback(state, status, axisPose, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const motion = status?.motionStatus?.motion || {};
|
||||
|
||||
@@ -160,7 +160,7 @@ button:active {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.machine-preview {
|
||||
.toolpath-preview {
|
||||
width: 100%;
|
||||
height: calc(100% - 64px);
|
||||
margin-top: 32px;
|
||||
@@ -169,7 +169,7 @@ button:active {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.machine-preview:active {
|
||||
.toolpath-preview:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
@@ -204,43 +204,6 @@ button:active {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.machine-envelope,
|
||||
.machine-grid {
|
||||
fill: none;
|
||||
stroke: #d21f1f;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.machine-grid {
|
||||
stroke: #3d3d3d;
|
||||
}
|
||||
|
||||
.machine-rapid {
|
||||
fill: none;
|
||||
stroke: #9e6b00;
|
||||
stroke-width: 1.6;
|
||||
}
|
||||
|
||||
.toolpath {
|
||||
fill: none;
|
||||
stroke: #f7f7f7;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.tool-axis {
|
||||
stroke: #22e6e6;
|
||||
stroke-width: 1.4;
|
||||
}
|
||||
|
||||
.tcp-point {
|
||||
fill: #21f2f2;
|
||||
}
|
||||
|
||||
.axis-label {
|
||||
fill: #3864ff;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.rtcp-preview-badge {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
|
||||
@@ -86,20 +86,18 @@ function renderPreview(element, state, dispatch) {
|
||||
const tcp = state.tcpPose;
|
||||
const tool = state.toolAxisVector;
|
||||
|
||||
element.innerHTML = `
|
||||
<div class="program-path">${escapeHtml(state.activeProgram)}</div>
|
||||
<canvas class="machine-preview" data-five-axis-canvas="true" aria-label="5 axis Three.js preview"></canvas>
|
||||
if (element.dataset.previewMounted !== "true") {
|
||||
element.innerHTML = `
|
||||
<div class="program-path" data-preview-program-path></div>
|
||||
<canvas class="toolpath-preview" data-five-axis-canvas="true" aria-label="5 axis toolpath preview"></canvas>
|
||||
<div class="tool-preview-card" data-tool-preview="summary">
|
||||
<strong>T${state.toolPreview.toolNumber}</strong>
|
||||
<span>D ${formatNumber(state.toolPreview.diameter, 2)} ${state.toolPreview.units}</span>
|
||||
<span>L ${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}</span>
|
||||
<span>${state.toolPreview.holder}</span>
|
||||
<strong data-tool-preview-number></strong>
|
||||
<span data-tool-preview-diameter></span>
|
||||
<span data-tool-preview-length></span>
|
||||
<span data-tool-preview-holder></span>
|
||||
</div>
|
||||
<div class="rtcp-preview-badge" data-rtcp-preview-state="${state.rtcpState}">
|
||||
RTCP ${state.rtcpState} | TCP ${formatNumber(tcp.x)} ${formatNumber(tcp.y)} ${formatNumber(tcp.z)}
|
||||
| V ${formatNumber(tool.x, 3)} ${formatNumber(tool.y, 3)} ${formatNumber(tool.z, 3)}
|
||||
</div>
|
||||
<div class="preview-toolbar" data-preview-points="${state.preview.pathPoints}">
|
||||
<div class="rtcp-preview-badge" data-rtcp-preview-state></div>
|
||||
<div class="preview-toolbar" data-preview-points>
|
||||
<button type="button" data-action="view-x">X</button>
|
||||
<button type="button" data-action="view-y">Y</button>
|
||||
<button type="button" data-action="view-z">Z</button>
|
||||
@@ -108,18 +106,56 @@ function renderPreview(element, state, dispatch) {
|
||||
</div>
|
||||
`;
|
||||
|
||||
element.querySelector('[data-action="reset-view"]').addEventListener("click", () => {
|
||||
dispatch({ type: "RESET_VIEW" });
|
||||
});
|
||||
element.querySelector('[data-action="clear-preview"]').addEventListener("click", () => {
|
||||
dispatch({ type: "CLEAR_PREVIEW" });
|
||||
});
|
||||
for (const view of ["x", "y", "z"]) {
|
||||
element.querySelector(`[data-action="view-${view}"]`).addEventListener("click", () => {
|
||||
dispatch({ type: "SET_VIEW", view });
|
||||
element.querySelector('[data-action="reset-view"]').addEventListener("click", () => {
|
||||
dispatch({ type: "RESET_VIEW" });
|
||||
});
|
||||
element.querySelector('[data-action="clear-preview"]').addEventListener("click", () => {
|
||||
dispatch({ type: "CLEAR_PREVIEW" });
|
||||
});
|
||||
for (const view of ["x", "y", "z"]) {
|
||||
element.querySelector(`[data-action="view-${view}"]`).addEventListener("click", () => {
|
||||
dispatch({ type: "SET_VIEW", view });
|
||||
});
|
||||
}
|
||||
element.dataset.previewMounted = "true";
|
||||
}
|
||||
|
||||
setText(element, "[data-preview-program-path]", state.activeProgram);
|
||||
setText(element, "[data-tool-preview-number]", `T${state.toolPreview.toolNumber}`);
|
||||
setText(element, "[data-tool-preview-diameter]", `D ${formatNumber(state.toolPreview.diameter, 2)} ${state.toolPreview.units}`);
|
||||
setText(element, "[data-tool-preview-length]", `L ${formatNumber(state.toolPreview.length, 3)} ${state.toolPreview.units}`);
|
||||
setText(element, "[data-tool-preview-holder]", state.toolPreview.holder);
|
||||
|
||||
const rtcpBadge = element.querySelector("[data-rtcp-preview-state]");
|
||||
if (rtcpBadge) {
|
||||
rtcpBadge.dataset.rtcpPreviewState = state.rtcpState;
|
||||
rtcpBadge.textContent = [
|
||||
`RTCP ${state.rtcpState}`,
|
||||
`TCP ${formatNumber(tcp.x)} ${formatNumber(tcp.y)} ${formatNumber(tcp.z)}`,
|
||||
`V ${formatNumber(tool.x, 3)} ${formatNumber(tool.y, 3)} ${formatNumber(tool.z, 3)}`,
|
||||
].join(" | ");
|
||||
}
|
||||
|
||||
const toolbar = element.querySelector("[data-preview-points]");
|
||||
if (toolbar) {
|
||||
toolbar.dataset.previewPoints = String(state.preview.pathPoints);
|
||||
for (const view of ["x", "y", "z"]) {
|
||||
toolbar.querySelector(`[data-action="view-${view}"]`)?.setAttribute(
|
||||
"data-active",
|
||||
state.preview.selectedView === view ? "true" : "false",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = element.querySelector("[data-five-axis-canvas]");
|
||||
renderFiveAxisScene(canvas, state);
|
||||
}
|
||||
|
||||
function setText(root, selector, value) {
|
||||
const element = root.querySelector(selector);
|
||||
if (element) {
|
||||
element.textContent = value;
|
||||
}
|
||||
renderFiveAxisScene(element.querySelector("[data-five-axis-canvas]"), state);
|
||||
}
|
||||
|
||||
function renderDro(element, state) {
|
||||
|
||||
@@ -33,21 +33,23 @@ export function renderFiveAxisScene(canvas, state) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pointCount = preview.previewPath.geometry.getAttribute("position").count;
|
||||
const executedPointCount = preview.executedPath.geometry.getAttribute("position").count;
|
||||
const pointCount = geometryPointCount(preview.previewPath.geometry);
|
||||
const executedPointCount = geometryPointCount(preview.executedPath.geometry);
|
||||
exposePreviewDataset(canvas, state, {
|
||||
pointCount,
|
||||
executedPointCount,
|
||||
feedPointCount: preview.feedPath.geometry.getAttribute("position").count,
|
||||
rapidPointCount: preview.rapidPath.geometry.getAttribute("position").count,
|
||||
arcPointCount: preview.arcPath.geometry.getAttribute("position").count,
|
||||
currentSegmentPointCount: preview.currentSegmentPath.geometry.getAttribute("position").count,
|
||||
feedPointCount: geometryPointCount(preview.feedPath.geometry),
|
||||
rapidPointCount: geometryPointCount(preview.rapidPath.geometry),
|
||||
arcPointCount: geometryPointCount(preview.arcPath.geometry),
|
||||
currentSegmentPointCount: geometryPointCount(preview.currentSegmentPath.geometry),
|
||||
sceneObjectCount: countSceneObjects(preview.scene),
|
||||
toolhead: preview.currentToolhead,
|
||||
renderer: "webgl",
|
||||
sceneMode: "program-preview-and-tool-execution",
|
||||
machineReferenceModel: "webgl-five-axis-reference",
|
||||
cameraControls: preview.controls.enabled,
|
||||
toolExecutionMarker: preview.toolMarker.visible,
|
||||
toolAxisMarker: preview.toolAxis.visible,
|
||||
pathFitBounds: preview.pathFitBoundsReady,
|
||||
});
|
||||
}
|
||||
@@ -72,6 +74,9 @@ function createScene(canvas) {
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(42, 1, 0.1, 100);
|
||||
|
||||
const machineModel = createMachineReferenceModel();
|
||||
scene.add(machineModel.root);
|
||||
|
||||
const previewPath = createLine(0x808892, 0.56);
|
||||
const feedPath = createLine(0x4fb3ff, 0.92);
|
||||
const executedPath = createLine(0x1ffff4, 1);
|
||||
@@ -103,6 +108,7 @@ function createScene(canvas) {
|
||||
rapidPath,
|
||||
arcPath,
|
||||
currentSegmentPath,
|
||||
machineModel,
|
||||
toolMarker,
|
||||
toolAxis,
|
||||
currentToolhead: new THREE.Vector3(),
|
||||
@@ -133,6 +139,35 @@ function renderFallbackPreview(preview, state) {
|
||||
canvas.height = height;
|
||||
}
|
||||
|
||||
const previewPoints = buildProgramPreviewPoints(state);
|
||||
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
||||
const rapidPoints = buildRapidPreviewPoints(state);
|
||||
const feedPoints = buildTypedPreviewPoints(state, "STRAIGHT_FEED");
|
||||
const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED");
|
||||
const currentSegmentPoints = buildCurrentSegmentPoints(state);
|
||||
const pointCount = previewPoints.length;
|
||||
const executedPointCount = executedPoints.length;
|
||||
const toolPosition = executionToolPosition(state, previewPoints);
|
||||
|
||||
exposePreviewDataset(canvas, state, {
|
||||
pointCount,
|
||||
executedPointCount,
|
||||
sceneObjectCount: 8 + (pointCount > 0 ? 1 : 0) + (executedPointCount > 0 ? 1 : 0),
|
||||
toolhead: toolPosition || { x: 0, y: 0, z: 0 },
|
||||
renderer: "2d-fallback",
|
||||
sceneMode: "program-preview-and-tool-execution",
|
||||
machineReferenceModel: "2d-five-axis-reference",
|
||||
cameraControls: false,
|
||||
toolExecutionMarker: Boolean(toolPosition),
|
||||
toolAxisMarker: Boolean(toolPosition),
|
||||
feedPointCount: feedPoints.length,
|
||||
rapidPointCount: rapidPoints.length,
|
||||
arcPointCount: arcPoints.length,
|
||||
currentSegmentPointCount: currentSegmentPoints.length,
|
||||
pathFitBounds: computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints)) !== null,
|
||||
});
|
||||
canvas.dataset.threeFallbackReason = preview.errorMessage;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
@@ -144,14 +179,8 @@ function renderFallbackPreview(preview, state) {
|
||||
const cy = height * 0.53;
|
||||
const scale = Math.min(width / 7.2, height / 4.8);
|
||||
|
||||
const previewPoints = buildProgramPreviewPoints(state);
|
||||
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
||||
const rapidPoints = buildRapidPreviewPoints(state);
|
||||
const feedPoints = buildTypedPreviewPoints(state, "STRAIGHT_FEED");
|
||||
const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED");
|
||||
const currentSegmentPoints = buildCurrentSegmentPoints(state);
|
||||
const pointCount = previewPoints.length;
|
||||
const executedPointCount = executedPoints.length;
|
||||
drawFallbackMachineReference(ctx, cx, cy, scale, state);
|
||||
|
||||
if (pointCount > 0) {
|
||||
ctx.strokeStyle = "#8d95a0";
|
||||
ctx.lineWidth = 2;
|
||||
@@ -175,16 +204,6 @@ function renderFallbackPreview(preview, state) {
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
const toolPosition = executionToolPosition(state, previewPoints);
|
||||
const fitPoints = collectFitPoints(previewPoints, executedPoints, currentSegmentPoints, toolPosition);
|
||||
const fitKey = [
|
||||
previewPoints.length,
|
||||
executedPoints.length,
|
||||
currentSegmentPoints.length,
|
||||
previewSourceMode(state),
|
||||
state.programExecutionMotionIndex || 0,
|
||||
state.programExecutionSampleIndex || 0,
|
||||
].join(":");
|
||||
if (toolPosition) {
|
||||
const toolX = cx + toolPosition.x * scale;
|
||||
const toolY = cy - toolPosition.y * scale;
|
||||
@@ -197,23 +216,6 @@ function renderFallbackPreview(preview, state) {
|
||||
ctx.fillStyle = "#b7c7b8";
|
||||
ctx.font = "12px Courier New, monospace";
|
||||
ctx.fillText("2D RTCP fallback", 12, height - 14);
|
||||
|
||||
exposePreviewDataset(canvas, state, {
|
||||
pointCount,
|
||||
executedPointCount,
|
||||
sceneObjectCount: (pointCount > 0 ? 1 : 0) + (executedPointCount > 0 ? 1 : 0),
|
||||
toolhead: toolPosition || { x: 0, y: 0, z: 0 },
|
||||
renderer: "2d-fallback",
|
||||
sceneMode: "program-preview-and-tool-execution",
|
||||
cameraControls: false,
|
||||
toolExecutionMarker: Boolean(toolPosition),
|
||||
feedPointCount: feedPoints.length,
|
||||
rapidPointCount: rapidPoints.length,
|
||||
arcPointCount: arcPoints.length,
|
||||
currentSegmentPointCount: currentSegmentPoints.length,
|
||||
pathFitBounds: computePointBounds(previewPoints.concat(executedPoints, currentSegmentPoints)) !== null,
|
||||
});
|
||||
canvas.dataset.threeFallbackReason = preview.errorMessage;
|
||||
}
|
||||
|
||||
function exposePreviewDataset(canvas, state, preview) {
|
||||
@@ -230,9 +232,15 @@ function exposePreviewDataset(canvas, state, preview) {
|
||||
canvas.dataset.threeFrameApi = state.rtcpFrame.apiName;
|
||||
canvas.dataset.threeRenderer = preview.renderer;
|
||||
canvas.dataset.threeSceneMode = preview.sceneMode;
|
||||
canvas.dataset.threePreviewScope = preview.machineReferenceModel
|
||||
? "machine-reference-and-toolpath"
|
||||
: "toolpath-only";
|
||||
canvas.dataset.threeMachineReferenceModel = preview.machineReferenceModel || "none";
|
||||
canvas.dataset.threeCameraControls = preview.cameraControls ? "orbit-pan-zoom" : "none";
|
||||
canvas.dataset.threeProgramPreviewSource = previewSourceMode(state);
|
||||
canvas.dataset.threeToolExecutionMarker = preview.toolExecutionMarker ? "true" : "false";
|
||||
canvas.dataset.threeTcpMarker = preview.toolExecutionMarker ? "sphere" : "hidden";
|
||||
canvas.dataset.threeToolAxisMarker = preview.toolAxisMarker ? "line" : "hidden";
|
||||
canvas.dataset.threeToolpathPreviewSource = toolpathPreviewSource(state);
|
||||
canvas.dataset.threeToolExecutionTraceSource = toolExecutionTraceSource(state);
|
||||
canvas.dataset.threePathFitBounds = preview.pathFitBounds ? "ok" : "pending";
|
||||
@@ -257,6 +265,70 @@ function createLine(color, opacity) {
|
||||
);
|
||||
}
|
||||
|
||||
function createMachineReferenceModel() {
|
||||
const root = new THREE.Group();
|
||||
root.name = "five-axis-machine-reference";
|
||||
|
||||
const base = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(4.8, 3.2, 0.08),
|
||||
new THREE.MeshBasicMaterial({ color: 0x222930 }),
|
||||
);
|
||||
base.position.z = -0.16;
|
||||
|
||||
const table = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(3.7, 2.35, 0.05),
|
||||
new THREE.MeshBasicMaterial({ color: 0x3a444d, transparent: true, opacity: 0.78 }),
|
||||
);
|
||||
table.position.z = -0.08;
|
||||
|
||||
const xAxis = createStaticLine([new THREE.Vector3(-2.2, 0, 0), new THREE.Vector3(2.25, 0, 0)], 0xff4d4d);
|
||||
const yAxis = createStaticLine([new THREE.Vector3(0, -1.55, 0), new THREE.Vector3(0, 1.6, 0)], 0x70df7d);
|
||||
const zAxis = createStaticLine([new THREE.Vector3(0, 0, -0.08), new THREE.Vector3(0, 0, 1.75)], 0x5aa7ff);
|
||||
|
||||
const rotaryA = new THREE.Mesh(
|
||||
new THREE.TorusGeometry(0.88, 0.018, 8, 72),
|
||||
new THREE.MeshBasicMaterial({ color: 0x1ffff4, transparent: true, opacity: 0.92 }),
|
||||
);
|
||||
rotaryA.rotation.y = Math.PI / 2;
|
||||
|
||||
const rotaryC = new THREE.Mesh(
|
||||
new THREE.TorusGeometry(1.1, 0.016, 8, 72),
|
||||
new THREE.MeshBasicMaterial({ color: 0xffd166, transparent: true, opacity: 0.9 }),
|
||||
);
|
||||
rotaryC.rotation.x = Math.PI / 2;
|
||||
rotaryC.position.z = 0.04;
|
||||
|
||||
const toolHolder = new THREE.Group();
|
||||
const holderBody = new THREE.Mesh(
|
||||
new THREE.CylinderGeometry(0.08, 0.08, 0.42, 18),
|
||||
new THREE.MeshBasicMaterial({ color: 0xf1f5f9 }),
|
||||
);
|
||||
holderBody.rotation.x = Math.PI / 2;
|
||||
holderBody.position.z = 0.32;
|
||||
const cutter = new THREE.Mesh(
|
||||
new THREE.ConeGeometry(0.06, 0.25, 18),
|
||||
new THREE.MeshBasicMaterial({ color: 0xfff176 }),
|
||||
);
|
||||
cutter.rotation.x = Math.PI;
|
||||
cutter.position.z = 0.08;
|
||||
toolHolder.add(holderBody, cutter);
|
||||
|
||||
root.add(base, table, xAxis, yAxis, zAxis, rotaryA, rotaryC, toolHolder);
|
||||
return {
|
||||
root,
|
||||
rotaryA,
|
||||
rotaryC,
|
||||
toolHolder,
|
||||
};
|
||||
}
|
||||
|
||||
function createStaticLine(points, color) {
|
||||
return new THREE.Line(
|
||||
new THREE.BufferGeometry().setFromPoints(points),
|
||||
new THREE.LineBasicMaterial({ color, transparent: true, opacity: 0.95 }),
|
||||
);
|
||||
}
|
||||
|
||||
function updateToolpathPreview(preview, state) {
|
||||
const previewPoints = buildProgramPreviewPoints(state);
|
||||
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
|
||||
@@ -265,6 +337,15 @@ function updateToolpathPreview(preview, state) {
|
||||
const arcPoints = buildTypedPreviewPoints(state, "ARC_FEED");
|
||||
const currentSegmentPoints = buildCurrentSegmentPoints(state);
|
||||
const toolPosition = executionToolPosition(state, previewPoints);
|
||||
const fitPoints = collectFitPoints(previewPoints, executedPoints, currentSegmentPoints, toolPosition);
|
||||
const fitKey = [
|
||||
previewPoints.length,
|
||||
executedPoints.length,
|
||||
currentSegmentPoints.length,
|
||||
previewSourceMode(state),
|
||||
state.programExecutionMotionIndex || 0,
|
||||
state.programExecutionSampleIndex || 0,
|
||||
].join(":");
|
||||
|
||||
updateLineGeometry(preview.previewPath, previewPoints);
|
||||
updateLineGeometry(preview.feedPath, feedPoints);
|
||||
@@ -273,6 +354,7 @@ function updateToolpathPreview(preview, state) {
|
||||
updateLineGeometry(preview.arcPath, arcPoints);
|
||||
updateLineGeometry(preview.currentSegmentPath, currentSegmentPoints);
|
||||
updateToolExecutionMarker(preview, state, toolPosition);
|
||||
updateMachineReferenceModel(preview, state, toolPosition);
|
||||
|
||||
const cameraRevision = state.preview.cameraRevision ?? 0;
|
||||
if (
|
||||
@@ -309,6 +391,22 @@ function updateToolExecutionMarker(preview, state, toolPosition) {
|
||||
]);
|
||||
}
|
||||
|
||||
function updateMachineReferenceModel(preview, state, toolPosition) {
|
||||
const model = preview.machineModel;
|
||||
if (!model) return;
|
||||
const a = degreesToRadians(state.axisPose?.a);
|
||||
const b = degreesToRadians(state.axisPose?.b);
|
||||
const c = degreesToRadians(state.axisPose?.c);
|
||||
model.rotaryA.rotation.x = a;
|
||||
model.rotaryA.rotation.y = Math.PI / 2 + b;
|
||||
model.rotaryC.rotation.z = c;
|
||||
|
||||
const tcpPosition = toolPosition || toPreviewVector(state.tcpPose || state.axisPose);
|
||||
model.toolHolder.position.copy(tcpPosition);
|
||||
const toolVector = toToolVector(state.toolAxisVector);
|
||||
model.toolHolder.lookAt(tcpPosition.clone().add(toolVector));
|
||||
}
|
||||
|
||||
function updateLineGeometry(line, points) {
|
||||
line.visible = points.length > 0;
|
||||
line.geometry.dispose();
|
||||
@@ -317,6 +415,10 @@ function updateLineGeometry(line, points) {
|
||||
: EMPTY_GEOMETRY.clone();
|
||||
}
|
||||
|
||||
function geometryPointCount(geometry) {
|
||||
return geometry?.getAttribute("position")?.count || 0;
|
||||
}
|
||||
|
||||
function buildProgramPreviewPoints(state) {
|
||||
const motion = state.programExecution?.motion;
|
||||
if (Array.isArray(motion) && motion.length > 0 && state.preview.pathPoints !== 0) {
|
||||
@@ -384,7 +486,6 @@ function buildFixturePreviewPoints(pointCount, tcpPosition) {
|
||||
}
|
||||
|
||||
function executionToolPosition(state, previewPoints) {
|
||||
if (state.preview.pathPoints === 0) return null;
|
||||
const feedbackAxes = state.programRuntimeFeedback?.axisPose || state.programRuntimeFeedback;
|
||||
if (feedbackAxes && hasLinearAxes(feedbackAxes)) return vectorFromAxes(feedbackAxes);
|
||||
if (hasLinearAxes(state.axisPose)) return vectorFromAxes(state.axisPose);
|
||||
@@ -505,6 +606,56 @@ function drawFallbackPolyline(ctx, points, cx, cy, scale) {
|
||||
}
|
||||
}
|
||||
|
||||
function drawFallbackMachineReference(ctx, cx, cy, scale, state) {
|
||||
const tableWidth = 4.8 * scale;
|
||||
const tableHeight = 3.2 * scale;
|
||||
ctx.fillStyle = "#20272e";
|
||||
ctx.strokeStyle = "#56616b";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.fillRect(cx - tableWidth / 2, cy - tableHeight / 2, tableWidth, tableHeight);
|
||||
ctx.strokeRect(cx - tableWidth / 2, cy - tableHeight / 2, tableWidth, tableHeight);
|
||||
|
||||
drawFallbackAxis(ctx, cx - 2.25 * scale, cy, cx + 2.25 * scale, cy, "#ff4d4d");
|
||||
drawFallbackAxis(ctx, cx, cy + 1.55 * scale, cx, cy - 1.6 * scale, "#70df7d");
|
||||
drawFallbackAxis(ctx, cx, cy + 0.2 * scale, cx, cy - 1.15 * scale, "#5aa7ff");
|
||||
|
||||
ctx.strokeStyle = "#1ffff4";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, 0.92 * scale, 0.42 * scale, degreesToRadians(state.axisPose?.a), 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.strokeStyle = "#ffd166";
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, 0.7 * scale, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
|
||||
const tcp = executionToolPosition(state, []);
|
||||
if (tcp) {
|
||||
const toolX = cx + tcp.x * scale;
|
||||
const toolY = cy - tcp.y * scale;
|
||||
ctx.strokeStyle = "#f1f5f9";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(toolX, toolY - 0.38 * scale);
|
||||
ctx.lineTo(toolX, toolY - 0.08 * scale);
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#1ffff4";
|
||||
ctx.beginPath();
|
||||
ctx.arc(toolX, toolY, 0.07 * scale, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
function drawFallbackAxis(ctx, x1, y1, x2, y2, color) {
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = 3;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function createToolpathCameraControls(canvas, camera, renderFrame) {
|
||||
const controls = {
|
||||
enabled: true,
|
||||
@@ -677,7 +828,7 @@ function applyCameraControls(controls) {
|
||||
}
|
||||
|
||||
function resizeRenderer(preview) {
|
||||
const { canvas } = preview.renderer.domElement;
|
||||
const canvas = preview.renderer.domElement;
|
||||
const width = Math.max(canvas.clientWidth, 320);
|
||||
const height = Math.max(canvas.clientHeight, 240);
|
||||
if (canvas.width !== width || canvas.height !== height) {
|
||||
@@ -717,6 +868,10 @@ function round(value) {
|
||||
return Math.round(Number(value) * 1000) / 1000;
|
||||
}
|
||||
|
||||
function degreesToRadians(value) {
|
||||
return (Number(value) || 0) * Math.PI / 180;
|
||||
}
|
||||
|
||||
function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
@@ -84,8 +84,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (!doc.querySelector(".machine-preview")) {
|
||||
throw new Error("missing machine preview");
|
||||
if (!doc.querySelector(".toolpath-preview")) {
|
||||
throw new Error("missing toolpath preview");
|
||||
}
|
||||
let canvas = doc.querySelector("[data-five-axis-canvas]");
|
||||
if (!canvas) {
|
||||
@@ -95,13 +95,17 @@
|
||||
canvas.dataset.threeReady !== "true" ||
|
||||
canvas.dataset.threeFrameApi !== "web-rtcp-5axis-motion-frame" ||
|
||||
canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" ||
|
||||
canvas.dataset.threePreviewScope !== "machine-reference-and-toolpath" ||
|
||||
canvas.dataset.threeMachineReferenceModel !== "webgl-five-axis-reference" ||
|
||||
canvas.dataset.threeCameraControls !== "orbit-pan-zoom" ||
|
||||
Number(canvas.dataset.threePathPoints ?? 0) < 64 ||
|
||||
Number(canvas.dataset.threeSceneObjects ?? 0) < 5 ||
|
||||
Number(canvas.dataset.threeSceneObjects ?? 0) < 12 ||
|
||||
!canvas.dataset.threeToolhead ||
|
||||
!canvas.dataset.threeToolAxis ||
|
||||
!canvas.dataset.threeTcpPose ||
|
||||
canvas.dataset.threeToolExecutionMarker !== "true"
|
||||
canvas.dataset.threeToolExecutionMarker !== "true" ||
|
||||
canvas.dataset.threeTcpMarker !== "sphere" ||
|
||||
canvas.dataset.threeToolAxisMarker !== "line"
|
||||
) {
|
||||
throw new Error(`Three.js preview did not expose ready render state: ${JSON.stringify(canvas.dataset)}`);
|
||||
}
|
||||
@@ -428,6 +432,10 @@
|
||||
}
|
||||
if (
|
||||
canvas.dataset.threeSceneMode !== "program-preview-and-tool-execution" ||
|
||||
canvas.dataset.threePreviewScope !== "machine-reference-and-toolpath" ||
|
||||
canvas.dataset.threeMachineReferenceModel !== "webgl-five-axis-reference" ||
|
||||
canvas.dataset.threeTcpMarker !== "sphere" ||
|
||||
canvas.dataset.threeToolAxisMarker !== "line" ||
|
||||
canvas.dataset.threeToolpathPreviewSource !== "linuxcnc_interpreter_canonical_motion" ||
|
||||
canvas.dataset.threeToolExecutionTraceSource !== "linuxcnc_tp_samples_or_task_motion_hal_feedback" ||
|
||||
canvas.dataset.threePathFitBounds !== "ok" ||
|
||||
@@ -813,23 +821,38 @@
|
||||
});
|
||||
|
||||
function assertCanvasNonblank(canvas, context) {
|
||||
const stats = canvasPixelStats(canvas, context);
|
||||
if (stats.nonBlackRatio <= 0.02) {
|
||||
throw new Error(`${context}: non-black pixel ratio too low ${JSON.stringify(stats)}`);
|
||||
}
|
||||
if (stats.averageLuminance <= 5) {
|
||||
throw new Error(`${context}: average luminance too low ${JSON.stringify(stats)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function canvasPixelStats(canvas, context) {
|
||||
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
|
||||
if (!gl) {
|
||||
throw new Error(`${context}: missing WebGL context`);
|
||||
}
|
||||
const pixel = new Uint8Array(4);
|
||||
gl.readPixels(
|
||||
Math.floor(canvas.width / 2),
|
||||
Math.floor(canvas.height / 2),
|
||||
1,
|
||||
1,
|
||||
gl.RGBA,
|
||||
gl.UNSIGNED_BYTE,
|
||||
pixel,
|
||||
);
|
||||
if (pixel[0] === 0 && pixel[1] === 0 && pixel[2] === 0 && pixel[3] === 0) {
|
||||
throw new Error(`${context}: center pixel was blank`);
|
||||
const width = Math.max(canvas.width || 0, 1);
|
||||
const height = Math.max(canvas.height || 0, 1);
|
||||
const pixels = new Uint8Array(width * height * 4);
|
||||
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
|
||||
let nonBlack = 0;
|
||||
let luminance = 0;
|
||||
for (let index = 0; index < pixels.length; index += 4) {
|
||||
const luma = pixels[index] * 0.2126 + pixels[index + 1] * 0.7152 + pixels[index + 2] * 0.0722;
|
||||
luminance += luma;
|
||||
if (luma > 8) nonBlack += 1;
|
||||
}
|
||||
const total = width * height;
|
||||
return {
|
||||
width,
|
||||
height,
|
||||
nonBlackRatio: nonBlack / total,
|
||||
averageLuminance: luminance / total,
|
||||
};
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -63,11 +63,27 @@ assert.equal(state.rtcpState, "on");
|
||||
|
||||
store.dispatch({ type: "SET_MODE", mode: "manual" });
|
||||
await waitForTaskHal(store);
|
||||
store.dispatch({ type: "HOME" });
|
||||
state = store.getState();
|
||||
const homePose = { ...state.axisPose };
|
||||
assert.equal(homePose.x, 43);
|
||||
assert.equal(homePose.y, -32.15);
|
||||
assert.equal(homePose.z, -11.306);
|
||||
store.dispatch({ type: "JOG", axis: "x", direction: 1, increment: 0.5 });
|
||||
await waitForTaskHal(store);
|
||||
state = store.getState();
|
||||
assert.equal(state.taskHalStatus.motionStatus.motion.teleopMode, 1);
|
||||
assert.equal(state.programRuntimeFeedback.sourceMode, "linuxcnc-task-motion-hal-wasm");
|
||||
assertNear(state.axisPose.x, homePose.x + 0.5, "task/HAL JOG X+ should keep HOME work-pose continuity");
|
||||
assertNear(state.axisPose.y, homePose.y, "task/HAL JOG X+ should not reset Y");
|
||||
assertNear(state.axisPose.z, homePose.z, "task/HAL JOG X+ should not reset Z");
|
||||
|
||||
store.dispatch({ type: "JOG", axis: "y", direction: -1, increment: 0.5 });
|
||||
await waitForTaskHal(store);
|
||||
state = store.getState();
|
||||
assertNear(state.axisPose.x, homePose.x + 0.5, "task/HAL JOG Y- should not reset X");
|
||||
assertNear(state.axisPose.y, homePose.y - 0.5, "task/HAL JOG Y- should keep HOME work-pose continuity");
|
||||
assertNear(state.axisPose.z, homePose.z, "task/HAL JOG Y- should not reset Z");
|
||||
|
||||
store.dispatch({ type: "SET_MODE", mode: "auto" });
|
||||
await waitForTaskHal(store);
|
||||
@@ -94,3 +110,7 @@ async function waitForTaskHal(store) {
|
||||
}
|
||||
throw new Error("task/HAL store command did not settle");
|
||||
}
|
||||
|
||||
function assertNear(actual, expected, message) {
|
||||
assert.equal(Math.abs(Number(actual) - Number(expected)) < 1e-9, true, `${message}: ${actual} !== ${expected}`);
|
||||
}
|
||||
|
||||
@@ -428,4 +428,90 @@ assert.equal(state.machine.powerOn, false);
|
||||
assert.equal(state.machine.taskState, "estop-reset");
|
||||
assert.equal(state.operatorMessage, "estop reset; machine off");
|
||||
|
||||
const taskHalSwitchkinsStore = createSimulationStore({
|
||||
programStartLine: 1,
|
||||
activeLine: 5,
|
||||
kinsType: "tcp-xyzac",
|
||||
rtcpState: "on",
|
||||
programExecutionSourceMode: "linuxcnc-interpreter-wasm",
|
||||
programExecution: {
|
||||
sourceMode: "linuxcnc-interpreter-wasm",
|
||||
motion: [
|
||||
{
|
||||
type: "STRAIGHT_TRAVERSE",
|
||||
line: 5,
|
||||
axes: { x: 1, y: 2, z: 3, a: 10, c: 20 },
|
||||
switchkinsType: 1,
|
||||
kinsType: "tcp",
|
||||
},
|
||||
{
|
||||
type: "STRAIGHT_FEED",
|
||||
line: 20,
|
||||
axes: { x: 2, y: 3, z: 4, a: 0, c: 0 },
|
||||
switchkinsType: 0,
|
||||
kinsType: "identity",
|
||||
},
|
||||
],
|
||||
summary: {
|
||||
motionEventCount: 2,
|
||||
switchkinsEventCount: 2,
|
||||
switchkinsCodes: ["M428", "M429"],
|
||||
},
|
||||
},
|
||||
});
|
||||
taskHalSwitchkinsStore.dispatch({
|
||||
type: "TASK_HAL_STATUS_APPLIED",
|
||||
status: taskHalStatusForSwitchkinsLine({ activeLine: 5, switchkinsType: 0 }),
|
||||
operatorMessage: "task/HAL status retained program TCP switchkins",
|
||||
});
|
||||
state = taskHalSwitchkinsStore.getState();
|
||||
assert.equal(state.activeLine, 5);
|
||||
assert.equal(state.kinsType, "tcp-xyzac");
|
||||
assert.equal(state.rtcpState, "on");
|
||||
|
||||
taskHalSwitchkinsStore.dispatch({
|
||||
type: "TASK_HAL_STATUS_APPLIED",
|
||||
status: taskHalStatusForSwitchkinsLine({ activeLine: 20, switchkinsType: 0 }),
|
||||
operatorMessage: "task/HAL status applied program identity switchkins",
|
||||
});
|
||||
state = taskHalSwitchkinsStore.getState();
|
||||
assert.equal(state.activeLine, 20);
|
||||
assert.equal(state.kinsType, "identity");
|
||||
assert.equal(state.rtcpState, "off");
|
||||
|
||||
console.log("rtcp_store_smoke=ok");
|
||||
|
||||
function taskHalStatusForSwitchkinsLine({ activeLine, switchkinsType }) {
|
||||
return {
|
||||
semanticBoundary: "linuxcnc_task_motion_hal_wasm_simulation_runtime",
|
||||
task: {
|
||||
state: "ON",
|
||||
mode: "AUTO",
|
||||
interpState: "READING",
|
||||
execState: "WAITING_FOR_MOTION",
|
||||
},
|
||||
motionStatus: {
|
||||
motion: {
|
||||
programLine: activeLine,
|
||||
motionType: 1,
|
||||
switchkinsType,
|
||||
currentVel: 1,
|
||||
requestedVel: 1,
|
||||
inPosition: false,
|
||||
},
|
||||
},
|
||||
ui: {
|
||||
taskState: "on",
|
||||
taskMode: "auto",
|
||||
interpState: "reading",
|
||||
activeLine,
|
||||
switchkinsType,
|
||||
axisPoseFrame: "work",
|
||||
axisPose: { x: 1, y: 2, z: 3, a: 10, b: 0, c: 20 },
|
||||
currentVelocity: 60,
|
||||
servoCycle: 1,
|
||||
taskCycle: 1,
|
||||
motionQueueDepth: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# 01 RTCP 刀具轨迹问题复盘
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 测试来源
|
||||
|
||||
专项测试脚本:
|
||||
|
||||
```text
|
||||
qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
```
|
||||
|
||||
专项报告:
|
||||
|
||||
```text
|
||||
qa/web-rtcp-5axis-site-test/output/web-rtcp-5axis-toolpath-preview-report-2026-06-22.docx
|
||||
```
|
||||
|
||||
原始数据:
|
||||
|
||||
```text
|
||||
qa/web-rtcp-5axis-site-test/output/toolpath-preview-cases.json
|
||||
```
|
||||
|
||||
## 2. 失败现象
|
||||
|
||||
失败场景:
|
||||
|
||||
```text
|
||||
06-running-rtcp-toolpath
|
||||
```
|
||||
|
||||
失败前证据:
|
||||
|
||||
```text
|
||||
05-vendored-impeller-toolpath:
|
||||
threeRtcpState=on
|
||||
pathPoints=1498
|
||||
executedPathPoints=1
|
||||
programSource=linuxcnc-vendored-5axis-gcode
|
||||
programExecution.summary.switchkinsEventCount=2
|
||||
switchkinsCodes=M428,M429
|
||||
```
|
||||
|
||||
失败时证据:
|
||||
|
||||
```text
|
||||
06-running-rtcp-toolpath:
|
||||
threeRtcpState=off
|
||||
state.rtcpState=off
|
||||
state.kinsType=identity
|
||||
programRuntimeFeedbackSource=linuxcnc-task-motion-hal-wasm
|
||||
```
|
||||
|
||||
## 3. 影响
|
||||
|
||||
该问题影响 G-code 执行时的刀具轨迹可信度:
|
||||
|
||||
```text
|
||||
1. 预览路径显示程序已经进入 TCP/RTCP 区间。
|
||||
2. RUN 后 task/HAL 状态把 kinsType 回退到 identity。
|
||||
3. canvas 上的 TCP/刀轴/RTCP 状态与程序 switchkins 语义不一致。
|
||||
4. 操作者可能误判当前刀具姿态和五轴 RTCP 执行状态。
|
||||
```
|
||||
|
||||
## 4. 根因定位
|
||||
|
||||
代码落点:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/app/src/state/store.js
|
||||
```
|
||||
|
||||
原逻辑:
|
||||
|
||||
```js
|
||||
const kinsType = kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
|
||||
```
|
||||
|
||||
问题:
|
||||
|
||||
```text
|
||||
task/HAL runtime status.ui.switchkinsType=0 时,store 直接解析为 identity。
|
||||
但当前 activeLine 对应的 interpreter/canonical motion 仍处于 M428 后的 TCP 区间。
|
||||
因此 task/HAL status 覆盖了程序语义。
|
||||
```
|
||||
|
||||
## 5. 正确边界
|
||||
|
||||
整改原则:
|
||||
|
||||
```text
|
||||
1. 不在可视化层硬编码 RTCP。
|
||||
2. 不用 JavaScript 重新解释 G-code。
|
||||
3. 优先消费 LinuxCNC interpreter 已输出的 canonical motion switchkins 信息。
|
||||
4. task/HAL status 非零 switchkinsType 可直接采用。
|
||||
5. task/HAL status 为 0 时,必须结合当前 activeLine 的 program motion 判断是否仍在 TCP 区间。
|
||||
```
|
||||
|
||||
## 6. 修复目标
|
||||
|
||||
目标状态:
|
||||
|
||||
```text
|
||||
预览态:
|
||||
threeRtcpState=on
|
||||
state.kinsType=tcp-xyzac
|
||||
|
||||
RUN 后:
|
||||
threeRtcpState=on
|
||||
state.kinsType=tcp-xyzac
|
||||
programRuntimeFeedbackSource=linuxcnc-task-motion-hal-wasm
|
||||
```
|
||||
|
||||
同时,当程序执行到 M429/identity 区间时,仍允许正确回退:
|
||||
|
||||
```text
|
||||
activeLine 对应 motion.switchkinsType=0 -> kinsType=identity, rtcpState=off
|
||||
```
|
||||
@@ -0,0 +1,189 @@
|
||||
# 02 RTCP 刀具轨迹整改完善程序详细步骤
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 修改目标
|
||||
|
||||
针对专项测试发现的运行态 RTCP 回退问题,整改 `TASK_HAL_STATUS_APPLIED` 状态合并逻辑。
|
||||
|
||||
## 2. 修改文件
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/app/src/state/store.js
|
||||
web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
|
||||
```
|
||||
|
||||
## 3. 程序修改步骤
|
||||
|
||||
### Step 3.1 替换 task/HAL kinsType 解析入口
|
||||
|
||||
位置:
|
||||
|
||||
```text
|
||||
store.js -> applyTaskHalStatusPatch()
|
||||
```
|
||||
|
||||
将原逻辑:
|
||||
|
||||
```js
|
||||
const kinsType = kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
|
||||
```
|
||||
|
||||
替换为:
|
||||
|
||||
```js
|
||||
const kinsType = resolveTaskHalKinsType(state, status, activeLine);
|
||||
```
|
||||
|
||||
目的:
|
||||
|
||||
```text
|
||||
让 task/HAL status 和 interpreter/canonical motion 的 switchkins 语义共同参与 kinsType 解析。
|
||||
```
|
||||
|
||||
### Step 3.2 增加 resolveTaskHalKinsType()
|
||||
|
||||
新增函数:
|
||||
|
||||
```js
|
||||
function resolveTaskHalKinsType(state, status, activeLine) {
|
||||
const ui = status?.ui || {};
|
||||
const numeric = Number(ui.switchkinsType);
|
||||
if (Number.isFinite(numeric) && numeric !== 0) {
|
||||
return kinsTypeFromSwitchkinsTypeValue(state, numeric);
|
||||
}
|
||||
|
||||
const programKinsType = kinsTypeFromProgramActiveLine(state, activeLine);
|
||||
if (programKinsType) {
|
||||
return programKinsType;
|
||||
}
|
||||
|
||||
return kinsTypeFromSwitchkinsTypeValue(state, ui.switchkinsType);
|
||||
}
|
||||
```
|
||||
|
||||
判定规则:
|
||||
|
||||
```text
|
||||
1. task/HAL switchkinsType 非 0:
|
||||
直接采用 task/HAL 状态。
|
||||
|
||||
2. task/HAL switchkinsType 为 0:
|
||||
查询当前 activeLine 对应的 program motion。
|
||||
|
||||
3. program motion 有 switchkins 语义:
|
||||
采用 program motion 的 kinsType。
|
||||
|
||||
4. program motion 无 switchkins 语义:
|
||||
回退到 task/HAL status 解析结果。
|
||||
```
|
||||
|
||||
### Step 3.3 增加 activeLine 到 program motion 的映射
|
||||
|
||||
新增函数:
|
||||
|
||||
```js
|
||||
function kinsTypeFromProgramActiveLine(state, activeLine) {
|
||||
const motion = programMotionAtOrBeforeLine(state, activeLine)
|
||||
|| state.programExecution?.motion?.[clampMotionIndex(state, state.programExecutionMotionIndex)];
|
||||
return kinsTypeFromProgramMotion(state, motion);
|
||||
}
|
||||
```
|
||||
|
||||
新增函数:
|
||||
|
||||
```js
|
||||
function programMotionAtOrBeforeLine(state, activeLine) {
|
||||
const motion = state.programExecution?.motion;
|
||||
if (!Array.isArray(motion) || motion.length === 0) return null;
|
||||
const line = Number(activeLine);
|
||||
if (!Number.isFinite(line)) return null;
|
||||
let candidate = null;
|
||||
for (const item of motion) {
|
||||
const itemLine = Number(item?.line);
|
||||
if (!Number.isFinite(itemLine)) continue;
|
||||
if (itemLine > line) break;
|
||||
candidate = item;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
```
|
||||
|
||||
目的:
|
||||
|
||||
```text
|
||||
用当前 task/HAL activeLine 找到最近的 canonical motion,
|
||||
再复用已有 kinsTypeFromProgramMotion() 解析 M428/M429/M430 语义。
|
||||
```
|
||||
|
||||
### Step 3.4 保持已有 program motion 解析函数不变
|
||||
|
||||
继续复用:
|
||||
|
||||
```js
|
||||
function kinsTypeFromProgramMotion(state, motion) { ... }
|
||||
function resolveProgramKinsType(state, requestedKinsType) { ... }
|
||||
function kinsTypeFromSwitchkinsType(state, switchkinsType) { ... }
|
||||
```
|
||||
|
||||
不要新增 G-code 字符串解析。
|
||||
|
||||
### Step 3.5 增加 node 回归测试
|
||||
|
||||
位置:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
|
||||
```
|
||||
|
||||
新增测试数据:
|
||||
|
||||
```text
|
||||
motion line 5: switchkinsType=1, kinsType=tcp
|
||||
motion line 20: switchkinsType=0, kinsType=identity
|
||||
```
|
||||
|
||||
新增断言:
|
||||
|
||||
```text
|
||||
当 task/HAL status activeLine=5 且 ui.switchkinsType=0:
|
||||
state.kinsType 必须保持 tcp-xyzac
|
||||
state.rtcpState 必须保持 on
|
||||
|
||||
当 task/HAL status activeLine=20 且 ui.switchkinsType=0:
|
||||
state.kinsType 必须为 identity
|
||||
state.rtcpState 必须为 off
|
||||
```
|
||||
|
||||
### Step 3.6 更新专项测试报告
|
||||
|
||||
重新运行:
|
||||
|
||||
```bash
|
||||
node qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
node qa/web-rtcp-5axis-site-test/generate-toolpath-preview-docx-report.mjs
|
||||
```
|
||||
|
||||
期望:
|
||||
|
||||
```text
|
||||
06-running-rtcp-toolpath -> PASS
|
||||
threeRtcpState=on
|
||||
```
|
||||
|
||||
## 4. 不允许的修复方式
|
||||
|
||||
```text
|
||||
1. 不允许在 five-axis-scene.js 中强制显示 RTCP on。
|
||||
2. 不允许在 canvas dataset 中伪造 threeRtcpState。
|
||||
3. 不允许直接忽略 task/HAL status。
|
||||
4. 不允许重新用字符串扫描 G-code 推断 M428/M429。
|
||||
```
|
||||
|
||||
## 5. 后续完善建议
|
||||
|
||||
```text
|
||||
1. task/HAL runtime 长期应输出更准确的 switchkins source metadata。
|
||||
2. programExecutionMotionIndex 可进一步按 activeLine 同步,提高当前段高亮精度。
|
||||
3. 专项测试可增加执行到 M429 后 RTCP off 的端到端截图。
|
||||
```
|
||||
@@ -0,0 +1,74 @@
|
||||
# 03 RTCP 刀具轨迹整改测试记录
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 修改摘要
|
||||
|
||||
```text
|
||||
问题: G-code RUN 后 RTCP 从 on 回退到 off。
|
||||
根因: TASK_HAL_STATUS_APPLIED 直接采用 task/HAL status.ui.switchkinsType=0。
|
||||
修复: task/HAL switchkinsType 为 0 时,按 activeLine 查询 programExecution.motion 的 switchkins 语义。
|
||||
```
|
||||
|
||||
## 2. 修改文件
|
||||
|
||||
| 文件 | 修改内容 |
|
||||
| --- | --- |
|
||||
| `app/src/state/store.js` | 新增 `resolveTaskHalKinsType()`、`kinsTypeFromProgramActiveLine()`、`programMotionAtOrBeforeLine()` |
|
||||
| `tests/node/verify_rtcp_store.mjs` | 增加 task/HAL status 与 program motion switchkins 合并回归测试 |
|
||||
| `qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs` | 专项截图测试已保留运行态 RTCP 检查 |
|
||||
| `qa/web-rtcp-5axis-site-test/generate-toolpath-preview-docx-report.mjs` | 专项 Word 报告生成 |
|
||||
|
||||
## 3. 验证命令
|
||||
|
||||
```bash
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
|
||||
bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
|
||||
node qa/web-rtcp-5axis-site-test/capture-toolpath-preview-cases.mjs
|
||||
node qa/web-rtcp-5axis-site-test/generate-toolpath-preview-docx-report.mjs
|
||||
```
|
||||
|
||||
## 4. 验证结果
|
||||
|
||||
```text
|
||||
gmoccapy_static_build=ok
|
||||
rtcp_store_smoke=ok
|
||||
linuxcnc_task_hal_runtime_smoke=ok
|
||||
task_hal_machine_file_smoke=ok
|
||||
switchkins_remap_hal_sync_smoke=ok
|
||||
browser_task_hal_worker_smoke=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
```
|
||||
|
||||
专项测试:
|
||||
|
||||
```text
|
||||
total=6
|
||||
PASS=6
|
||||
FAIL=0
|
||||
06-running-rtcp-toolpath threeRtcpState=on
|
||||
```
|
||||
|
||||
Word 报告校验:
|
||||
|
||||
```text
|
||||
unzip -t qa/web-rtcp-5axis-site-test/output/web-rtcp-5axis-toolpath-preview-report-2026-06-22.docx
|
||||
No errors detected
|
||||
```
|
||||
|
||||
## 5. 证据文件
|
||||
|
||||
```text
|
||||
qa/web-rtcp-5axis-site-test/output/toolpath-preview-cases.json
|
||||
qa/web-rtcp-5axis-site-test/output/web-rtcp-5axis-toolpath-preview-report-2026-06-22.docx
|
||||
qa/web-rtcp-5axis-site-test/screenshots/toolpath-preview-cases/06-running-rtcp-toolpath.png
|
||||
```
|
||||
|
||||
## 6. 回归结论
|
||||
|
||||
```text
|
||||
整改后,G-code RUN 状态下 task/HAL runtime feedback 不再把程序 switchkins TCP 区间错误回退到 identity/off。
|
||||
刀具预览、TCP 球、刀轴线、长路径、执行轨迹、rapid/feed 分层和 RTCP 状态在专项测试中全部通过。
|
||||
```
|
||||
35
web-rtcp-5axis-sim-plan/working/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# working 整改文档索引
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
本目录记录针对“刀具预览与 G-code 执行刀具轨迹专项测试”发现问题的整改方案、程序修改步骤和验证证据。
|
||||
|
||||
## 文件清单
|
||||
|
||||
| 文件 | 用途 |
|
||||
| --- | --- |
|
||||
| `01-rtcp-toolpath-problem-review.md` | 记录专项测试问题、现象、影响和代码落点 |
|
||||
| `02-rtcp-toolpath-remediation-steps.md` | 面向程序员的详细整改完善步骤 |
|
||||
| `03-rtcp-toolpath-test-record.md` | 整改后测试命令、结果和证据路径 |
|
||||
|
||||
## 本轮问题结论
|
||||
|
||||
专项测试 `06-running-rtcp-toolpath` 发现:
|
||||
|
||||
```text
|
||||
预览态: RTCP=on, kinsType=tcp-xyzac
|
||||
G-code RUN 后: canvas dataset threeRtcpState=off, state.kinsType=identity
|
||||
```
|
||||
|
||||
根因:
|
||||
|
||||
```text
|
||||
TASK_HAL_STATUS_APPLIED 使用 task/HAL status.ui.switchkinsType=0 直接覆盖了 interpreter/canonical motion 已解析出的程序 switchkins 状态。
|
||||
```
|
||||
|
||||
整改后:
|
||||
|
||||
```text
|
||||
6 个刀具预览/G-code 轨迹专项场景全部 PASS。
|
||||
运行态 threeRtcpState 保持 on。
|
||||
```
|
||||
212
web-rtcp-5axis-sim-plan/working1/01-problem-review.md
Normal file
@@ -0,0 +1,212 @@
|
||||
# 01 问题复盘
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 输入证据
|
||||
|
||||
测试报告:
|
||||
|
||||
```text
|
||||
/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/web-rtcp-5axis-site-test-report-2026-06-22.docx
|
||||
```
|
||||
|
||||
原始自动化结果:
|
||||
|
||||
```text
|
||||
/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/site-test-report.json
|
||||
```
|
||||
|
||||
截图目录:
|
||||
|
||||
```text
|
||||
/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/screenshots/
|
||||
```
|
||||
|
||||
## 2. D1:RTCP/运动学边界未自动挂接
|
||||
|
||||
### 现象
|
||||
|
||||
页面稳定加载后,内部运行时已经加载 LinuxCNC kinematics worker:
|
||||
|
||||
```text
|
||||
kinematicsRuntimeReadiness.loaded=true
|
||||
semanticBoundary=linuxcnc_kinematics_wasm_c_abi
|
||||
sourceMode=source-derived-kinematics-wasm
|
||||
```
|
||||
|
||||
但 RTCP frame 仍显示:
|
||||
|
||||
```text
|
||||
state.sourceMode=fixture-ui-only
|
||||
state.frameSourceMode=fixture-ui-only
|
||||
frameBoundary=fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof
|
||||
```
|
||||
|
||||
手动执行:
|
||||
|
||||
```js
|
||||
await window.webRtcp5AxisSimulation.refreshKinematicsFrame()
|
||||
```
|
||||
|
||||
后可立即切换为:
|
||||
|
||||
```text
|
||||
state.sourceMode=source-derived-kinematics-wasm
|
||||
frameBoundary=linuxcnc_kinematics_wasm_c_abi
|
||||
```
|
||||
|
||||
### 影响
|
||||
|
||||
- 首屏 RTCP/TCP 姿态并非自动来自 LinuxCNC source-derived kinematics。
|
||||
- 界面 `Boundary ready` 可能显示 ready,但 RTCP frame 仍在 fixture 边界,诊断信息不一致。
|
||||
- 用户和测试脚本都会误判当前仿真语义边界。
|
||||
|
||||
### 涉及代码
|
||||
|
||||
```text
|
||||
app/src/main.js
|
||||
attachDefaultKinematicsRuntime()
|
||||
|
||||
app/src/state/store.js
|
||||
ATTACH_KINEMATICS_RUNTIME
|
||||
setState()
|
||||
buildFrameForState()
|
||||
scheduleAsyncKinematicsRefresh()
|
||||
refreshAsyncKinematicsFrame()
|
||||
|
||||
app/src/runtime/rtcp-frame.js
|
||||
buildRtcpFrame()
|
||||
```
|
||||
|
||||
### 初步根因
|
||||
|
||||
当前 `setState()` 在异步 worker kinematics runtime 已加载但 frame 尚未刷新时,会通过 `buildFrameForState()` 生成 fixture frame;随后又把:
|
||||
|
||||
```js
|
||||
frameSourceMode: frame.sourceMode
|
||||
```
|
||||
|
||||
写回 state。这样本来期望进入 LinuxCNC kinematics 的请求状态被 fixture 结果覆盖。
|
||||
|
||||
`scheduleAsyncKinematicsRefresh()` 又依赖:
|
||||
|
||||
```js
|
||||
state.frameSourceMode === "source-derived-kinematics-wasm"
|
||||
```
|
||||
|
||||
当 `frameSourceMode` 已被覆盖成 `fixture-ui-only` 后,自动刷新不再触发。手动调用 `refreshKinematicsFrame()` 能成功,说明底层 worker 能用,问题在自动刷新状态机。
|
||||
|
||||
## 3. D2:3D 预览不可见
|
||||
|
||||
### 现象
|
||||
|
||||
首屏、程序运行、本地 G-code 导入、Audit 后截图中,左侧预览区均为黑底,未观察到五轴机床、刀具或刀路可见对象。
|
||||
|
||||
相关截图:
|
||||
|
||||
```text
|
||||
01-home.png
|
||||
04-run-state.png
|
||||
05-local-program-opened.png
|
||||
06-after-audit.png
|
||||
```
|
||||
|
||||
### 影响
|
||||
|
||||
- 五轴/RTCP 仿真最重要的视觉反馈缺失。
|
||||
- 用户无法通过界面验证 TCP 点、刀轴、程序轨迹和执行轨迹。
|
||||
- 不满足项目文档中“Three.js 视口非空,能显示机床、刀具、刀路”的第一版完成定义。
|
||||
|
||||
### 涉及代码
|
||||
|
||||
```text
|
||||
app/src/visualization/five-axis-scene.js
|
||||
renderFiveAxisScene()
|
||||
createScene()
|
||||
updateToolpathPreview()
|
||||
renderFallbackPreview()
|
||||
exposePreviewDataset()
|
||||
|
||||
app/src/ui/gmoccapy-shell.js
|
||||
renderPreview()
|
||||
|
||||
app/src/styles/gmoccapy.css
|
||||
preview/canvas 尺寸和布局
|
||||
```
|
||||
|
||||
### 初步根因
|
||||
|
||||
需要重点检查两类问题:
|
||||
|
||||
1. `updateToolpathPreview()` 内部使用了 `fitPoints`、`fitKey`,但当前函数片段中未看到局部定义。若实际执行进入 WebGL 分支,可能触发运行时异常并降级或中断渲染。
|
||||
2. 当前 WebGL scene 只添加了路径线、tool marker、tool axis,没有明确的机床基准模型、工作台、旋转轴、坐标轴等常驻对象。即使路径为空或颜色很暗,用户也应看到基础机床对象。
|
||||
|
||||
另外,fallback 分支必须在无 WebGL 或 WebGL 初始化失败时仍绘制明显对象,而不是仅写一个很小的文字提示。
|
||||
|
||||
## 4. D3:HOME 后 JOG 坐标连续性异常
|
||||
|
||||
### 现象
|
||||
|
||||
稳定复核结果:
|
||||
|
||||
```text
|
||||
after HOME:
|
||||
X=43, Y=-32.15, Z=-11.306
|
||||
|
||||
after JOG X+:
|
||||
X=1, Y=0, Z=0
|
||||
|
||||
after JOG Y-:
|
||||
X=1, Y=-1, Z=0
|
||||
```
|
||||
|
||||
### 影响
|
||||
|
||||
- DRO 中的手动移动不连续。
|
||||
- 操作员会误以为机床从 HOME 坐标突然跳到任务/HAL 局部原点。
|
||||
- 会话恢复、手动定位、RTCP 视觉反馈都可能被错误坐标污染。
|
||||
|
||||
### 涉及代码
|
||||
|
||||
```text
|
||||
app/src/state/store.js
|
||||
HOME
|
||||
JOG
|
||||
TASK_HAL_STATUS_APPLIED
|
||||
applyTaskHalStatusPatch()
|
||||
|
||||
app/src/runtime/linuxcnc-task-hal-runtime.js
|
||||
normalize/readStatus 输出的 ui.axisPose
|
||||
|
||||
app/src/state/linuxcnc-task-policy.js
|
||||
JOG/HOME gate
|
||||
```
|
||||
|
||||
### 初步根因
|
||||
|
||||
当前启用 task/HAL runtime 后,JOG 走 task/HAL command path。`applyTaskHalStatusPatch()` 会把 runtime status 中的 `ui.axisPose` 直接覆盖 UI state:
|
||||
|
||||
```js
|
||||
const axisPose = clampAxisPoseToProfile({
|
||||
...state.axisPose,
|
||||
...ui.axisPose,
|
||||
}, state.profile);
|
||||
```
|
||||
|
||||
但 task/HAL runtime 返回的 `ui.axisPose` 看起来是 task-local 或 motion-local 坐标,初始值从 0 开始,并非当前 UI DRO/HOME 坐标系。因此 JOG 后坐标被另一套坐标系覆盖。
|
||||
|
||||
## 5. 非缺陷说明
|
||||
|
||||
以下现象在复核后不作为缺陷记录:
|
||||
|
||||
1. 页面初始数秒内 Task/HAL 和 machine-file staging 从 pending 过渡到 ready/staged,属于异步启动暂态。
|
||||
2. `M428/M429/M430` 经稳定态复核均可执行:
|
||||
|
||||
```text
|
||||
M428 -> kins=tcp-xyzac, rtcp=on
|
||||
M429 -> kins=identity, rtcp=off
|
||||
M430 -> kins=userk, rtcp=off
|
||||
```
|
||||
|
||||
3. Open 本地 G-code 使用 `programSource=operator-file`,这是当前实现命名,不是功能失败。
|
||||
|
||||
240
web-rtcp-5axis-sim-plan/working1/02-repair-plan.md
Normal file
@@ -0,0 +1,240 @@
|
||||
# 02 修复总体方案
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 修复原则
|
||||
|
||||
1. 不用 JavaScript 重写 LinuxCNC 运动学语义。
|
||||
2. RTCP/TCP frame 必须优先来自 LinuxCNC source-derived kinematics WASM。
|
||||
3. 视觉层只能消费 runtime frame、canonical motion、task/HAL feedback,不生成 G-code/CNC 语义。
|
||||
4. JOG/HOME/DRO 必须明确坐标系,不允许 task-local 坐标无标记覆盖 UI work pose。
|
||||
5. 每项修复必须新增或更新测试,先复现问题,再验证修复。
|
||||
|
||||
## 2. 修复优先级
|
||||
|
||||
| 优先级 | 问题 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| P0 | D1 RTCP/运动学边界自动挂接 | 影响语义边界可信度,且修复面较集中 |
|
||||
| P0 | D2 3D 预览可见性 | 影响产品第一视觉和核心仿真价值 |
|
||||
| P1 | D3 HOME/JOG 坐标连续性 | 影响手动操作正确性,需谨慎处理坐标系 |
|
||||
|
||||
## 3. D1 修复方案
|
||||
|
||||
### 目标状态
|
||||
|
||||
页面稳定加载后无需手动调用,自动达到:
|
||||
|
||||
```text
|
||||
state.sourceMode=source-derived-kinematics-wasm
|
||||
state.frameSourceMode=source-derived-kinematics-wasm
|
||||
state.rtcpFrame.semanticBoundary=linuxcnc_kinematics_wasm_c_abi
|
||||
data-rtcp-diagnostic="kinematics-ready" -> ready
|
||||
data-rtcp-diagnostic="boundary" -> linuxcnc_kinematics_wasm_c_abi
|
||||
```
|
||||
|
||||
### 推荐设计
|
||||
|
||||
把“期望使用的 frame source”和“当前已经解析出的 frame source”分开:
|
||||
|
||||
```text
|
||||
desiredFrameSourceMode: source-derived-kinematics-wasm | fixture-ui-only
|
||||
frameSourceMode: 当前 rtcpFrame.sourceMode
|
||||
```
|
||||
|
||||
或者在不新增字段的情况下,至少保证 `setState()` 不用异步 runtime 的临时 fixture frame 覆盖 kinematics 请求状态。
|
||||
|
||||
推荐更清晰的做法:
|
||||
|
||||
1. 新增 `desiredFrameSourceMode`。
|
||||
2. `ATTACH_KINEMATICS_RUNTIME` 成功后设置:
|
||||
|
||||
```js
|
||||
desiredFrameSourceMode: "source-derived-kinematics-wasm"
|
||||
```
|
||||
|
||||
3. `buildFrameForState()` 对 async worker 不直接降级修改 desired state,只生成临时 fixture frame,并标记:
|
||||
|
||||
```text
|
||||
asyncFrameRefreshPending=true
|
||||
```
|
||||
|
||||
4. `scheduleAsyncKinematicsRefresh()` 判断:
|
||||
|
||||
```js
|
||||
state.kinematicsRuntime?.loaded &&
|
||||
state.desiredFrameSourceMode === "source-derived-kinematics-wasm"
|
||||
```
|
||||
|
||||
而不是依赖已经被 fixture 覆盖的 `frameSourceMode`。
|
||||
|
||||
5. `refreshAsyncKinematicsFrame()` 成功后写入:
|
||||
|
||||
```js
|
||||
sourceMode: "source-derived-kinematics-wasm"
|
||||
frameSourceMode: "source-derived-kinematics-wasm"
|
||||
desiredFrameSourceMode: "source-derived-kinematics-wasm"
|
||||
```
|
||||
|
||||
### 防回退要求
|
||||
|
||||
任何以下动作后都不能把 frame 永久退回 fixture:
|
||||
|
||||
```text
|
||||
ATTACH_INI_CONFIG
|
||||
SET_PROFILE
|
||||
MACHINE_FILE_STAGING_COMPLETE
|
||||
TASK_HAL_STATUS_APPLIED
|
||||
LOAD_PROGRAM
|
||||
LOAD_LINUXCNC_GCODE_SOURCE
|
||||
HOME/JOG/RUN/STEP
|
||||
```
|
||||
|
||||
如果某次 frame 刷新失败,应显示错误并保留 retry 能力,不能静默永久降级。
|
||||
|
||||
## 4. D2 修复方案
|
||||
|
||||
### 目标状态
|
||||
|
||||
首屏不加载任何用户程序时也必须可见:
|
||||
|
||||
```text
|
||||
机床基准/工作台
|
||||
XYZ 坐标轴
|
||||
旋转轴标识
|
||||
刀具/TCP marker
|
||||
预览路径或占位路径
|
||||
```
|
||||
|
||||
WebGL 不可用时,2D fallback 也必须绘制清晰的轴线、路径和 TCP 点。
|
||||
|
||||
### 推荐设计
|
||||
|
||||
1. 修复 `updateToolpathPreview()` 中未定义变量风险:
|
||||
|
||||
```js
|
||||
const toolPosition = executionToolPosition(state, previewPoints);
|
||||
const fitPoints = collectFitPoints(previewPoints, executedPoints, currentSegmentPoints, toolPosition);
|
||||
const fitKey = buildFitKey(...);
|
||||
```
|
||||
|
||||
2. 在 `createScene()` 中加入常驻机床模型:
|
||||
|
||||
```text
|
||||
machineRoot
|
||||
tableGroup
|
||||
rotaryA/rotaryB/rotaryC visual rings
|
||||
toolHolder
|
||||
axisHelper
|
||||
grid/reference plane
|
||||
```
|
||||
|
||||
3. 增加单独函数:
|
||||
|
||||
```js
|
||||
createMachineReferenceModel()
|
||||
updateMachineReferenceModel(preview, state)
|
||||
```
|
||||
|
||||
4. `renderFallbackPreview()` 中绘制:
|
||||
|
||||
```text
|
||||
灰色工作台矩形
|
||||
绿色/红色/蓝色 XYZ 轴
|
||||
青色 TCP 点
|
||||
亮色刀路 polyline
|
||||
明显的 fallback 标签和点数
|
||||
```
|
||||
|
||||
5. `exposePreviewDataset()` 必须稳定输出:
|
||||
|
||||
```text
|
||||
data-three-ready=true
|
||||
data-three-renderer=webgl | 2d-fallback
|
||||
data-three-scene-objects > 0
|
||||
data-three-path-points > 0 或 data-three-tool-execution-marker=true
|
||||
```
|
||||
|
||||
### 可见性门槛
|
||||
|
||||
自动化测试应检查 canvas 像素,不只检查 DOM:
|
||||
|
||||
```text
|
||||
nonBlackRatio > 0.02
|
||||
averageLuminance > 5
|
||||
```
|
||||
|
||||
## 5. D3 修复方案
|
||||
|
||||
### 目标状态
|
||||
|
||||
HOME 后执行 JOG:
|
||||
|
||||
```text
|
||||
after HOME: X=43, Y=-32.15, Z=-11.306
|
||||
after JOG X+: X=44, Y=-32.15, Z=-11.306
|
||||
after JOG Y-: X=44, Y=-33.15, Z=-11.306
|
||||
```
|
||||
|
||||
实际增量以 `state.machine.jogIncrement` 为准。
|
||||
|
||||
### 推荐设计
|
||||
|
||||
必须明确 task/HAL status 中 `ui.axisPose` 的坐标系。
|
||||
|
||||
推荐新增字段:
|
||||
|
||||
```js
|
||||
ui.axisPoseFrame = "work" | "machine" | "task-local" | "joint-local"
|
||||
ui.axisPoseDelta = { x, y, z, a, b, c } // JOG 增量可选
|
||||
```
|
||||
|
||||
处理规则:
|
||||
|
||||
1. 如果 `axisPoseFrame === "work"`,可直接覆盖 UI work pose。
|
||||
2. 如果 `axisPoseFrame === "task-local"` 且本次 motion type 是 JOG,优先使用 `axisPoseDelta` 加到当前 state.axisPose。
|
||||
3. 如果没有 frame 标记,不允许直接覆盖非零 UI pose;必须保守保留旧 pose 或走 fallback 增量。
|
||||
4. HOME 命令要把 task/HAL runtime 的参考 pose 与 UI HOME pose 同步,或者返回 `axisPoseFrame="work"`。
|
||||
|
||||
### 短期修复方案
|
||||
|
||||
如果 task/HAL runtime 暂时不能增加 frame 元数据,可在 store 层先做保护:
|
||||
|
||||
```text
|
||||
当 status motion type 为 JOG 且 ui.axisPose 接近局部原点时,
|
||||
不要整体覆盖 state.axisPose;
|
||||
改用本次 JOG action 的 axis/direction/increment 计算 UI pose。
|
||||
```
|
||||
|
||||
为了实现这点,store 需要在发送 task/HAL JOG 命令时记录 pending jog context:
|
||||
|
||||
```js
|
||||
pendingJogCommand: { axis, direction, increment, basePose }
|
||||
```
|
||||
|
||||
收到 `TASK_HAL_STATUS_APPLIED` 后:
|
||||
|
||||
```js
|
||||
axisPose = {
|
||||
...pendingJogCommand.basePose,
|
||||
[axis]: pendingJogCommand.basePose[axis] + direction * increment
|
||||
}
|
||||
```
|
||||
|
||||
这是短期 UI 连续性修复;长期仍应让 runtime 明确坐标系。
|
||||
|
||||
## 6. 风险控制
|
||||
|
||||
| 风险 | 控制方式 |
|
||||
| --- | --- |
|
||||
| D1 修复导致 fixture fallback 不可用 | 保留 fallback,但作为显式错误/降级状态,不覆盖 desired frame source |
|
||||
| D2 加机床模型影响性能 | 常驻模型低面数,路径点仍受 `MAX_TOOLPATH_POINTS` 限制 |
|
||||
| D3 坐标修复与真实 task/HAL 状态冲突 | 用 `axisPoseFrame` 标记,避免无标记状态直接覆盖 |
|
||||
| 测试只在 headless 下通过 | 同时跑本地浏览器截图和 headless pixel 检查 |
|
||||
|
||||
## 7. 建议提交拆分
|
||||
|
||||
1. `fix: keep desired LinuxCNC kinematics frame source across async refresh`
|
||||
2. `fix: render visible five-axis preview model and fallback scene`
|
||||
3. `fix: preserve work-pose continuity for task-hal jog feedback`
|
||||
4. `test: add browser regression for kinematics auto-refresh preview and jog continuity`
|
||||
|
||||
488
web-rtcp-5axis-sim-plan/working1/03-implementation-steps.md
Normal file
@@ -0,0 +1,488 @@
|
||||
# 03 程序修复详细步骤
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
本文档面向实际编码人员,按问题给出具体修改步骤。执行前建议先创建修复分支。
|
||||
|
||||
```bash
|
||||
git status --short
|
||||
git switch -c fix/web-rtcp-5axis-working1
|
||||
```
|
||||
|
||||
## 1. D1:自动挂接 LinuxCNC kinematics frame
|
||||
|
||||
### Step 1.1 增加期望 frame source 状态
|
||||
|
||||
文件:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/app/src/state/store.js
|
||||
```
|
||||
|
||||
在 `initialState` 增加字段:
|
||||
|
||||
```js
|
||||
desiredFrameSourceMode: "fixture-ui-only",
|
||||
```
|
||||
|
||||
保留现有:
|
||||
|
||||
```js
|
||||
sourceMode
|
||||
frameSourceMode
|
||||
```
|
||||
|
||||
三者语义:
|
||||
|
||||
```text
|
||||
desiredFrameSourceMode: 用户/运行时希望使用的 frame 来源
|
||||
frameSourceMode: 当前 rtcpFrame 实际来源
|
||||
sourceMode: UI 总体展示来源,可继续跟当前 frame source 同步
|
||||
```
|
||||
|
||||
### Step 1.2 修改 ATTACH_KINEMATICS_RUNTIME
|
||||
|
||||
位置:
|
||||
|
||||
```text
|
||||
store.js -> case "ATTACH_KINEMATICS_RUNTIME"
|
||||
```
|
||||
|
||||
runtime loaded 时设置:
|
||||
|
||||
```js
|
||||
desiredFrameSourceMode: "source-derived-kinematics-wasm",
|
||||
```
|
||||
|
||||
runtime missing 时设置:
|
||||
|
||||
```js
|
||||
desiredFrameSourceMode: "fixture-ui-only",
|
||||
```
|
||||
|
||||
不要只依赖 `sourceMode` / `frameSourceMode`。
|
||||
|
||||
### Step 1.3 修改 buildFrameForState
|
||||
|
||||
当前逻辑大意:
|
||||
|
||||
```js
|
||||
const requestedSourceMode = state.frameSourceMode || state.sourceMode;
|
||||
...
|
||||
if (requestedSourceMode === "source-derived-kinematics-wasm") {
|
||||
if (runtime loaded && !async) {
|
||||
...
|
||||
} else {
|
||||
sourceMode = "fixture-ui-only";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
建议改为:
|
||||
|
||||
```js
|
||||
const requestedSourceMode =
|
||||
state.desiredFrameSourceMode ||
|
||||
state.frameSourceMode ||
|
||||
state.sourceMode;
|
||||
```
|
||||
|
||||
async runtime 已加载但尚未返回 frame 时,可以临时生成 fixture frame,但必须带上可诊断原因,不要覆盖 desired source。
|
||||
|
||||
### Step 1.4 修改 setState 写回策略
|
||||
|
||||
当前 `setState()` 中:
|
||||
|
||||
```js
|
||||
sourceMode: frame.sourceMode,
|
||||
frameSourceMode: frame.sourceMode,
|
||||
```
|
||||
|
||||
建议改为:
|
||||
|
||||
```js
|
||||
sourceMode: frame.sourceMode,
|
||||
frameSourceMode: frame.sourceMode,
|
||||
desiredFrameSourceMode: next.desiredFrameSourceMode || frame.sourceMode,
|
||||
```
|
||||
|
||||
关键点:不要因为临时 fixture frame 把 `desiredFrameSourceMode` 变成 fixture。
|
||||
|
||||
### Step 1.5 修改 scheduleAsyncKinematicsRefresh
|
||||
|
||||
当前 guard 不应依赖已解析 frame source:
|
||||
|
||||
```js
|
||||
if (state.frameSourceMode !== "source-derived-kinematics-wasm") return null;
|
||||
```
|
||||
|
||||
改为:
|
||||
|
||||
```js
|
||||
if (state.desiredFrameSourceMode !== "source-derived-kinematics-wasm") return null;
|
||||
if (!state.kinematicsRuntime?.loaded) return null;
|
||||
if (!isAsyncKinematicsRuntime(state.kinematicsRuntime)) return null;
|
||||
```
|
||||
|
||||
如果当前 frame 已经 ready 且 activeLine/axisPose/kinsType 未变化,可继续跳过刷新。
|
||||
|
||||
### Step 1.6 修改 refreshAsyncKinematicsFrame 成功写回
|
||||
|
||||
成功后确保:
|
||||
|
||||
```js
|
||||
sourceMode: "source-derived-kinematics-wasm",
|
||||
frameSourceMode: "source-derived-kinematics-wasm",
|
||||
desiredFrameSourceMode: "source-derived-kinematics-wasm",
|
||||
```
|
||||
|
||||
失败时:
|
||||
|
||||
```js
|
||||
operatorMessage: `LinuxCNC kinematics refresh failed: ${error.message}`
|
||||
```
|
||||
|
||||
并保留 retry 能力。
|
||||
|
||||
### Step 1.7 修改 main.js 初始化顺序
|
||||
|
||||
文件:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/app/src/main.js
|
||||
```
|
||||
|
||||
`attachDefaultKinematicsRuntime()` 已调用:
|
||||
|
||||
```js
|
||||
await store.refreshKinematicsFrame(...)
|
||||
```
|
||||
|
||||
修复后保留该调用,并在 profile/INI 变更订阅中,runtime attach 完成后再次刷新:
|
||||
|
||||
```js
|
||||
await attachDefaultKinematicsRuntime(...)
|
||||
await store.refreshKinematicsFrame({ operatorMessage: "..." })
|
||||
```
|
||||
|
||||
注意避免无限刷新。可通过 `asyncFrameRefreshSequence` 或 readiness 状态判断。
|
||||
|
||||
### Step 1.8 D1 测试
|
||||
|
||||
新增或更新测试:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/tests/browser/gmoccapy_shell_smoke.html
|
||||
```
|
||||
|
||||
或新增:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/tests/browser/kinematics_auto_refresh_smoke.html
|
||||
```
|
||||
|
||||
断言:
|
||||
|
||||
```js
|
||||
await waitUntil(() => window.webRtcp5AxisSimulation.getState().sourceMode === "source-derived-kinematics-wasm")
|
||||
assertText('[data-rtcp-diagnostic="boundary"]', 'linuxcnc_kinematics_wasm_c_abi')
|
||||
assertText('[data-rtcp-diagnostic="kinematics-ready"]', 'ready')
|
||||
```
|
||||
|
||||
## 2. D2:修复 3D 预览可见性
|
||||
|
||||
### Step 2.1 修复 updateToolpathPreview 未定义变量
|
||||
|
||||
文件:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/app/src/visualization/five-axis-scene.js
|
||||
```
|
||||
|
||||
在 `updateToolpathPreview(preview, state)` 中补齐:
|
||||
|
||||
```js
|
||||
const toolPosition = executionToolPosition(state, previewPoints);
|
||||
const fitPoints = collectFitPoints(previewPoints, executedPoints, currentSegmentPoints, toolPosition);
|
||||
const fitKey = [
|
||||
previewPoints.length,
|
||||
executedPoints.length,
|
||||
currentSegmentPoints.length,
|
||||
previewSourceMode(state),
|
||||
state.programExecutionMotionIndex || 0,
|
||||
state.programExecutionSampleIndex || 0,
|
||||
].join(":");
|
||||
```
|
||||
|
||||
确保 `updateToolExecutionMarker()` 使用同一个 `toolPosition`。
|
||||
|
||||
### Step 2.2 增加常驻机床参考模型
|
||||
|
||||
新增函数:
|
||||
|
||||
```js
|
||||
function createMachineReferenceModel() { ... }
|
||||
function updateMachineReferenceModel(preview, state) { ... }
|
||||
```
|
||||
|
||||
推荐对象:
|
||||
|
||||
```text
|
||||
grid/table base: dark gray plane/box
|
||||
X axis: red line
|
||||
Y axis: green line
|
||||
Z axis: blue line
|
||||
rotary ring: cyan/yellow ring
|
||||
tool holder: small cylinder/cone
|
||||
TCP marker: existing sphere
|
||||
```
|
||||
|
||||
在 `createScene()` 中:
|
||||
|
||||
```js
|
||||
const machineModel = createMachineReferenceModel();
|
||||
scene.add(machineModel.root);
|
||||
```
|
||||
|
||||
在 preview 对象中保存:
|
||||
|
||||
```js
|
||||
machineModel
|
||||
```
|
||||
|
||||
在 `updateToolpathPreview()` 中:
|
||||
|
||||
```js
|
||||
updateMachineReferenceModel(preview, state);
|
||||
```
|
||||
|
||||
### Step 2.3 提高 fallback 可见性
|
||||
|
||||
在 `renderFallbackPreview()` 中,路径为空也要绘制:
|
||||
|
||||
```text
|
||||
工作台矩形
|
||||
XYZ 坐标轴
|
||||
旋转中心
|
||||
TCP 点
|
||||
```
|
||||
|
||||
要求颜色和尺寸足够明显,避免黑底上不可见。
|
||||
|
||||
### Step 2.4 强化 canvas dataset
|
||||
|
||||
`exposePreviewDataset()` 已存在,修复后确保任意路径都稳定输出:
|
||||
|
||||
```text
|
||||
data-three-ready="true"
|
||||
data-three-renderer
|
||||
data-three-scene-objects
|
||||
data-three-path-points
|
||||
data-three-tool-execution-marker
|
||||
```
|
||||
|
||||
如果 fallback:
|
||||
|
||||
```text
|
||||
data-three-fallback-reason
|
||||
```
|
||||
|
||||
如果 WebGL:
|
||||
|
||||
```text
|
||||
data-three-renderer="webgl"
|
||||
```
|
||||
|
||||
### Step 2.5 D2 测试
|
||||
|
||||
新增 pixel 检查:
|
||||
|
||||
```js
|
||||
const stats = canvasPixelStats(canvas)
|
||||
assert(stats.nonBlackRatio > 0.02)
|
||||
assert(stats.averageLuminance > 5)
|
||||
```
|
||||
|
||||
同时检查:
|
||||
|
||||
```js
|
||||
Number(canvas.dataset.threeSceneObjects) > 0
|
||||
canvas.dataset.threeReady === "true"
|
||||
```
|
||||
|
||||
建议覆盖 desktop 和 mobile viewport。
|
||||
|
||||
## 3. D3:修复 HOME/JOG 坐标连续性
|
||||
|
||||
### Step 3.1 记录 pending JOG 上下文
|
||||
|
||||
文件:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/app/src/state/store.js
|
||||
```
|
||||
|
||||
在 `initialState` 增加:
|
||||
|
||||
```js
|
||||
pendingJogCommand: null,
|
||||
```
|
||||
|
||||
在 `case "JOG"` 的 task/HAL runtime 分支,发送命令前记录:
|
||||
|
||||
```js
|
||||
setState({
|
||||
pendingJogCommand: {
|
||||
axis,
|
||||
direction,
|
||||
increment,
|
||||
basePose: state.axisPose,
|
||||
createdAtLine: state.activeLine,
|
||||
},
|
||||
operatorMessage: `task/HAL jog ${axis.toUpperCase()} ...`,
|
||||
})
|
||||
```
|
||||
|
||||
注意现有代码直接调用 `runTaskHalCommandSequence()`,需要避免两次 setState 引发顺序混乱。可把 pending context 作为 `runTaskHalCommandSequence()` 的 options 传入,最终在 command started patch 中写入。
|
||||
|
||||
### Step 3.2 给 task/HAL status 增加坐标系元数据
|
||||
|
||||
文件:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-sim-plan/app/src/runtime/linuxcnc-task-hal-runtime.js
|
||||
```
|
||||
|
||||
在 `ui` 对象中增加:
|
||||
|
||||
```js
|
||||
axisPoseFrame: "task-local",
|
||||
```
|
||||
|
||||
如果 runtime 能确定是 work pose,则写:
|
||||
|
||||
```js
|
||||
axisPoseFrame: "work",
|
||||
```
|
||||
|
||||
如果能计算增量,增加:
|
||||
|
||||
```js
|
||||
axisPoseDelta: { x, y, z, a, b, c }
|
||||
```
|
||||
|
||||
短期不能准确判断时,不要伪装成 work。
|
||||
|
||||
### Step 3.3 修改 applyTaskHalStatusPatch
|
||||
|
||||
当前:
|
||||
|
||||
```js
|
||||
const axisPose = clampAxisPoseToProfile({
|
||||
...state.axisPose,
|
||||
...ui.axisPose,
|
||||
}, state.profile);
|
||||
```
|
||||
|
||||
改为单独函数:
|
||||
|
||||
```js
|
||||
const axisPose = resolveTaskHalAxisPose(state, status);
|
||||
```
|
||||
|
||||
建议实现:
|
||||
|
||||
```js
|
||||
function resolveTaskHalAxisPose(state, status) {
|
||||
const ui = status?.ui || {};
|
||||
if (ui.axisPoseFrame === "work") {
|
||||
return clampAxisPoseToProfile({ ...state.axisPose, ...ui.axisPose }, state.profile);
|
||||
}
|
||||
|
||||
if (ui.axisPoseDelta) {
|
||||
return addAxisDelta(state.axisPose, ui.axisPoseDelta, state.profile);
|
||||
}
|
||||
|
||||
if (state.pendingJogCommand && isJogStatus(status)) {
|
||||
const { axis, direction, increment, basePose } = state.pendingJogCommand;
|
||||
return clampAxisPoseToProfile({
|
||||
...basePose,
|
||||
[axis]: Number(basePose[axis] || 0) + direction * increment,
|
||||
}, state.profile);
|
||||
}
|
||||
|
||||
if (!ui.axisPoseFrame && wouldResetNonZeroPoseToLocalZero(state.axisPose, ui.axisPose)) {
|
||||
return state.axisPose;
|
||||
}
|
||||
|
||||
return clampAxisPoseToProfile({ ...state.axisPose, ...ui.axisPose }, state.profile);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3.4 清理 pending JOG
|
||||
|
||||
`TASK_HAL_STATUS_APPLIED` 后,如果使用了 pending JOG:
|
||||
|
||||
```js
|
||||
pendingJogCommand: null
|
||||
```
|
||||
|
||||
如果 command failed:
|
||||
|
||||
```js
|
||||
pendingJogCommand: null
|
||||
```
|
||||
|
||||
### Step 3.5 HOME 同步
|
||||
|
||||
HOME 成功后,UI 和 task/HAL runtime 必须同一坐标基准。短期可在 HOME fallback patch 中明确:
|
||||
|
||||
```js
|
||||
axisPose: initialAxisPose
|
||||
```
|
||||
|
||||
task/HAL HOME status 如果返回局部原点,不能覆盖 `initialAxisPose`,除非 status 标记 `axisPoseFrame="work"`。
|
||||
|
||||
### Step 3.6 D3 测试
|
||||
|
||||
新增测试步骤:
|
||||
|
||||
```js
|
||||
power on
|
||||
manual
|
||||
home
|
||||
capture X/Y/Z
|
||||
jog X+
|
||||
assert X === previousX + jogIncrement
|
||||
assert Y/Z unchanged
|
||||
jog Y-
|
||||
assert Y === previousY - jogIncrement
|
||||
assert X/Z unchanged
|
||||
```
|
||||
|
||||
同时验证 DRO 文本:
|
||||
|
||||
```js
|
||||
document.querySelector('[data-region="dro"]').textContent
|
||||
```
|
||||
|
||||
## 4. 本地验证命令
|
||||
|
||||
建议按顺序执行:
|
||||
|
||||
```bash
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_five_axis_session.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
|
||||
```
|
||||
|
||||
如果项目已有 browser smoke:
|
||||
|
||||
```bash
|
||||
web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
|
||||
```
|
||||
|
||||
修复后再执行 QA 站点测试脚本或新增等价本地测试。
|
||||
|
||||
72
web-rtcp-5axis-sim-plan/working1/04-traceability-matrix.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# 04 修复溯源矩阵
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 总体追溯原则
|
||||
|
||||
```text
|
||||
测试问题 -> 证据 -> 本地代码落点 -> LinuxCNC/项目参考 -> 修复项 -> 验收项
|
||||
```
|
||||
|
||||
任何修复都不能只修改 UI 文案掩盖问题,必须让 runtime state、DOM diagnostics、截图和自动化断言一致。
|
||||
|
||||
## 2. 问题到修复追溯表
|
||||
|
||||
| 问题 ID | 测试证据 | 本地代码落点 | LinuxCNC/项目参考 | 修复项 | 验收项 |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| D1 | `sourceMode=fixture-ui-only`,`frameBoundary=fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof`,但 `kinematicsRuntimeReadiness.loaded=true` | `app/src/state/store.js` `ATTACH_KINEMATICS_RUNTIME`、`setState()`、`scheduleAsyncKinematicsRefresh()`、`refreshAsyncKinematicsFrame()`;`app/src/main.js` `attachDefaultKinematicsRuntime()` | `docs/traceability-matrix.md` 中 `Five-axis kinematics`、`RTCP/TCP frame`;LinuxCNC `xyzac-trt-kins.c`、`xyzbc-trt-kins.c`、`trtfuncs.c` | 分离 `desiredFrameSourceMode` 与当前 `frameSourceMode`,异步 worker ready 后自动刷新 frame | 首屏 8 秒内 `sourceMode=source-derived-kinematics-wasm`,DOM boundary 为 `linuxcnc_kinematics_wasm_c_abi` |
|
||||
| D2 | 截图中 preview 黑屏,无可见机床/刀路;canvas dataset 不稳定 | `app/src/visualization/five-axis-scene.js` `createScene()`、`updateToolpathPreview()`、`renderFallbackPreview()`、`exposePreviewDataset()`;`app/src/ui/gmoccapy-shell.js` `renderPreview()` | `docs/implementation-plan.md` 5.4 Visualization/Playback;LinuxCNC `lib/python/vismach.py`、5-axis vismach GUI screenshots | 修复未定义变量,加入常驻机床模型,fallback 绘制明显对象,增加 pixel smoke | canvas 非空,`data-three-ready=true`,`sceneObjects>0`,截图可见机床/刀路/TCP |
|
||||
| D3 | HOME 后 `X=43/Y=-32.15/Z=-11.306`,JOG X+ 后跳到 `X=1/Y=0/Z=0` | `app/src/state/store.js` `JOG`、`HOME`、`applyTaskHalStatusPatch()`;`app/src/runtime/linuxcnc-task-hal-runtime.js` status `ui.axisPose` | LinuxCNC task/motion command model:`EMC_JOG_INCR`、`EMC_JOINT_HOME`;项目 `linuxcnc-task-policy.js` | 为 task/HAL axis pose 增加坐标系元数据,pending jog context,防止局部坐标覆盖 work pose | HOME 后 JOG X+/Y- 基于当前 DRO 连续增减 |
|
||||
|
||||
## 3. 文件级追溯
|
||||
|
||||
| 文件 | 当前职责 | 本次修复关注点 | 需要新增测试 |
|
||||
| --- | --- | --- | --- |
|
||||
| `app/src/main.js` | app 启动、runtime attach、profile 变更订阅 | kinematics runtime attach 后可靠触发 frame refresh | browser smoke 检查首屏 kinematics ready |
|
||||
| `app/src/state/store.js` | 全局状态机、RTCP frame、task policy、运行控制 | desired/current frame source 分离;JOG 坐标连续性;task/HAL status 坐标解析 | node store test + browser operator workflow |
|
||||
| `app/src/runtime/rtcp-frame.js` | RTCP frame 构建和边界标识 | 确认 source-derived frame 输出 diagnostics 明确 | RTCP frame source smoke |
|
||||
| `app/src/visualization/five-axis-scene.js` | Three.js/fallback 预览渲染 | 可见机床模型、fallback 非空、dataset 稳定 | canvas pixel smoke |
|
||||
| `app/src/runtime/linuxcnc-task-hal-runtime.js` | task/HAL runtime adapter 和 status normalize | `axisPoseFrame` / `axisPoseDelta` 输出 | task/HAL JOG status test |
|
||||
| `app/src/state/linuxcnc-task-policy.js` | LinuxCNC task mode/state gate | 一般不需要改;作为 JOG/HOME gate 参考 | 维持现有 gate tests |
|
||||
|
||||
## 4. 测试证据追溯
|
||||
|
||||
| 证据文件 | 用途 |
|
||||
| --- | --- |
|
||||
| `qa/web-rtcp-5axis-site-test/output/web-rtcp-5axis-site-test-report-2026-06-22.docx` | 人类可读测试报告和截图 |
|
||||
| `qa/web-rtcp-5axis-site-test/output/site-test-report.json` | 自动化原始 findings、state、console、request 数据 |
|
||||
| `qa/web-rtcp-5axis-site-test/screenshots/01-home.png` | 首屏预览黑屏和初始界面证据 |
|
||||
| `qa/web-rtcp-5axis-site-test/screenshots/04-run-state.png` | Run 后预览仍不可见证据 |
|
||||
| `qa/web-rtcp-5axis-site-test/screenshots/05-local-program-opened.png` | 本地程序导入后预览仍不可见证据 |
|
||||
| `qa/web-rtcp-5axis-site-test/screenshots/06-after-audit.png` | Audit 后预览仍不可见证据 |
|
||||
|
||||
## 5. LinuxCNC 参考追溯
|
||||
|
||||
| 能力 | LinuxCNC 参考 | Web 边界 |
|
||||
| --- | --- | --- |
|
||||
| TRT kinematics | `src/emc/kinematics/trtfuncs.c`、`xyzac-trt-kins.c`、`xyzbc-trt-kins.c` | 只能通过 source-derived WASM frame 使用 |
|
||||
| switchkins | `src/emc/kinematics/switchkins.c`、`switchkins.h` | `M428/M429/M430` 和 kins type UI |
|
||||
| task/JOG/HOME | `src/emc/task/emctaskmain.cc`、`src/emc/nml_intf/emc.hh` | Web task policy + task/HAL WASM simulation |
|
||||
| vismach visual model | `lib/python/vismach.py`、5-axis vismach configs/xml | Three.js scene graph reference,不移植 Python runtime |
|
||||
|
||||
## 6. 后续修复批次记录要求
|
||||
|
||||
每一批修复完成后,在 `06-work-log-template.md` 模板基础上追加一份记录,例如:
|
||||
|
||||
```text
|
||||
working1-log-2026-06-22-d1.md
|
||||
working1-log-2026-06-22-d2.md
|
||||
working1-log-2026-06-22-d3.md
|
||||
```
|
||||
|
||||
每份记录必须包含:
|
||||
|
||||
```text
|
||||
问题 ID
|
||||
修改文件
|
||||
关键代码变更
|
||||
运行测试
|
||||
截图/日志证据
|
||||
剩余风险
|
||||
```
|
||||
|
||||
195
web-rtcp-5axis-sim-plan/working1/05-acceptance-test-plan.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# 05 修复验收测试计划
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## 1. 验收目标
|
||||
|
||||
修复完成后,必须证明以下目标同时成立:
|
||||
|
||||
1. RTCP frame 自动进入 LinuxCNC kinematics WASM 边界。
|
||||
2. 3D 预览首屏和程序运行后均可见。
|
||||
3. HOME 后 JOG 坐标连续。
|
||||
4. 原已通过功能不回归。
|
||||
|
||||
## 2. 本地静态和 Node 测试
|
||||
|
||||
### 2.1 构建
|
||||
|
||||
```bash
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
```
|
||||
|
||||
预期:
|
||||
|
||||
```text
|
||||
exit code 0
|
||||
无 TypeScript/ES module 打包错误
|
||||
```
|
||||
|
||||
### 2.2 状态和 runtime smoke
|
||||
|
||||
按项目已有测试能力执行:
|
||||
|
||||
```bash
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_five_axis_session.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_full_execution_boundary.mjs
|
||||
```
|
||||
|
||||
如某个测试依赖本地构建产物缺失,应先按项目原有构建流程补齐,不要跳过。
|
||||
|
||||
## 3. 浏览器自动化验收
|
||||
|
||||
### 3.1 D1 验收:首屏 kinematics 自动 ready
|
||||
|
||||
打开本地或部署页面后等待最多 8 秒,断言:
|
||||
|
||||
```js
|
||||
const state = window.webRtcp5AxisSimulation.getState();
|
||||
state.kinematicsRuntimeReadiness.loaded === true
|
||||
state.sourceMode === "source-derived-kinematics-wasm"
|
||||
state.frameSourceMode === "source-derived-kinematics-wasm"
|
||||
state.rtcpFrame.semanticBoundary === "linuxcnc_kinematics_wasm_c_abi"
|
||||
```
|
||||
|
||||
DOM 断言:
|
||||
|
||||
```js
|
||||
document.querySelector('[data-rtcp-diagnostic="boundary"]').textContent.includes("linuxcnc_kinematics_wasm_c_abi")
|
||||
document.querySelector('[data-rtcp-diagnostic="kinematics-ready"]').textContent.includes("ready")
|
||||
```
|
||||
|
||||
禁止状态:
|
||||
|
||||
```text
|
||||
fixture_frame_ui_plumbing_not_linuxcnc_kinematics_proof
|
||||
```
|
||||
|
||||
不得作为稳定态出现。
|
||||
|
||||
### 3.2 D2 验收:预览非空
|
||||
|
||||
断言 canvas dataset:
|
||||
|
||||
```js
|
||||
canvas.dataset.threeReady === "true"
|
||||
Number(canvas.dataset.threeSceneObjects) > 0
|
||||
canvas.dataset.threeRenderer === "webgl" || canvas.dataset.threeRenderer === "2d-fallback"
|
||||
```
|
||||
|
||||
像素断言:
|
||||
|
||||
```js
|
||||
const stats = readCanvasPixelStats(canvas)
|
||||
stats.nonBlackRatio > 0.02
|
||||
stats.averageLuminance > 5
|
||||
```
|
||||
|
||||
截图人工检查:
|
||||
|
||||
```text
|
||||
能看到工作台/坐标轴/刀具或 TCP 点/路径
|
||||
```
|
||||
|
||||
至少覆盖:
|
||||
|
||||
```text
|
||||
首屏
|
||||
加载 LinuxCNC vendored 程序后
|
||||
Run 后
|
||||
Open 本地 G-code 后
|
||||
Audit 后
|
||||
```
|
||||
|
||||
### 3.3 D3 验收:HOME/JOG 坐标连续
|
||||
|
||||
自动化步骤:
|
||||
|
||||
```js
|
||||
click POWER
|
||||
click MANUAL
|
||||
click HOME
|
||||
const home = state.axisPose
|
||||
click X+
|
||||
assert state.axisPose.x === home.x + state.machine.jogIncrement
|
||||
assert state.axisPose.y === home.y
|
||||
assert state.axisPose.z === home.z
|
||||
click Y-
|
||||
assert state.axisPose.y === home.y - state.machine.jogIncrement
|
||||
assert state.axisPose.x === home.x + state.machine.jogIncrement
|
||||
```
|
||||
|
||||
允许浮点误差:
|
||||
|
||||
```text
|
||||
abs(actual - expected) <= 0.001
|
||||
```
|
||||
|
||||
人工检查:
|
||||
|
||||
```text
|
||||
DRO 中 X/Y/Z 数字连续变化,没有跳到 0/1 局部原点。
|
||||
```
|
||||
|
||||
## 4. 全功能回归清单
|
||||
|
||||
修复完成后,至少回归以下功能:
|
||||
|
||||
| 功能 | 预期 |
|
||||
| --- | --- |
|
||||
| 主界面九大区域 | 全部存在 |
|
||||
| POWER on/off | taskState 正确 |
|
||||
| E-STOP/RESET | 急停和复位正确 |
|
||||
| AUTO/MANUAL/JOG/MDI | 模式切换正确 |
|
||||
| HOME | allHomed=true,DRO 到 HOME pose |
|
||||
| JOG X+/X-/Y+/Y- | DRO 连续变化 |
|
||||
| MDI M428 | RTCP on,tcp-xyzac |
|
||||
| MDI M429 | RTCP off,identity |
|
||||
| MDI M430 | userk |
|
||||
| TCP/IDENTITY 侧栏按钮 | RTCP 状态正确 |
|
||||
| Rapid/Feed/Spindle override | 数值可调 |
|
||||
| Flood/Mist | 状态可切换 |
|
||||
| View X/Y/Z/Fit/Clear/Full | preview state 正确,画面仍可见 |
|
||||
| Profile xyzac/xyzbc 切换 | INI 与 runtime 重新 ready |
|
||||
| Stage LinuxCNC sources | staged files/gcode 数量正常 |
|
||||
| Load vendored G-code | programSource 正确,G-code 行显示 |
|
||||
| Run/Pause/Resume/Step/Stop/Reload | runState 和 activeLine 正确 |
|
||||
| Open local G-code | programSource=operator-file |
|
||||
| Save/Restore Session | OPFS 保存恢复正确 |
|
||||
| Audit Full Boundary | 审计入口可执行,结果可诊断 |
|
||||
|
||||
## 5. QA 报告复跑
|
||||
|
||||
修复后建议复用或更新:
|
||||
|
||||
```text
|
||||
/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/run-site-test.mjs
|
||||
```
|
||||
|
||||
重点更新:
|
||||
|
||||
1. 首屏等待稳定后,不再把 D1 作为 WARN,而是强制 PASS/FAIL。
|
||||
2. canvas 检查加入像素统计。
|
||||
3. HOME/JOG 检查使用稳定态并验证连续坐标。
|
||||
4. 重新生成 Word 报告,文件名建议:
|
||||
|
||||
```text
|
||||
web-rtcp-5axis-site-test-report-2026-06-22-after-working1-fix.docx
|
||||
```
|
||||
|
||||
## 6. 验收通过定义
|
||||
|
||||
```text
|
||||
P0: D1 PASS
|
||||
P0: D2 PASS
|
||||
P1: D3 PASS
|
||||
全功能回归无新增 FAIL
|
||||
console error = 0
|
||||
page error = 0
|
||||
request failure = 0
|
||||
```
|
||||
|
||||
如果 headless WebGL 不稳定,必须证明 2D fallback 可见且自动化 pixel 检查通过。
|
||||
|
||||
100
web-rtcp-5axis-sim-plan/working1/06-work-log-template.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# 06 修复工作记录模板
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
后续每一轮修复建议复制本模板,新建独立记录文件。
|
||||
|
||||
文件命名建议:
|
||||
|
||||
```text
|
||||
working1-log-YYYY-MM-DD-D1.md
|
||||
working1-log-YYYY-MM-DD-D2.md
|
||||
working1-log-YYYY-MM-DD-D3.md
|
||||
```
|
||||
|
||||
## Batch
|
||||
|
||||
```text
|
||||
Batch:
|
||||
Date:
|
||||
Owner:
|
||||
Problem IDs:
|
||||
Branch:
|
||||
Commit:
|
||||
```
|
||||
|
||||
## 1. 修复目标
|
||||
|
||||
```text
|
||||
本轮要修复什么问题:
|
||||
预期用户可见结果:
|
||||
预期 runtime/DOM 诊断结果:
|
||||
```
|
||||
|
||||
## 2. 修改文件
|
||||
|
||||
| 文件 | 修改内容 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| | | |
|
||||
|
||||
## 3. 关键实现说明
|
||||
|
||||
```text
|
||||
状态字段变化:
|
||||
核心函数变化:
|
||||
runtime 边界变化:
|
||||
UI/可视化变化:
|
||||
```
|
||||
|
||||
## 4. LinuxCNC/项目溯源
|
||||
|
||||
```text
|
||||
参考 LinuxCNC 源码或配置:
|
||||
参考项目文档:
|
||||
为什么该修复没有重写 CNC 语义:
|
||||
```
|
||||
|
||||
## 5. 测试记录
|
||||
|
||||
### 5.1 命令
|
||||
|
||||
```bash
|
||||
# paste commands here
|
||||
```
|
||||
|
||||
### 5.2 结果
|
||||
|
||||
```text
|
||||
PASS/FAIL:
|
||||
关键输出:
|
||||
截图路径:
|
||||
JSON/日志路径:
|
||||
```
|
||||
|
||||
## 6. 回归范围
|
||||
|
||||
| 功能 | 是否回归 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| D1 kinematics auto refresh | | |
|
||||
| D2 preview visible | | |
|
||||
| D3 jog continuity | | |
|
||||
| POWER/E-STOP/RESET | | |
|
||||
| MDI M428/M429/M430 | | |
|
||||
| Load/Run/Step/Stop | | |
|
||||
| Save/Restore Session | | |
|
||||
|
||||
## 7. 剩余风险
|
||||
|
||||
```text
|
||||
仍未解决的问题:
|
||||
需要后续确认的问题:
|
||||
可能影响线上部署的问题:
|
||||
```
|
||||
|
||||
## 8. 下一步
|
||||
|
||||
```text
|
||||
Next:
|
||||
Blockers:
|
||||
```
|
||||
|
||||
45
web-rtcp-5axis-sim-plan/working1/README.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# web-rtcp-5axis-sim-plan working1 修复指导目录
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
本目录用于指导修复测试报告中确认的问题:
|
||||
|
||||
```text
|
||||
/home/meswork/cnc_wams/qa/web-rtcp-5axis-site-test/output/web-rtcp-5axis-site-test-report-2026-06-22.docx
|
||||
```
|
||||
|
||||
本目录只提供修复设计、实施步骤、溯源和验收文档,不直接修改程序代码。后续开发人员可按本目录文件逐步修复本地项目。
|
||||
|
||||
## 文档清单
|
||||
|
||||
| 文件 | 用途 |
|
||||
| --- | --- |
|
||||
| `01-problem-review.md` | 测试问题复盘、证据、影响范围和初步根因 |
|
||||
| `02-repair-plan.md` | 总体修复方案、优先级、风险和代码落点 |
|
||||
| `03-implementation-steps.md` | 程序修改的详细步骤,按问题拆分到具体文件和函数 |
|
||||
| `04-traceability-matrix.md` | 问题、测试证据、源码、LinuxCNC 参考、修复项、验收项的追溯矩阵 |
|
||||
| `05-acceptance-test-plan.md` | 修复完成后的本地/浏览器/线上回归验收清单 |
|
||||
| `06-work-log-template.md` | 后续每轮修复过程记录模板 |
|
||||
|
||||
## 本轮确认问题
|
||||
|
||||
| ID | 问题 | 严重级别 | 修复优先级 |
|
||||
| --- | --- | --- | --- |
|
||||
| D1 | 首屏 RTCP/运动学边界未自动挂接 LinuxCNC kinematics | 高 | P0 |
|
||||
| D2 | 3D 预览画布存在,但未观察到机床/刀路可见对象 | 中 | P0 |
|
||||
| D3 | HOME 后 JOG 坐标连续性异常 | 中 | P1 |
|
||||
|
||||
## 修复目标
|
||||
|
||||
1. 页面稳定加载后,RTCP frame 自动进入 `source-derived-kinematics-wasm`,边界显示 `linuxcnc_kinematics_wasm_c_abi`。
|
||||
2. 3D 预览在 WebGL 和 2D fallback 两条路径下均可见,至少能看到机床基准、刀具/TCP 标记和路径。
|
||||
3. HOME 后执行 JOG,DRO 坐标必须在当前显示坐标基础上连续变化,不允许跳到另一套坐标原点。
|
||||
4. 修复后新增或更新自动化测试,防止同类问题回归。
|
||||
|
||||
## 推荐执行顺序
|
||||
|
||||
1. 先修 D1,确保 LinuxCNC kinematics frame 自动刷新链路稳定。
|
||||
2. 再修 D2,确保视觉层有可见基础机床对象和路径对象。
|
||||
3. 最后修 D3,统一 task/HAL runtime 反馈坐标系与 UI DRO 坐标系。
|
||||
4. 执行 `05-acceptance-test-plan.md` 中的本地和浏览器验收。
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
# working1 修复执行记录 D1-D2-D3
|
||||
|
||||
生成时间:2026-06-22
|
||||
|
||||
## Batch
|
||||
|
||||
```text
|
||||
Batch: working1-implementation-d1-d2-d3
|
||||
Date: 2026-06-22
|
||||
Owner: Codex
|
||||
Problem IDs: D1, D2, D3
|
||||
Branch: current worktree
|
||||
Commit: not committed
|
||||
```
|
||||
|
||||
## 1. 修复目标
|
||||
|
||||
```text
|
||||
D1: kinematics worker ready 后,页面自动保持 LinuxCNC source-derived kinematics frame,不被临时 fixture frame 永久覆盖。
|
||||
D2: 首屏和路径清空后仍显示可见五轴参考机床、坐标轴、旋转轴、刀具/TCP;canvas dataset 和像素测试可验证。
|
||||
D3: HOME 后执行 task/HAL JOG,DRO/work pose 保持连续增量,不被 task-local/local-zero 轴位覆盖。
|
||||
```
|
||||
|
||||
## 2. 修改文件
|
||||
|
||||
| 文件 | 修改内容 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| `app/src/state/store.js` | 增加 `desiredFrameSourceMode`、`pendingJogCommand`;调整 async kinematics refresh;新增 `resolveTaskHalAxisPose()` | 分离“期望 frame source”和“当前 frame source”;保护 HOME 后 JOG 坐标连续性 |
|
||||
| `app/src/main.js` | profile kinematics runtime attach 完成后再次调用 `refreshKinematicsFrame()` | 满足 profile/INI 变更后的自动 frame 刷新要求 |
|
||||
| `app/src/runtime/linuxcnc-task-hal-runtime.js` | `ui.axisPoseFrame` 标记 JOG 为 `task-local`,非 JOG 为 `work` | 让 store 能区分 task/HAL 轴位坐标系 |
|
||||
| `app/src/visualization/five-axis-scene.js` | 新增 WebGL 五轴参考机床模型和 2D fallback 参考绘制;路径清空时仍显示 TCP | 修复 3D 预览黑屏/不可见和 fallback 信息不足 |
|
||||
| `tests/browser/gmoccapy_shell_smoke.html` | 更新 reference model dataset 断言;新增 canvas pixel stats 检查 | 防止只通过 DOM dataset 误判可见性 |
|
||||
| `tests/node/verify_linuxcnc_task_hal_runtime.mjs` | 增加 HOME 后 JOG X+/Y- 连续性断言 | 覆盖 D3 复现路径 |
|
||||
|
||||
## 3. 关键实现说明
|
||||
|
||||
```text
|
||||
状态字段变化:
|
||||
- desiredFrameSourceMode: 保留用户/运行时希望的 frame source。
|
||||
- pendingJogCommand: 记录 task/HAL JOG 的 axis、direction、increment、basePose。
|
||||
|
||||
核心函数变化:
|
||||
- buildFrameForState(): 优先读取 desiredFrameSourceMode。
|
||||
- scheduleAsyncKinematicsRefresh(): 以 desiredFrameSourceMode + runtime loaded 作为刷新条件。
|
||||
- refreshAsyncKinematicsFrame(): 成功后写回 source/frame/desired 均为 source-derived-kinematics-wasm,失败显示 operatorMessage 并保留重试能力。
|
||||
- resolveTaskHalAxisPose(): work pose 直接合并,axis delta 累加,pending JOG 用 basePose + increment。
|
||||
|
||||
UI/可视化变化:
|
||||
- WebGL scene 增加 table/base、XYZ axis、A/C rotary ring、tool holder。
|
||||
- 2D fallback 始终绘制工作台、XYZ 轴、旋转中心、TCP 点。
|
||||
- canvas dataset 输出 machine-reference-and-toolpath / webgl-five-axis-reference。
|
||||
```
|
||||
|
||||
## 4. LinuxCNC/项目溯源
|
||||
|
||||
```text
|
||||
参考项目文档:
|
||||
- working1/03-implementation-steps.md
|
||||
- working1/04-traceability-matrix.md
|
||||
- docs/traceability-matrix.md
|
||||
|
||||
参考 LinuxCNC 边界:
|
||||
- kinematics frame 仍来自 LinuxCNC kinematics WASM C ABI。
|
||||
- task/HAL JOG 仍通过 EMC_JOG_INCR 模拟 runtime 执行;UI 只处理坐标系归并,不重写 CNC 运动语义。
|
||||
- Three.js 只消费 runtime frame/canonical motion/task-HAL feedback,不生成 G-code 或 CNC 语义。
|
||||
```
|
||||
|
||||
## 5. 测试记录
|
||||
|
||||
### 5.1 已通过命令
|
||||
|
||||
```bash
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run build
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_task_hal_runtime.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_rtcp_store.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_five_axis_session.mjs
|
||||
node web-rtcp-5axis-sim-plan/tests/node/verify_linuxcnc_kinematics_runtime.mjs
|
||||
bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_shell_browser.sh
|
||||
bash web-rtcp-5axis-sim-plan/tests/browser/verify_gmoccapy_dist_browser.sh
|
||||
```
|
||||
|
||||
### 5.2 关键输出
|
||||
|
||||
```text
|
||||
gmoccapy_static_build=ok
|
||||
linuxcnc_task_hal_runtime_smoke=ok
|
||||
rtcp_store_smoke=ok
|
||||
five_axis_session_smoke=ok
|
||||
linuxcnc_kinematics_runtime_smoke=ok
|
||||
gmoccapy_shell_smoke=ok
|
||||
gmoccapy_dist_smoke=ok
|
||||
```
|
||||
|
||||
### 5.3 未通过/阻塞命令
|
||||
|
||||
```bash
|
||||
npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```text
|
||||
前置 kinematics/interpreter/INI/task-HAL 均通过。
|
||||
失败位置: verify_native_task_hal_audit.mjs -> wasm-port/tests/native/verify_task_hal_phase0.sh
|
||||
失败原因: verify_task_hal_source_manifest.sh 返回非零,manifest ready=0。
|
||||
证据: wasm-port/build/task-hal/verify_task_hal_source_manifest.stdout.log
|
||||
关键字段:
|
||||
- task_hal_reference_source_ready=0
|
||||
- task_hal_vendor_source_ready=0
|
||||
- task_hal_missing_reference_source_count=20
|
||||
```
|
||||
|
||||
该失败属于 native LinuxCNC reference source/probe 前置条件,不是本轮 Web D1/D2/D3 代码回归。
|
||||
|
||||
## 6. 回归范围
|
||||
|
||||
| 功能 | 是否回归 | 结果 |
|
||||
| --- | --- | --- |
|
||||
| D1 kinematics auto refresh | 是 | PASS |
|
||||
| D2 preview visible | 是 | PASS |
|
||||
| D3 jog continuity | 是 | PASS |
|
||||
| POWER/E-STOP/RESET | 是 | PASS via browser smoke |
|
||||
| MDI M428/M429/M430 | 是 | PASS via browser/node smoke |
|
||||
| Load/Run/Step/Stop | 是 | PASS via browser smoke |
|
||||
| Save/Restore Session | 是 | PASS via browser/node smoke |
|
||||
| Native source manifest/probe | 是 | BLOCKED: missing native reference sources |
|
||||
|
||||
## 7. 剩余风险
|
||||
|
||||
```text
|
||||
1. native task/HAL source manifest 仍未 ready,缺少 20 个 native reference source,完整 smoke:node 不能全绿。
|
||||
2. WebGL reference model 已通过 smoke,但仍建议后续保留人工截图复核首屏和 clear-preview 后视觉效果。
|
||||
3. task/HAL axisPoseFrame 当前按 JOG/non-JOG 推断;长期方案仍应由 runtime 输出更细的 work/task-local/axisPoseDelta 元数据。
|
||||
```
|
||||
|
||||
## 8. 下一步
|
||||
|
||||
```text
|
||||
Next:
|
||||
- 补齐或恢复 wasm-port/tools/task-hal-source-manifest.txt 对应的 native LinuxCNC reference sources。
|
||||
- native source manifest ready 后重跑 npm --prefix web-rtcp-5axis-sim-plan/app run smoke:node。
|
||||
- 部署到 https://82.156.24.101:8092/ 后重跑 QA 站点测试,更新 Word 测试报告。
|
||||
|
||||
Blockers:
|
||||
- wasm-port/build/task-hal/verify_task_hal_source_manifest.stdout.log 显示 native reference source 缺失。
|
||||
```
|
||||
45
备忘/备忘2.txt
Normal file
@@ -0,0 +1,45 @@
|
||||
首屏已经跑起来了,而且我拿到了完整渲染后的 DOM。
|
||||
可以确认一件关键事:线上部署当前不是“完全 ready”状态,
|
||||
至少首屏就暴露出一批运行时边界和降级路径,
|
||||
我会继续用自动化把这些都验证成明确结论,而不是只看静态页面。
|
||||
|
||||
|
||||
|
||||
|
||||
我还会把页面自身暴露的 window.webRtcp5AxisSimulation 状态一并采进来。
|
||||
这样不仅知道“按钮能不能点”,
|
||||
还知道点完以后运行态、模式、RTCP、程序状态、会话状态到底有没有按设计变化。
|
||||
|
||||
|
||||
我已经把结论收敛了,真正的问题现在只剩两类:
|
||||
首屏初始化后的 RTCP/运动学边界没有自动接上,
|
||||
以及左侧 3D 预览在实际测试中始终看不到机床/刀路。
|
||||
现在我补最后一条画布状态证据,然后直接生成 Word 文档。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
文档覆盖了测试报告中的 3 个确认问题:
|
||||
首屏 RTCP/运动学边界未自动挂接 LinuxCNC kinematics。
|
||||
3D 预览画布存在但机床/刀路不可见。
|
||||
HOME 后 JOG 坐标连续性异常。
|
||||
我也把对应代码落点、推荐修复方案、详细编码步骤、
|
||||
测试验收标准、溯源矩阵和后续工作记录模板都写进去了,
|
||||
后续可以按 03-implementation-steps.md 开始实际修复。
|
||||
|
||||
|
||||
我已经拿到一份直接证据:当前浏览器首屏里,tcpPose、toolAxisVector 这类基础字段是有的,但真实路径数据 programExecution.motion / programExecutionTiming.samples 没出来,而且画布也没暴露完整诊断字段。再把相关代码行号补一下,我给你明确结论。
|
||||
|
||||
|
||||
先解决:
|
||||
刀具预览需要的数据,目前只正确产生了“TCP 球/刀轴线的基础默认数据”,但没有正确产生“真实刀具路径与执行轨迹数据”。
|
||||
下一步该盯的是浏览器态为什么还停在 fixture-ui-only,以及为什么首屏没有把 programExecution.motion / programExecutionTiming.samples 和 three* 诊断字段稳定写到 canvas。
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||