完善五轴 RTCP 仿真与验证资料
This commit is contained in:
630
web-rtcp-5axis-sim-plan/tools/collect-linuxcnc-web-parity.mjs
Normal file
630
web-rtcp-5axis-sim-plan/tools/collect-linuxcnc-web-parity.mjs
Normal file
@@ -0,0 +1,630 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createHash } from "node:crypto";
|
||||
import { copyFileSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { createMemorySessionStorage } from "../app/src/runtime/five-axis-session.js";
|
||||
import {
|
||||
createLinuxCncInterpreterRuntime,
|
||||
} from "../app/src/runtime/linuxcnc-interpreter-runtime.js";
|
||||
import {
|
||||
selectMachineFileProgram,
|
||||
stageProfileMachineFiles,
|
||||
} from "../app/src/runtime/linuxcnc-machine-file-staging.js";
|
||||
import { getFiveAxisProfile } from "../app/src/profiles/index.js";
|
||||
|
||||
const PROJECT_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const WORKSPACE_ROOT = resolve(PROJECT_ROOT, "..");
|
||||
const LINUXCNC_ROOT = resolve(WORKSPACE_ROOT, "linuxcnc");
|
||||
const GMOCAPY_MACHINE_REL = "configs/sim/gmoccapy/non_trivial_kinematics/table-rotary-tilting";
|
||||
const GMOCAPY_MACHINE_DIR = resolve(LINUXCNC_ROOT, GMOCAPY_MACHINE_REL);
|
||||
const GMOCAPY_SOURCE_PREFIX = `${GMOCAPY_MACHINE_REL}/examples/`;
|
||||
const DEFAULT_CASES = [
|
||||
"impeller-7bl-xyzac.ngc",
|
||||
"boat-xyzac.ngc",
|
||||
];
|
||||
const AXES = ["x", "y", "z", "a", "b", "c", "u", "v", "w"];
|
||||
const PLANE_AXIS_MAP = {
|
||||
170: ["x", "y", "z"],
|
||||
180: ["z", "x", "y"],
|
||||
190: ["y", "z", "x"],
|
||||
};
|
||||
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const outputDir = resolve(PROJECT_ROOT, options.outputDir || "working_run/linuxcnc-web-parity");
|
||||
const cases = options.cases.length > 0 ? options.cases : DEFAULT_CASES;
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
const runtime = await createLinuxCncInterpreterRuntime();
|
||||
const profile = getFiveAxisProfile("gmoccapy-xyzac-trt");
|
||||
const staged = await stageProfileMachineFiles(profile, {
|
||||
storage: createMemorySessionStorage(),
|
||||
});
|
||||
|
||||
const reports = [];
|
||||
for (const filename of cases) {
|
||||
const sourceRel = `${GMOCAPY_SOURCE_PREFIX}${filename}`;
|
||||
const programPath = resolve(GMOCAPY_MACHINE_DIR, "examples", filename);
|
||||
const programText = readFileSync(programPath, "utf8");
|
||||
const native = collectNativeLinuxCncResult({ filename, programPath, programText });
|
||||
const web = collectWebResult({ filename, sourceRel, programText, runtime, staged });
|
||||
const comparison = compareResults(native, web);
|
||||
const slug = filename.replace(/\.ngc$/i, "");
|
||||
|
||||
const nativePath = resolve(outputDir, `linuxcnc-native-${slug}.json`);
|
||||
const webPath = resolve(outputDir, `web-runtime-${slug}.json`);
|
||||
const comparisonPath = resolve(outputDir, `comparison-${slug}.json`);
|
||||
|
||||
writeJson(nativePath, native);
|
||||
writeJson(webPath, web);
|
||||
writeJson(comparisonPath, comparison);
|
||||
|
||||
reports.push({
|
||||
filename,
|
||||
nativePath,
|
||||
webPath,
|
||||
comparisonPath,
|
||||
passed: comparison.summary.passed,
|
||||
mismatchCount: comparison.summary.mismatchCount,
|
||||
maxAxisDelta: comparison.summary.maxAxisDelta,
|
||||
nativeMotionCount: native.summary.motionEventCount,
|
||||
webMotionCount: web.summary.motionEventCount,
|
||||
});
|
||||
}
|
||||
|
||||
const indexPath = resolve(outputDir, "index.json");
|
||||
writeJson(indexPath, {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-web-parity-index",
|
||||
generatedAt: new Date().toISOString(),
|
||||
profileId: profile.id,
|
||||
outputDir,
|
||||
linuxCncRoot: LINUXCNC_ROOT,
|
||||
projectRoot: PROJECT_ROOT,
|
||||
cases: reports,
|
||||
});
|
||||
|
||||
console.log(`parity_output_dir=${outputDir}`);
|
||||
for (const report of reports) {
|
||||
console.log(`${report.filename}: passed=${report.passed} mismatches=${report.mismatchCount} native_motion=${report.nativeMotionCount} web_motion=${report.webMotionCount}`);
|
||||
}
|
||||
console.log(`parity_index=${indexPath}`);
|
||||
|
||||
function collectNativeLinuxCncResult({ filename, programPath, programText }) {
|
||||
const tempDir = resolve("/tmp", `web-rtcp-native-rs274-${process.pid}-${Date.now()}-${filename.replace(/[^a-z0-9]/gi, "_")}`);
|
||||
mkdirSync(tempDir, { recursive: true });
|
||||
const tempVarPath = resolve(tempDir, "xyzac.var");
|
||||
copyFileSync(resolve(GMOCAPY_MACHINE_DIR, "xyzac.var"), tempVarPath);
|
||||
|
||||
try {
|
||||
const result = spawnSync(resolve(LINUXCNC_ROOT, "scripts/rip-environment"), [
|
||||
"rs274",
|
||||
"-g",
|
||||
"-i",
|
||||
"xyzac-trt.ini",
|
||||
"-t",
|
||||
"xyzac-trt.tbl",
|
||||
"-v",
|
||||
tempVarPath,
|
||||
`examples/${basename(programPath)}`,
|
||||
], {
|
||||
cwd: GMOCAPY_MACHINE_DIR,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 256 * 1024 * 1024,
|
||||
});
|
||||
const resultText = `${result.stdout || ""}${result.stderr || ""}`;
|
||||
const canonical = parseRs274CanonicalOutput(resultText, programText);
|
||||
const executionPath = buildExecutionPathFromMotion(canonical.motion);
|
||||
return createResultArtifact({
|
||||
source: "linuxcnc-native-rs274",
|
||||
semanticBoundary: "linuxcnc_source_tree_rs274_gmoccapy_ini_canonical_output",
|
||||
filename,
|
||||
sourceRel: `${GMOCAPY_SOURCE_PREFIX}${filename}`,
|
||||
programText,
|
||||
resultText,
|
||||
motion: canonical.motion,
|
||||
canonicalEvents: canonical.canonicalEvents,
|
||||
lineExecution: canonical.lineExecution,
|
||||
executionPath,
|
||||
runtime: {
|
||||
command: "scripts/rip-environment rs274 -g -i xyzac-trt.ini -t xyzac-trt.tbl -v <temp-var> examples/<program>",
|
||||
exitCode: result.status,
|
||||
signal: result.signal,
|
||||
cwd: GMOCAPY_MACHINE_DIR,
|
||||
tempParameterFile: tempVarPath,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function collectWebResult({ filename, sourceRel, programText, runtime, staged }) {
|
||||
const plan = selectMachineFileProgram(staged.plan, staged.save, sourceRel);
|
||||
const machineFileExecution = runtime.runMachineFileProgram({
|
||||
plan,
|
||||
files: staged.save.files,
|
||||
executionMode: "fiveAxisRemap",
|
||||
});
|
||||
const directExecution = runtime.runProgram(programText);
|
||||
const selectedExecution = machineFileExecution.summary.motionEventCount > 0
|
||||
? machineFileExecution
|
||||
: directExecution;
|
||||
return createResultArtifact({
|
||||
source: "web-runtime-wasm",
|
||||
semanticBoundary: selectedExecution.semanticBoundary,
|
||||
filename,
|
||||
sourceRel,
|
||||
programText,
|
||||
resultText: selectedExecution.resultText,
|
||||
motion: selectedExecution.motion,
|
||||
canonicalEvents: extractWebCanonicalEvents(selectedExecution.resultText),
|
||||
lineExecution: buildLineExecution(selectedExecution.motion, programText),
|
||||
executionPath: selectedExecution.plannerTiming?.samples || [],
|
||||
plannerTiming: selectedExecution.plannerTiming,
|
||||
runtime: {
|
||||
selectedMode: selectedExecution === machineFileExecution
|
||||
? "linuxcnc-machine-file-remap-wasm"
|
||||
: "linuxcnc-interpreter-wasm",
|
||||
directSummary: directExecution.summary,
|
||||
machineFileSummary: machineFileExecution.summary,
|
||||
machineFilePlan: machineFileExecution.machineFilePlan,
|
||||
machineFileDiagnostics: extractDiagnostics(machineFileExecution.resultText),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createResultArtifact({
|
||||
source,
|
||||
semanticBoundary,
|
||||
filename,
|
||||
sourceRel,
|
||||
programText,
|
||||
resultText,
|
||||
motion,
|
||||
canonicalEvents,
|
||||
lineExecution,
|
||||
executionPath,
|
||||
plannerTiming = null,
|
||||
runtime,
|
||||
}) {
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-gcode-run-result",
|
||||
generatedAt: new Date().toISOString(),
|
||||
source,
|
||||
semanticBoundary,
|
||||
profileId: "gmoccapy-xyzac-trt",
|
||||
filename,
|
||||
sourceRel,
|
||||
program: {
|
||||
lineCount: programText.split(/\r?\n/).length,
|
||||
nonBlankLineCount: programText.split(/\r?\n/).filter((line) => line.trim()).length,
|
||||
sha256: sha256(programText),
|
||||
},
|
||||
summary: {
|
||||
ready: motion.length > 0,
|
||||
canonicalEventCount: canonicalEvents.length,
|
||||
motionEventCount: motion.length,
|
||||
executedPathPointCount: executionPath.length,
|
||||
lineExecutionCount: lineExecution.length,
|
||||
motionTypes: [...new Set(motion.map((event) => event.type))],
|
||||
finalAxes: motion.at(-1)?.axes || Object.fromEntries(AXES.map((axis) => [axis, 0])),
|
||||
firstMotionLine: firstFiniteLine(motion),
|
||||
lastMotionLine: lastFiniteLine(motion),
|
||||
plannerRuntimeReady: plannerTiming?.plannerRuntimeReady === true || executionPath.length > 0,
|
||||
},
|
||||
toolPreviewPath: motion,
|
||||
toolExecutionPath: executionPath,
|
||||
lineExecution,
|
||||
canonicalEvents,
|
||||
runtime,
|
||||
plannerTiming,
|
||||
rawResultText: resultText,
|
||||
};
|
||||
}
|
||||
|
||||
function parseRs274CanonicalOutput(resultText, programText) {
|
||||
const canonicalEvents = [];
|
||||
const motion = [];
|
||||
const axes = Object.fromEntries(AXES.map((axis) => [axis, 0]));
|
||||
const sourceLines = programLineMap(programText);
|
||||
const candidateLines = candidateMotionSourceLines(programText);
|
||||
let candidateIndex = 0;
|
||||
let activePlane = 170;
|
||||
let activeFeedRate = null;
|
||||
let activeFeedMode = "units-per-minute";
|
||||
let activeLinearUnits = "mm";
|
||||
|
||||
for (const textLine of String(resultText).split(/\r?\n/)) {
|
||||
const parsed = parseRs274Line(textLine);
|
||||
if (!parsed) continue;
|
||||
canonicalEvents.push(parsed);
|
||||
|
||||
if (parsed.name === "USE_LENGTH_UNITS") {
|
||||
activeLinearUnits = parsed.args[0] === "CANON_UNITS_INCHES" ? "inch" : "mm";
|
||||
}
|
||||
if (parsed.name === "SET_FEED_MODE") {
|
||||
activeFeedMode = "units-per-minute";
|
||||
}
|
||||
if (parsed.name === "COMMENT" && /inverse time/i.test(parsed.args.join(" "))) {
|
||||
activeFeedMode = "inverse-time";
|
||||
}
|
||||
if (parsed.name === "SELECT_PLANE") {
|
||||
activePlane = Number(parsed.args[0]) || activePlane;
|
||||
}
|
||||
if (parsed.name === "SET_FEED_RATE") {
|
||||
const feed = Number(parsed.args[0]);
|
||||
activeFeedRate = Number.isFinite(feed) && feed > 0 ? feed : activeFeedRate;
|
||||
}
|
||||
|
||||
if (!["STRAIGHT_TRAVERSE", "STRAIGHT_FEED", "ARC_FEED"].includes(parsed.name)) continue;
|
||||
const mappedLine = candidateLines[candidateIndex++] || null;
|
||||
|
||||
if (parsed.name === "ARC_FEED") {
|
||||
const [firstAxis, secondAxis, thirdAxis] = PLANE_AXIS_MAP[activePlane] ?? PLANE_AXIS_MAP[170];
|
||||
const [firstEnd, secondEnd, centerFirst, centerSecond, rotation, axisEndPoint] = parsed.args.map(Number);
|
||||
if (Number.isFinite(firstEnd)) axes[firstAxis] = firstEnd;
|
||||
if (Number.isFinite(secondEnd)) axes[secondAxis] = secondEnd;
|
||||
if (Number.isFinite(axisEndPoint)) axes[thirdAxis] = axisEndPoint;
|
||||
axes.arc = {
|
||||
plane: activePlane,
|
||||
firstAxis,
|
||||
secondAxis,
|
||||
thirdAxis,
|
||||
firstEnd,
|
||||
secondEnd,
|
||||
centerFirst,
|
||||
centerSecond,
|
||||
rotation,
|
||||
axisEndPoint,
|
||||
};
|
||||
} else {
|
||||
for (const [index, axis] of AXES.slice(0, 6).entries()) {
|
||||
const value = Number(parsed.args[index]);
|
||||
if (Number.isFinite(value)) axes[axis] = value;
|
||||
}
|
||||
}
|
||||
|
||||
motion.push({
|
||||
index: motion.length,
|
||||
type: parsed.name,
|
||||
canonicalSequence: parsed.sequence,
|
||||
line: mappedLine?.line ?? null,
|
||||
statement: mappedLine?.statement ?? "-",
|
||||
axes: { ...axes },
|
||||
feedRate: activeFeedRate,
|
||||
feedMode: activeFeedMode,
|
||||
linearUnits: activeLinearUnits,
|
||||
raw: parsed.raw,
|
||||
lineMappingSource: mappedLine ? "source-motion-line-order" : "unmapped-native-canonical-order",
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
canonicalEvents,
|
||||
motion,
|
||||
lineExecution: buildLineExecution(motion, programText),
|
||||
};
|
||||
}
|
||||
|
||||
function parseRs274Line(line) {
|
||||
const match = String(line).match(/^\s*(\d+)\s+N\.+\s+([A-Z_]+)\((.*)\)\s*$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
sequence: Number(match[1]),
|
||||
name: match[2],
|
||||
args: splitCanonicalArgs(match[3]),
|
||||
raw: line,
|
||||
};
|
||||
}
|
||||
|
||||
function splitCanonicalArgs(text) {
|
||||
const args = [];
|
||||
let current = "";
|
||||
let quoted = false;
|
||||
for (const char of String(text)) {
|
||||
if (char === "\"") {
|
||||
quoted = !quoted;
|
||||
current += char;
|
||||
continue;
|
||||
}
|
||||
if (char === "," && !quoted) {
|
||||
args.push(cleanArg(current));
|
||||
current = "";
|
||||
continue;
|
||||
}
|
||||
current += char;
|
||||
}
|
||||
if (current.length > 0 || text.length > 0) args.push(cleanArg(current));
|
||||
return args;
|
||||
}
|
||||
|
||||
function cleanArg(value) {
|
||||
return String(value).trim().replace(/^"(.*)"$/s, "$1");
|
||||
}
|
||||
|
||||
function extractWebCanonicalEvents(resultText) {
|
||||
return String(resultText).split(/\r?\n/)
|
||||
.filter((line) => line.startsWith("canon_event="))
|
||||
.map((line, index) => ({
|
||||
sequence: index + 1,
|
||||
name: line.slice("canon_event=".length).split(/\s+/)[0],
|
||||
raw: line,
|
||||
}));
|
||||
}
|
||||
|
||||
function candidateMotionSourceLines(programText) {
|
||||
const candidates = [];
|
||||
let activeMotion = null;
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
const codeOnly = stripComments(line);
|
||||
const lineNumber = index + 1;
|
||||
let lineMotion = null;
|
||||
for (const match of codeOnly.matchAll(/\bG\s*([0-9]+(?:\.[0-9]+)?)\b/gi)) {
|
||||
const value = Number(match[1]);
|
||||
if (value === 0 || value === 1 || value === 2 || value === 3) {
|
||||
lineMotion = value;
|
||||
activeMotion = value;
|
||||
}
|
||||
}
|
||||
const hasAxisWord = /\b[XYZABCUVW]\s*[-+#0-9.]/i.test(codeOnly);
|
||||
if (lineMotion !== null || (hasAxisWord && activeMotion !== null)) {
|
||||
candidates.push({
|
||||
line: lineNumber,
|
||||
statement: line.trim() || "(blank)",
|
||||
});
|
||||
}
|
||||
});
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function buildExecutionPathFromMotion(motion) {
|
||||
let elapsedSeconds = 0;
|
||||
return motion.map((event, index) => {
|
||||
const previous = motion[index - 1]?.axes || Object.fromEntries(AXES.map((axis) => [axis, 0]));
|
||||
const distance = vectorLength(["x", "y", "z"].map((axis) => (
|
||||
Number(event.axes?.[axis] || 0) - Number(previous?.[axis] || 0)
|
||||
)));
|
||||
const velocity = event.type === "STRAIGHT_TRAVERSE"
|
||||
? 2100
|
||||
: Math.max(Number(event.feedRate) || 100, 1);
|
||||
const durationSeconds = distance > 0 ? distance / (velocity / 60) : 0;
|
||||
elapsedSeconds += durationSeconds;
|
||||
return {
|
||||
sampleIndex: index,
|
||||
timeSeconds: elapsedSeconds,
|
||||
motionIndex: index,
|
||||
line: event.line,
|
||||
type: event.type,
|
||||
axes: event.axes,
|
||||
currentVelocityMmPerMin: velocity,
|
||||
source: "native-rs274-canonical-endpoint-timing-estimate",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function buildLineExecution(motion, programText) {
|
||||
const lines = programLineMap(programText);
|
||||
const byLine = new Map();
|
||||
for (const [line, statement] of lines.entries()) {
|
||||
const executable = stripComments(statement).trim().length > 0;
|
||||
byLine.set(line, {
|
||||
line,
|
||||
statement,
|
||||
executable,
|
||||
executionState: executable ? "no-motion" : "blank-or-comment",
|
||||
canonicalEventCount: 0,
|
||||
motionEventCount: 0,
|
||||
motionTypes: [],
|
||||
firstMotionIndex: null,
|
||||
lastMotionIndex: null,
|
||||
finalAxes: null,
|
||||
events: [],
|
||||
});
|
||||
}
|
||||
for (const [motionOrder, event] of motion.entries()) {
|
||||
const motionIndex = event.index ?? motionOrder;
|
||||
const line = Number(event.line);
|
||||
if (!Number.isFinite(line)) continue;
|
||||
const entry = byLine.get(line) || {
|
||||
line,
|
||||
statement: event.statement || "-",
|
||||
executable: true,
|
||||
executionState: "no-motion",
|
||||
canonicalEventCount: 0,
|
||||
motionEventCount: 0,
|
||||
motionTypes: [],
|
||||
firstMotionIndex: null,
|
||||
lastMotionIndex: null,
|
||||
finalAxes: null,
|
||||
events: [],
|
||||
};
|
||||
byLine.set(line, entry);
|
||||
entry.executionState = "motion";
|
||||
entry.canonicalEventCount += 1;
|
||||
entry.motionEventCount += 1;
|
||||
if (entry.firstMotionIndex === null) entry.firstMotionIndex = motionIndex;
|
||||
entry.lastMotionIndex = motionIndex;
|
||||
entry.finalAxes = event.axes;
|
||||
if (!entry.motionTypes.includes(event.type)) entry.motionTypes.push(event.type);
|
||||
entry.events.push({
|
||||
index: motionIndex,
|
||||
type: event.type,
|
||||
axes: event.axes,
|
||||
feedRate: event.feedRate,
|
||||
raw: event.raw,
|
||||
});
|
||||
}
|
||||
return [...byLine.values()].sort((left, right) => left.line - right.line);
|
||||
}
|
||||
|
||||
function compareResults(native, web) {
|
||||
const mismatches = [];
|
||||
compareNumber("motionEventCount", native.summary.motionEventCount, web.summary.motionEventCount, 0, mismatches);
|
||||
const maxCount = Math.min(native.toolPreviewPath.length, web.toolPreviewPath.length);
|
||||
let maxAxisDelta = 0;
|
||||
for (let index = 0; index < maxCount; index += 1) {
|
||||
const nativeEvent = native.toolPreviewPath[index];
|
||||
const webEvent = web.toolPreviewPath[index];
|
||||
if (nativeEvent.type !== webEvent.type) {
|
||||
mismatches.push({
|
||||
kind: "motion-type",
|
||||
index,
|
||||
native: nativeEvent.type,
|
||||
web: webEvent.type,
|
||||
});
|
||||
}
|
||||
for (const axis of AXES) {
|
||||
const delta = Math.abs(Number(nativeEvent.axes?.[axis] || 0) - Number(webEvent.axes?.[axis] || 0));
|
||||
maxAxisDelta = Math.max(maxAxisDelta, delta);
|
||||
if (delta > 0.001) {
|
||||
mismatches.push({
|
||||
kind: "axis",
|
||||
index,
|
||||
axis,
|
||||
native: nativeEvent.axes?.[axis] ?? null,
|
||||
web: webEvent.axes?.[axis] ?? null,
|
||||
delta,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
const nativeFeed = Number(nativeEvent.feedRate);
|
||||
const webFeed = Number(webEvent.feedRate);
|
||||
if (Number.isFinite(nativeFeed) && Number.isFinite(webFeed) && Math.abs(nativeFeed - webFeed) > 0.01) {
|
||||
mismatches.push({
|
||||
kind: "feed-rate",
|
||||
index,
|
||||
native: nativeFeed,
|
||||
web: webFeed,
|
||||
delta: Math.abs(nativeFeed - webFeed),
|
||||
});
|
||||
}
|
||||
if (mismatches.length > 200) break;
|
||||
}
|
||||
|
||||
const diagnostics = {
|
||||
nativeExecutionPathBasis: native.toolExecutionPath[0]?.source || "linuxcnc-native-rs274-canonical",
|
||||
webExecutionPathBasis: web.plannerTiming?.semanticBoundary || web.toolExecutionPath[0]?.source || "unknown",
|
||||
nativeLineMapping: native.toolPreviewPath[0]?.lineMappingSource || null,
|
||||
webSelectedMode: web.runtime.selectedMode,
|
||||
webMachineFileDiagnostics: web.runtime.machineFileDiagnostics,
|
||||
};
|
||||
|
||||
return {
|
||||
apiName: "web-rtcp-5axis-linuxcnc-web-parity-comparison",
|
||||
generatedAt: new Date().toISOString(),
|
||||
profileId: "gmoccapy-xyzac-trt",
|
||||
filename: native.filename,
|
||||
summary: {
|
||||
passed: mismatches.length === 0,
|
||||
mismatchCount: mismatches.length,
|
||||
comparedMotionEvents: maxCount,
|
||||
maxAxisDelta,
|
||||
nativeMotionCount: native.summary.motionEventCount,
|
||||
webMotionCount: web.summary.motionEventCount,
|
||||
nativeExecutedPathPointCount: native.summary.executedPathPointCount,
|
||||
webExecutedPathPointCount: web.summary.executedPathPointCount,
|
||||
webPlannerRuntimeReady: web.summary.plannerRuntimeReady,
|
||||
},
|
||||
tolerances: {
|
||||
axis: 0.001,
|
||||
feedRate: 0.01,
|
||||
},
|
||||
diagnostics,
|
||||
mismatches,
|
||||
};
|
||||
}
|
||||
|
||||
function compareNumber(field, nativeValue, webValue, tolerance, mismatches) {
|
||||
const delta = Math.abs(Number(nativeValue) - Number(webValue));
|
||||
if (delta > tolerance) {
|
||||
mismatches.push({ kind: "summary", field, native: nativeValue, web: webValue, delta });
|
||||
}
|
||||
}
|
||||
|
||||
function extractDiagnostics(resultText) {
|
||||
const diagnostics = {};
|
||||
for (const line of String(resultText).split(/\r?\n/)) {
|
||||
if (line.startsWith("fiveaxis_hal_switchkins:")) {
|
||||
const rc = line.match(/\brc=([-+0-9.eE]+)/);
|
||||
const found = line.match(/\bfound=([-+0-9.eE]+)/);
|
||||
const value = line.match(/\bvalue=([-+0-9.eE]+)/);
|
||||
diagnostics.fiveaxis_hal_switchkins = {
|
||||
rc: rc ? Number(rc[1]) : null,
|
||||
found: found ? Number(found[1]) : null,
|
||||
value: value ? Number(value[1]) : null,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
if (!line.startsWith("fiveaxis_")) continue;
|
||||
const equals = line.indexOf("=");
|
||||
if (equals < 0) continue;
|
||||
diagnostics[line.slice(0, equals)] = line.slice(equals + 1);
|
||||
}
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
function programLineMap(programText) {
|
||||
const map = new Map();
|
||||
String(programText).split(/\r?\n/).forEach((line, index) => {
|
||||
map.set(index + 1, line.trim() || "(blank)");
|
||||
});
|
||||
return map;
|
||||
}
|
||||
|
||||
function stripComments(line) {
|
||||
return String(line)
|
||||
.replace(/\([^)]*\)/g, " ")
|
||||
.replace(/;.*$/g, " ");
|
||||
}
|
||||
|
||||
function vectorLength(values) {
|
||||
return Math.sqrt(values.reduce((total, value) => total + value * value, 0));
|
||||
}
|
||||
|
||||
function firstFiniteLine(motion) {
|
||||
return motion.find((event) => Number.isFinite(Number(event.line)))?.line ?? null;
|
||||
}
|
||||
|
||||
function lastFiniteLine(motion) {
|
||||
return [...motion].reverse().find((event) => Number.isFinite(Number(event.line)))?.line ?? null;
|
||||
}
|
||||
|
||||
function writeJson(path, value) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||
}
|
||||
|
||||
function sha256(text) {
|
||||
return createHash("sha256").update(text).digest("hex");
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
const parsed = {
|
||||
cases: [],
|
||||
outputDir: null,
|
||||
};
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--out" || arg === "--output-dir") {
|
||||
parsed.outputDir = args[++index];
|
||||
continue;
|
||||
}
|
||||
if (arg === "--case") {
|
||||
parsed.cases.push(args[++index]);
|
||||
continue;
|
||||
}
|
||||
if (arg === "--cases") {
|
||||
parsed.cases.push(...String(args[++index] || "").split(",").filter(Boolean));
|
||||
continue;
|
||||
}
|
||||
if (arg === "--help" || arg === "-h") {
|
||||
console.log("Usage: node tools/collect-linuxcnc-web-parity.mjs [--out DIR] [--case file.ngc] [--cases a.ngc,b.ngc]");
|
||||
process.exit(0);
|
||||
}
|
||||
parsed.cases.push(arg);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
Reference in New Issue
Block a user