631 lines
20 KiB
JavaScript
631 lines
20 KiB
JavaScript
#!/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;
|
|
}
|