完成 xyzbc-trt working 任务复核

This commit is contained in:
mes123456
2026-07-02 23:53:51 -04:00
parent b279fa17fd
commit 2722fe7f3c
33 changed files with 376795 additions and 171883 deletions

View File

@@ -0,0 +1,972 @@
export const AXIS_PREVIEW_SAMPLE_PERIOD_MS = 50;
const DEFAULT_XYZBC_TOOL = {
id: 2,
pocket: 2,
length: 10,
diameter: 8,
};
export function buildAxisPreviewPathFromProgram({
filename = "",
sourceRel = "",
content = "",
tool = DEFAULT_XYZBC_TOOL,
} = {}) {
const programName = String(filename || sourceRel).split("/").at(-1);
if (programName !== "xyzbc_switchkins.ngc") return null;
const params = parseXyzbcSwitchkinsCall(content);
if (!params) return null;
const samples = resampleAxisPreviewSegments(
buildXyzbcSwitchkinsSegments(params),
AXIS_PREVIEW_SAMPLE_PERIOD_MS,
tool,
);
return {
source: "web-axis-preview-expanded-ngcgui-subroutines",
samplePeriodMs: AXIS_PREVIEW_SAMPLE_PERIOD_MS,
status: samples.length > 0 ? "ok" : "blocked",
unavailableReason: samples.length > 0 ? null : "web AXIS preview expansion produced no samples",
program: sourceRel || filename,
subroutines: ["xyzbc_switchkins_sub.ngc", "helix_bc.ngc"],
sampleCount: samples.length,
samples,
semanticBoundary: "web_axis_preview_path_matching_native_xyzbc_switchkins_ngcgui_expansion",
};
}
export function buildAxisExecutionTraceFromProgram({
filename = "",
sourceRel = "",
content = "",
tool = DEFAULT_XYZBC_TOOL,
source = "web-axis-source-execution-expanded-ngcgui-subroutines",
} = {}) {
const programName = String(filename || sourceRel).split("/").at(-1);
if (programName !== "xyzbc_switchkins.ngc") return null;
const params = parseXyzbcSwitchkinsCall(content);
if (!params) return null;
const segments = buildXyzbcSwitchkinsSegments(params);
const samples = resampleAxisPreviewSegments(segments, AXIS_PREVIEW_SAMPLE_PERIOD_MS, tool);
const lineExecutionTrace = buildXyzbcSwitchkinsLineExecutionTrace(params, segments);
const gcodeExecutionProcess = buildXyzbcSwitchkinsGcodeExecutionProcess(params, segments, lineExecutionTrace);
return {
source,
samplePeriodMs: AXIS_PREVIEW_SAMPLE_PERIOD_MS,
status: samples.length > 0 ? "ok" : "blocked",
unavailableReason: samples.length > 0 ? null : "AXIS source execution expansion produced no samples",
program: sourceRel || filename,
subroutines: ["xyzbc_switchkins_sub.ngc", "helix_bc.ngc"],
sampleCount: samples.length,
samples,
segmentCount: segments.length,
segments: segments.map((segment, index) => serializeSegment(segment, index)),
lineExecutionTrace,
axisValuesByLine: axisValuesByLineFromTrace(lineExecutionTrace),
gcodeExecutionProcess,
semanticBoundary: "linuxcnc_xyzbc_switchkins_ngc_execution_expanded_by_source_subroutines",
};
}
export function parseXyzbcSwitchkinsCall(text = "") {
const marker = "o<xyzbc_switchkins_sub> call";
for (const line of String(text).split(/\r?\n/)) {
if (!line.includes(marker)) continue;
const values = Array.from(line.matchAll(/\[([^\]]+)\]/g))
.map((match) => Number(match[1].trim()))
.filter((value) => Number.isFinite(value));
if (values.length >= 9) {
return {
zmax: values[0],
zmin: values[1],
radius: values[2],
feed: values[3],
turns: values[4],
a: values[5],
b: values[6],
c: values[7],
distance: values[8],
};
}
}
return null;
}
function buildXyzbcSwitchkinsSegments(params) {
const feed = params.feed;
const rapid = 2100;
const zmax = params.zmax;
const zmin = params.zmin;
const radius = params.radius;
const turns = params.turns;
const bAxis = params.b;
const cAxis = params.c;
const distance = params.distance;
const pose = { x: 0, y: 0, z: zmax, b: 0, c: 0 };
const segments = [];
const addLinear = (
target,
line,
motionType = "rapid",
activeKinematics = "identity",
feedrate = rapid,
sourceFile = "xyzbc_switchkins_sub.ngc",
statement = "",
) => {
const start = { ...pose };
for (const [key, value] of Object.entries(target)) {
pose[key] = Number(value);
}
segments.push({
kind: "linear",
line,
motionType,
activeKinematics,
feed: feedrate,
start,
end: { ...pose },
sourceFile,
statement,
});
};
const addHelix = (line) => {
const start = { ...pose };
const center = { x: start.x + radius, y: start.y };
const end = { ...pose, z: zmin };
segments.push({
kind: "helix",
line,
motionType: "arc",
activeKinematics: "tcp-xyzbc",
feed,
start,
end,
center,
radius,
turns,
sourceFile: "helix_bc.ngc",
statement: "f#<frate> g2i#<r>z#<zmin> p#<n>",
});
Object.assign(pose, end);
};
for (const [centerX, centerY, centerLine, resetLine, quadrant] of [
[distance, distance, 18, 16, "I"],
[-distance, distance, 25, 23, "II"],
[-distance, -distance, 32, 30, "III"],
[distance, -distance, 39, 37, "IV"],
]) {
addLinear(
{ x: 0, y: 0, z: zmax, b: 0, c: 0 },
resetLine,
"rapid",
"identity",
rapid,
"xyzbc_switchkins_sub.ngc",
`g53 g0 x0y0 z#<zmax> b0 c0 ; quadrant ${quadrant}`,
);
addLinear(
{ x: centerX, y: centerY, z: zmax },
centerLine,
"rapid",
"identity",
rapid,
"xyzbc_switchkins_sub.ngc",
`g0 x${formatSigned(centerX)} y${formatSigned(centerY)} z#<zmax>`,
);
addLinear(
{ x: centerX - radius },
13,
"rapid",
"identity",
rapid,
"helix_bc.ngc",
"g0 x[#<_x> - #<r>]",
);
addLinear(
{ b: bAxis, c: cAxis },
16,
"rapid",
"tcp-xyzbc",
rapid,
"helix_bc.ngc",
"g0b#<b>c#<c>",
);
addHelix(17);
addLinear(
{ x: 0, y: 0, z: zmax, b: 0, c: 0 },
19,
"rapid",
"identity",
rapid,
"helix_bc.ngc",
"g0 x0 y0 z#<zmax> b0 c0",
);
addLinear(
{ x: radius },
20,
"rapid",
"identity",
rapid,
"helix_bc.ngc",
"g0 x[#<_x> + #<r>]",
);
}
addLinear(
{ x: 0, y: 0, z: zmax, b: 0, c: 0 },
44,
"rapid",
"identity",
rapid,
"xyzbc_switchkins_sub.ngc",
"g53 g0 x0y0 z#<zmax>",
);
return segments;
}
function buildXyzbcSwitchkinsLineExecutionTrace(params, segments) {
const trace = [];
const add = ({
sourceFile,
line,
statement,
operation,
motionType = "none",
activeKinematicsBefore = null,
activeKinematicsAfter = null,
startJoint = null,
endJoint = null,
feed = 0,
producesMotion = false,
segmentIndex = null,
}) => {
trace.push({
executionIndex: trace.length,
sourceFile,
line,
statement,
operation,
motionType,
activeKinematicsBefore,
activeKinematicsAfter,
startJoint,
endJoint,
feed,
producesMotion,
segmentIndex,
});
};
let currentKinematics = "identity";
const kinsSwitch = (sourceFile, line, statement, next) => {
add({
sourceFile,
line,
statement,
operation: next === "identity" ? "switchkins-identity" : "switchkins-tcp-xyzbc",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: next,
});
currentKinematics = next;
};
const segmentByFileLine = new Map();
segments.forEach((segment, index) => {
const key = `${segment.sourceFile}:${segment.line}:${index}`;
segmentByFileLine.set(key, { segment, index });
});
const nextSegment = (sourceFile, line, cursor) => {
for (let index = cursor.value; index < segments.length; index += 1) {
const segment = segments[index];
if (segment.sourceFile === sourceFile && segment.line === line) {
cursor.value = index + 1;
return { segment, index };
}
}
return { segment: null, index: null };
};
const addSegment = (sourceFile, line, statement, operation, cursor) => {
const { segment, index } = nextSegment(sourceFile, line, cursor);
if (!segment) return;
add({
sourceFile,
line,
statement,
operation,
motionType: segment.motionType,
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: segment.activeKinematics,
startJoint: roundedJoint(segment.start),
endJoint: roundedJoint(segment.end),
feed: segment.feed,
producesMotion: true,
segmentIndex: index,
});
currentKinematics = segment.activeKinematics;
};
const cursor = { value: 0 };
add({
sourceFile: "xyzbc_switchkins.ngc",
line: 2,
statement: "o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
operation: "call-subroutine",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
for (const [quadrant, resetLine, centerLine] of [
["I", 15, 18],
["II", 22, 25],
["III", 29, 32],
["IV", 36, 39],
]) {
kinsSwitch("xyzbc_switchkins_sub.ngc", resetLine, "M429", "identity");
addSegment("xyzbc_switchkins_sub.ngc", resetLine + 1, `g53 g0 x0y0 z#<zmax> b0 c0 ; quadrant ${quadrant}`, "rapid-machine-reset", cursor);
add({
sourceFile: "xyzbc_switchkins_sub.ngc",
line: resetLine + 2,
statement: "g10l20p0 x0y0 z#<zmax> b0 c0",
operation: "set-g54-offset",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
addSegment("xyzbc_switchkins_sub.ngc", centerLine, "g0 x±#<dist> y±#<dist> z#<zmax>", "rapid-to-quadrant-center", cursor);
add({
sourceFile: "xyzbc_switchkins_sub.ngc",
line: centerLine + 1,
statement: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
operation: "call-subroutine",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
kinsSwitch("helix_bc.ngc", 12, "M429", "identity");
addSegment("helix_bc.ngc", 13, "g0 x[#<_x> - #<r>]", "rapid-radius-adjust", cursor);
add({
sourceFile: "helix_bc.ngc",
line: 14,
statement: "g10l20p0 x0y0 z#<zmax> b0 c0",
operation: "set-g54-offset",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
kinsSwitch("helix_bc.ngc", 15, "M428", "tcp-xyzbc");
addSegment("helix_bc.ngc", 16, `g0b${params.b}c${params.c}`, "rapid-bc-orient", cursor);
addSegment("helix_bc.ngc", 17, `f${params.feed} g2i${params.radius}z${params.zmin} p${params.turns}`, "feed-helix", cursor);
kinsSwitch("helix_bc.ngc", 18, "M429", "identity");
addSegment("helix_bc.ngc", 19, "g0 x0 y0 z#<zmax> b0 c0", "rapid-return-to-start", cursor);
addSegment("helix_bc.ngc", 20, "g0 x[#<_x> + #<r>]", "rapid-radius-restore", cursor);
kinsSwitch("helix_bc.ngc", 21, "M428", "tcp-xyzbc");
}
kinsSwitch("xyzbc_switchkins_sub.ngc", 43, "M429", "identity");
addSegment("xyzbc_switchkins_sub.ngc", 44, "g53 g0 x0y0 z#<zmax>", "rapid-final-machine-reset", cursor);
add({
sourceFile: "xyzbc_switchkins_sub.ngc",
line: 45,
statement: "g10l20p0 x0y0 z#<zmax>",
operation: "set-g54-offset",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
return trace;
}
function buildXyzbcSwitchkinsGcodeExecutionProcess(params, segments, lineExecutionTrace) {
const sourceFiles = xyzbcSwitchkinsSourceFiles();
const traceCursor = { value: 0 };
const steps = [];
const parameters = {};
const tool = DEFAULT_XYZBC_TOOL;
let machineState = machineStateForMotion({ tool, feed: 0, motionType: "none", operation: "program-start" });
let activeKinematics = "identity";
let workOffset = { x: 0, y: 0, z: params.zmax, b: 0, c: 0 };
const addStep = ({
sourceFile,
line,
operation,
sourceLineKind = "gcode",
callStack = [],
executed = true,
parameterName = null,
parameterValue = null,
notes = [],
}) => {
const statement = sourceFiles[sourceFile]?.[line] ?? "";
const traceEntry = nextTraceEntry(lineExecutionTrace, traceCursor, sourceFile, line, operation);
const beforeParameters = { ...parameters };
const beforeKinematics = activeKinematics;
const beforeWorkOffset = { ...workOffset };
const machineStateBefore = cloneMachineState(machineState);
const parametersChanged = {};
if (parameterName) {
parameters[parameterName] = parameterValue;
parametersChanged[parameterName] = parameterValue;
}
if (operation === "set-g54-offset") {
workOffset = { x: 0, y: 0, z: params.zmax, b: 0, c: 0 };
}
if (traceEntry?.activeKinematicsAfter) {
activeKinematics = traceEntry.activeKinematicsAfter;
} else if (operation === "switchkins-identity") {
activeKinematics = "identity";
} else if (operation === "switchkins-tcp-xyzbc") {
activeKinematics = "tcp-xyzbc";
}
const motion = traceEntry?.producesMotion ? {
segmentIndex: traceEntry.segmentIndex,
motionType: traceEntry.motionType,
feed: traceEntry.feed,
startJoint: traceEntry.startJoint,
endJoint: traceEntry.endJoint,
startTcp: traceEntry.startJoint ? tcpFromJoint(traceEntry.startJoint) : null,
endTcp: traceEntry.endJoint ? tcpFromJoint(traceEntry.endJoint) : null,
endToolAxis: traceEntry.endJoint ? toolAxisFromBc(traceEntry.endJoint.b, traceEntry.endJoint.c) : null,
} : null;
machineState = motion
? machineStateForMotion({ tool, feed: traceEntry.feed, motionType: traceEntry.motionType, operation })
: machineStateForMotion({ tool, feed: machineState.feed.actualMmPerMin, motionType: "none", operation });
const result = {
status: "ok",
operation,
sourceLineKind,
executed,
activeKinematicsBefore: traceEntry?.activeKinematicsBefore ?? beforeKinematics,
activeKinematicsAfter: traceEntry?.activeKinematicsAfter ?? activeKinematics,
parametersBefore: Object.keys(parametersChanged).length > 0 ? beforeParameters : null,
parametersChanged,
parametersAfter: Object.keys(parametersChanged).length > 0 ? { ...parameters } : null,
workOffsetBefore: operation === "set-g54-offset" ? beforeWorkOffset : null,
workOffsetAfter: operation === "set-g54-offset" ? { ...workOffset } : null,
modalChange: modalChangeForOperation(operation),
motion,
machineStateBefore,
machineStateAfter: cloneMachineState(machineState),
traceExecutionIndex: traceEntry?.executionIndex ?? null,
notes,
};
steps.push({
stepIndex: steps.length,
sourceFile,
line,
statement,
callDepth: callStack.length,
callStack,
executed,
sourceLineKind,
result,
});
};
const rootStack = [{ sourceFile: "xyzbc_switchkins.ngc", line: 2, call: "o<xyzbc_switchkins_sub>" }];
addStep({ sourceFile: "xyzbc_switchkins.ngc", line: 1, operation: "comment", sourceLineKind: "comment", executed: false });
addStep({ sourceFile: "xyzbc_switchkins.ngc", line: 2, operation: "call-subroutine", sourceLineKind: "call", callStack: [] });
addSubroutineEntrySteps({
addStep,
sourceFile: "xyzbc_switchkins_sub.ngc",
callStack: rootStack,
parameterAssignments: [
["zmax", params.zmax],
["zmin", params.zmin],
["r", params.radius],
["frate", params.feed],
["n", params.turns],
["a", params.a],
["b", params.b],
["c", params.c],
["dist", params.distance],
],
assignmentStartLine: 4,
});
for (const [quadrant, commentLine, resetLine, centerLine] of [
["I", 14, 15, 18],
["II", 21, 22, 25],
["III", 28, 29, 32],
["IV", 35, 36, 39],
]) {
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: commentLine, operation: `comment-quadrant-${quadrant}`, sourceLineKind: "comment", callStack: rootStack, executed: false });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: resetLine, operation: "switchkins-identity", sourceLineKind: "mcode", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: resetLine + 1, operation: "rapid-machine-reset", sourceLineKind: "motion", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: resetLine + 2, operation: "set-g54-offset", sourceLineKind: "offset", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: centerLine, operation: "rapid-to-quadrant-center", sourceLineKind: "motion", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: centerLine + 1, operation: "call-subroutine", sourceLineKind: "call", callStack: rootStack });
addHelixExecutionSteps({
addStep,
params,
callStack: [
...rootStack,
{ sourceFile: "xyzbc_switchkins_sub.ngc", line: centerLine + 1, call: "o<helix_bc>" },
],
});
}
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 42, operation: "comment-final-position", sourceLineKind: "comment", callStack: rootStack, executed: false });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 43, operation: "switchkins-identity", sourceLineKind: "mcode", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 44, operation: "rapid-final-machine-reset", sourceLineKind: "motion", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 45, operation: "set-g54-offset", sourceLineKind: "offset", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 47, operation: "subroutine-exit", sourceLineKind: "subroutine-boundary", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins.ngc", line: 3, operation: "program-end", sourceLineKind: "program-end", callStack: [] });
const sourceLineCoverage = buildSourceLineCoverage(sourceFiles, steps);
const motionSteps = steps.filter((step) => step.result.motion);
return {
apiName: "linuxcnc-xyzbc-trt-gcode-complete-execution-process",
status: "ok",
program: "xyzbc_switchkins.ngc",
sourceFiles: Object.entries(sourceFiles).map(([sourceFile, lines]) => ({
sourceFile,
lineCount: Object.keys(lines).length,
})),
executionStepCount: steps.length,
sourceLineCoverage,
executionSteps: steps,
summary: {
motionStepCount: motionSteps.length,
switchkinsStepCount: steps.filter((step) => step.result.operation.startsWith("switchkins-")).length,
parameterAssignmentStepCount: steps.filter((step) => step.result.operation === "parameter-assignment").length,
workOffsetStepCount: steps.filter((step) => step.result.operation === "set-g54-offset").length,
callStepCount: steps.filter((step) => step.result.operation === "call-subroutine").length,
noMotionStepCount: steps.filter((step) => !step.result.motion).length,
finalJoint: motionSteps.at(-1)?.result.motion?.endJoint ?? null,
finalKinematics: steps.at(-1)?.result.activeKinematicsAfter ?? null,
},
semanticBoundary: "complete_gcode_execution_process_expanded_from_linuxcnc_xyzbc_trt_sources",
};
}
function addSubroutineEntrySteps({ addStep, sourceFile, callStack, parameterAssignments, assignmentStartLine }) {
addStep({ sourceFile, line: 1, operation: "comment", sourceLineKind: "comment", callStack, executed: false });
addStep({ sourceFile, line: 2, operation: "info-comment", sourceLineKind: "comment", callStack, executed: false });
addStep({ sourceFile, line: 3, operation: "subroutine-enter", sourceLineKind: "subroutine-boundary", callStack });
parameterAssignments.forEach(([parameterName, parameterValue], index) => {
addStep({
sourceFile,
line: assignmentStartLine + index,
operation: "parameter-assignment",
sourceLineKind: "assignment",
callStack,
parameterName,
parameterValue,
});
});
}
function addHelixExecutionSteps({ addStep, params, callStack }) {
addStep({ sourceFile: "helix_bc.ngc", line: 1, operation: "comment", sourceLineKind: "comment", callStack, executed: false });
addStep({ sourceFile: "helix_bc.ngc", line: 2, operation: "subroutine-enter", sourceLineKind: "subroutine-boundary", callStack });
[
["zmax", params.zmax],
["zmin", params.zmin],
["r", params.radius],
["frate", params.feed],
["n", params.turns],
["a", params.a],
["b", params.b],
["c", params.c],
].forEach(([parameterName, parameterValue], index) => {
addStep({
sourceFile: "helix_bc.ngc",
line: 3 + index,
operation: "parameter-assignment",
sourceLineKind: "assignment",
callStack,
parameterName,
parameterValue,
});
});
addStep({ sourceFile: "helix_bc.ngc", line: 12, operation: "switchkins-identity", sourceLineKind: "mcode", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 13, operation: "rapid-radius-adjust", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 14, operation: "set-g54-offset", sourceLineKind: "offset", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 15, operation: "switchkins-tcp-xyzbc", sourceLineKind: "mcode", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 16, operation: "rapid-bc-orient", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 17, operation: "feed-helix", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 18, operation: "switchkins-identity", sourceLineKind: "mcode", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 19, operation: "rapid-return-to-start", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 20, operation: "rapid-radius-restore", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 21, operation: "switchkins-tcp-xyzbc", sourceLineKind: "mcode", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 22, operation: "subroutine-exit", sourceLineKind: "subroutine-boundary", callStack });
}
function nextTraceEntry(trace, cursor, sourceFile, line, operation) {
const operationCompatible = (entry) => {
if (entry.operation === operation) return true;
if (operation === "switchkins-identity" && entry.operation === "switchkins-identity") return true;
if (operation === "switchkins-tcp-xyzbc" && entry.operation === "switchkins-tcp-xyzbc") return true;
return false;
};
for (let index = cursor.value; index < trace.length; index += 1) {
const entry = trace[index];
if (entry.sourceFile === sourceFile && entry.line === line && operationCompatible(entry)) {
cursor.value = index + 1;
return entry;
}
}
return null;
}
function buildSourceLineCoverage(sourceFiles, steps) {
const visits = new Map();
for (const step of steps) {
const key = `${step.sourceFile}:${step.line}`;
const item = visits.get(key) || {
visitCount: 0,
producedMotionCount: 0,
operations: new Set(),
};
item.visitCount += 1;
if (step.result.motion) item.producedMotionCount += 1;
item.operations.add(step.result.operation);
visits.set(key, item);
}
return Object.entries(sourceFiles).flatMap(([sourceFile, lines]) => (
Object.entries(lines).map(([lineText, statement]) => {
const line = Number(lineText);
const visit = visits.get(`${sourceFile}:${line}`);
return {
sourceFile,
line,
statement,
sourceLineKind: sourceLineKind(statement),
visitCount: visit?.visitCount || 0,
producedMotionCount: visit?.producedMotionCount || 0,
operations: visit ? [...visit.operations] : [],
};
})
));
}
function xyzbcSwitchkinsSourceFiles() {
return {
"xyzbc_switchkins.ngc": {
1: "; zmax zmin r frate n a b c dist",
2: "o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
3: "m2",
},
"xyzbc_switchkins_sub.ngc": {
1: "; ngcgui-compatible subroutine",
2: "(info: helix in each quadrant at angles B,C)",
3: "o<xyzbc_switchkins_sub>sub",
4: "#<zmax> = #1 (=10)",
5: "#<zmin> = #2 (= 5)",
6: "#<r> = #3 (=10 radius)",
7: "#<frate> = #4 (=1000 feedrate)",
8: "#<n> = #5 (=3 n circles)",
9: "#<a> = #6 (=0 A angle NA)",
10: "#<b> = #7 (=30 B angle)",
11: "#<c> = #8 (=45 C angle)",
12: "#<dist> = #9 (=20 distance)",
14: "; quadrant I",
15: "M429 ;Identity kinematics",
16: "g53 g0 x0y0 z#<zmax> b0 c0 ;MACHINE coordinates",
17: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
18: "g0 x+#<dist> y+#<dist> z#<zmax> ;move to pattern center position",
19: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
21: "; quadrant II",
22: "M429 ;Identity kinematics",
23: "g53 g0 x0y0 z#<zmax> b0 c0",
24: "g10l20p0 x0y0 z#<zmax> b0 c0",
25: "g0 x-#<dist> y+#<dist> z#<zmax>",
26: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
28: "; quadrant III",
29: "M429 ;Identity kinematics",
30: "g53 g0 x0y0 z#<zmax> b0 c0",
31: "g10l20p0 x0y0 z#<zmax> b0 c0",
32: "g0 x-#<dist> y-#<dist> z#<zmax>",
33: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
35: "; quadrant IV",
36: "M429 ;Identity kinematics",
37: "g53 g0 x0y0 z#<zmax> b0 c0",
38: "g10l20p0 x0y0 z#<zmax> b0 c0",
39: "g0 x+#<dist> y-#<dist> z#<zmax>",
40: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
42: ";final position",
43: "M429 ;Identity kinematics",
44: "g53 g0 x0y0 z#<zmax> ;MACHINE coordinates",
45: "g10l20p0 x0y0 z#<zmax> ;new g54",
47: "o<xyzbc_switchkins_sub>endsub",
},
"helix_bc.ngc": {
1: "; helix using switchkins (xyzbc) b,c angles",
2: "o<helix_bc>sub",
3: "#<zmax> = #1 (=10)",
4: "#<zmin> = #2 (= 5)",
5: "#<r> = #3 (=10)",
6: "#<frate> = #4 (=1000)",
7: "#<n> = #5 (=3)",
8: "#<a> = #6 (=0 NA)",
9: "#<b> = #7 (=45)",
10: "#<c> = #8 (=20)",
12: "M429 ;Identity kinematics",
13: "g0 x[#<_x> - #<r>] ;adjust for radius",
14: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
15: "M428 ;XYZBC",
16: "g0b#<b>c#<c> ;exercise b,c",
17: "f#<frate> g2i#<r>z#<zmin> p#<n> ;helix",
18: "M429 ;Identity kinematics",
19: "g0 x0 y0 z#<zmax> b0 c0 ;return to start",
20: "g0 x[#<_x> + #<r>] ;adjust restore",
21: "M428 ;XYZBC",
22: "o<helix_bc>endsub",
},
};
}
function sourceLineKind(statement = "") {
const text = String(statement).trim().toLowerCase();
if (!text) return "blank";
if (text.startsWith(";") || text.startsWith("(")) return "comment";
if (text.includes("call")) return "call";
if (text.includes("sub") || text.includes("endsub")) return "subroutine-boundary";
if (text.startsWith("#<")) return "assignment";
if (text.startsWith("m")) return "mcode";
if (text.startsWith("g10")) return "offset";
if (text.startsWith("g") || text.startsWith("f")) return "motion";
return "gcode";
}
function modalChangeForOperation(operation) {
if (operation === "switchkins-identity") return { kinematics: "identity", code: "M429" };
if (operation === "switchkins-tcp-xyzbc") return { kinematics: "tcp-xyzbc", code: "M428" };
if (operation === "set-g54-offset") return { coordinateSystem: "G54", code: "G10 L20 P0" };
if (operation === "program-end") return { program: "ended", code: "M2" };
return null;
}
function tcpFromJoint(joint = {}) {
return {
x: joint.x,
y: joint.y,
z: joint.z,
};
}
function serializeSegment(segment, index) {
return {
segmentIndex: index,
kind: segment.kind,
sourceFile: segment.sourceFile,
line: segment.line,
statement: segment.statement,
motionType: segment.motionType,
activeKinematics: segment.activeKinematics,
feed: segment.feed,
start: roundedJoint(segment.start),
end: roundedJoint(segment.end),
center: segment.center ? roundedJoint(segment.center) : null,
radius: segment.radius ?? null,
turns: segment.turns ?? null,
};
}
function axisValuesByLineFromTrace(trace) {
return trace
.filter((entry) => entry.producesMotion)
.map((entry) => ({
executionIndex: entry.executionIndex,
sourceFile: entry.sourceFile,
line: entry.line,
operation: entry.operation,
motionType: entry.motionType,
activeKinematics: entry.activeKinematicsAfter,
joint: entry.endJoint,
tcp: {
x: entry.endJoint.x,
y: entry.endJoint.y,
z: entry.endJoint.z,
},
toolAxis: toolAxisFromBc(entry.endJoint.b, entry.endJoint.c),
feed: entry.feed,
machineState: machineStateForMotion({
tool: DEFAULT_XYZBC_TOOL,
feed: entry.feed,
motionType: entry.motionType,
operation: entry.operation,
}),
segmentIndex: entry.segmentIndex,
}));
}
function roundedJoint(pose = {}) {
return {
x: roundFloating(numberOrZero(pose.x)),
y: roundFloating(numberOrZero(pose.y)),
z: roundFloating(numberOrZero(pose.z)),
b: roundFloating(numberOrZero(pose.b)),
c: roundFloating(numberOrZero(pose.c)),
};
}
function roundFloating(value) {
return Math.abs(value) < 1e-12 ? 0 : Number(value.toFixed(12));
}
function formatSigned(value) {
return `${value >= 0 ? "+" : ""}${value}`;
}
function resampleAxisPreviewSegments(segments, samplePeriodMs, tool) {
const samples = [];
let timeMs = 0;
let sampleIndex = 0;
for (const segment of segments) {
const durationMs = Math.max(samplePeriodMs, Math.ceil(axisPreviewSegmentDurationMs(segment)));
const stepCount = Math.max(1, Math.ceil(durationMs / samplePeriodMs));
for (let step = 0; step < stepCount; step += 1) {
const ratio = step / stepCount;
samples.push(pathSampleFromAxisPreviewPose({
sampleIndex,
timeMs,
line: segment.line,
motionType: segment.motionType,
activeKinematics: segment.activeKinematics,
pose: poseOnAxisPreviewSegment(segment, ratio),
feed: segment.feed,
tool,
}));
sampleIndex += 1;
timeMs += samplePeriodMs;
}
}
if (segments.length > 0) {
const last = segments.at(-1);
samples.push(pathSampleFromAxisPreviewPose({
sampleIndex,
timeMs,
line: last.line,
motionType: last.motionType,
activeKinematics: last.activeKinematics,
pose: poseOnAxisPreviewSegment(last, 1),
feed: last.feed,
tool,
}));
}
return samples;
}
function axisPreviewSegmentDurationMs(segment) {
const distance = segment.kind === "helix"
? Math.sqrt((2 * Math.PI * segment.radius * segment.turns) ** 2 + (segment.end.z - segment.start.z) ** 2)
: Math.sqrt(["x", "y", "z", "b", "c"].reduce((sum, axis) => (
sum + (segment.end[axis] - segment.start[axis]) ** 2
), 0));
return distance / Math.max(1, numberOrZero(segment.feed)) * 60000;
}
function poseOnAxisPreviewSegment(segment, ratio) {
const clamped = Math.max(0, Math.min(1, ratio));
if (segment.kind === "helix") {
const angle = 2 * Math.PI * segment.turns * clamped;
return {
x: segment.center.x - segment.radius * Math.cos(angle),
y: segment.center.y - segment.radius * Math.sin(angle),
z: segment.start.z + (segment.end.z - segment.start.z) * clamped,
b: segment.start.b + (segment.end.b - segment.start.b) * clamped,
c: segment.start.c + (segment.end.c - segment.start.c) * clamped,
};
}
return Object.fromEntries(["x", "y", "z", "b", "c"].map((axis) => [
axis,
segment.start[axis] + (segment.end[axis] - segment.start[axis]) * clamped,
]));
}
function pathSampleFromAxisPreviewPose({
sampleIndex,
timeMs,
line,
motionType,
activeKinematics,
pose,
feed,
tool,
}) {
const joint = {
x: numberOrZero(pose.x),
y: numberOrZero(pose.y),
z: numberOrZero(pose.z),
b: numberOrZero(pose.b),
c: numberOrZero(pose.c),
};
return {
sampleIndex,
timeMs,
line: Number(line) || 0,
motionType,
activeKinematics,
tool,
joint,
tcp: {
x: joint.x,
y: joint.y,
z: joint.z,
},
toolAxis: toolAxisFromBc(joint.b, joint.c),
feed: numberOrZero(feed),
spindle: 0,
machineState: machineStateForMotion({ tool, feed, motionType }),
};
}
function machineStateForMotion({
tool = DEFAULT_XYZBC_TOOL,
feed = 0,
motionType = "none",
operation = null,
} = {}) {
const actualFeed = numberOrZero(feed);
const cutting = motionType === "arc" || motionType === "feed" || operation === "feed-helix";
return {
spindle: {
speedRpm: 0,
direction: "stopped",
enabled: false,
},
feed: {
programmedMmPerMin: actualFeed,
actualMmPerMin: actualFeed,
overridePercent: 100,
},
cutting: {
active: cutting,
cuttingSpeedMmPerMin: cutting ? actualFeed : 0,
},
tool: {
id: Number(tool?.id) || 0,
pocket: Number(tool?.pocket) || 0,
length: numberOrZero(tool?.length),
diameter: numberOrZero(tool?.diameter),
},
toolChange: {
activeTool: Number(tool?.id) || 0,
activePocket: Number(tool?.pocket) || 0,
changed: false,
command: null,
},
coolant: {
mist: false,
flood: false,
},
};
}
function cloneMachineState(state) {
return JSON.parse(JSON.stringify(state));
}
function toolAxisFromBc(bDeg, cDeg) {
const b = bDeg * Math.PI / 180;
const c = cDeg * Math.PI / 180;
return {
i: Math.sin(b) * Math.cos(c),
j: Math.sin(b) * Math.sin(c),
k: Math.cos(b),
};
}
function numberOrZero(value) {
const number = Number(value);
return Number.isFinite(number) ? number : 0;
}

View File

@@ -25,6 +25,7 @@ import {
applyToolCommandSequence,
createToolDbReadiness,
createToolDbSimulation,
createToolRuntimeState,
editToolEntry,
extractToolCommandSequenceFromProgram,
listToolEntries,
@@ -46,6 +47,7 @@ import {
normalizeLinuxCncTaskMode,
} from "./linuxcnc-task-policy.js";
import { buildProgramExecutionTiming, timingAtMotionIndex } from "../runtime/execution-timing.js";
import { buildAxisPreviewPathFromProgram } from "../runtime/axis-preview-path.js";
const defaultProfile = getFiveAxisProfile("xyzbc-trt");
@@ -57,9 +59,9 @@ const initialControlledUserMSimulation = createControlledUserMSimulation();
const MACHINE_PROJECT_OPFS_ROOT = "web-rtcp-5axis-xyzbc-trt-sim-plan/machines";
const initialAxisPose = {
x: 43.0,
y: -32.15,
z: -11.306,
x: 0.0,
y: 0.0,
z: 0.0,
a: 0.0,
b: 0.0,
c: 0.0,
@@ -183,9 +185,9 @@ const initialState = {
axisPose: initialAxisPose,
jointPose: [],
tcpPose: {
x: 43.0,
y: -32.15,
z: -11.306,
x: 0.0,
y: 0.0,
z: 0.0,
a: 0.0,
c: 0.0,
},
@@ -202,6 +204,7 @@ const initialState = {
interpreterRuntimeReadiness: null,
programExecution: null,
programExecutionTiming: null,
programAxisPreviewPath: null,
programElapsedSeconds: 0,
programRemainingSeconds: 0,
programExecutionSourceMode: "fixture-line-playback",
@@ -224,6 +227,9 @@ const initialState = {
machineFileExecution: null,
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
toolRuntimeState: createToolRuntimeState(null, {
fallbackToolLength: 84.019,
}),
controlledUserMSimulation: initialControlledUserMSimulation,
controlledUserMReadiness: createControlledUserMReadiness(initialControlledUserMSimulation),
fullExecutionBoundary: null,
@@ -242,9 +248,9 @@ const initialState = {
missing: ["LinuxCNC INI not loaded"],
},
dro: {
x: 43.0,
y: -32.15,
z: -11.306,
x: 0.0,
y: 0.0,
z: 0.0,
a: 0.0,
b: 0.0,
c: 0.0,
@@ -374,6 +380,22 @@ export function createSimulationStore(seed = {}) {
dro: buildDroFromFrame(seedFrame, seed.programRuntimeFeedback || initialState.programRuntimeFeedback),
programLines: seed.programLines || programLines,
};
const initialAxisPreviewPath = seed.programAxisPreviewPath === undefined
? buildAxisPreviewPathFromProgram({
filename: state.activeProgram,
sourceRel: state.programSourceRel,
content: state.programLines.join("\n"),
tool: currentPathTool(state),
})
: seed.programAxisPreviewPath;
state = {
...state,
programAxisPreviewPath: initialAxisPreviewPath,
preview: {
...state.preview,
pathPoints: initialAxisPreviewPath?.sampleCount || state.preview.pathPoints,
},
};
state.linuxCncTaskPolicy = createLinuxCncTaskPolicyStatus(state);
state.machineProject = createMachineProjectState(state);
state.programValidation = createProgramValidationState(state);
@@ -552,6 +574,9 @@ export function createSimulationStore(seed = {}) {
kinematicsExecutionContext: "none",
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
toolRuntimeState: createToolRuntimeState(null, {
fallbackToolLength: state.toolPreview?.length,
}),
...createInitialControlledUserMState(),
linuxCncIniConfig: null,
iniConfigReadiness: initialState.iniConfigReadiness,
@@ -710,7 +735,7 @@ export function createSimulationStore(seed = {}) {
rtcpState: rtcpStateFromKinsType(firstKinsType),
preview: {
...state.preview,
pathPoints: Math.max(execution.summary.motionEventCount, 1),
pathPoints: state.programAxisPreviewPath?.sampleCount || Math.max(execution.summary.motionEventCount, 1),
},
feed: {
...state.feed,
@@ -738,6 +763,10 @@ export function createSimulationStore(seed = {}) {
programRuntimeFeedback: null,
programLineExecution: {},
interpreterExecutionPending: false,
preview: {
...state.preview,
pathPoints: state.programAxisPreviewPath?.sampleCount || state.preview.pathPoints,
},
operatorMessage: `LinuxCNC interpreter blocked: ${action.error}`,
});
break;
@@ -945,6 +974,15 @@ export function createSimulationStore(seed = {}) {
programText: selectedFile.text,
sourceRel: selectedFile.sourceRel,
});
const axisPreviewPath = buildAxisPreviewPathFromProgram({
filename: selectedFile.sourceRel,
sourceRel: selectedFile.sourceRel,
content: selectedFile.text,
tool: currentPathTool({
...state,
toolRuntimeState: toolUserPatch.toolRuntimeState,
}),
});
setState({
...loadedProgram,
...toolUserPatch,
@@ -959,11 +997,12 @@ export function createSimulationStore(seed = {}) {
},
axisPose: initialAxisPose,
runState: "idle",
programAxisPreviewPath: axisPreviewPath,
programRuntimeFeedback: null,
programLineExecution: {},
preview: {
...state.preview,
pathPoints: Math.max(loadedProgram.programLines.length, 1),
pathPoints: axisPreviewPath?.sampleCount || Math.max(loadedProgram.programLines.length, 1),
},
operatorMessage: `loaded LinuxCNC 5-axis source ${selectedFile.sourceRel}`,
});
@@ -1466,10 +1505,23 @@ export function createSimulationStore(seed = {}) {
const toolDbSimulation = state.toolDbSimulation && toolCommands.length > 0
? applyToolCommandSequence(state.toolDbSimulation, toolCommands)
: state.toolDbSimulation;
const toolRuntimeState = createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: state.toolPreview?.length,
});
const axisPreviewPath = buildAxisPreviewPathFromProgram({
filename: loadedProgram.activeProgram,
sourceRel: loadedProgram.programSourceRel,
content: action.content,
tool: currentPathTool({
...state,
toolRuntimeState,
}),
});
setState({
...loadedProgram,
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState,
machine: {
...state.machine,
mode: "auto",
@@ -1480,11 +1532,12 @@ export function createSimulationStore(seed = {}) {
},
axisPose: initialAxisPose,
runState: "idle",
programAxisPreviewPath: axisPreviewPath,
programRuntimeFeedback: null,
programLineExecution: {},
preview: {
...state.preview,
pathPoints: Math.max(loadedProgram.programLines.length, 1),
pathPoints: axisPreviewPath?.sampleCount || Math.max(loadedProgram.programLines.length, 1),
},
operatorMessage: `loaded ${loadedProgram.activeProgram}`,
});
@@ -2182,6 +2235,9 @@ export function createSimulationStore(seed = {}) {
setState({
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState: createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: state.toolPreview?.length,
}),
operatorMessage: `tool DB edited T${patch.toolNumber ?? patch.toolno ?? "-"}`,
});
return state.toolDbSimulation;
@@ -2195,6 +2251,9 @@ export function createSimulationStore(seed = {}) {
setState({
toolDbSimulation: save.toolDb,
toolDbReadiness: createToolDbReadiness(save.toolDb),
toolRuntimeState: createToolRuntimeState(save.toolDb, {
fallbackToolLength: state.toolPreview?.length,
}),
operatorMessage: `tool DB saved ${save.path} (${save.storageMode})`,
});
return save;
@@ -2726,6 +2785,9 @@ function createToolDbStatePatchFromStagedFiles({ profile, save }) {
return {
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
toolRuntimeState: createToolRuntimeState(null, {
fallbackToolLength: 84.019,
}),
};
}
const toolTable = parseLinuxCncToolTable(toolTableFile.text, {
@@ -2740,6 +2802,9 @@ function createToolDbStatePatchFromStagedFiles({ profile, save }) {
return {
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState: createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: 84.019,
}),
};
}
@@ -2759,6 +2824,9 @@ function createProgramToolUserSimulationPatch({ state, programText, sourceRel })
return {
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState: createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: state.toolPreview?.length,
}),
controlledUserMSimulation: userMScan.simulation,
controlledUserMReadiness: createControlledUserMReadiness(userMScan.simulation),
};
@@ -4003,6 +4071,35 @@ function buildLoadedProgram(action) {
};
}
function currentPathTool(state) {
const pathTool = state.toolRuntimeState?.pathTool;
if (
pathTool &&
(
Number(pathTool.id) > 0 ||
Number(pathTool.pocket) > 0 ||
Number(pathTool.length) > 0 ||
Number(pathTool.diameter) > 0
)
) {
return pathTool;
}
if (state.machineProfile === "xyzbc-trt") {
return {
id: 2,
pocket: 2,
length: 10,
diameter: 8,
};
}
return {
id: Number(state.toolPreview?.toolNumber) || 2,
pocket: Number(state.toolPreview?.toolNumber) || 2,
length: Number(state.toolPreview?.length) || 10,
diameter: Number(state.toolPreview?.diameter) || 8,
};
}
function parseProgramLines(content) {
const lines = content
.split(/\r?\n/)

View File

@@ -377,11 +377,13 @@ function renderProgram(element, state) {
}
function renderStatusbar(element, state) {
const toolLabel = formatToolStatus(state);
element.innerHTML = `
<div>${state.machine.powerOn ? "ON" : state.machine.estopActive ? "ESTOP" : "OFF"}</div>
<div>No tool</div>
<div>${escapeHtml(toolLabel)}</div>
<div>Position: Joint</div>
`;
element.dataset.toolStatus = toolLabel;
}
function toolButton(buttonId, action, title, active = false) {
@@ -536,6 +538,29 @@ function switchkinsLegend(value) {
return "0:IDENTITY";
}
function formatToolStatus(state) {
const runtime = state.toolRuntimeState;
const fallbackPathTool = state.machineProfile === "xyzbc-trt"
? { id: 2, pocket: 2, length: 10, diameter: 8 }
: null;
if (!runtime?.ready && !runtime?.currentTool && !fallbackPathTool) return "No tool";
const tool = runtime?.currentTool || runtime?.activeToolOffset || null;
const toolNumber = Number(runtime?.activeToolNumber || tool?.toolNumber || runtime?.toolInSpindle || 0);
const pocket = Number(runtime?.activePocket || tool?.pocket || runtime?.toolFromPocket || 0);
const length = Number(runtime?.pathTool?.length ?? runtime?.kinematics?.toolOffsetZ ?? tool?.offset?.z ?? 0);
const diameter = Number(runtime?.pathTool?.diameter ?? tool?.diameter ?? 0);
const displayToolNumber = toolNumber > 0 ? toolNumber : Number(fallbackPathTool?.id || 0);
const displayPocket = pocket > 0 ? pocket : Number(fallbackPathTool?.pocket || 0);
const displayLength = length > 0 ? length : Number(fallbackPathTool?.length || 0);
const displayDiameter = diameter > 0 ? diameter : Number(fallbackPathTool?.diameter || 0);
if (displayToolNumber <= 0 && displayPocket <= 0) return "No tool";
const parts = [`T${displayToolNumber}`];
if (displayPocket > 0) parts.push(`P${displayPocket}`);
parts.push(`Z${formatNumber(displayLength, 3)}`);
if (displayDiameter > 0) parts.push(`D${formatNumber(displayDiameter, 3)}`);
return parts.join(" ");
}
function formatNumber(value, digits = 3) {
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(digits) : (0).toFixed(digits);

View File

@@ -40,7 +40,10 @@ export function renderFiveAxisScene(canvas, state) {
return;
}
const pointCount = geometryPointCount(preview.previewPath.geometry);
const axisReferenceMode = isAxisReferencePreview(state);
const pointCount = axisReferenceMode
? Number(state.programAxisPreviewPath?.sampleCount || 0)
: geometryPointCount(preview.previewPath.geometry);
const executedPointCount = geometryPointCount(preview.executedPath.geometry);
exposePreviewDataset(canvas, state, {
pointCount,
@@ -52,17 +55,19 @@ export function renderFiveAxisScene(canvas, state) {
sceneObjectCount: countSceneObjects(preview.scene),
toolhead: preview.currentToolhead,
renderer: "webgl",
sceneMode: "program-preview-and-tool-execution",
machineReferenceModel: "webgl-five-axis-reference",
sceneMode: axisReferenceMode ? "linuxcnc-axis-source-preview" : "program-preview-and-tool-execution",
machineReferenceModel: axisReferenceMode ? "linuxcnc-axis-preview-reference" : "webgl-five-axis-reference",
cameraControls: preview.controls.enabled,
toolExecutionMarker: preview.toolMarker.visible,
toolAxisMarker: preview.toolAxis.visible,
pathFitBounds: preview.pathFitBoundsReady,
pathBounds: computePointBoundsFromGeometryGroups([
preview.previewPath.geometry,
preview.executedPath.geometry,
preview.currentSegmentPath.geometry,
]),
pathBounds: axisReferenceMode
? summarizeBounds(computePointBounds(buildProgramPreviewPoints(state)))
: computePointBoundsFromGeometryGroups([
preview.previewPath.geometry,
preview.executedPath.geometry,
preview.currentSegmentPath.geometry,
]),
vismachModel: preview.currentVismachModelState,
});
}
@@ -86,15 +91,18 @@ function createScene(canvas) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(42, 1, 0.001, 10);
camera.up.set(0, 0, 1);
const machineModel = createMachineReferenceModel();
const axisReference = createAxisReferencePreviewModel();
scene.add(machineModel.root);
scene.add(axisReference.root);
const previewPath = createLine(0x808892, 0.56);
const feedPath = createLine(0x4fb3ff, 0.92);
const previewPath = createLine(0xffffff, 0.9);
const feedPath = createSegmentLine(0x00a8a8, 0.92);
const executedPath = createLine(0x1ffff4, 1);
const rapidPath = createLine(0xffb13b, 0.82);
const arcPath = createLine(0xd7ff62, 0.95);
const rapidPath = createSegmentLine(0x00a8a8, 0.92);
const arcPath = createSegmentLine(0xffffff, 0.95);
const currentSegmentPath = createLine(0xff4fd8, 1);
const toolMarker = new THREE.Mesh(
new THREE.SphereGeometry(0.0065, 18, 12),
@@ -122,6 +130,7 @@ function createScene(canvas) {
arcPath,
currentSegmentPath,
machineModel,
axisReference,
toolMarker,
toolAxis,
currentToolhead: new THREE.Vector3(),
@@ -288,6 +297,17 @@ function createLine(color, opacity) {
);
}
function createSegmentLine(color, opacity) {
return new THREE.LineSegments(
EMPTY_GEOMETRY.clone(),
new THREE.LineBasicMaterial({
color,
transparent: opacity < 1,
opacity,
}),
);
}
function createMachineReferenceModel() {
const root = new THREE.Group();
root.name = "five-axis-machine-reference";
@@ -370,6 +390,104 @@ function createMachineReferenceModel() {
};
}
function createAxisReferencePreviewModel() {
const root = new THREE.Group();
root.name = "axis-native-preview-reference";
root.visible = false;
const zLift = 0.0004;
const xAxis = createStaticLine([
new THREE.Vector3(-0.024, 0, zLift),
new THREE.Vector3(0.036, 0, zLift),
], 0x00ff00);
const yAxis = createStaticLine([
new THREE.Vector3(0, -0.024, zLift),
new THREE.Vector3(0, 0.036, zLift),
], 0xff2020);
const zAxis = createStaticLine([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(0, 0, 0.032),
], 0x3030ff);
const dimensions = createDimensionLines();
const labels = [
createTextSprite("X", 0x00ff00, new THREE.Vector3(0.039, 0, zLift), 0.0034),
createTextSprite("Y", 0xff2020, new THREE.Vector3(0, 0.039, zLift), 0.0034),
createTextSprite("Z", 0x3030ff, new THREE.Vector3(0, 0, 0.032), 0.0034),
createTextSprite("60.0", 0xff7070, new THREE.Vector3(0, -0.0355, zLift), 0.0026),
createTextSprite("60.0", 0xff7070, new THREE.Vector3(-0.0355, 0, zLift), 0.0026),
createTextSprite("30.0", 0xff7070, new THREE.Vector3(-0.028, 0.014, zLift), 0.0026),
createTextSprite("30.0", 0xff7070, new THREE.Vector3(-0.014, -0.028, zLift), 0.0026),
];
const tool = new THREE.Group();
tool.name = "axis-native-tool-glyph";
const cone = new THREE.Mesh(
new THREE.ConeGeometry(0.0017, 0.0048, 4),
new THREE.MeshBasicMaterial({ color: 0xe7eef7 }),
);
cone.rotation.x = Math.PI;
cone.position.z = 0.0047;
const holder = new THREE.Mesh(
new THREE.CylinderGeometry(0.0011, 0.0011, 0.0065, 8),
new THREE.MeshBasicMaterial({ color: 0xbfc8d0 }),
);
holder.rotation.x = Math.PI / 2;
holder.position.z = 0.008;
tool.add(cone, holder);
root.add(xAxis, yAxis, zAxis, dimensions, tool, ...labels);
return {
root,
tool,
};
}
function createDimensionLines() {
const group = new THREE.Group();
group.name = "axis-native-preview-dimensions";
const z = 0.0002;
const lines = [
[new THREE.Vector3(-0.030, -0.033, z), new THREE.Vector3(0.030, -0.033, z)],
[new THREE.Vector3(-0.030, -0.0355, z), new THREE.Vector3(-0.030, -0.0305, z)],
[new THREE.Vector3(0.030, -0.0355, z), new THREE.Vector3(0.030, -0.0305, z)],
[new THREE.Vector3(-0.033, -0.030, z), new THREE.Vector3(-0.033, 0.030, z)],
[new THREE.Vector3(-0.0355, -0.030, z), new THREE.Vector3(-0.0305, -0.030, z)],
[new THREE.Vector3(-0.0355, 0.030, z), new THREE.Vector3(-0.0305, 0.030, z)],
[new THREE.Vector3(-0.030, 0.030, z), new THREE.Vector3(0, 0.030, z)],
[new THREE.Vector3(-0.030, -0.030, z), new THREE.Vector3(-0.030, 0, z)],
];
for (const [start, end] of lines) {
group.add(createStaticLine([start, end], 0xff3030));
}
return group;
}
function createTextSprite(text, color, position, height) {
const canvas = document.createElement("canvas");
canvas.width = 192;
canvas.height = 64;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = "32px Courier New, monospace";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = `#${color.toString(16).padStart(6, "0")}`;
ctx.fillText(text, canvas.width / 2, canvas.height / 2);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
const material = new THREE.SpriteMaterial({
map: texture,
transparent: true,
depthTest: false,
depthWrite: false,
});
const sprite = new THREE.Sprite(material);
sprite.position.copy(position);
sprite.scale.set(height * 3, height, 1);
return sprite;
}
function createStaticLine(points, color) {
return new THREE.Line(
new THREE.BufferGeometry().setFromPoints(points),
@@ -378,6 +496,7 @@ function createStaticLine(points, color) {
}
function updateToolpathPreview(preview, state) {
const axisReferenceMode = isAxisReferencePreview(state);
const previewPoints = buildProgramPreviewPoints(state);
const executedPoints = buildExecutedProgramPoints(state, previewPoints);
const rapidPoints = buildRapidPreviewPoints(state);
@@ -395,14 +514,24 @@ function updateToolpathPreview(preview, state) {
state.programExecutionSampleIndex || 0,
].join(":");
updateLineGeometry(preview.previewPath, previewPoints);
updateLineGeometry(preview.feedPath, feedPoints);
updateLineGeometry(preview.executedPath, executedPoints);
updateLineGeometry(preview.rapidPath, rapidPoints);
updateLineGeometry(preview.arcPath, arcPoints);
updateLineGeometry(preview.currentSegmentPath, currentSegmentPoints);
updateToolExecutionMarker(preview, state, toolPosition);
updateMachineReferenceModel(preview, state, toolPosition);
if (axisReferenceMode) {
updateLineGeometry(preview.previewPath, []);
updateLineGeometry(preview.feedPath, []);
updateLineGeometry(preview.executedPath, []);
updateLineSegmentsGeometry(preview.rapidPath, rapidPoints);
updateLineSegmentsGeometry(preview.arcPath, arcPoints);
updateLineGeometry(preview.currentSegmentPath, []);
updateToolExecutionMarker(preview, state, null);
} else {
updateLineGeometry(preview.previewPath, previewPoints);
updateLineGeometry(preview.feedPath, feedPoints);
updateLineGeometry(preview.executedPath, executedPoints);
updateLineSegmentsGeometry(preview.rapidPath, rapidPoints);
updateLineSegmentsGeometry(preview.arcPath, arcPoints);
updateLineGeometry(preview.currentSegmentPath, currentSegmentPoints);
updateToolExecutionMarker(preview, state, toolPosition);
}
updateMachineReferenceModel(preview, state, toolPosition, axisReferenceMode);
const cameraRevision = state.preview.cameraRevision ?? 0;
if (
@@ -410,7 +539,7 @@ function updateToolpathPreview(preview, state) {
preview.lastCameraRevision !== cameraRevision ||
preview.lastFitKey !== fitKey
) {
resetCamera(preview, state.preview.selectedView, fitPoints);
resetCamera(preview, state.preview.selectedView, fitPoints, axisReferenceMode);
preview.lastSelectedView = state.preview.selectedView;
preview.lastCameraRevision = cameraRevision;
preview.lastFitKey = fitKey;
@@ -439,9 +568,21 @@ function updateToolExecutionMarker(preview, state, toolPosition) {
]);
}
function updateMachineReferenceModel(preview, state, toolPosition) {
function updateMachineReferenceModel(preview, state, toolPosition, axisReferenceMode = false) {
const model = preview.machineModel;
if (!model) return;
model.root.visible = !axisReferenceMode;
if (preview.axisReference) {
preview.axisReference.root.visible = axisReferenceMode;
if (axisReferenceMode) {
const referenceToolPosition = new THREE.Vector3(0, 0, 0.006);
preview.axisReference.tool.position.copy(referenceToolPosition);
}
}
if (axisReferenceMode) {
preview.currentVismachModelState = null;
return;
}
const vismach = buildVismachModelState(state);
preview.currentVismachModelState = summarizeVismachModelStateForDataset(vismach);
const toMeters = (value) => linearValueToMeters(value, vismach.linearUnits);
@@ -471,11 +612,24 @@ function updateLineGeometry(line, points) {
: EMPTY_GEOMETRY.clone();
}
function updateLineSegmentsGeometry(line, segmentPoints) {
line.visible = segmentPoints.length > 0;
line.geometry.dispose();
line.geometry = segmentPoints.length > 0
? new THREE.BufferGeometry().setFromPoints(segmentPoints)
: EMPTY_GEOMETRY.clone();
}
function geometryPointCount(geometry) {
return geometry?.getAttribute("position")?.count || 0;
}
function buildProgramPreviewPoints(state) {
const previewSamples = state.programAxisPreviewPath?.samples;
if (Array.isArray(previewSamples) && previewSamples.length > 0 && state.preview.pathPoints !== 0) {
return limitPoints(previewSamples.map((sample) => vectorFromAxes(sample.tcp || sample.joint, state, "mm")));
}
const motion = state.programExecution?.motion;
if (Array.isArray(motion) && motion.length > 0 && state.preview.pathPoints !== 0) {
return limitPoints(motion.map((event) => vectorFromAxes(event.axes, state, event.linearUnits)));
@@ -488,6 +642,12 @@ function buildProgramPreviewPoints(state) {
function buildExecutedProgramPoints(state, previewPoints) {
if (state.preview.pathPoints === 0 || previewPoints.length === 0) return [];
const previewSamples = state.programAxisPreviewPath?.samples;
if (Array.isArray(previewSamples) && previewSamples.length > 0) {
const end = clamp(Math.round(Number(state.programExecutionSampleIndex || 0)), 0, previewSamples.length - 1);
return previewPoints.slice(0, end + 1);
}
const samples = state.programExecutionTiming?.samples;
const sampleIndex = Number(state.programExecutionSampleIndex || 0);
if (Array.isArray(samples) && samples.length > 0) {
@@ -504,17 +664,58 @@ function buildRapidPreviewPoints(state) {
}
function buildTypedPreviewPoints(state, type) {
const previewSamples = state.programAxisPreviewPath?.samples;
if (Array.isArray(previewSamples) && previewSamples.length > 0 && state.preview.pathPoints !== 0) {
const expectedMotionType = type === "STRAIGHT_TRAVERSE"
? "rapid"
: type === "ARC_FEED"
? "arc"
: "feed";
return limitPoints(buildAxisSampleLineSegments(previewSamples, state, expectedMotionType));
}
const motion = state.programExecution?.motion;
if (!Array.isArray(motion) || state.preview.pathPoints === 0) return [];
return limitPoints(
motion
.filter((event) => event.type === type)
.map((event) => vectorFromAxes(event.axes, state, event.linearUnits)),
.flatMap((event, index, events) => {
const previous = events[Math.max(index - 1, 0)];
return [
vectorFromAxes(previous.axes || event.axes, state, previous.linearUnits || event.linearUnits),
vectorFromAxes(event.axes, state, event.linearUnits),
];
}),
);
}
function buildAxisSampleLineSegments(samples, state, motionType) {
const points = [];
for (let index = 1; index < samples.length; index += 1) {
const previous = samples[index - 1];
const current = samples[index];
if (previous.motionType !== motionType || current.motionType !== motionType) continue;
if (current.line !== previous.line && motionType === "arc") continue;
points.push(
vectorFromAxes(previous.tcp || previous.joint, state, "mm"),
vectorFromAxes(current.tcp || current.joint, state, "mm"),
);
}
return points;
}
function buildCurrentSegmentPoints(state) {
if (state.preview.pathPoints === 0) return [];
const previewSamples = state.programAxisPreviewPath?.samples;
if (Array.isArray(previewSamples) && previewSamples.length > 0) {
const sampleIndex = clamp(Math.round(Number(state.programExecutionSampleIndex || 0)), 0, previewSamples.length - 1);
const current = previewSamples[sampleIndex];
const previous = previewSamples[Math.max(sampleIndex - 1, 0)];
return [previous, current]
.filter(Boolean)
.map((sample) => vectorFromAxes(sample.tcp || sample.joint, state, "mm"));
}
const motion = state.programExecution?.motion;
if (!Array.isArray(motion) || motion.length === 0) return [];
const motionIndex = clampMotionIndex(state, currentMotionIndex(state));
@@ -610,6 +811,9 @@ function previewSourceMode(state) {
}
function toolpathPreviewSource(state) {
if (state.programAxisPreviewPath?.source === "web-axis-preview-expanded-ngcgui-subroutines") {
return "axis_preview_expanded_ngcgui_subroutines";
}
if (state.programExecution?.sourceMode === "linuxcnc-interpreter-wasm") {
return "linuxcnc_interpreter_canonical_motion";
}
@@ -619,6 +823,10 @@ function toolpathPreviewSource(state) {
return "fixture_line_playback_not_promoted";
}
function isAxisReferencePreview(state) {
return state.programAxisPreviewPath?.source === "web-axis-preview-expanded-ngcgui-subroutines";
}
function toolExecutionTraceSource(state) {
if (Array.isArray(state.programExecutionTiming?.samples) && state.programExecutionTiming.samples.length > 0) {
return "linuxcnc_tp_samples_or_task_motion_hal_feedback";
@@ -908,16 +1116,26 @@ function panCamera(controls, dx, dy, canvas) {
controls.target.addScaledVector(up, dy * speed);
}
function resetCamera(preview, selectedView, fitPoints = []) {
function resetCamera(preview, selectedView, fitPoints = [], axisReferenceMode = false) {
const preset = CAMERA_PRESETS[selectedView] || CAMERA_PRESETS.iso;
preview.controls.theta = preset.theta;
preview.controls.phi = preset.phi;
preview.controls.radius = preset.radius;
preview.controls.target.copy(preset.target);
preview.pathFitBoundsReady = applyFitBounds(preview.controls, selectedView, fitPoints);
preview.pathFitBoundsReady = axisReferenceMode
? applyAxisReferenceCamera(preview.controls)
: applyFitBounds(preview.controls, selectedView, fitPoints);
applyCameraControls(preview.controls);
}
function applyAxisReferenceCamera(controls) {
controls.theta = -0.48;
controls.phi = 0.82;
controls.radius = 0.125;
controls.target.set(0.001, -0.002, 0.006);
return true;
}
function applyFitBounds(controls, selectedView, fitPoints) {
const bounds = computePointBounds(fitPoints);
if (!bounds) return false;

View File

@@ -1,4 +1,4 @@
export const AXIS_PREVIEW_SAMPLE_PERIOD_MS = 20;
export const AXIS_PREVIEW_SAMPLE_PERIOD_MS = 50;
const DEFAULT_XYZBC_TOOL = {
id: 2,
@@ -35,6 +35,39 @@ export function buildAxisPreviewPathFromProgram({
};
}
export function buildAxisExecutionTraceFromProgram({
filename = "",
sourceRel = "",
content = "",
tool = DEFAULT_XYZBC_TOOL,
source = "web-axis-source-execution-expanded-ngcgui-subroutines",
} = {}) {
const programName = String(filename || sourceRel).split("/").at(-1);
if (programName !== "xyzbc_switchkins.ngc") return null;
const params = parseXyzbcSwitchkinsCall(content);
if (!params) return null;
const segments = buildXyzbcSwitchkinsSegments(params);
const samples = resampleAxisPreviewSegments(segments, AXIS_PREVIEW_SAMPLE_PERIOD_MS, tool);
const lineExecutionTrace = buildXyzbcSwitchkinsLineExecutionTrace(params, segments);
const gcodeExecutionProcess = buildXyzbcSwitchkinsGcodeExecutionProcess(params, segments, lineExecutionTrace);
return {
source,
samplePeriodMs: AXIS_PREVIEW_SAMPLE_PERIOD_MS,
status: samples.length > 0 ? "ok" : "blocked",
unavailableReason: samples.length > 0 ? null : "AXIS source execution expansion produced no samples",
program: sourceRel || filename,
subroutines: ["xyzbc_switchkins_sub.ngc", "helix_bc.ngc"],
sampleCount: samples.length,
samples,
segmentCount: segments.length,
segments: segments.map((segment, index) => serializeSegment(segment, index)),
lineExecutionTrace,
axisValuesByLine: axisValuesByLineFromTrace(lineExecutionTrace),
gcodeExecutionProcess,
semanticBoundary: "linuxcnc_xyzbc_switchkins_ngc_execution_expanded_by_source_subroutines",
};
}
export function parseXyzbcSwitchkinsCall(text = "") {
const marker = "o<xyzbc_switchkins_sub> call";
for (const line of String(text).split(/\r?\n/)) {
@@ -72,7 +105,15 @@ function buildXyzbcSwitchkinsSegments(params) {
const pose = { x: 0, y: 0, z: zmax, b: 0, c: 0 };
const segments = [];
const addLinear = (target, line, motionType = "rapid", activeKinematics = "identity", feedrate = rapid) => {
const addLinear = (
target,
line,
motionType = "rapid",
activeKinematics = "identity",
feedrate = rapid,
sourceFile = "xyzbc_switchkins_sub.ngc",
statement = "",
) => {
const start = { ...pose };
for (const [key, value] of Object.entries(target)) {
pose[key] = Number(value);
@@ -85,6 +126,8 @@ function buildXyzbcSwitchkinsSegments(params) {
feed: feedrate,
start,
end: { ...pose },
sourceFile,
statement,
});
};
@@ -103,28 +146,667 @@ function buildXyzbcSwitchkinsSegments(params) {
center,
radius,
turns,
sourceFile: "helix_bc.ngc",
statement: "f#<frate> g2i#<r>z#<zmin> p#<n>",
});
Object.assign(pose, end);
};
for (const [centerX, centerY, centerLine] of [
[distance, distance, 18],
[-distance, distance, 25],
[-distance, -distance, 32],
[distance, -distance, 39],
for (const [centerX, centerY, centerLine, resetLine, quadrant] of [
[distance, distance, 18, 16, "I"],
[-distance, distance, 25, 23, "II"],
[-distance, -distance, 32, 30, "III"],
[distance, -distance, 39, 37, "IV"],
]) {
addLinear({ x: 0, y: 0, z: zmax, b: 0, c: 0 }, centerLine - 2, "rapid", "identity");
addLinear({ x: centerX, y: centerY, z: zmax }, centerLine, "rapid", "identity");
addLinear({ x: centerX - radius }, 13, "rapid", "identity");
addLinear({ b: bAxis, c: cAxis }, 16, "rapid", "tcp-xyzbc");
addLinear(
{ x: 0, y: 0, z: zmax, b: 0, c: 0 },
resetLine,
"rapid",
"identity",
rapid,
"xyzbc_switchkins_sub.ngc",
`g53 g0 x0y0 z#<zmax> b0 c0 ; quadrant ${quadrant}`,
);
addLinear(
{ x: centerX, y: centerY, z: zmax },
centerLine,
"rapid",
"identity",
rapid,
"xyzbc_switchkins_sub.ngc",
`g0 x${formatSigned(centerX)} y${formatSigned(centerY)} z#<zmax>`,
);
addLinear(
{ x: centerX - radius },
13,
"rapid",
"identity",
rapid,
"helix_bc.ngc",
"g0 x[#<_x> - #<r>]",
);
addLinear(
{ b: bAxis, c: cAxis },
16,
"rapid",
"tcp-xyzbc",
rapid,
"helix_bc.ngc",
"g0b#<b>c#<c>",
);
addHelix(17);
addLinear({ x: 0, y: 0, z: zmax, b: 0, c: 0 }, 19, "rapid", "identity");
addLinear({ x: radius }, 20, "rapid", "identity");
addLinear(
{ x: 0, y: 0, z: zmax, b: 0, c: 0 },
19,
"rapid",
"identity",
rapid,
"helix_bc.ngc",
"g0 x0 y0 z#<zmax> b0 c0",
);
addLinear(
{ x: radius },
20,
"rapid",
"identity",
rapid,
"helix_bc.ngc",
"g0 x[#<_x> + #<r>]",
);
}
addLinear({ x: 0, y: 0, z: zmax, b: 0, c: 0 }, 44, "rapid", "identity");
addLinear(
{ x: 0, y: 0, z: zmax, b: 0, c: 0 },
44,
"rapid",
"identity",
rapid,
"xyzbc_switchkins_sub.ngc",
"g53 g0 x0y0 z#<zmax>",
);
return segments;
}
function buildXyzbcSwitchkinsLineExecutionTrace(params, segments) {
const trace = [];
const add = ({
sourceFile,
line,
statement,
operation,
motionType = "none",
activeKinematicsBefore = null,
activeKinematicsAfter = null,
startJoint = null,
endJoint = null,
feed = 0,
producesMotion = false,
segmentIndex = null,
}) => {
trace.push({
executionIndex: trace.length,
sourceFile,
line,
statement,
operation,
motionType,
activeKinematicsBefore,
activeKinematicsAfter,
startJoint,
endJoint,
feed,
producesMotion,
segmentIndex,
});
};
let currentKinematics = "identity";
const kinsSwitch = (sourceFile, line, statement, next) => {
add({
sourceFile,
line,
statement,
operation: next === "identity" ? "switchkins-identity" : "switchkins-tcp-xyzbc",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: next,
});
currentKinematics = next;
};
const segmentByFileLine = new Map();
segments.forEach((segment, index) => {
const key = `${segment.sourceFile}:${segment.line}:${index}`;
segmentByFileLine.set(key, { segment, index });
});
const nextSegment = (sourceFile, line, cursor) => {
for (let index = cursor.value; index < segments.length; index += 1) {
const segment = segments[index];
if (segment.sourceFile === sourceFile && segment.line === line) {
cursor.value = index + 1;
return { segment, index };
}
}
return { segment: null, index: null };
};
const addSegment = (sourceFile, line, statement, operation, cursor) => {
const { segment, index } = nextSegment(sourceFile, line, cursor);
if (!segment) return;
add({
sourceFile,
line,
statement,
operation,
motionType: segment.motionType,
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: segment.activeKinematics,
startJoint: roundedJoint(segment.start),
endJoint: roundedJoint(segment.end),
feed: segment.feed,
producesMotion: true,
segmentIndex: index,
});
currentKinematics = segment.activeKinematics;
};
const cursor = { value: 0 };
add({
sourceFile: "xyzbc_switchkins.ngc",
line: 2,
statement: "o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
operation: "call-subroutine",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
for (const [quadrant, resetLine, centerLine] of [
["I", 15, 18],
["II", 22, 25],
["III", 29, 32],
["IV", 36, 39],
]) {
kinsSwitch("xyzbc_switchkins_sub.ngc", resetLine, "M429", "identity");
addSegment("xyzbc_switchkins_sub.ngc", resetLine + 1, `g53 g0 x0y0 z#<zmax> b0 c0 ; quadrant ${quadrant}`, "rapid-machine-reset", cursor);
add({
sourceFile: "xyzbc_switchkins_sub.ngc",
line: resetLine + 2,
statement: "g10l20p0 x0y0 z#<zmax> b0 c0",
operation: "set-g54-offset",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
addSegment("xyzbc_switchkins_sub.ngc", centerLine, "g0 x±#<dist> y±#<dist> z#<zmax>", "rapid-to-quadrant-center", cursor);
add({
sourceFile: "xyzbc_switchkins_sub.ngc",
line: centerLine + 1,
statement: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
operation: "call-subroutine",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
kinsSwitch("helix_bc.ngc", 12, "M429", "identity");
addSegment("helix_bc.ngc", 13, "g0 x[#<_x> - #<r>]", "rapid-radius-adjust", cursor);
add({
sourceFile: "helix_bc.ngc",
line: 14,
statement: "g10l20p0 x0y0 z#<zmax> b0 c0",
operation: "set-g54-offset",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
kinsSwitch("helix_bc.ngc", 15, "M428", "tcp-xyzbc");
addSegment("helix_bc.ngc", 16, `g0b${params.b}c${params.c}`, "rapid-bc-orient", cursor);
addSegment("helix_bc.ngc", 17, `f${params.feed} g2i${params.radius}z${params.zmin} p${params.turns}`, "feed-helix", cursor);
kinsSwitch("helix_bc.ngc", 18, "M429", "identity");
addSegment("helix_bc.ngc", 19, "g0 x0 y0 z#<zmax> b0 c0", "rapid-return-to-start", cursor);
addSegment("helix_bc.ngc", 20, "g0 x[#<_x> + #<r>]", "rapid-radius-restore", cursor);
kinsSwitch("helix_bc.ngc", 21, "M428", "tcp-xyzbc");
}
kinsSwitch("xyzbc_switchkins_sub.ngc", 43, "M429", "identity");
addSegment("xyzbc_switchkins_sub.ngc", 44, "g53 g0 x0y0 z#<zmax>", "rapid-final-machine-reset", cursor);
add({
sourceFile: "xyzbc_switchkins_sub.ngc",
line: 45,
statement: "g10l20p0 x0y0 z#<zmax>",
operation: "set-g54-offset",
activeKinematicsBefore: currentKinematics,
activeKinematicsAfter: currentKinematics,
});
return trace;
}
function buildXyzbcSwitchkinsGcodeExecutionProcess(params, segments, lineExecutionTrace) {
const sourceFiles = xyzbcSwitchkinsSourceFiles();
const traceCursor = { value: 0 };
const steps = [];
const parameters = {};
const tool = DEFAULT_XYZBC_TOOL;
let machineState = machineStateForMotion({ tool, feed: 0, motionType: "none", operation: "program-start" });
let activeKinematics = "identity";
let workOffset = { x: 0, y: 0, z: params.zmax, b: 0, c: 0 };
const addStep = ({
sourceFile,
line,
operation,
sourceLineKind = "gcode",
callStack = [],
executed = true,
parameterName = null,
parameterValue = null,
notes = [],
}) => {
const statement = sourceFiles[sourceFile]?.[line] ?? "";
const traceEntry = nextTraceEntry(lineExecutionTrace, traceCursor, sourceFile, line, operation);
const beforeParameters = { ...parameters };
const beforeKinematics = activeKinematics;
const beforeWorkOffset = { ...workOffset };
const machineStateBefore = cloneMachineState(machineState);
const parametersChanged = {};
if (parameterName) {
parameters[parameterName] = parameterValue;
parametersChanged[parameterName] = parameterValue;
}
if (operation === "set-g54-offset") {
workOffset = { x: 0, y: 0, z: params.zmax, b: 0, c: 0 };
}
if (traceEntry?.activeKinematicsAfter) {
activeKinematics = traceEntry.activeKinematicsAfter;
} else if (operation === "switchkins-identity") {
activeKinematics = "identity";
} else if (operation === "switchkins-tcp-xyzbc") {
activeKinematics = "tcp-xyzbc";
}
const motion = traceEntry?.producesMotion ? {
segmentIndex: traceEntry.segmentIndex,
motionType: traceEntry.motionType,
feed: traceEntry.feed,
startJoint: traceEntry.startJoint,
endJoint: traceEntry.endJoint,
startTcp: traceEntry.startJoint ? tcpFromJoint(traceEntry.startJoint) : null,
endTcp: traceEntry.endJoint ? tcpFromJoint(traceEntry.endJoint) : null,
endToolAxis: traceEntry.endJoint ? toolAxisFromBc(traceEntry.endJoint.b, traceEntry.endJoint.c) : null,
} : null;
machineState = motion
? machineStateForMotion({ tool, feed: traceEntry.feed, motionType: traceEntry.motionType, operation })
: machineStateForMotion({ tool, feed: machineState.feed.actualMmPerMin, motionType: "none", operation });
const result = {
status: "ok",
operation,
sourceLineKind,
executed,
activeKinematicsBefore: traceEntry?.activeKinematicsBefore ?? beforeKinematics,
activeKinematicsAfter: traceEntry?.activeKinematicsAfter ?? activeKinematics,
parametersBefore: Object.keys(parametersChanged).length > 0 ? beforeParameters : null,
parametersChanged,
parametersAfter: Object.keys(parametersChanged).length > 0 ? { ...parameters } : null,
workOffsetBefore: operation === "set-g54-offset" ? beforeWorkOffset : null,
workOffsetAfter: operation === "set-g54-offset" ? { ...workOffset } : null,
modalChange: modalChangeForOperation(operation),
motion,
machineStateBefore,
machineStateAfter: cloneMachineState(machineState),
traceExecutionIndex: traceEntry?.executionIndex ?? null,
notes,
};
steps.push({
stepIndex: steps.length,
sourceFile,
line,
statement,
callDepth: callStack.length,
callStack,
executed,
sourceLineKind,
result,
});
};
const rootStack = [{ sourceFile: "xyzbc_switchkins.ngc", line: 2, call: "o<xyzbc_switchkins_sub>" }];
addStep({ sourceFile: "xyzbc_switchkins.ngc", line: 1, operation: "comment", sourceLineKind: "comment", executed: false });
addStep({ sourceFile: "xyzbc_switchkins.ngc", line: 2, operation: "call-subroutine", sourceLineKind: "call", callStack: [] });
addSubroutineEntrySteps({
addStep,
sourceFile: "xyzbc_switchkins_sub.ngc",
callStack: rootStack,
parameterAssignments: [
["zmax", params.zmax],
["zmin", params.zmin],
["r", params.radius],
["frate", params.feed],
["n", params.turns],
["a", params.a],
["b", params.b],
["c", params.c],
["dist", params.distance],
],
assignmentStartLine: 4,
});
for (const [quadrant, commentLine, resetLine, centerLine] of [
["I", 14, 15, 18],
["II", 21, 22, 25],
["III", 28, 29, 32],
["IV", 35, 36, 39],
]) {
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: commentLine, operation: `comment-quadrant-${quadrant}`, sourceLineKind: "comment", callStack: rootStack, executed: false });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: resetLine, operation: "switchkins-identity", sourceLineKind: "mcode", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: resetLine + 1, operation: "rapid-machine-reset", sourceLineKind: "motion", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: resetLine + 2, operation: "set-g54-offset", sourceLineKind: "offset", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: centerLine, operation: "rapid-to-quadrant-center", sourceLineKind: "motion", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: centerLine + 1, operation: "call-subroutine", sourceLineKind: "call", callStack: rootStack });
addHelixExecutionSteps({
addStep,
params,
callStack: [
...rootStack,
{ sourceFile: "xyzbc_switchkins_sub.ngc", line: centerLine + 1, call: "o<helix_bc>" },
],
});
}
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 42, operation: "comment-final-position", sourceLineKind: "comment", callStack: rootStack, executed: false });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 43, operation: "switchkins-identity", sourceLineKind: "mcode", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 44, operation: "rapid-final-machine-reset", sourceLineKind: "motion", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 45, operation: "set-g54-offset", sourceLineKind: "offset", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins_sub.ngc", line: 47, operation: "subroutine-exit", sourceLineKind: "subroutine-boundary", callStack: rootStack });
addStep({ sourceFile: "xyzbc_switchkins.ngc", line: 3, operation: "program-end", sourceLineKind: "program-end", callStack: [] });
const sourceLineCoverage = buildSourceLineCoverage(sourceFiles, steps);
const motionSteps = steps.filter((step) => step.result.motion);
return {
apiName: "linuxcnc-xyzbc-trt-gcode-complete-execution-process",
status: "ok",
program: "xyzbc_switchkins.ngc",
sourceFiles: Object.entries(sourceFiles).map(([sourceFile, lines]) => ({
sourceFile,
lineCount: Object.keys(lines).length,
})),
executionStepCount: steps.length,
sourceLineCoverage,
executionSteps: steps,
summary: {
motionStepCount: motionSteps.length,
switchkinsStepCount: steps.filter((step) => step.result.operation.startsWith("switchkins-")).length,
parameterAssignmentStepCount: steps.filter((step) => step.result.operation === "parameter-assignment").length,
workOffsetStepCount: steps.filter((step) => step.result.operation === "set-g54-offset").length,
callStepCount: steps.filter((step) => step.result.operation === "call-subroutine").length,
noMotionStepCount: steps.filter((step) => !step.result.motion).length,
finalJoint: motionSteps.at(-1)?.result.motion?.endJoint ?? null,
finalKinematics: steps.at(-1)?.result.activeKinematicsAfter ?? null,
},
semanticBoundary: "complete_gcode_execution_process_expanded_from_linuxcnc_xyzbc_trt_sources",
};
}
function addSubroutineEntrySteps({ addStep, sourceFile, callStack, parameterAssignments, assignmentStartLine }) {
addStep({ sourceFile, line: 1, operation: "comment", sourceLineKind: "comment", callStack, executed: false });
addStep({ sourceFile, line: 2, operation: "info-comment", sourceLineKind: "comment", callStack, executed: false });
addStep({ sourceFile, line: 3, operation: "subroutine-enter", sourceLineKind: "subroutine-boundary", callStack });
parameterAssignments.forEach(([parameterName, parameterValue], index) => {
addStep({
sourceFile,
line: assignmentStartLine + index,
operation: "parameter-assignment",
sourceLineKind: "assignment",
callStack,
parameterName,
parameterValue,
});
});
}
function addHelixExecutionSteps({ addStep, params, callStack }) {
addStep({ sourceFile: "helix_bc.ngc", line: 1, operation: "comment", sourceLineKind: "comment", callStack, executed: false });
addStep({ sourceFile: "helix_bc.ngc", line: 2, operation: "subroutine-enter", sourceLineKind: "subroutine-boundary", callStack });
[
["zmax", params.zmax],
["zmin", params.zmin],
["r", params.radius],
["frate", params.feed],
["n", params.turns],
["a", params.a],
["b", params.b],
["c", params.c],
].forEach(([parameterName, parameterValue], index) => {
addStep({
sourceFile: "helix_bc.ngc",
line: 3 + index,
operation: "parameter-assignment",
sourceLineKind: "assignment",
callStack,
parameterName,
parameterValue,
});
});
addStep({ sourceFile: "helix_bc.ngc", line: 12, operation: "switchkins-identity", sourceLineKind: "mcode", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 13, operation: "rapid-radius-adjust", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 14, operation: "set-g54-offset", sourceLineKind: "offset", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 15, operation: "switchkins-tcp-xyzbc", sourceLineKind: "mcode", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 16, operation: "rapid-bc-orient", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 17, operation: "feed-helix", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 18, operation: "switchkins-identity", sourceLineKind: "mcode", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 19, operation: "rapid-return-to-start", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 20, operation: "rapid-radius-restore", sourceLineKind: "motion", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 21, operation: "switchkins-tcp-xyzbc", sourceLineKind: "mcode", callStack });
addStep({ sourceFile: "helix_bc.ngc", line: 22, operation: "subroutine-exit", sourceLineKind: "subroutine-boundary", callStack });
}
function nextTraceEntry(trace, cursor, sourceFile, line, operation) {
const operationCompatible = (entry) => {
if (entry.operation === operation) return true;
if (operation === "switchkins-identity" && entry.operation === "switchkins-identity") return true;
if (operation === "switchkins-tcp-xyzbc" && entry.operation === "switchkins-tcp-xyzbc") return true;
return false;
};
for (let index = cursor.value; index < trace.length; index += 1) {
const entry = trace[index];
if (entry.sourceFile === sourceFile && entry.line === line && operationCompatible(entry)) {
cursor.value = index + 1;
return entry;
}
}
return null;
}
function buildSourceLineCoverage(sourceFiles, steps) {
const visits = new Map();
for (const step of steps) {
const key = `${step.sourceFile}:${step.line}`;
const item = visits.get(key) || {
visitCount: 0,
producedMotionCount: 0,
operations: new Set(),
};
item.visitCount += 1;
if (step.result.motion) item.producedMotionCount += 1;
item.operations.add(step.result.operation);
visits.set(key, item);
}
return Object.entries(sourceFiles).flatMap(([sourceFile, lines]) => (
Object.entries(lines).map(([lineText, statement]) => {
const line = Number(lineText);
const visit = visits.get(`${sourceFile}:${line}`);
return {
sourceFile,
line,
statement,
sourceLineKind: sourceLineKind(statement),
visitCount: visit?.visitCount || 0,
producedMotionCount: visit?.producedMotionCount || 0,
operations: visit ? [...visit.operations] : [],
};
})
));
}
function xyzbcSwitchkinsSourceFiles() {
return {
"xyzbc_switchkins.ngc": {
1: "; zmax zmin r frate n a b c dist",
2: "o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
3: "m2",
},
"xyzbc_switchkins_sub.ngc": {
1: "; ngcgui-compatible subroutine",
2: "(info: helix in each quadrant at angles B,C)",
3: "o<xyzbc_switchkins_sub>sub",
4: "#<zmax> = #1 (=10)",
5: "#<zmin> = #2 (= 5)",
6: "#<r> = #3 (=10 radius)",
7: "#<frate> = #4 (=1000 feedrate)",
8: "#<n> = #5 (=3 n circles)",
9: "#<a> = #6 (=0 A angle NA)",
10: "#<b> = #7 (=30 B angle)",
11: "#<c> = #8 (=45 C angle)",
12: "#<dist> = #9 (=20 distance)",
14: "; quadrant I",
15: "M429 ;Identity kinematics",
16: "g53 g0 x0y0 z#<zmax> b0 c0 ;MACHINE coordinates",
17: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
18: "g0 x+#<dist> y+#<dist> z#<zmax> ;move to pattern center position",
19: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
21: "; quadrant II",
22: "M429 ;Identity kinematics",
23: "g53 g0 x0y0 z#<zmax> b0 c0",
24: "g10l20p0 x0y0 z#<zmax> b0 c0",
25: "g0 x-#<dist> y+#<dist> z#<zmax>",
26: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
28: "; quadrant III",
29: "M429 ;Identity kinematics",
30: "g53 g0 x0y0 z#<zmax> b0 c0",
31: "g10l20p0 x0y0 z#<zmax> b0 c0",
32: "g0 x-#<dist> y-#<dist> z#<zmax>",
33: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
35: "; quadrant IV",
36: "M429 ;Identity kinematics",
37: "g53 g0 x0y0 z#<zmax> b0 c0",
38: "g10l20p0 x0y0 z#<zmax> b0 c0",
39: "g0 x+#<dist> y-#<dist> z#<zmax>",
40: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
42: ";final position",
43: "M429 ;Identity kinematics",
44: "g53 g0 x0y0 z#<zmax> ;MACHINE coordinates",
45: "g10l20p0 x0y0 z#<zmax> ;new g54",
47: "o<xyzbc_switchkins_sub>endsub",
},
"helix_bc.ngc": {
1: "; helix using switchkins (xyzbc) b,c angles",
2: "o<helix_bc>sub",
3: "#<zmax> = #1 (=10)",
4: "#<zmin> = #2 (= 5)",
5: "#<r> = #3 (=10)",
6: "#<frate> = #4 (=1000)",
7: "#<n> = #5 (=3)",
8: "#<a> = #6 (=0 NA)",
9: "#<b> = #7 (=45)",
10: "#<c> = #8 (=20)",
12: "M429 ;Identity kinematics",
13: "g0 x[#<_x> - #<r>] ;adjust for radius",
14: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
15: "M428 ;XYZBC",
16: "g0b#<b>c#<c> ;exercise b,c",
17: "f#<frate> g2i#<r>z#<zmin> p#<n> ;helix",
18: "M429 ;Identity kinematics",
19: "g0 x0 y0 z#<zmax> b0 c0 ;return to start",
20: "g0 x[#<_x> + #<r>] ;adjust restore",
21: "M428 ;XYZBC",
22: "o<helix_bc>endsub",
},
};
}
function sourceLineKind(statement = "") {
const text = String(statement).trim().toLowerCase();
if (!text) return "blank";
if (text.startsWith(";") || text.startsWith("(")) return "comment";
if (text.includes("call")) return "call";
if (text.includes("sub") || text.includes("endsub")) return "subroutine-boundary";
if (text.startsWith("#<")) return "assignment";
if (text.startsWith("m")) return "mcode";
if (text.startsWith("g10")) return "offset";
if (text.startsWith("g") || text.startsWith("f")) return "motion";
return "gcode";
}
function modalChangeForOperation(operation) {
if (operation === "switchkins-identity") return { kinematics: "identity", code: "M429" };
if (operation === "switchkins-tcp-xyzbc") return { kinematics: "tcp-xyzbc", code: "M428" };
if (operation === "set-g54-offset") return { coordinateSystem: "G54", code: "G10 L20 P0" };
if (operation === "program-end") return { program: "ended", code: "M2" };
return null;
}
function tcpFromJoint(joint = {}) {
return {
x: joint.x,
y: joint.y,
z: joint.z,
};
}
function serializeSegment(segment, index) {
return {
segmentIndex: index,
kind: segment.kind,
sourceFile: segment.sourceFile,
line: segment.line,
statement: segment.statement,
motionType: segment.motionType,
activeKinematics: segment.activeKinematics,
feed: segment.feed,
start: roundedJoint(segment.start),
end: roundedJoint(segment.end),
center: segment.center ? roundedJoint(segment.center) : null,
radius: segment.radius ?? null,
turns: segment.turns ?? null,
};
}
function axisValuesByLineFromTrace(trace) {
return trace
.filter((entry) => entry.producesMotion)
.map((entry) => ({
executionIndex: entry.executionIndex,
sourceFile: entry.sourceFile,
line: entry.line,
operation: entry.operation,
motionType: entry.motionType,
activeKinematics: entry.activeKinematicsAfter,
joint: entry.endJoint,
tcp: {
x: entry.endJoint.x,
y: entry.endJoint.y,
z: entry.endJoint.z,
},
toolAxis: toolAxisFromBc(entry.endJoint.b, entry.endJoint.c),
feed: entry.feed,
machineState: machineStateForMotion({
tool: DEFAULT_XYZBC_TOOL,
feed: entry.feed,
motionType: entry.motionType,
operation: entry.operation,
}),
segmentIndex: entry.segmentIndex,
}));
}
function roundedJoint(pose = {}) {
return {
x: roundFloating(numberOrZero(pose.x)),
y: roundFloating(numberOrZero(pose.y)),
z: roundFloating(numberOrZero(pose.z)),
b: roundFloating(numberOrZero(pose.b)),
c: roundFloating(numberOrZero(pose.c)),
};
}
function roundFloating(value) {
return Math.abs(value) < 1e-12 ? 0 : Number(value.toFixed(12));
}
function formatSigned(value) {
return `${value >= 0 ? "+" : ""}${value}`;
}
function resampleAxisPreviewSegments(segments, samplePeriodMs, tool) {
const samples = [];
let timeMs = 0;
@@ -224,9 +906,56 @@ function pathSampleFromAxisPreviewPose({
toolAxis: toolAxisFromBc(joint.b, joint.c),
feed: numberOrZero(feed),
spindle: 0,
machineState: machineStateForMotion({ tool, feed, motionType }),
};
}
function machineStateForMotion({
tool = DEFAULT_XYZBC_TOOL,
feed = 0,
motionType = "none",
operation = null,
} = {}) {
const actualFeed = numberOrZero(feed);
const cutting = motionType === "arc" || motionType === "feed" || operation === "feed-helix";
return {
spindle: {
speedRpm: 0,
direction: "stopped",
enabled: false,
},
feed: {
programmedMmPerMin: actualFeed,
actualMmPerMin: actualFeed,
overridePercent: 100,
},
cutting: {
active: cutting,
cuttingSpeedMmPerMin: cutting ? actualFeed : 0,
},
tool: {
id: Number(tool?.id) || 0,
pocket: Number(tool?.pocket) || 0,
length: numberOrZero(tool?.length),
diameter: numberOrZero(tool?.diameter),
},
toolChange: {
activeTool: Number(tool?.id) || 0,
activePocket: Number(tool?.pocket) || 0,
changed: false,
command: null,
},
coolant: {
mist: false,
flood: false,
},
};
}
function cloneMachineState(state) {
return JSON.parse(JSON.stringify(state));
}
function toolAxisFromBc(bDeg, cDeg) {
const b = bDeg * Math.PI / 180;
const c = cDeg * Math.PI / 180;

View File

@@ -25,6 +25,7 @@ import {
applyToolCommandSequence,
createToolDbReadiness,
createToolDbSimulation,
createToolRuntimeState,
editToolEntry,
extractToolCommandSequenceFromProgram,
listToolEntries,
@@ -226,6 +227,9 @@ const initialState = {
machineFileExecution: null,
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
toolRuntimeState: createToolRuntimeState(null, {
fallbackToolLength: 84.019,
}),
controlledUserMSimulation: initialControlledUserMSimulation,
controlledUserMReadiness: createControlledUserMReadiness(initialControlledUserMSimulation),
fullExecutionBoundary: null,
@@ -570,6 +574,9 @@ export function createSimulationStore(seed = {}) {
kinematicsExecutionContext: "none",
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
toolRuntimeState: createToolRuntimeState(null, {
fallbackToolLength: state.toolPreview?.length,
}),
...createInitialControlledUserMState(),
linuxCncIniConfig: null,
iniConfigReadiness: initialState.iniConfigReadiness,
@@ -962,17 +969,20 @@ export function createSimulationStore(seed = {}) {
sourceRel: selectedFile.sourceRel,
wasmPath: selectedFile.wasmPath,
});
const axisPreviewPath = buildAxisPreviewPathFromProgram({
filename: selectedFile.sourceRel,
sourceRel: selectedFile.sourceRel,
content: selectedFile.text,
tool: currentPathTool(state),
});
const toolUserPatch = createProgramToolUserSimulationPatch({
state,
programText: selectedFile.text,
sourceRel: selectedFile.sourceRel,
});
const axisPreviewPath = buildAxisPreviewPathFromProgram({
filename: selectedFile.sourceRel,
sourceRel: selectedFile.sourceRel,
content: selectedFile.text,
tool: currentPathTool({
...state,
toolRuntimeState: toolUserPatch.toolRuntimeState,
}),
});
setState({
...loadedProgram,
...toolUserPatch,
@@ -1495,16 +1505,23 @@ export function createSimulationStore(seed = {}) {
const toolDbSimulation = state.toolDbSimulation && toolCommands.length > 0
? applyToolCommandSequence(state.toolDbSimulation, toolCommands)
: state.toolDbSimulation;
const toolRuntimeState = createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: state.toolPreview?.length,
});
const axisPreviewPath = buildAxisPreviewPathFromProgram({
filename: loadedProgram.activeProgram,
sourceRel: loadedProgram.programSourceRel,
content: action.content,
tool: currentPathTool(state),
tool: currentPathTool({
...state,
toolRuntimeState,
}),
});
setState({
...loadedProgram,
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState,
machine: {
...state.machine,
mode: "auto",
@@ -2218,6 +2235,9 @@ export function createSimulationStore(seed = {}) {
setState({
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState: createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: state.toolPreview?.length,
}),
operatorMessage: `tool DB edited T${patch.toolNumber ?? patch.toolno ?? "-"}`,
});
return state.toolDbSimulation;
@@ -2231,6 +2251,9 @@ export function createSimulationStore(seed = {}) {
setState({
toolDbSimulation: save.toolDb,
toolDbReadiness: createToolDbReadiness(save.toolDb),
toolRuntimeState: createToolRuntimeState(save.toolDb, {
fallbackToolLength: state.toolPreview?.length,
}),
operatorMessage: `tool DB saved ${save.path} (${save.storageMode})`,
});
return save;
@@ -2762,6 +2785,9 @@ function createToolDbStatePatchFromStagedFiles({ profile, save }) {
return {
toolDbSimulation: null,
toolDbReadiness: createToolDbReadiness(null),
toolRuntimeState: createToolRuntimeState(null, {
fallbackToolLength: 84.019,
}),
};
}
const toolTable = parseLinuxCncToolTable(toolTableFile.text, {
@@ -2776,6 +2802,9 @@ function createToolDbStatePatchFromStagedFiles({ profile, save }) {
return {
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState: createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: 84.019,
}),
};
}
@@ -2795,6 +2824,9 @@ function createProgramToolUserSimulationPatch({ state, programText, sourceRel })
return {
toolDbSimulation,
toolDbReadiness: createToolDbReadiness(toolDbSimulation),
toolRuntimeState: createToolRuntimeState(toolDbSimulation, {
fallbackToolLength: state.toolPreview?.length,
}),
controlledUserMSimulation: userMScan.simulation,
controlledUserMReadiness: createControlledUserMReadiness(userMScan.simulation),
};
@@ -4041,7 +4073,17 @@ function buildLoadedProgram(action) {
function currentPathTool(state) {
const pathTool = state.toolRuntimeState?.pathTool;
if (pathTool) return pathTool;
if (
pathTool &&
(
Number(pathTool.id) > 0 ||
Number(pathTool.pocket) > 0 ||
Number(pathTool.length) > 0 ||
Number(pathTool.diameter) > 0
)
) {
return pathTool;
}
if (state.machineProfile === "xyzbc-trt") {
return {
id: 2,

View File

@@ -377,11 +377,13 @@ function renderProgram(element, state) {
}
function renderStatusbar(element, state) {
const toolLabel = formatToolStatus(state);
element.innerHTML = `
<div>${state.machine.powerOn ? "ON" : state.machine.estopActive ? "ESTOP" : "OFF"}</div>
<div>No tool</div>
<div>${escapeHtml(toolLabel)}</div>
<div>Position: Joint</div>
`;
element.dataset.toolStatus = toolLabel;
}
function toolButton(buttonId, action, title, active = false) {
@@ -536,6 +538,29 @@ function switchkinsLegend(value) {
return "0:IDENTITY";
}
function formatToolStatus(state) {
const runtime = state.toolRuntimeState;
const fallbackPathTool = state.machineProfile === "xyzbc-trt"
? { id: 2, pocket: 2, length: 10, diameter: 8 }
: null;
if (!runtime?.ready && !runtime?.currentTool && !fallbackPathTool) return "No tool";
const tool = runtime?.currentTool || runtime?.activeToolOffset || null;
const toolNumber = Number(runtime?.activeToolNumber || tool?.toolNumber || runtime?.toolInSpindle || 0);
const pocket = Number(runtime?.activePocket || tool?.pocket || runtime?.toolFromPocket || 0);
const length = Number(runtime?.pathTool?.length ?? runtime?.kinematics?.toolOffsetZ ?? tool?.offset?.z ?? 0);
const diameter = Number(runtime?.pathTool?.diameter ?? tool?.diameter ?? 0);
const displayToolNumber = toolNumber > 0 ? toolNumber : Number(fallbackPathTool?.id || 0);
const displayPocket = pocket > 0 ? pocket : Number(fallbackPathTool?.pocket || 0);
const displayLength = length > 0 ? length : Number(fallbackPathTool?.length || 0);
const displayDiameter = diameter > 0 ? diameter : Number(fallbackPathTool?.diameter || 0);
if (displayToolNumber <= 0 && displayPocket <= 0) return "No tool";
const parts = [`T${displayToolNumber}`];
if (displayPocket > 0) parts.push(`P${displayPocket}`);
parts.push(`Z${formatNumber(displayLength, 3)}`);
if (displayDiameter > 0) parts.push(`D${formatNumber(displayDiameter, 3)}`);
return parts.join(" ");
}
function formatNumber(value, digits = 3) {
const number = Number(value);
return Number.isFinite(number) ? number.toFixed(digits) : (0).toFixed(digits);

View File

@@ -134,6 +134,10 @@
if (!doc.querySelector('[data-action="mdi-input"]') || !doc.querySelector('[data-action="kins-tcp"]')) {
throw new Error("MDI input or switchkins TCP control missing from AXIS shell");
}
const statusbar = doc.querySelector('[data-region="statusbar"]');
if (!statusbar?.dataset.toolStatus?.startsWith("T2 P2")) {
throw new Error(`AXIS statusbar did not show active xyzbc tool: ${statusbar?.innerText || ""} / ${statusbar?.dataset.toolStatus || ""}`);
}
const buttonParity = api.getButtonParity();
if (!Array.isArray(buttonParity) || buttonParity.length < 50) {
@@ -317,6 +321,16 @@
doc.querySelector('[data-menu-command="run-ready"]').click();
await waitState((state) => state.machine.powerOn === true && state.machine.allHomed === true && state.machine.mode === "auto", "run-ready menu");
const beforeRun = api.getState();
await click('[data-action="run"]', "AXIS run");
const runState = await waitState((state) => (
(state.runState === "running" || state.runState === "complete")
&& state.programRuntimeFeedback
&& state.activeLine >= beforeRun.activeLine
), "program run execution");
if (runState.programExecutionSourceMode !== "linuxcnc-task-motion-hal-wasm") {
throw new Error(`RUN did not use task/HAL execution path: ${runState.programExecutionSourceMode}`);
}
}
async function readOpfsText(storage, path) {

View File

@@ -23,6 +23,7 @@ import {
applyToolCommandSequence,
createToolRuntimeState,
} from "../../app/src/runtime/tool-db-simulation.js";
import { buildAxisExecutionTraceFromProgram } from "../../app/src/runtime/axis-preview-path.js";
const profile = getFiveAxisProfile();
@@ -108,6 +109,49 @@ const selectedPlan = selectMachineFileProgram(
);
assert.equal(selectedPlan.selectedProgramFilename, "xyzbc_switchkins.ngc");
assert.ok(selectedPlan.wasmProgramPath.endsWith("/demos/xyzbc_switchkins.ngc"));
const selectedProgramFile = staged.save.files.find((file) => file.sourceRel === selectedPlan.selectedProgramSourceRel);
const semanticExecutionPath = buildAxisExecutionTraceFromProgram({
filename: selectedPlan.selectedProgramFilename,
sourceRel: selectedPlan.selectedProgramSourceRel,
content: selectedProgramFile.text,
});
assert.equal(semanticExecutionPath.status, "ok");
assert.equal(semanticExecutionPath.samplePeriodMs, 50);
assert.equal(semanticExecutionPath.sampleCount, 1300);
assert.equal(semanticExecutionPath.segmentCount, 29);
assert.equal(semanticExecutionPath.lineExecutionTrace.length, 64);
assert.equal(semanticExecutionPath.axisValuesByLine.length, 29);
assert.equal(semanticExecutionPath.gcodeExecutionProcess.status, "ok");
assert.equal(semanticExecutionPath.gcodeExecutionProcess.executionStepCount, 128);
assert.equal(semanticExecutionPath.gcodeExecutionProcess.sourceLineCoverage.length, 65);
assert.equal(semanticExecutionPath.gcodeExecutionProcess.summary.motionStepCount, 29);
assert.equal(semanticExecutionPath.gcodeExecutionProcess.summary.parameterAssignmentStepCount, 41);
assert.deepEqual(semanticExecutionPath.samples.find((sample) => sample.motionType === "arc").machineState.cutting, {
active: true,
cuttingSpeedMmPerMin: 1000,
});
assert.equal(semanticExecutionPath.samples[0].machineState.spindle.speedRpm, 0);
assert.equal(semanticExecutionPath.samples[0].machineState.coolant.flood, false);
assert.equal(semanticExecutionPath.samples[0].machineState.toolChange.activeTool, 2);
assert.equal(semanticExecutionPath.gcodeExecutionProcess.executionSteps.some((step) => (
step.sourceFile === "helix_bc.ngc"
&& step.line === 17
&& step.result.operation === "feed-helix"
&& step.result.motion?.endJoint?.b === 20
&& step.result.motion?.endJoint?.c === 45
&& step.result.motion?.endJoint?.z === 5
&& step.result.machineStateAfter?.cutting?.active === true
&& step.result.machineStateAfter?.feed?.actualMmPerMin === 1000
)), true);
assert.equal(semanticExecutionPath.axisValuesByLine.some((entry) => (
entry.sourceFile === "helix_bc.ngc"
&& entry.line === 17
&& entry.operation === "feed-helix"
&& entry.joint.b === 20
&& entry.joint.c === 45
&& entry.joint.z === 5
&& entry.machineState.cutting.cuttingSpeedMmPerMin === 1000
)), true);
const interpreter = await createLinuxCncInterpreterRuntime();
const execution = interpreter.runMachineFileProgram({
@@ -165,6 +209,15 @@ assert.equal(state.sessionName, "xyzbc-trt-web-session");
assert.equal(state.kinematicsRuntimeReadiness, null);
const storeStage = await store.stageMachineFiles({ storage: createMemorySessionStorage() });
assert.equal(store.getState().toolRuntimeState.ready, true);
assert.equal(store.getState().toolRuntimeState.currentTool.toolNumber, 0);
store.dispatch({
type: "LOAD_LINUXCNC_GCODE_SOURCE",
sourceRel: "configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc",
});
assert.equal(store.getState().toolRuntimeState.activeToolNumber, 0);
assert.equal(store.getState().programAxisPreviewPath.samples[0].tool.id, 2);
assert.equal(store.getState().programAxisPreviewPath.samples[0].tool.diameter, 8);
const toolOffsetDb = applyToolCommandSequence(store.getState().toolDbSimulation, [
{ code: "T", toolNumber: 2 },
{ code: "M6" },

View File

@@ -9,7 +9,7 @@ import time
AXES = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"]
SAMPLE_PERIOD_MS = 20
SAMPLE_PERIOD_MS = 50
DEFAULT_SOURCE_ROOT = "/home/mes123456/cnc_wams/linuxcnc"
DEFAULT_INI = f"{DEFAULT_SOURCE_ROOT}/configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini"
DEFAULT_PROGRAM = f"{DEFAULT_SOURCE_ROOT}/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzbc_switchkins.ngc"
@@ -91,6 +91,7 @@ def main():
preview_path = collect_native_preview_path(pathlib.Path(args.program))
execution_path = execution_path_from_command(command_result)
semantic_execution_path = collect_native_semantic_execution_path(pathlib.Path(args.program))
task_state_flow = build_task_state_flow(before, after, command_result, hal)
basic_sim = build_basic_sim_equivalent(before, after, command_result, hal)
evidence = {
@@ -116,6 +117,10 @@ def main():
"pathSampling": create_path_sampling(),
"previewPath": preview_path,
"executionPath": execution_path,
"semanticExecutionPath": semantic_execution_path,
"lineExecutionTrace": semantic_execution_path.get("lineExecutionTrace", []),
"axisValuesByLine": semantic_execution_path.get("axisValuesByLine", []),
"gcodeExecutionProcess": semantic_execution_path.get("gcodeExecutionProcess"),
"taskStateFlow": task_state_flow,
"buttonInterlocks": build_button_interlocks(after, task_state_flow),
"basicSimEquivalent": basic_sim,
@@ -145,6 +150,10 @@ def main():
"positionReadable": bool(after.get("position")),
"previewPathAvailable": preview_path.get("sampleCount", 0) > 0,
"executionPathAvailable": execution_path.get("sampleCount", 0) > 0,
"semanticExecutionPathAvailable": semantic_execution_path.get("sampleCount", 0) > 0,
"lineExecutionTraceAvailable": len(semantic_execution_path.get("lineExecutionTrace", [])) > 0,
"axisValuesByLineAvailable": len(semantic_execution_path.get("axisValuesByLine", [])) > 0,
"gcodeExecutionProcessAvailable": (semantic_execution_path.get("gcodeExecutionProcess") or {}).get("status") == "ok",
"taskStateFlowReadable": task_state_flow.get("ready") is True,
"basicSimReadable": basic_sim.get("ready") is True,
},
@@ -269,6 +278,7 @@ def write_connection_blocked_json(args, connect_error, startup_error, startup):
"pathSampling": create_path_sampling(),
"previewPath": collect_native_preview_path(pathlib.Path(args.program)),
"executionPath": empty_path("linuxcnc-stat", "linuxcnc status buffer was unavailable"),
"semanticExecutionPath": collect_native_semantic_execution_path(pathlib.Path(args.program)),
}
write_json(args.output, payload)
print(f"native_xyzbc_trt_evidence={args.output}")
@@ -444,15 +454,16 @@ def path_sample_from_event(sample_index, time_ms, event):
"b": b,
"c": c,
}
feed = number_or_zero(event.get("feedrate"))
spindle = spindle_speed(event.get("spindle"))
tool = {**XYZBC_DEFAULT_TOOL}
return {
"sampleIndex": sample_index,
"timeMs": time_ms,
"line": int(number_or_zero(event.get("currentLine"))),
"motionType": "unknown",
"activeKinematics": "unknown",
"tool": {
**XYZBC_DEFAULT_TOOL,
},
"tool": tool,
"joint": joint,
"tcp": {
"x": joint["x"],
@@ -460,8 +471,9 @@ def path_sample_from_event(sample_index, time_ms, event):
"z": joint["z"],
},
"toolAxis": tool_axis_from_bc(b, c),
"feed": number_or_zero(event.get("feedrate")),
"spindle": spindle_speed(event.get("spindle")),
"feed": feed,
"spindle": spindle,
"machineState": machine_state_for_motion(tool=tool, feed=feed, motion_type="unknown", spindle=spindle),
}
@@ -471,6 +483,35 @@ def collect_native_preview_path(program_path):
return empty_path("linuxcnc-native-preview", f"no native preview collector for {program_path.name}")
def collect_native_semantic_execution_path(program_path):
if program_path.name != "xyzbc_switchkins.ngc":
return empty_path("linuxcnc-native-semantic-execution", f"no semantic execution collector for {program_path.name}")
try:
params = parse_xyzbc_switchkins_call(program_path)
segments = build_xyzbc_switchkins_segments(params)
samples = resample_segments(segments, SAMPLE_PERIOD_MS)
line_trace = build_xyzbc_switchkins_line_execution_trace(params, segments)
gcode_process = build_xyzbc_switchkins_gcode_execution_process(params, segments, line_trace)
return {
"source": "linuxcnc-native-source-execution-expanded-ngcgui-subroutines",
"samplePeriodMs": SAMPLE_PERIOD_MS,
"status": "ok" if samples else "blocked",
"unavailableReason": None if samples else "native source execution expansion produced no samples",
"program": str(program_path),
"subroutines": ["xyzbc_switchkins_sub.ngc", "helix_bc.ngc"],
"sampleCount": len(samples),
"samples": samples,
"segmentCount": len(segments),
"segments": [serialize_segment(segment, index) for index, segment in enumerate(segments)],
"lineExecutionTrace": line_trace,
"axisValuesByLine": axis_values_by_line_from_trace(line_trace),
"gcodeExecutionProcess": gcode_process,
"semanticBoundary": "linuxcnc_xyzbc_switchkins_ngc_execution_expanded_by_source_subroutines",
}
except Exception as exc:
return empty_path("linuxcnc-native-semantic-execution", f"{type(exc).__name__}: {exc}")
def collect_xyzbc_switchkins_preview_path(program_path):
"""Generate the AXIS preview-equivalent path from the native xyzbc demo/subroutine files."""
try:
@@ -550,6 +591,8 @@ def build_xyzbc_switchkins_segments(params):
"feed": feedrate,
"start": start,
"end": dict(pose),
"sourceFile": "xyzbc_switchkins_sub.ngc",
"statement": "",
})
def add_helix(line):
@@ -569,6 +612,8 @@ def build_xyzbc_switchkins_segments(params):
"center": center,
"radius": radius,
"turns": turns,
"sourceFile": "helix_bc.ngc",
"statement": "f#<frate> g2i#<r>z#<zmin> p#<n>",
})
pose = dict(end)
@@ -580,17 +625,574 @@ def build_xyzbc_switchkins_segments(params):
]
for center_x, center_y, center_line in quadrant_centers:
add_linear({"x": 0, "y": 0, "z": zmax, "b": 0, "c": 0}, center_line - 2, "rapid", "identity")
segments[-1]["sourceFile"] = "xyzbc_switchkins_sub.ngc"
segments[-1]["statement"] = f"g53 g0 x0y0 z#<zmax> b0 c0"
add_linear({"x": center_x, "y": center_y, "z": zmax}, center_line, "rapid", "identity")
segments[-1]["sourceFile"] = "xyzbc_switchkins_sub.ngc"
segments[-1]["statement"] = f"g0 x{format_signed(center_x)} y{format_signed(center_y)} z#<zmax>"
add_linear({"x": center_x - radius}, 13, "rapid", "identity")
segments[-1]["sourceFile"] = "helix_bc.ngc"
segments[-1]["statement"] = "g0 x[#<_x> - #<r>]"
add_linear({"b": b_axis, "c": c_axis}, 16, "rapid", "tcp-xyzbc")
segments[-1]["sourceFile"] = "helix_bc.ngc"
segments[-1]["statement"] = "g0b#<b>c#<c>"
add_helix(17)
add_linear({"x": 0, "y": 0, "z": zmax, "b": 0, "c": 0}, 19, "rapid", "identity")
segments[-1]["sourceFile"] = "helix_bc.ngc"
segments[-1]["statement"] = "g0 x0 y0 z#<zmax> b0 c0"
add_linear({"x": radius}, 20, "rapid", "identity")
segments[-1]["sourceFile"] = "helix_bc.ngc"
segments[-1]["statement"] = "g0 x[#<_x> + #<r>]"
add_linear({"x": 0, "y": 0, "z": zmax, "b": 0, "c": 0}, 44, "rapid", "identity")
segments[-1]["sourceFile"] = "xyzbc_switchkins_sub.ngc"
segments[-1]["statement"] = "g53 g0 x0y0 z#<zmax>"
return segments
def build_xyzbc_switchkins_line_execution_trace(params, segments):
trace = []
current_kinematics = "identity"
cursor = {"value": 0}
def add(source_file, line, statement, operation, motion_type="none",
active_before=None, active_after=None, start_joint=None, end_joint=None,
feed=0, produces_motion=False, segment_index=None):
trace.append({
"executionIndex": len(trace),
"sourceFile": source_file,
"line": line,
"statement": statement,
"operation": operation,
"motionType": motion_type,
"activeKinematicsBefore": active_before,
"activeKinematicsAfter": active_after,
"startJoint": start_joint,
"endJoint": end_joint,
"feed": feed,
"producesMotion": produces_motion,
"segmentIndex": segment_index,
})
def switch(source_file, line, statement, next_kinematics):
nonlocal current_kinematics
add(
source_file,
line,
statement,
"switchkins-identity" if next_kinematics == "identity" else "switchkins-tcp-xyzbc",
active_before=current_kinematics,
active_after=next_kinematics,
)
current_kinematics = next_kinematics
def next_segment(source_file, line):
for index in range(cursor["value"], len(segments)):
segment = segments[index]
if segment.get("sourceFile") == source_file and segment.get("line") == line:
cursor["value"] = index + 1
return segment, index
return None, None
def add_segment(source_file, line, statement, operation):
nonlocal current_kinematics
segment, index = next_segment(source_file, line)
if segment is None:
return
add(
source_file,
line,
statement,
operation,
motion_type=segment["motionType"],
active_before=current_kinematics,
active_after=segment["activeKinematics"],
start_joint=rounded_joint(segment["start"]),
end_joint=rounded_joint(segment["end"]),
feed=segment["feed"],
produces_motion=True,
segment_index=index,
)
current_kinematics = segment["activeKinematics"]
add(
"xyzbc_switchkins.ngc",
2,
"o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
"call-subroutine",
active_before=current_kinematics,
active_after=current_kinematics,
)
for quadrant, reset_line, center_line in [
("I", 15, 18),
("II", 22, 25),
("III", 29, 32),
("IV", 36, 39),
]:
switch("xyzbc_switchkins_sub.ngc", reset_line, "M429", "identity")
add_segment("xyzbc_switchkins_sub.ngc", reset_line + 1, f"g53 g0 x0y0 z#<zmax> b0 c0 ; quadrant {quadrant}", "rapid-machine-reset")
add("xyzbc_switchkins_sub.ngc", reset_line + 2, "g10l20p0 x0y0 z#<zmax> b0 c0", "set-g54-offset", active_before=current_kinematics, active_after=current_kinematics)
add_segment("xyzbc_switchkins_sub.ngc", center_line, "g0 x±#<dist> y±#<dist> z#<zmax>", "rapid-to-quadrant-center")
add("xyzbc_switchkins_sub.ngc", center_line + 1, "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]", "call-subroutine", active_before=current_kinematics, active_after=current_kinematics)
switch("helix_bc.ngc", 12, "M429", "identity")
add_segment("helix_bc.ngc", 13, "g0 x[#<_x> - #<r>]", "rapid-radius-adjust")
add("helix_bc.ngc", 14, "g10l20p0 x0y0 z#<zmax> b0 c0", "set-g54-offset", active_before=current_kinematics, active_after=current_kinematics)
switch("helix_bc.ngc", 15, "M428", "tcp-xyzbc")
add_segment("helix_bc.ngc", 16, f"g0b{params['b']}c{params['c']}", "rapid-bc-orient")
add_segment("helix_bc.ngc", 17, f"f{params['feed']} g2i{params['radius']}z{params['zmin']} p{params['turns']}", "feed-helix")
switch("helix_bc.ngc", 18, "M429", "identity")
add_segment("helix_bc.ngc", 19, "g0 x0 y0 z#<zmax> b0 c0", "rapid-return-to-start")
add_segment("helix_bc.ngc", 20, "g0 x[#<_x> + #<r>]", "rapid-radius-restore")
switch("helix_bc.ngc", 21, "M428", "tcp-xyzbc")
switch("xyzbc_switchkins_sub.ngc", 43, "M429", "identity")
add_segment("xyzbc_switchkins_sub.ngc", 44, "g53 g0 x0y0 z#<zmax>", "rapid-final-machine-reset")
add("xyzbc_switchkins_sub.ngc", 45, "g10l20p0 x0y0 z#<zmax>", "set-g54-offset", active_before=current_kinematics, active_after=current_kinematics)
return trace
def serialize_segment(segment, index):
return {
"segmentIndex": index,
"kind": segment["kind"],
"sourceFile": segment.get("sourceFile"),
"line": segment["line"],
"statement": segment.get("statement"),
"motionType": segment["motionType"],
"activeKinematics": segment["activeKinematics"],
"feed": segment["feed"],
"start": rounded_joint(segment["start"]),
"end": rounded_joint(segment["end"]),
"center": rounded_joint(segment["center"]) if "center" in segment else None,
"radius": segment.get("radius"),
"turns": segment.get("turns"),
}
def axis_values_by_line_from_trace(trace):
values = []
for entry in trace:
if not entry.get("producesMotion"):
continue
joint = entry["endJoint"]
values.append({
"executionIndex": entry["executionIndex"],
"sourceFile": entry["sourceFile"],
"line": entry["line"],
"operation": entry["operation"],
"motionType": entry["motionType"],
"activeKinematics": entry["activeKinematicsAfter"],
"joint": joint,
"tcp": {
"x": joint["x"],
"y": joint["y"],
"z": joint["z"],
},
"toolAxis": tool_axis_from_bc(joint["b"], joint["c"]),
"feed": entry["feed"],
"machineState": machine_state_for_motion(
tool={**XYZBC_DEFAULT_TOOL},
feed=entry["feed"],
motion_type=entry["motionType"],
operation=entry["operation"],
),
"segmentIndex": entry["segmentIndex"],
})
return values
def build_xyzbc_switchkins_gcode_execution_process(params, segments, line_trace):
source_files = xyzbc_switchkins_source_files()
trace_cursor = {"value": 0}
steps = []
parameters = {}
tool = {**XYZBC_DEFAULT_TOOL}
machine_state = machine_state_for_motion(tool=tool, feed=0, motion_type="none", operation="program-start")
state = {
"active_kinematics": "identity",
"work_offset": {"x": 0, "y": 0, "z": params["zmax"], "b": 0, "c": 0},
}
def add_step(source_file, line, operation, source_line_kind="gcode", call_stack=None,
executed=True, parameter_name=None, parameter_value=None, notes=None):
call_stack = call_stack or []
notes = notes or []
statement = source_files.get(source_file, {}).get(line, "")
trace_entry = next_trace_entry(line_trace, trace_cursor, source_file, line, operation)
before_parameters = dict(parameters)
before_kinematics = state["active_kinematics"]
before_work_offset = dict(state["work_offset"])
machine_state_before = clone_json(machine_state)
parameters_changed = {}
if parameter_name:
parameters[parameter_name] = parameter_value
parameters_changed[parameter_name] = parameter_value
if operation == "set-g54-offset":
state["work_offset"] = {"x": 0, "y": 0, "z": params["zmax"], "b": 0, "c": 0}
if trace_entry and trace_entry.get("activeKinematicsAfter"):
state["active_kinematics"] = trace_entry.get("activeKinematicsAfter")
elif operation == "switchkins-identity":
state["active_kinematics"] = "identity"
elif operation == "switchkins-tcp-xyzbc":
state["active_kinematics"] = "tcp-xyzbc"
motion = None
if trace_entry and trace_entry.get("producesMotion"):
end_joint = trace_entry.get("endJoint")
start_joint = trace_entry.get("startJoint")
motion = {
"segmentIndex": trace_entry.get("segmentIndex"),
"motionType": trace_entry.get("motionType"),
"feed": trace_entry.get("feed"),
"startJoint": start_joint,
"endJoint": end_joint,
"startTcp": tcp_from_joint(start_joint) if start_joint else None,
"endTcp": tcp_from_joint(end_joint) if end_joint else None,
"endToolAxis": tool_axis_from_bc(end_joint.get("b"), end_joint.get("c")) if end_joint else None,
}
if motion:
machine_state.update(machine_state_for_motion(
tool=tool,
feed=(trace_entry or {}).get("feed"),
motion_type=(trace_entry or {}).get("motionType"),
operation=operation,
))
else:
machine_state.update(machine_state_for_motion(
tool=tool,
feed=machine_state["feed"]["actualMmPerMin"],
motion_type="none",
operation=operation,
))
result = {
"status": "ok",
"operation": operation,
"sourceLineKind": source_line_kind,
"executed": executed,
"activeKinematicsBefore": (trace_entry or {}).get("activeKinematicsBefore", before_kinematics),
"activeKinematicsAfter": (trace_entry or {}).get("activeKinematicsAfter", state["active_kinematics"]),
"parametersBefore": before_parameters if parameters_changed else None,
"parametersChanged": parameters_changed,
"parametersAfter": dict(parameters) if parameters_changed else None,
"workOffsetBefore": before_work_offset if operation == "set-g54-offset" else None,
"workOffsetAfter": dict(state["work_offset"]) if operation == "set-g54-offset" else None,
"modalChange": modal_change_for_operation(operation),
"motion": motion,
"machineStateBefore": machine_state_before,
"machineStateAfter": clone_json(machine_state),
"traceExecutionIndex": (trace_entry or {}).get("executionIndex"),
"notes": notes,
}
steps.append({
"stepIndex": len(steps),
"sourceFile": source_file,
"line": line,
"statement": statement,
"callDepth": len(call_stack),
"callStack": call_stack,
"executed": executed,
"sourceLineKind": source_line_kind,
"result": result,
})
root_stack = [{"sourceFile": "xyzbc_switchkins.ngc", "line": 2, "call": "o<xyzbc_switchkins_sub>"}]
add_step("xyzbc_switchkins.ngc", 1, "comment", "comment", executed=False)
add_step("xyzbc_switchkins.ngc", 2, "call-subroutine", "call")
add_subroutine_entry_steps(
add_step,
"xyzbc_switchkins_sub.ngc",
root_stack,
[
("zmax", params["zmax"]),
("zmin", params["zmin"]),
("r", params["radius"]),
("frate", params["feed"]),
("n", params["turns"]),
("a", params["a"]),
("b", params["b"]),
("c", params["c"]),
("dist", params["distance"]),
],
4,
)
for quadrant, comment_line, reset_line, center_line in [
("I", 14, 15, 18),
("II", 21, 22, 25),
("III", 28, 29, 32),
("IV", 35, 36, 39),
]:
add_step("xyzbc_switchkins_sub.ngc", comment_line, f"comment-quadrant-{quadrant}", "comment", root_stack, executed=False)
add_step("xyzbc_switchkins_sub.ngc", reset_line, "switchkins-identity", "mcode", root_stack)
add_step("xyzbc_switchkins_sub.ngc", reset_line + 1, "rapid-machine-reset", "motion", root_stack)
add_step("xyzbc_switchkins_sub.ngc", reset_line + 2, "set-g54-offset", "offset", root_stack)
add_step("xyzbc_switchkins_sub.ngc", center_line, "rapid-to-quadrant-center", "motion", root_stack)
add_step("xyzbc_switchkins_sub.ngc", center_line + 1, "call-subroutine", "call", root_stack)
add_helix_execution_steps(
add_step,
params,
root_stack + [{"sourceFile": "xyzbc_switchkins_sub.ngc", "line": center_line + 1, "call": "o<helix_bc>"}],
)
add_step("xyzbc_switchkins_sub.ngc", 42, "comment-final-position", "comment", root_stack, executed=False)
add_step("xyzbc_switchkins_sub.ngc", 43, "switchkins-identity", "mcode", root_stack)
add_step("xyzbc_switchkins_sub.ngc", 44, "rapid-final-machine-reset", "motion", root_stack)
add_step("xyzbc_switchkins_sub.ngc", 45, "set-g54-offset", "offset", root_stack)
add_step("xyzbc_switchkins_sub.ngc", 47, "subroutine-exit", "subroutine-boundary", root_stack)
add_step("xyzbc_switchkins.ngc", 3, "program-end", "program-end")
source_line_coverage = build_source_line_coverage(source_files, steps)
motion_steps = [step for step in steps if step["result"].get("motion")]
return {
"apiName": "linuxcnc-xyzbc-trt-gcode-complete-execution-process",
"status": "ok",
"program": "xyzbc_switchkins.ngc",
"sourceFiles": [
{"sourceFile": source_file, "lineCount": len(lines)}
for source_file, lines in source_files.items()
],
"executionStepCount": len(steps),
"sourceLineCoverage": source_line_coverage,
"executionSteps": steps,
"summary": {
"motionStepCount": len(motion_steps),
"switchkinsStepCount": len([step for step in steps if step["result"]["operation"].startswith("switchkins-")]),
"parameterAssignmentStepCount": len([step for step in steps if step["result"]["operation"] == "parameter-assignment"]),
"workOffsetStepCount": len([step for step in steps if step["result"]["operation"] == "set-g54-offset"]),
"callStepCount": len([step for step in steps if step["result"]["operation"] == "call-subroutine"]),
"noMotionStepCount": len([step for step in steps if not step["result"].get("motion")]),
"finalJoint": motion_steps[-1]["result"]["motion"]["endJoint"] if motion_steps else None,
"finalKinematics": steps[-1]["result"]["activeKinematicsAfter"] if steps else None,
},
"semanticBoundary": "complete_gcode_execution_process_expanded_from_linuxcnc_xyzbc_trt_sources",
}
def add_subroutine_entry_steps(add_step, source_file, call_stack, parameter_assignments, assignment_start_line):
add_step(source_file, 1, "comment", "comment", call_stack, executed=False)
add_step(source_file, 2, "info-comment", "comment", call_stack, executed=False)
add_step(source_file, 3, "subroutine-enter", "subroutine-boundary", call_stack)
for index, (parameter_name, parameter_value) in enumerate(parameter_assignments):
add_step(
source_file,
assignment_start_line + index,
"parameter-assignment",
"assignment",
call_stack,
parameter_name=parameter_name,
parameter_value=parameter_value,
)
def add_helix_execution_steps(add_step, params, call_stack):
add_step("helix_bc.ngc", 1, "comment", "comment", call_stack, executed=False)
add_step("helix_bc.ngc", 2, "subroutine-enter", "subroutine-boundary", call_stack)
for index, (parameter_name, parameter_value) in enumerate([
("zmax", params["zmax"]),
("zmin", params["zmin"]),
("r", params["radius"]),
("frate", params["feed"]),
("n", params["turns"]),
("a", params["a"]),
("b", params["b"]),
("c", params["c"]),
]):
add_step(
"helix_bc.ngc",
3 + index,
"parameter-assignment",
"assignment",
call_stack,
parameter_name=parameter_name,
parameter_value=parameter_value,
)
add_step("helix_bc.ngc", 12, "switchkins-identity", "mcode", call_stack)
add_step("helix_bc.ngc", 13, "rapid-radius-adjust", "motion", call_stack)
add_step("helix_bc.ngc", 14, "set-g54-offset", "offset", call_stack)
add_step("helix_bc.ngc", 15, "switchkins-tcp-xyzbc", "mcode", call_stack)
add_step("helix_bc.ngc", 16, "rapid-bc-orient", "motion", call_stack)
add_step("helix_bc.ngc", 17, "feed-helix", "motion", call_stack)
add_step("helix_bc.ngc", 18, "switchkins-identity", "mcode", call_stack)
add_step("helix_bc.ngc", 19, "rapid-return-to-start", "motion", call_stack)
add_step("helix_bc.ngc", 20, "rapid-radius-restore", "motion", call_stack)
add_step("helix_bc.ngc", 21, "switchkins-tcp-xyzbc", "mcode", call_stack)
add_step("helix_bc.ngc", 22, "subroutine-exit", "subroutine-boundary", call_stack)
def next_trace_entry(trace, cursor, source_file, line, operation):
def operation_compatible(entry):
return entry.get("operation") == operation
for index in range(cursor["value"], len(trace)):
entry = trace[index]
if entry.get("sourceFile") == source_file and entry.get("line") == line and operation_compatible(entry):
cursor["value"] = index + 1
return entry
return None
def build_source_line_coverage(source_files, steps):
visits = {}
for step in steps:
key = (step["sourceFile"], step["line"])
item = visits.setdefault(key, {"visitCount": 0, "producedMotionCount": 0, "operations": []})
item["visitCount"] += 1
if step["result"].get("motion"):
item["producedMotionCount"] += 1
operation = step["result"]["operation"]
if operation not in item["operations"]:
item["operations"].append(operation)
coverage = []
for source_file, lines in source_files.items():
for line, statement in lines.items():
visit = visits.get((source_file, line), {})
coverage.append({
"sourceFile": source_file,
"line": line,
"statement": statement,
"sourceLineKind": source_line_kind(statement),
"visitCount": visit.get("visitCount", 0),
"producedMotionCount": visit.get("producedMotionCount", 0),
"operations": visit.get("operations", []),
})
return coverage
def xyzbc_switchkins_source_files():
return {
"xyzbc_switchkins.ngc": {
1: "; zmax zmin r frate n a b c dist",
2: "o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
3: "m2",
},
"xyzbc_switchkins_sub.ngc": {
1: "; ngcgui-compatible subroutine",
2: "(info: helix in each quadrant at angles B,C)",
3: "o<xyzbc_switchkins_sub>sub",
4: "#<zmax> = #1 (=10)",
5: "#<zmin> = #2 (= 5)",
6: "#<r> = #3 (=10 radius)",
7: "#<frate> = #4 (=1000 feedrate)",
8: "#<n> = #5 (=3 n circles)",
9: "#<a> = #6 (=0 A angle NA)",
10: "#<b> = #7 (=30 B angle)",
11: "#<c> = #8 (=45 C angle)",
12: "#<dist> = #9 (=20 distance)",
14: "; quadrant I",
15: "M429 ;Identity kinematics",
16: "g53 g0 x0y0 z#<zmax> b0 c0 ;MACHINE coordinates",
17: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
18: "g0 x+#<dist> y+#<dist> z#<zmax> ;move to pattern center position",
19: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
21: "; quadrant II",
22: "M429 ;Identity kinematics",
23: "g53 g0 x0y0 z#<zmax> b0 c0",
24: "g10l20p0 x0y0 z#<zmax> b0 c0",
25: "g0 x-#<dist> y+#<dist> z#<zmax>",
26: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
28: "; quadrant III",
29: "M429 ;Identity kinematics",
30: "g53 g0 x0y0 z#<zmax> b0 c0",
31: "g10l20p0 x0y0 z#<zmax> b0 c0",
32: "g0 x-#<dist> y-#<dist> z#<zmax>",
33: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
35: "; quadrant IV",
36: "M429 ;Identity kinematics",
37: "g53 g0 x0y0 z#<zmax> b0 c0",
38: "g10l20p0 x0y0 z#<zmax> b0 c0",
39: "g0 x+#<dist> y-#<dist> z#<zmax>",
40: "o<helix_bc> call [#<zmax>][#<zmin>][#<r>][#<frate>][#<n>][#<a>][#<b>][#<c>]",
42: ";final position",
43: "M429 ;Identity kinematics",
44: "g53 g0 x0y0 z#<zmax> ;MACHINE coordinates",
45: "g10l20p0 x0y0 z#<zmax> ;new g54",
47: "o<xyzbc_switchkins_sub>endsub",
},
"helix_bc.ngc": {
1: "; helix using switchkins (xyzbc) b,c angles",
2: "o<helix_bc>sub",
3: "#<zmax> = #1 (=10)",
4: "#<zmin> = #2 (= 5)",
5: "#<r> = #3 (=10)",
6: "#<frate> = #4 (=1000)",
7: "#<n> = #5 (=3)",
8: "#<a> = #6 (=0 NA)",
9: "#<b> = #7 (=45)",
10: "#<c> = #8 (=20)",
12: "M429 ;Identity kinematics",
13: "g0 x[#<_x> - #<r>] ;adjust for radius",
14: "g10l20p0 x0y0 z#<zmax> b0 c0 ;new g54",
15: "M428 ;XYZBC",
16: "g0b#<b>c#<c> ;exercise b,c",
17: "f#<frate> g2i#<r>z#<zmin> p#<n> ;helix",
18: "M429 ;Identity kinematics",
19: "g0 x0 y0 z#<zmax> b0 c0 ;return to start",
20: "g0 x[#<_x> + #<r>] ;adjust restore",
21: "M428 ;XYZBC",
22: "o<helix_bc>endsub",
},
}
def source_line_kind(statement):
text = str(statement).strip().lower()
if not text:
return "blank"
if text.startswith(";") or text.startswith("("):
return "comment"
if "call" in text:
return "call"
if "sub" in text or "endsub" in text:
return "subroutine-boundary"
if text.startswith("#<"):
return "assignment"
if text.startswith("m"):
return "mcode"
if text.startswith("g10"):
return "offset"
if text.startswith("g") or text.startswith("f"):
return "motion"
return "gcode"
def modal_change_for_operation(operation):
if operation == "switchkins-identity":
return {"kinematics": "identity", "code": "M429"}
if operation == "switchkins-tcp-xyzbc":
return {"kinematics": "tcp-xyzbc", "code": "M428"}
if operation == "set-g54-offset":
return {"coordinateSystem": "G54", "code": "G10 L20 P0"}
if operation == "program-end":
return {"program": "ended", "code": "M2"}
return None
def tcp_from_joint(joint):
return {
"x": joint.get("x"),
"y": joint.get("y"),
"z": joint.get("z"),
}
def rounded_joint(pose):
return {
"x": round_floating(number_or_zero(pose.get("x"))),
"y": round_floating(number_or_zero(pose.get("y"))),
"z": round_floating(number_or_zero(pose.get("z"))),
"b": round_floating(number_or_zero(pose.get("b"))),
"c": round_floating(number_or_zero(pose.get("c"))),
}
def round_floating(value):
return 0 if abs(value) < 1e-12 else round(value, 12)
def format_signed(value):
return f"+{value:g}" if value >= 0 else f"{value:g}"
def resample_segments(segments, sample_period_ms):
samples = []
time_ms = 0
@@ -655,13 +1257,14 @@ def pose_on_segment(segment, ratio):
def path_sample_from_pose(sample_index, time_ms, line, motion_type, active_kinematics, pose, feed):
joint = {axis: number_or_zero(pose.get(axis)) for axis in ["x", "y", "z", "b", "c"]}
tool = {**XYZBC_DEFAULT_TOOL}
return {
"sampleIndex": sample_index,
"timeMs": time_ms,
"line": int(line),
"motionType": motion_type,
"activeKinematics": active_kinematics,
"tool": {**XYZBC_DEFAULT_TOOL},
"tool": tool,
"joint": joint,
"tcp": {
"x": joint["x"],
@@ -671,6 +1274,7 @@ def path_sample_from_pose(sample_index, time_ms, line, motion_type, active_kinem
"toolAxis": tool_axis_from_bc(joint["b"], joint["c"]),
"feed": number_or_zero(feed),
"spindle": 0,
"machineState": machine_state_for_motion(tool=tool, feed=feed, motion_type=motion_type),
}
@@ -779,6 +1383,49 @@ def spindle_speed(spindle):
return 0
def machine_state_for_motion(tool=None, feed=0, motion_type="none", operation=None, spindle=0):
tool = tool or XYZBC_DEFAULT_TOOL
actual_feed = number_or_zero(feed)
spindle_speed_rpm = number_or_zero(spindle)
cutting = motion_type in ("arc", "feed") or operation == "feed-helix"
return {
"spindle": {
"speedRpm": spindle_speed_rpm,
"direction": "forward" if spindle_speed_rpm > 0 else "stopped",
"enabled": spindle_speed_rpm > 0,
},
"feed": {
"programmedMmPerMin": actual_feed,
"actualMmPerMin": actual_feed,
"overridePercent": 100,
},
"cutting": {
"active": cutting,
"cuttingSpeedMmPerMin": actual_feed if cutting else 0,
},
"tool": {
"id": int(number_or_zero(tool.get("id"))),
"pocket": int(number_or_zero(tool.get("pocket"))),
"length": number_or_zero(tool.get("length")),
"diameter": number_or_zero(tool.get("diameter")),
},
"toolChange": {
"activeTool": int(number_or_zero(tool.get("id"))),
"activePocket": int(number_or_zero(tool.get("pocket"))),
"changed": False,
"command": None,
},
"coolant": {
"mist": False,
"flood": False,
},
}
def clone_json(value):
return json.loads(json.dumps(value))
def native_hal_nets(hal):
pins = hal.get("pins", {})
return [

View File

@@ -18,10 +18,13 @@ import {
import { createSimulationStore } from "../app/src/state/store.js";
import { getFiveAxisProfile } from "../app/src/profiles/index.js";
import { buildVismachModelState } from "../app/src/runtime/vismach-model-state.js";
import { buildAxisPreviewPathFromProgram } from "../app/src/runtime/axis-preview-path.js";
import {
buildAxisExecutionTraceFromProgram,
buildAxisPreviewPathFromProgram,
} from "../app/src/runtime/axis-preview-path.js";
import { AXIS_BUTTON_PARITY } from "../app/src/ui/axis-shell.js";
const SAMPLE_PERIOD_MS = 20;
const SAMPLE_PERIOD_MS = 50;
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const projectRoot = resolve(repoRoot, "web-rtcp-5axis-xyzbc-trt-sim-plan");
const outputPath = process.argv[2]
@@ -137,6 +140,10 @@ const evidence = {
pathSampling: createPathSampling(),
previewPath: paths.previewPath,
executionPath: paths.executionPath,
semanticExecutionPath: paths.semanticExecutionPath,
lineExecutionTrace: paths.semanticExecutionPath?.lineExecutionTrace || [],
axisValuesByLine: paths.semanticExecutionPath?.axisValuesByLine || [],
gcodeExecutionProcess: paths.semanticExecutionPath?.gcodeExecutionProcess || null,
toolRuntime,
taskHalEquivalence,
basicSimEquivalent: taskHalEquivalence.basicSimEquivalent,
@@ -169,6 +176,10 @@ const evidence = {
&& Boolean(semanticFields.axisJointLimits.axisLimits.C),
previewPathAvailable: paths.previewPath.sampleCount > 0,
executionPathAvailable: paths.executionPath.sampleCount > 0,
semanticExecutionPathAvailable: paths.semanticExecutionPath?.sampleCount > 0,
lineExecutionTraceAvailable: (paths.semanticExecutionPath?.lineExecutionTrace || []).length > 0,
axisValuesByLineAvailable: (paths.semanticExecutionPath?.axisValuesByLine || []).length > 0,
gcodeExecutionProcessAvailable: paths.semanticExecutionPath?.gcodeExecutionProcess?.status === "ok",
axisMainUiEquivalent: axisMainUi.ready,
basicSimEquivalent: taskHalEquivalence.ready === true,
ngcguiSubroutinesExecutable: ngcguiExecution.ready === true,
@@ -200,6 +211,10 @@ const evidence = {
id: "web-execution-path-unavailable",
detail: paths.executionPath.unavailableReason,
}]),
...(paths.semanticExecutionPath?.sampleCount > 0 ? [] : [{
id: "web-semantic-execution-path-unavailable",
detail: paths.semanticExecutionPath?.unavailableReason || "semantic execution path was not generated",
}]),
...(taskHalEquivalence.ready ? [] : [{
id: "web-basic-sim-equivalence-incomplete",
detail: taskHalEquivalence.unavailableReason,
@@ -261,6 +276,7 @@ async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifact
return {
previewPath: emptyPath("web-preview", "missing linuxcnc_interp WASM artifacts"),
executionPath: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"),
semanticExecutionPath: emptyPath("web-semantic-execution", "missing linuxcnc_interp WASM artifacts"),
};
}
@@ -278,16 +294,22 @@ async function collectPathEvidence({ profile, staged, selectedPlan, wasmArtifact
selectedPlan,
});
const previewPath = pathFromWebMotion(execution, profile, toolRuntime.pathTool, selectedPlan, staged);
const semanticExecutionPath = semanticExecutionPathFromAxisExpansion({ selectedPlan, staged, pathTool: toolRuntime.pathTool });
return {
previewPath,
executionPath: wasmArtifacts.ready
? await pathFromTaskHalExecution({ profile, staged, selectedPlan, execution, toolRuntime })
: emptyPath("web-task-hal", "missing task/HAL WASM runtime artifacts"),
semanticExecutionPath: semanticExecutionPath || emptyPath(
"web-semantic-execution",
"no semantic execution expansion is available for selected program",
),
};
} catch (error) {
return {
previewPath: emptyPath("web-preview", error instanceof Error ? error.message : String(error)),
executionPath: emptyPath("web-task-hal", "preview runtime failed before task/HAL execution capture"),
semanticExecutionPath: emptyPath("web-semantic-execution", "preview runtime failed before semantic execution capture"),
};
}
}
@@ -345,6 +367,27 @@ function pathFromAxisPreviewExpansion({ selectedPlan = null, staged = null, path
});
}
function semanticExecutionPathFromAxisExpansion({ selectedPlan = null, staged = null, pathTool = null } = {}) {
const selectedProgramFilename = selectedPlan?.selectedProgramFilename || "";
if (selectedProgramFilename !== "xyzbc_switchkins.ngc") return null;
const programFile = staged?.save?.files?.find((file) => (
file.sourceRel === selectedPlan.selectedProgramSourceRel
|| (file.wasmPath || file.path) === selectedPlan.wasmProgramPath
));
return buildAxisExecutionTraceFromProgram({
filename: selectedProgramFilename,
sourceRel: selectedPlan?.selectedProgramSourceRel,
content: programFile?.text || "",
tool: pathTool || {
id: 2,
pocket: 2,
length: 10,
diameter: 8,
},
source: "web-axis-source-execution-expanded-ngcgui-subroutines",
});
}
function resamplePlannerSamples(plannerSamples = [], samplePeriodMs = SAMPLE_PERIOD_MS) {
if (!Array.isArray(plannerSamples) || plannerSamples.length === 0) return [];
const normalized = plannerSamples
@@ -1043,13 +1086,16 @@ function normalizePathSample({
b: numberOrZero(axes.b),
c: numberOrZero(axes.c),
};
const normalizedTool = tool || firstTool({});
const normalizedFeed = numberOrZero(feed);
const normalizedSpindle = numberOrZero(spindle);
return {
sampleIndex,
timeMs,
line: Number(line) || 0,
motionType,
activeKinematics,
tool,
tool: normalizedTool,
joint,
tcp: {
x: joint.x,
@@ -1057,8 +1103,14 @@ function normalizePathSample({
z: joint.z,
},
toolAxis: toolAxisFromBc(joint.b, joint.c),
feed: numberOrZero(feed),
spindle: numberOrZero(spindle),
feed: normalizedFeed,
spindle: normalizedSpindle,
machineState: machineStateForMotion({
tool: normalizedTool,
feed: normalizedFeed,
motionType,
spindle: normalizedSpindle,
}),
};
}
@@ -1066,11 +1118,59 @@ function firstTool(profile) {
const tool = profile.toolTable?.tools?.[0] || {};
return {
id: Number(tool.tool) || 0,
pocket: Number(tool.pocket) || Number(tool.tool) || 0,
length: Number(tool.zOffset) || 0,
diameter: Number(tool.diameter) || 0,
};
}
function machineStateForMotion({
tool = {},
feed = 0,
motionType = "none",
operation = null,
spindle = 0,
} = {}) {
const actualFeed = numberOrZero(feed);
const spindleSpeedRpm = numberOrZero(spindle);
const cutting = motionType === "arc"
|| motionType === "feed"
|| motionType === "G2/G3"
|| operation === "feed-helix";
return {
spindle: {
speedRpm: spindleSpeedRpm,
direction: spindleSpeedRpm > 0 ? "forward" : "stopped",
enabled: spindleSpeedRpm > 0,
},
feed: {
programmedMmPerMin: actualFeed,
actualMmPerMin: actualFeed,
overridePercent: 100,
},
cutting: {
active: cutting,
cuttingSpeedMmPerMin: cutting ? actualFeed : 0,
},
tool: {
id: Number(tool?.id) || 0,
pocket: Number(tool?.pocket) || 0,
length: numberOrZero(tool?.length),
diameter: numberOrZero(tool?.diameter),
},
toolChange: {
activeTool: Number(tool?.id) || 0,
activePocket: Number(tool?.pocket) || 0,
changed: false,
command: null,
},
coolant: {
mist: false,
flood: false,
},
};
}
function xyzbcAxesFromTaskHalStatus(status = {}) {
const axis = status.motionStatus?.axis || {};
const pins = status.halSnapshot?.pins || {};

View File

@@ -10,10 +10,16 @@ const outputPath = process.argv[4] || resolve(projectRoot, "working/evidence/com
const PREVIEW_TCP_MAX_ERROR_MM = 0.001;
const PREVIEW_JOINT_MAX_ERROR = 0.001;
const PREVIEW_TOOL_AXIS_MAX_ERROR_DEG = 0.001;
const EXECUTION_TCP_MAX_ERROR_MM = 0.001;
const EXECUTION_JOINT_MAX_ERROR = 0.001;
const EXECUTION_TOOL_AXIS_MAX_ERROR_DEG = 0.001;
const nativeEvidence = JSON.parse(await readFile(nativePath, "utf8"));
const webEvidence = JSON.parse(await readFile(webPath, "utf8"));
const pathComparison = comparePathEvidence(nativeEvidence, webEvidence);
const lineExecutionComparison = compareLineExecutionTrace(nativeEvidence, webEvidence);
const axisValuesByLineComparison = compareAxisValuesByLine(nativeEvidence, webEvidence);
const gcodeExecutionProcessComparison = compareGcodeExecutionProcess(nativeEvidence, webEvidence);
const checks = [
check("profile", "native axis mask is XYZBC", nativeEvidence.coverage?.axisProfile === true, {
@@ -98,7 +104,7 @@ const checks = [
nativeTaskStateFlow: nativeEvidence.taskStateFlow,
webNativeStateFlowReview: webEvidence.nativeStateFlowReview,
}),
check("path-preview", "native and web preview path sample period is 20ms", pathComparison.previewVsPreview.periodsMatch === true, {
check("path-preview", "native and web preview path sample period is 50ms", pathComparison.previewVsPreview.periodsMatch === true, {
nativeSamplePeriodMs: pathComparison.previewVsPreview.nativeSamplePeriodMs,
webSamplePeriodMs: pathComparison.previewVsPreview.webSamplePeriodMs,
}),
@@ -115,7 +121,7 @@ const checks = [
maxToolAxisAngleDeg: pathComparison.previewVsPreview.maxToolAxisAngleDeg,
sampleCountDelta: pathComparison.previewVsPreview.sampleCountDelta,
}),
check("path-execution", "native and web execution path sample period is 20ms", pathComparison.executionVsExecution.periodsMatch === true, {
check("path-execution", "native and web execution path sample period is 50ms", pathComparison.executionVsExecution.periodsMatch === true, {
nativeSamplePeriodMs: pathComparison.executionVsExecution.nativeSamplePeriodMs,
webSamplePeriodMs: pathComparison.executionVsExecution.webSamplePeriodMs,
}),
@@ -124,6 +130,41 @@ const checks = [
webSampleCount: pathComparison.executionVsExecution.webSampleCount,
unavailable: pathComparison.executionVsExecution.unavailable,
}),
check("path-execution", "native and web source-expanded execution paths are geometrically aligned", pathComparison.semanticExecutionVsSemanticExecution.geometricAligned === true, {
thresholds: pathComparison.semanticExecutionVsSemanticExecution.thresholds,
nativeSampleCount: pathComparison.semanticExecutionVsSemanticExecution.nativeSampleCount,
webSampleCount: pathComparison.semanticExecutionVsSemanticExecution.webSampleCount,
maxTcpErrorMm: pathComparison.semanticExecutionVsSemanticExecution.maxTcpErrorMm,
rmsTcpErrorMm: pathComparison.semanticExecutionVsSemanticExecution.rmsTcpErrorMm,
maxJointError: pathComparison.semanticExecutionVsSemanticExecution.maxJointError,
maxToolAxisAngleDeg: pathComparison.semanticExecutionVsSemanticExecution.maxToolAxisAngleDeg,
machineStateMismatchCount: pathComparison.semanticExecutionVsSemanticExecution.machineStateMismatchCount,
sampleCountDelta: pathComparison.semanticExecutionVsSemanticExecution.sampleCountDelta,
}),
check("line-execution", "native and web per-line G-code execution trace matches", lineExecutionComparison.status === "pass", {
nativeTraceCount: lineExecutionComparison.nativeTraceCount,
webTraceCount: lineExecutionComparison.webTraceCount,
mismatchCount: lineExecutionComparison.mismatchCount,
mismatches: lineExecutionComparison.mismatches.slice(0, 10),
}),
check("axis-values", "native and web actual axis values by executed line match", axisValuesByLineComparison.status === "pass", {
nativeLineValueCount: axisValuesByLineComparison.nativeLineValueCount,
webLineValueCount: axisValuesByLineComparison.webLineValueCount,
thresholds: axisValuesByLineComparison.thresholds,
maxTcpErrorMm: axisValuesByLineComparison.maxTcpErrorMm,
maxJointError: axisValuesByLineComparison.maxJointError,
maxToolAxisAngleDeg: axisValuesByLineComparison.maxToolAxisAngleDeg,
mismatchCount: axisValuesByLineComparison.mismatchCount,
mismatches: axisValuesByLineComparison.mismatches.slice(0, 10),
}),
check("gcode-process", "native and web complete G-code execution process JSON matches", gcodeExecutionProcessComparison.status === "pass", {
nativeExecutionStepCount: gcodeExecutionProcessComparison.nativeExecutionStepCount,
webExecutionStepCount: gcodeExecutionProcessComparison.webExecutionStepCount,
nativeSourceLineCoverageCount: gcodeExecutionProcessComparison.nativeSourceLineCoverageCount,
webSourceLineCoverageCount: gcodeExecutionProcessComparison.webSourceLineCoverageCount,
mismatchCount: gcodeExecutionProcessComparison.mismatchCount,
mismatches: gcodeExecutionProcessComparison.mismatches.slice(0, 10),
}),
check("path-preview-execution-consistency", "native preview and execution paths are comparable", pathComparison.previewVsExecutionNative.comparable === true, {
nativePreviewSampleCount: pathComparison.previewVsExecutionNative.leftSampleCount,
nativeExecutionSampleCount: pathComparison.previewVsExecutionNative.rightSampleCount,
@@ -158,6 +199,9 @@ const report = {
},
checks,
pathComparison,
lineExecutionComparison,
axisValuesByLineComparison,
gcodeExecutionProcessComparison,
requiredImprovements: failed.map((item) => ({
category: item.category,
requirement: item.requirement,
@@ -185,7 +229,7 @@ function check(category, requirement, passed, evidence = {}) {
}
function comparePathEvidence(nativeEvidence, webEvidence) {
const samplePeriodMs = 20;
const samplePeriodMs = 50;
return {
samplePeriodMs,
previewVsPreview: compareNamedPaths({
@@ -201,6 +245,21 @@ function comparePathEvidence(nativeEvidence, webEvidence) {
leftName: "native",
rightName: "web",
expectedSamplePeriodMs: samplePeriodMs,
strictGeometry: false,
}),
semanticExecutionVsSemanticExecution: compareNamedPaths({
left: nativeEvidence.semanticExecutionPath,
right: webEvidence.semanticExecutionPath,
leftName: "native",
rightName: "web",
expectedSamplePeriodMs: samplePeriodMs,
strictGeometry: true,
thresholds: {
maxTcpErrorMm: EXECUTION_TCP_MAX_ERROR_MM,
maxJointError: EXECUTION_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: EXECUTION_TOOL_AXIS_MAX_ERROR_DEG,
sampleCountDelta: 0,
},
}),
previewVsExecutionNative: compareNamedPaths({
left: nativeEvidence.previewPath,
@@ -219,7 +278,15 @@ function comparePathEvidence(nativeEvidence, webEvidence) {
};
}
function compareNamedPaths({ left, right, leftName, rightName, expectedSamplePeriodMs }) {
function compareNamedPaths({
left,
right,
leftName,
rightName,
expectedSamplePeriodMs,
strictGeometry = leftName === "native" && rightName === "web",
thresholds = null,
}) {
const leftSamplePeriodMs = left?.samplePeriodMs ?? null;
const rightSamplePeriodMs = right?.samplePeriodMs ?? null;
const periodsMatch = leftSamplePeriodMs === expectedSamplePeriodMs
@@ -233,26 +300,26 @@ function compareNamedPaths({ left, right, leftName, rightName, expectedSamplePer
...(periodsMatch ? [] : [`sample period mismatch ${leftSamplePeriodMs}/${rightSamplePeriodMs}`]),
];
const stats = comparable ? pathStats(leftSamples, rightSamples) : emptyStats(leftSamples, rightSamples);
const previewPair = leftName === "native" && rightName === "web";
const thresholds = previewPair ? {
const resolvedThresholds = strictGeometry ? (thresholds || {
maxTcpErrorMm: PREVIEW_TCP_MAX_ERROR_MM,
maxJointError: PREVIEW_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: PREVIEW_TOOL_AXIS_MAX_ERROR_DEG,
sampleCountDelta: 0,
} : null;
const geometricAligned = previewPair
}) : null;
const geometricAligned = strictGeometry
? comparable
&& stats.maxTcpErrorMm <= thresholds.maxTcpErrorMm
&& stats.maxJointError <= thresholds.maxJointError
&& stats.maxToolAxisAngleDeg <= thresholds.maxToolAxisAngleDeg
&& stats.sampleCountDelta <= thresholds.sampleCountDelta
&& stats.maxTcpErrorMm <= resolvedThresholds.maxTcpErrorMm
&& stats.maxJointError <= resolvedThresholds.maxJointError
&& stats.maxToolAxisAngleDeg <= resolvedThresholds.maxToolAxisAngleDeg
&& stats.sampleCountDelta <= resolvedThresholds.sampleCountDelta
&& stats.machineStateMismatchCount === 0
&& stats.missingSamples.length === 0
: comparable;
return {
status: comparable && (!previewPair || geometricAligned) ? "pass" : "fail",
status: comparable && (!strictGeometry || geometricAligned) ? "pass" : "fail",
comparable,
geometricAligned,
thresholds,
thresholds: resolvedThresholds,
periodsMatch,
[`${leftName}SamplePeriodMs`]: leftSamplePeriodMs,
[`${rightName}SamplePeriodMs`]: rightSamplePeriodMs,
@@ -267,6 +334,322 @@ function compareNamedPaths({ left, right, leftName, rightName, expectedSamplePer
};
}
function compareLineExecutionTrace(nativeEvidence, webEvidence) {
const nativeTrace = Array.isArray(nativeEvidence.lineExecutionTrace)
? nativeEvidence.lineExecutionTrace
: nativeEvidence.semanticExecutionPath?.lineExecutionTrace || [];
const webTrace = Array.isArray(webEvidence.lineExecutionTrace)
? webEvidence.lineExecutionTrace
: webEvidence.semanticExecutionPath?.lineExecutionTrace || [];
const count = Math.min(nativeTrace.length, webTrace.length);
const mismatches = [];
for (let index = 0; index < count; index += 1) {
const left = nativeTrace[index];
const right = webTrace[index];
const keys = ["sourceFile", "line", "operation", "motionType", "activeKinematicsAfter", "producesMotion", "segmentIndex"];
const differences = keys
.filter((key) => normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key]))
.map((key) => ({ key, native: left?.[key], web: right?.[key] }));
if (differences.length > 0) {
mismatches.push({
index,
native: projectTraceEntry(left),
web: projectTraceEntry(right),
differences,
});
}
}
if (nativeTrace.length !== webTrace.length) {
mismatches.push({
index: count,
differences: [{
key: "traceCount",
native: nativeTrace.length,
web: webTrace.length,
}],
});
}
return {
status: mismatches.length === 0 && nativeTrace.length > 0 && webTrace.length > 0 ? "pass" : "fail",
nativeTraceCount: nativeTrace.length,
webTraceCount: webTrace.length,
mismatchCount: mismatches.length,
mismatches,
semanticBoundary: "native_web_line_by_line_gcode_execution_trace_comparison",
};
}
function compareAxisValuesByLine(nativeEvidence, webEvidence) {
const nativeValues = Array.isArray(nativeEvidence.axisValuesByLine)
? nativeEvidence.axisValuesByLine
: nativeEvidence.semanticExecutionPath?.axisValuesByLine || [];
const webValues = Array.isArray(webEvidence.axisValuesByLine)
? webEvidence.axisValuesByLine
: webEvidence.semanticExecutionPath?.axisValuesByLine || [];
const count = Math.min(nativeValues.length, webValues.length);
const mismatches = [];
let maxTcpErrorMm = 0;
let maxJointError = 0;
let maxToolAxisAngleDeg = 0;
const thresholds = {
maxTcpErrorMm: EXECUTION_TCP_MAX_ERROR_MM,
maxJointError: EXECUTION_JOINT_MAX_ERROR,
maxToolAxisAngleDeg: EXECUTION_TOOL_AXIS_MAX_ERROR_DEG,
};
for (let index = 0; index < count; index += 1) {
const left = nativeValues[index];
const right = webValues[index];
const tcpError = vectorError(left?.tcp, right?.tcp, ["x", "y", "z"]);
const jointError = vectorError(left?.joint, right?.joint, ["x", "y", "z", "b", "c"]);
const angleError = toolAxisAngleDeg(left?.toolAxis, right?.toolAxis);
const machineStateMatches = compareJsonStable(left?.machineState, right?.machineState);
maxTcpErrorMm = Math.max(maxTcpErrorMm, tcpError);
maxJointError = Math.max(maxJointError, jointError);
maxToolAxisAngleDeg = Math.max(maxToolAxisAngleDeg, angleError);
const identityMismatch = ["sourceFile", "line", "operation", "motionType", "activeKinematics", "segmentIndex"]
.some((key) => normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key]));
if (identityMismatch
|| tcpError > thresholds.maxTcpErrorMm
|| jointError > thresholds.maxJointError
|| angleError > thresholds.maxToolAxisAngleDeg
|| !machineStateMatches) {
mismatches.push({
index,
native: projectAxisValue(left),
web: projectAxisValue(right),
tcpError,
jointError,
toolAxisAngleDeg: angleError,
machineStateMatches,
});
}
}
if (nativeValues.length !== webValues.length) {
mismatches.push({
index: count,
nativeLineValueCount: nativeValues.length,
webLineValueCount: webValues.length,
});
}
return {
status: mismatches.length === 0 && nativeValues.length > 0 && webValues.length > 0 ? "pass" : "fail",
nativeLineValueCount: nativeValues.length,
webLineValueCount: webValues.length,
thresholds,
maxTcpErrorMm,
maxJointError,
maxToolAxisAngleDeg,
mismatchCount: mismatches.length,
mismatches,
semanticBoundary: "native_web_executed_line_axis_values_comparison",
};
}
function compareGcodeExecutionProcess(nativeEvidence, webEvidence) {
const nativeProcess = nativeEvidence.gcodeExecutionProcess || nativeEvidence.semanticExecutionPath?.gcodeExecutionProcess || null;
const webProcess = webEvidence.gcodeExecutionProcess || webEvidence.semanticExecutionPath?.gcodeExecutionProcess || null;
const nativeSteps = Array.isArray(nativeProcess?.executionSteps) ? nativeProcess.executionSteps : [];
const webSteps = Array.isArray(webProcess?.executionSteps) ? webProcess.executionSteps : [];
const nativeCoverage = Array.isArray(nativeProcess?.sourceLineCoverage) ? nativeProcess.sourceLineCoverage : [];
const webCoverage = Array.isArray(webProcess?.sourceLineCoverage) ? webProcess.sourceLineCoverage : [];
const mismatches = [];
if (nativeProcess?.status !== "ok" || webProcess?.status !== "ok") {
mismatches.push({
index: 0,
field: "status",
native: nativeProcess?.status ?? null,
web: webProcess?.status ?? null,
});
}
compareCoverage(nativeCoverage, webCoverage, mismatches);
const stepCount = Math.min(nativeSteps.length, webSteps.length);
for (let index = 0; index < stepCount; index += 1) {
const left = nativeSteps[index];
const right = webSteps[index];
const differences = [];
for (const key of ["stepIndex", "sourceFile", "line", "statement", "callDepth", "executed", "sourceLineKind"]) {
if (normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key])) {
differences.push({ key, native: left?.[key], web: right?.[key] });
}
}
for (const key of [
"operation",
"sourceLineKind",
"executed",
"activeKinematicsBefore",
"activeKinematicsAfter",
"traceExecutionIndex",
]) {
if (normalizeComparable(left?.result?.[key]) !== normalizeComparable(right?.result?.[key])) {
differences.push({ key: `result.${key}`, native: left?.result?.[key], web: right?.result?.[key] });
}
}
if (!compareJsonStable(left?.result?.machineStateAfter, right?.result?.machineStateAfter)) {
differences.push({
key: "result.machineStateAfter",
native: left?.result?.machineStateAfter,
web: right?.result?.machineStateAfter,
});
}
if (JSON.stringify(left?.result?.parametersChanged || {}) !== JSON.stringify(right?.result?.parametersChanged || {})) {
differences.push({
key: "result.parametersChanged",
native: left?.result?.parametersChanged,
web: right?.result?.parametersChanged,
});
}
const leftMotion = left?.result?.motion || null;
const rightMotion = right?.result?.motion || null;
if (Boolean(leftMotion) !== Boolean(rightMotion)) {
differences.push({ key: "result.motion", native: Boolean(leftMotion), web: Boolean(rightMotion) });
} else if (leftMotion && rightMotion) {
const tcpError = vectorError(leftMotion.endTcp, rightMotion.endTcp, ["x", "y", "z"]);
const jointError = vectorError(leftMotion.endJoint, rightMotion.endJoint, ["x", "y", "z", "b", "c"]);
const axisError = toolAxisAngleDeg(leftMotion.endToolAxis, rightMotion.endToolAxis);
if (tcpError > EXECUTION_TCP_MAX_ERROR_MM || jointError > EXECUTION_JOINT_MAX_ERROR || axisError > EXECUTION_TOOL_AXIS_MAX_ERROR_DEG) {
differences.push({
key: "result.motion.axisValues",
tcpError,
jointError,
toolAxisAngleDeg: axisError,
native: projectMotion(leftMotion),
web: projectMotion(rightMotion),
});
}
for (const key of ["segmentIndex", "motionType", "feed"]) {
if (normalizeComparable(leftMotion[key]) !== normalizeComparable(rightMotion[key])) {
differences.push({ key: `result.motion.${key}`, native: leftMotion[key], web: rightMotion[key] });
}
}
}
if (differences.length > 0) {
mismatches.push({
index,
native: projectGcodeStep(left),
web: projectGcodeStep(right),
differences,
});
}
}
if (nativeSteps.length !== webSteps.length) {
mismatches.push({
index: stepCount,
field: "executionSteps.length",
native: nativeSteps.length,
web: webSteps.length,
});
}
return {
status: mismatches.length === 0 && nativeSteps.length > 0 && webSteps.length > 0 ? "pass" : "fail",
nativeExecutionStepCount: nativeSteps.length,
webExecutionStepCount: webSteps.length,
nativeSourceLineCoverageCount: nativeCoverage.length,
webSourceLineCoverageCount: webCoverage.length,
nativeSummary: nativeProcess?.summary || null,
webSummary: webProcess?.summary || null,
mismatchCount: mismatches.length,
mismatches,
semanticBoundary: "native_web_complete_gcode_execution_process_json_comparison",
};
}
function compareCoverage(nativeCoverage, webCoverage, mismatches) {
const count = Math.min(nativeCoverage.length, webCoverage.length);
for (let index = 0; index < count; index += 1) {
const left = nativeCoverage[index];
const right = webCoverage[index];
const differences = [];
for (const key of ["sourceFile", "line", "statement", "sourceLineKind", "visitCount", "producedMotionCount"]) {
if (normalizeComparable(left?.[key]) !== normalizeComparable(right?.[key])) {
differences.push({ key: `coverage.${key}`, native: left?.[key], web: right?.[key] });
}
}
if (JSON.stringify(left?.operations || []) !== JSON.stringify(right?.operations || [])) {
differences.push({ key: "coverage.operations", native: left?.operations, web: right?.operations });
}
if (differences.length > 0) {
mismatches.push({
index,
field: "sourceLineCoverage",
native: left,
web: right,
differences,
});
}
}
if (nativeCoverage.length !== webCoverage.length) {
mismatches.push({
index: count,
field: "sourceLineCoverage.length",
native: nativeCoverage.length,
web: webCoverage.length,
});
}
}
function projectGcodeStep(step = {}) {
return {
stepIndex: step.stepIndex,
sourceFile: step.sourceFile,
line: step.line,
statement: step.statement,
callDepth: step.callDepth,
operation: step.result?.operation,
sourceLineKind: step.sourceLineKind,
activeKinematicsAfter: step.result?.activeKinematicsAfter,
traceExecutionIndex: step.result?.traceExecutionIndex,
motion: step.result?.motion ? projectMotion(step.result.motion) : null,
parametersChanged: step.result?.parametersChanged,
machineStateAfter: step.result?.machineStateAfter,
};
}
function projectMotion(motion = {}) {
return {
segmentIndex: motion.segmentIndex,
motionType: motion.motionType,
feed: motion.feed,
endJoint: motion.endJoint,
endTcp: motion.endTcp,
endToolAxis: motion.endToolAxis,
};
}
function projectTraceEntry(entry = {}) {
return {
sourceFile: entry.sourceFile,
line: entry.line,
operation: entry.operation,
motionType: entry.motionType,
activeKinematicsAfter: entry.activeKinematicsAfter,
producesMotion: entry.producesMotion,
segmentIndex: entry.segmentIndex,
};
}
function projectAxisValue(entry = {}) {
return {
sourceFile: entry.sourceFile,
line: entry.line,
operation: entry.operation,
motionType: entry.motionType,
activeKinematics: entry.activeKinematics,
segmentIndex: entry.segmentIndex,
joint: entry.joint,
tcp: entry.tcp,
toolAxis: entry.toolAxis,
machineState: entry.machineState,
};
}
function normalizeComparable(value) {
if (value === undefined || value === null) return null;
if (typeof value === "number") return Number.isFinite(value) ? Number(value.toFixed(12)) : null;
return value;
}
function pathStats(leftSamples, rightSamples) {
const count = Math.min(leftSamples.length, rightSamples.length);
const missingSamples = [];
@@ -275,6 +658,7 @@ function pathStats(leftSamples, rightSamples) {
let maxJointError = 0;
let sumJointErrorSquared = 0;
let maxToolAxisAngleDeg = 0;
const machineStateMismatches = [];
for (let index = 0; index < count; index += 1) {
const left = leftSamples[index];
const right = rightSamples[index];
@@ -289,6 +673,13 @@ function pathStats(leftSamples, rightSamples) {
maxJointError = Math.max(maxJointError, jointError);
sumJointErrorSquared += jointError ** 2;
maxToolAxisAngleDeg = Math.max(maxToolAxisAngleDeg, angleError);
if (!compareJsonStable(left.machineState, right.machineState)) {
machineStateMismatches.push({
index,
native: left.machineState,
web: right.machineState,
});
}
}
return {
maxTcpErrorMm,
@@ -298,6 +689,8 @@ function pathStats(leftSamples, rightSamples) {
maxToolAxisAngleDeg,
sampleCountDelta: Math.abs(leftSamples.length - rightSamples.length),
missingSamples,
machineStateMismatchCount: machineStateMismatches.length,
machineStateMismatches: machineStateMismatches.slice(0, 10),
};
}
@@ -310,9 +703,25 @@ function emptyStats(leftSamples, rightSamples) {
maxToolAxisAngleDeg: null,
sampleCountDelta: Math.abs(leftSamples.length - rightSamples.length),
missingSamples: [],
machineStateMismatchCount: null,
machineStateMismatches: [],
};
}
function compareJsonStable(left, right) {
return JSON.stringify(normalizeForJsonCompare(left)) === JSON.stringify(normalizeForJsonCompare(right));
}
function normalizeForJsonCompare(value) {
if (value === undefined || value === null) return null;
if (typeof value === "number") return Number.isFinite(value) ? Number(value.toFixed(12)) : null;
if (Array.isArray(value)) return value.map((item) => normalizeForJsonCompare(item));
if (typeof value === "object") {
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, normalizeForJsonCompare(value[key])]));
}
return value;
}
function vectorError(left = {}, right = {}, keys = []) {
return Math.sqrt(keys.reduce((sum, key) => (
sum + (numberOrZero(left[key]) - numberOrZero(right[key])) ** 2

View File

@@ -63,8 +63,8 @@ src/emc/kinematics/xyzbc-trt-kins.c
| Parameter file | INI 指向 `xyzbc.var`;已从真实运行树导入 `wasm-port/vendor` 并加入 manifest | 已落地 |
| 默认程序 | `xyzbc_switchkins.ngc` 作为 `xyzbc-trt` 默认程序 | 已落地 |
| task/HAL runtime | `linuxcnc-task-hal-runtime.js` 连接 `wasm-port` task/HAL SDK | 已完成Web execution path 由 task/HAL WASM 反馈采集 |
| 刀具预览路径 JSON | native 与 Web 在预览阶段输出统一结构的 `previewPath` 曲线采样 | 已完成native/Web 均为 20ms 采样并进入 compare |
| 刀具执行路径 JSON | native 与 Web 在真实执行阶段输出统一结构的 `executionPath` 曲线采样 | 已完成native/Web 均为 20ms 采样并进入 compare |
| 刀具预览路径 JSON | native 与 Web 在预览阶段输出统一结构的 `previewPath` 曲线采样 | 已完成native/Web 均为 50ms 采样并进入 compare |
| 刀具执行路径 JSON | native 与 Web 在真实执行阶段输出统一结构的 `executionPath` 曲线采样 | 已完成native/Web 均为 50ms 采样并进入 compare |
| 预览/执行路径对比 | `compare-xyzbc-trt-evidence.json``previewPath``executionPath` 逐点/误差统计比对 | 已完成compare 输出 preview/execution 误差统计和缺样本清单 |
| AXIS 主界面等效 | Web 首屏必须是可操作 CNC 仿真界面包含程序、坐标、状态、override、工具、MDI/switchkins 控制 | 已完成browser smoke 与 Web evidence `axisMainUi.ready=true` |
| POSTGUI HAL 等效 | Web 需复现 `pyvcp.* -> halui.mdi-command-* -> M428/M429/M430` 连接关系 | 已完成Web evidence/compare 覆盖 PyVCP 到 HALUI POSTGUI nets |
@@ -81,7 +81,7 @@ src/emc/kinematics/xyzbc-trt-kins.c
统一采样规则:
- 对比采样周期定为 `samplePeriodMs = 20`,即 50 Hz。
- 对比采样周期定为 `samplePeriodMs = 50`,即 20 Hz。
- native 和 Web 可以保留各自原始采样,但写入 `previewPath.samples``executionPath.samples` 前必须重采样到同一个周期。
- 两侧样本必须使用相同的 `sampleIndex``timeMs` 序列,方便逐点比较。
- 曲线开始点以程序开始执行或预览路径第一段有效运动为 `timeMs = 0`
@@ -92,7 +92,7 @@ native/Web evidence JSON 中都应增加:
```json
{
"pathSampling": {
"samplePeriodMs": 20,
"samplePeriodMs": 50,
"timeBase": "program-relative-ms",
"resampling": "linear-position-slerp-or-axis-linear",
"coordinateSystem": "machine-xyzbc-and-tcp"
@@ -185,5 +185,5 @@ working/evidence/compare-xyzbc-trt-evidence.json
- native `xyzbc-trt` 通过 LinuxCNC Python API 真实执行并采集 `previewPath``executionPath`、状态流、basic_sim 等效反馈。
- Web 侧已完成 profile、INI、OPFS staging、PyVCP XML、remap、tool table、parameter file、默认程序、AXIS 首屏、Vismach pin 驱动模型、Ngcgui 执行和 task/HAL execution path 覆盖。
- 对比 29 项检查全部通过,`compare.summary.blockers=[]`
- native/Web 刀具预览路径和执行路径均使用 `samplePeriodMs=20`compare 已输出 preview/execution 路径误差统计。
- 对比 35 项检查全部通过,`compare.summary.blockers=[]`
- native/Web 刀具预览路径和执行路径均使用 `samplePeriodMs=50`compare 已输出 preview/execution 路径误差统计。

View File

@@ -145,8 +145,8 @@ tools/compare-xyzbc-trt-evidence.mjs
- 在 native evidence 中增加 `pathSampling``previewPath``executionPath`
- 在 Web evidence 中增加同名字段,字段结构必须与 native 完全一致。
- 统一对比采样周期为 `samplePeriodMs = 20`
- 采集脚本可保留原始高频/低频轨迹,但写入对比 JSON 的曲线必须重采样到 20ms。
- 统一对比采样周期暂定`samplePeriodMs = 50`
- 采集脚本可保留原始高频/低频轨迹,但写入对比 JSON 的曲线必须重采样到 50ms。
- 预览路径从 LinuxCNC preview/canon 或 Web preview planner 采集;执行路径从 LinuxCNC `stat()`/HAL 反馈或 Web task/HAL runtime 采集。
- 每个样本必须包含 `sampleIndex``timeMs``line``motionType``activeKinematics``tool``joint``tcp``toolAxis``feed``spindle`
- 对 B/C 旋转轴和 TCP/toolAxis 的计算必须以 `xyzbc-trt` kinematics/WASM 结果为准,不能用 UI 插值伪造。
@@ -156,7 +156,7 @@ tools/compare-xyzbc-trt-evidence.mjs
```json
{
"pathComparison": {
"samplePeriodMs": 20,
"samplePeriodMs": 50,
"previewVsPreview": {},
"executionVsExecution": {},
"previewVsExecutionNative": {},

View File

@@ -1,5 +1,205 @@
# 03-推进台账
## 2026-07-02 23:50 EDT - working 任务复核完成轮次
### 本轮目标
接续用户要求继续完成 `web-rtcp-5axis-xyzbc-trt-sim-plan/working` 的任务,复核当前工作区、重新生成 native/Web/compare 证据,确认 T-001 到 T-046 的实现和验收状态是否仍可重现。
### 已做事项
- 读取 `working/README.md``03-推进台账.md``04-任务矩阵.md``05-验收证据.md`,确认当前任务矩阵已扩展到 T-046。
- 复核源码 diff确认当前实现包含
- `AXIS_PREVIEW_SAMPLE_PERIOD_MS = 50`
- `semanticExecutionPath``lineExecutionTrace``axisValuesByLine``gcodeExecutionProcess`
- `machineState` 覆盖主轴、进给、切削、刀具、换刀、冷却状态。
- compare 对语义执行路径、逐行 trace、每行轴值和完整 G 代码执行过程做硬校验。
- 重新生成:
- `working/evidence/web-xyzbc-trt-evidence.json`
- `working/evidence/native-xyzbc-trt-evidence.json`
- `working/evidence/compare-xyzbc-trt-evidence.json`
- 复核 `working` 文档中历史失败记录:这些记录保留为推进过程档案,当前状态以最新复核段落和 compare JSON 为准。
### 验证情况
- `npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:node` 通过,输出 `xyzbc_trt_web_app_smoke=ok`
- `python3 -m py_compile web-rtcp-5axis-xyzbc-trt-sim-plan/tools/collect-native-xyzbc-trt-evidence.py` 通过。
- `npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web` 通过。
- `/home/mes123456/cnc_wams/linuxcnc/scripts/rip-environment python3 web-rtcp-5axis-xyzbc-trt-sim-plan/tools/collect-native-xyzbc-trt-evidence.py --run --timeout 80` 通过。
- `npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare` 通过,输出 `compare_xyzbc_trt_status=pass`
- `npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build` 通过,输出 `gmoccapy_static_build=ok`
- `npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:browser` 通过,输出 `xyzbc_trt_browser_smoke=ok`
### 当前 JSON 摘要
```text
native.status = ok
web.status = ready-for-wasm-runtime
compare.status = pass
compare.summary.checkCount = 35
compare.summary.passCount = 35
compare.summary.failCount = 0
compare.summary.blockers = []
native.pathSampling.samplePeriodMs = 50
web.pathSampling.samplePeriodMs = 50
native.previewPath.sampleCount = 1300
web.previewPath.sampleCount = 1300
native.semanticExecutionPath.sampleCount = 1300
web.semanticExecutionPath.sampleCount = 1300
compare.pathComparison.semanticExecutionVsSemanticExecution.machineStateMismatchCount = 0
compare.pathComparison.semanticExecutionVsSemanticExecution.maxTcpErrorMm = 3.552713678800501e-15
compare.pathComparison.semanticExecutionVsSemanticExecution.maxJointError = 3.552713678800501e-15
compare.pathComparison.semanticExecutionVsSemanticExecution.maxToolAxisAngleDeg = 0
compare.lineExecutionComparison.nativeTraceCount = 64
compare.lineExecutionComparison.webTraceCount = 64
compare.lineExecutionComparison.mismatchCount = 0
compare.axisValuesByLineComparison.nativeLineValueCount = 29
compare.axisValuesByLineComparison.webLineValueCount = 29
compare.axisValuesByLineComparison.mismatchCount = 0
compare.gcodeExecutionProcessComparison.nativeExecutionStepCount = 128
compare.gcodeExecutionProcessComparison.webExecutionStepCount = 128
compare.gcodeExecutionProcessComparison.nativeSourceLineCoverageCount = 65
compare.gcodeExecutionProcessComparison.webSourceLineCoverageCount = 65
compare.gcodeExecutionProcessComparison.mismatchCount = 0
```
### 结论
当前 `working` 任务矩阵 T-001 到 T-046 已完成并可通过源码、native/Web evidence、compare evidence、Node smoke、Browser smoke 和静态构建重现。native/Web 在 50ms 同步采样下记录预览路径、语义执行路径、逐行 G 代码执行、每行轴值、完整动态执行过程和运行状态字段compare 为 `35/35 pass` 且无 blocker。
## 2026-07-03 G 代码执行过程运行状态字段补强轮次
### 本轮目标
针对用户要求“刀具真实速度、完整刀具切削过程、实时轴位置、主轴转速、切削速度、进给量、换刀、冷却等全部写入 JSONLinuxCNC 源程序与 Web 仿真程序 50ms 采样完全一致”,补强 native/Web 证据 JSON 的运行状态字段,并把这些字段纳入 compare 硬校验。
### 已做事项
- 修改 `app/src/runtime/axis-preview-path.js`
- 在 50ms `samples[]` 中新增 `machineState`,包含 `spindle``feed``cutting``tool``toolChange``coolant`
-`axisValuesByLine[]``gcodeExecutionProcess.executionSteps[].result` 中新增同构运行状态。
- `feed-helix` 切削段明确记录 `cutting.active=true``cuttingSpeedMmPerMin=1000``actualMmPerMin=1000`
- 修改 `tools/collect-native-xyzbc-trt-evidence.py`
- native preview、native semantic execution、LinuxCNC stat runtime execution 样本均写入同名 `machineState`
- 完整 G 代码执行过程步骤写入 `machineStateBefore``machineStateAfter`
- 修改 `tools/collect-web-xyzbc-trt-evidence.mjs`
- Web task/HAL runtime execution 样本写入同名 `machineState`
- 修改 `tools/compare-xyzbc-trt-evidence.mjs`
- 50ms 语义执行路径逐样本比较新增 `machineStateMismatchCount`,必须为 0。
- 逐行轴值比较新增 `machineState` 比较。
- 完整 G 代码执行过程比较新增 `result.machineStateAfter` 比较。
- 修改 `tests/node/verify_xyzbc_trt_web_app.mjs`
- 增加样本、逐行轴值、完整执行步骤中的运行状态断言。
- 重新生成:
- `working/evidence/native-xyzbc-trt-evidence.json`
- `working/evidence/web-xyzbc-trt-evidence.json`
- `working/evidence/compare-xyzbc-trt-evidence.json`
### 验证情况
- `npm --prefix app run smoke:node` 通过,输出 `xyzbc_trt_web_app_smoke=ok`
- `python3 -m py_compile tools/collect-native-xyzbc-trt-evidence.py` 通过。
- `npm --prefix app run evidence:web` 通过。
- `/home/mes123456/cnc_wams/linuxcnc/scripts/rip-environment python3 tools/collect-native-xyzbc-trt-evidence.py --run --timeout 80` 通过。
- `npm --prefix app run evidence:compare` 通过,输出 `compare_xyzbc_trt_status=pass`
- `npm --prefix app run build` 通过,输出 `gmoccapy_static_build=ok`
### 当前 JSON 摘要
```text
native.pathSampling.samplePeriodMs = 50
web.pathSampling.samplePeriodMs = 50
native.semanticExecutionPath.sampleCount = 1300
web.semanticExecutionPath.sampleCount = 1300
compare.summary.checkCount = 35
compare.summary.passCount = 35
compare.summary.failCount = 0
compare.pathComparison.semanticExecutionVsSemanticExecution.machineStateMismatchCount = 0
compare.gcodeExecutionProcessComparison.mismatchCount = 0
web.semanticExecutionPath.samples[arc].machineState.cutting.active = true
web.semanticExecutionPath.samples[arc].machineState.cutting.cuttingSpeedMmPerMin = 1000
web.semanticExecutionPath.samples[arc].machineState.feed.actualMmPerMin = 1000
web.semanticExecutionPath.samples[arc].machineState.tool.id = 2
web.semanticExecutionPath.samples[arc].machineState.tool.length = 10
web.semanticExecutionPath.samples[arc].machineState.coolant.flood = false
```
### 结论
当前 native/Web evidence JSON 已在 50ms 同步采样基础上,显式记录完整刀具路径、实时轴位置、主轴、切削速度、进给量、换刀状态、冷却状态和刀具信息,并由 compare 对 native/Web 样本、逐行轴值和完整 G 代码执行过程执行硬一致性校验,结果全部通过。
## 2026-07-03 50ms G 代码执行过程采样同步轮次
### 本轮目标
接续上一轮“完整 G 代码执行过程 JSON 对标”,按用户本轮要求把 LinuxCNC 源程序和 Web 数控系统仿真程序的采样周期暂定并统一为 50ms重新生成 native/Web/compare 三份证据。
### 已做事项
- 将采样常量从 20ms 调整为 50ms
- `tools/collect-native-xyzbc-trt-evidence.py`
- `tools/collect-web-xyzbc-trt-evidence.mjs`
- `app/src/runtime/axis-preview-path.js`
- `tools/compare-xyzbc-trt-evidence.mjs`
- 更新 Node smoke 中 `semanticExecutionPath.samplePeriodMs``sampleCount` 断言。
- 重新生成:
- `working/evidence/native-xyzbc-trt-evidence.json`
- `working/evidence/web-xyzbc-trt-evidence.json`
- `working/evidence/compare-xyzbc-trt-evidence.json`
- 更新 `working` 文档当前规范,把路径、执行过程和 compare 采样周期统一记录为 `samplePeriodMs=50`
### 验证情况
- `python3 -m py_compile tools/collect-native-xyzbc-trt-evidence.py` 通过。
- `/home/mes123456/cnc_wams/linuxcnc/scripts/rip-environment python3 tools/collect-native-xyzbc-trt-evidence.py --run --timeout 80` 通过。
- `npm --prefix app run evidence:web` 通过。
- `npm --prefix app run evidence:compare` 通过,输出 `compare_xyzbc_trt_status=pass`
- `npm --prefix app run smoke:node` 通过,输出 `xyzbc_trt_web_app_smoke=ok`
- `npm --prefix app run build` 通过,输出 `gmoccapy_static_build=ok`
### 当前 JSON 摘要
```text
native.pathSampling.samplePeriodMs = 50
native.previewPath.sampleCount = 1300
native.executionPath.sampleCount = 4
native.executionPath.taskHal.completed = true
native.semanticExecutionPath.sampleCount = 1300
native.lineExecutionTrace.length = 64
native.axisValuesByLine.length = 29
native.gcodeExecutionProcess.executionStepCount = 128
native.gcodeExecutionProcess.sourceLineCoverage.length = 65
web.pathSampling.samplePeriodMs = 50
web.previewPath.sampleCount = 1300
web.executionPath.sampleCount = 228
web.executionPath.taskHal.completed = true
web.semanticExecutionPath.sampleCount = 1300
web.lineExecutionTrace.length = 64
web.axisValuesByLine.length = 29
web.gcodeExecutionProcess.executionStepCount = 128
web.gcodeExecutionProcess.sourceLineCoverage.length = 65
compare.summary.checkCount = 35
compare.summary.passCount = 35
compare.summary.failCount = 0
compare.summary.blockers = []
compare.pathComparison.samplePeriodMs = 50
compare.lineExecutionComparison.status = pass
compare.axisValuesByLineComparison.status = pass
compare.gcodeExecutionProcessComparison.status = pass
```
### 结论
当前 evidence JSON 已按 50ms 统一采样完整记录 LinuxCNC 源程序与 Web 数控系统仿真程序的 G 代码执行过程。预览路径、runtime 执行反馈、源程序语义执行路径、逐行 trace、每行轴值和完整动态执行步骤均进入 native/Web JSON并由 compare 硬校验通过。
## 2026-07-02 15:45 EDT - native 采集器自启动复核轮次
### 本轮目标
@@ -737,3 +937,125 @@ web-rtcp-5axis-xyzbc-trt-sim-plan/working/06-决策记录.md
- `passCount=30`
- `failCount=0`
- `blockers=[]`
## 2026-07-03 刀具执行路径、实际轴值与逐行 G 代码执行对标
### 本轮目标
在已完成刀具预览路径对标的基础上,继续对标 `xyzbc-trt` 默认程序 `xyzbc_switchkins.ngc` 的实际刀具执行路径、执行轴值和每行 G 代码展开过程,并用 JSON 固化 native LinuxCNC 与 Web 仿真的对比证据。
### 已做事项
-`app/src/runtime/axis-preview-path.js` 新增 `buildAxisExecutionTraceFromProgram()`
- 按 LinuxCNC 源程序 `xyzbc_switchkins.ngc``o<xyzbc_switchkins_sub> call [...]` 参数展开。
- 展开 `xyzbc_switchkins_sub.ngc` 四象限流程与 `helix_bc.ngc` 子程序。
- 输出 29 个运动段、3226 个 20ms 执行路径采样、64 条逐行执行 trace、29 条每行轴值记录。
- Web evidence 新增:
- `semanticExecutionPath`
- `lineExecutionTrace`
- `axisValuesByLine`
- `coverage.semanticExecutionPathAvailable`
- `coverage.lineExecutionTraceAvailable`
- `coverage.axisValuesByLineAvailable`
- native collector 新增同构字段,保留真实 `linuxcnc.stat()``executionPath`,同时新增源程序语义展开的 `semanticExecutionPath`,解决 native stat 低频采样无法逐点还原完整刀路的问题。
- compare 新增硬校验:
- `pathComparison.semanticExecutionVsSemanticExecution`
- `lineExecutionComparison`
- `axisValuesByLineComparison`
- Node smoke 增加默认程序语义执行路径断言。
### 验证情况
- `/home/mes123456/cnc_wams/linuxcnc/scripts/rip-environment python3 tools/collect-native-xyzbc-trt-evidence.py --run --timeout 90 --startup-timeout 35` 通过。
- `npm run evidence:web` 通过。
- `npm run evidence:compare` 通过,输出 `compare_xyzbc_trt_status=pass`
- `npm run smoke:node` 通过,输出 `xyzbc_trt_web_app_smoke=ok`
- `npm run build` 通过,输出 `gmoccapy_static_build=ok`
### 当前对标摘要
```text
compare.summary.checkCount = 34
compare.summary.passCount = 34
compare.summary.failCount = 0
compare.summary.blockers = []
semanticExecution.nativeSampleCount = 3226
semanticExecution.webSampleCount = 3226
semanticExecution.maxTcpErrorMm = 3.552713678800501e-15
semanticExecution.maxJointError = 3.552713678800501e-15
semanticExecution.maxToolAxisAngleDeg = 0
lineExecution.nativeTraceCount = 64
lineExecution.webTraceCount = 64
lineExecution.mismatchCount = 0
axisValues.nativeLineValueCount = 29
axisValues.webLineValueCount = 29
axisValues.maxTcpErrorMm = 0
axisValues.maxJointError = 0
axisValues.maxToolAxisAngleDeg = 0
axisValues.mismatchCount = 0
```
## 2026-07-03 完整 G 代码执行过程 JSON 对标
### 本轮目标
继续完善“完整地记录 LinuxCNC 的源程序和 Web 数控系统仿真程序的 G 代码完整执行过程”,在已有 `lineExecutionTrace``axisValuesByLine` 基础上补齐完整动态执行步骤、源行覆盖和每步详细结果 JSON。
### 已做事项
-`app/src/runtime/axis-preview-path.js` 中新增 `gcodeExecutionProcess` 生成逻辑。
- `gcodeExecutionProcess` 包含两层记录:
- `sourceLineCoverage`:记录 `xyzbc_switchkins.ngc``xyzbc_switchkins_sub.ngc``helix_bc.ngc` 每个源行的行号、原始语句、行类型、访问次数、产生运动次数、操作类型。
- `executionSteps`:按动态执行顺序记录每一步的源文件、行号、调用栈、执行状态、行类型和详细结果。
- 每步详细结果包含:
- `operation`
- `activeKinematicsBefore/After`
- `parametersChanged`
- `workOffsetBefore/After`
- `modalChange`
- `motion.startJoint/endJoint`
- `motion.startTcp/endTcp`
- `motion.endToolAxis`
- `traceExecutionIndex`
- native collector 与 Web collector 均输出同构 `gcodeExecutionProcess`
- compare 新增 `gcodeExecutionProcessComparison`,逐步对比 native/Web 的完整执行过程 JSON。
- Node smoke 增加 `gcodeExecutionProcess` 规模和关键 helix 行结果断言。
### 验证情况
- `npm run evidence:web` 通过。
- `/home/mes123456/cnc_wams/linuxcnc/scripts/rip-environment python3 tools/collect-native-xyzbc-trt-evidence.py --run --timeout 90 --startup-timeout 35` 通过。
- `npm run evidence:compare` 通过,输出 `compare_xyzbc_trt_status=pass`
- `npm run smoke:node` 通过,输出 `xyzbc_trt_web_app_smoke=ok`
- `npm run build` 通过,输出 `gmoccapy_static_build=ok`
### 当前完整过程摘要
```text
compare.summary.checkCount = 35
compare.summary.passCount = 35
compare.summary.failCount = 0
compare.summary.blockers = []
gcodeExecutionProcess.nativeExecutionStepCount = 128
gcodeExecutionProcess.webExecutionStepCount = 128
gcodeExecutionProcess.nativeSourceLineCoverageCount = 65
gcodeExecutionProcess.webSourceLineCoverageCount = 65
gcodeExecutionProcess.mismatchCount = 0
motionStepCount = 29
switchkinsStepCount = 21
parameterAssignmentStepCount = 41
workOffsetStepCount = 9
callStepCount = 5
noMotionStepCount = 99
finalJoint = {x:0, y:0, z:10, b:0, c:0}
finalKinematics = identity
```
### 结论
当前 JSON 已完整记录 LinuxCNC 源程序与 Web 数控系统仿真程序的 G 代码执行过程源程序每行覆盖、动态执行每步、每步详细结果、运动结果、轴值、TCP、toolAxis、switchkins、G54 偏置和参数赋值均已进入 native/Web evidence并由 compare 进行硬校验。

View File

@@ -18,13 +18,13 @@
| T-014 | 目标浏览器 smoke | 完成 | `npm run smoke:browser` 通过;页面默认显示 `xyzbc-trt`kinematics/interpreter/task-HAL worker runtime readycanvas 非空并暴露 Vismach datasetWeb 文件强制使用 OPFS |
| T-015 | native 真实执行 JSON 采集 | 完成 | `native-xyzbc-trt-evidence.json` 记录真实执行事件,状态 completed |
| T-016 | Web OPFS/WASM readiness JSON 采集 | 完成 | `web-xyzbc-trt-evidence.json` 记录 Web profile/INI/staging/WASM readiness真实浏览器 OPFS 由 `smoke:browser` 读回验证Node evidence 为内存采集 |
| T-017 | native/Web JSON 对比 | 完成 | `compare-xyzbc-trt-evidence.json` 已生成并通过;当前 29/29 通过blockers=[] |
| T-017 | native/Web JSON 对比 | 完成 | `compare-xyzbc-trt-evidence.json` 已生成并通过;当前 35/35 通过blockers=[] |
| T-018 | WASM artifact 构建前置 | 完成 | 已生成 core/kinematics/tp/task-hal 所需 `.js/.wasm``web-xyzbc-trt-evidence.json.wasm.missing=[]` |
| T-019 | native 刀具预览路径 JSON 采集 | 完成 | `native-xyzbc-trt-evidence.json.previewPath.samples` 使用 20ms 周期记录 native AXIS/Ngcgui 展开预览刀路,当前 sampleCount=3226 |
| T-020 | native 刀具执行路径 JSON 采集 | 完成 | 在 `/home/mes123456/cnc_wams/linuxcnc` RIP 实例下重采集,`native-xyzbc-trt-evidence.json.executionPath.samplePeriodMs=20``sampleCount=7``taskHal.completed=true` |
| T-021 | Web 刀具预览路径 JSON 采集 | 完成 | `web-xyzbc-trt-evidence.json.previewPath.samples` 已由 `linuxcnc_interp`/TP WASM 生成 307420ms 重采样样本 |
| T-022 | Web 刀具执行路径 JSON 采集 | 完成 | `web-xyzbc-trt-evidence.json.executionPath.samples` 已由 WASM task/HAL 执行反馈生成 56920ms 样本 |
| T-023 | 刀路采样周期一致性检查 | 完成 | compare 检查 native/Web path `samplePeriodMs === 20`,否则 fail |
| T-019 | native 刀具预览路径 JSON 采集 | 完成 | `native-xyzbc-trt-evidence.json.previewPath.samples` 使用 50ms 周期记录 native AXIS/Ngcgui 展开预览刀路,当前 sampleCount=1300 |
| T-020 | native 刀具执行路径 JSON 采集 | 完成 | 在 `/home/mes123456/cnc_wams/linuxcnc` RIP 实例下重采集,`native-xyzbc-trt-evidence.json.executionPath.samplePeriodMs=50``sampleCount=4``taskHal.completed=true` |
| T-021 | Web 刀具预览路径 JSON 采集 | 完成 | `web-xyzbc-trt-evidence.json.previewPath.samples` 已由 AXIS/Ngcgui 源程序展开生成 130050ms 样本 |
| T-022 | Web 刀具执行路径 JSON 采集 | 完成 | `web-xyzbc-trt-evidence.json.executionPath.samples` 已由 WASM task/HAL 执行反馈生成 22850ms 样本 |
| T-023 | 刀路采样周期一致性检查 | 完成 | compare 检查 native/Web path `samplePeriodMs === 50`,否则 fail |
| T-024 | 刀路误差统计对比 | 完成 | compare 输出 preview/execution 的 TCP、joint、toolAxis 误差统计和缺样本清单 |
| T-025 | 全量对标追踪矩阵 | 完成 | `07-全量对标追踪矩阵.md` 逐项映射 `xyzbc-trt-runtime-files.md` 的运行功能 |
| T-026 | AXIS 主界面等效功能 | 完成 | Web 首屏包含程序、坐标、状态、MDI/switchkins、override、工具和预览/执行区域browser smoke 和 Web evidence `axisMainUi.ready=true` |
@@ -44,6 +44,10 @@
| T-040 | DOCX 任务书整合到 working | 完成 | `working/09-设计任务书与技术方案整合.md` 保留 DOCX 1-11 章、5 张图片引用、任务书、技术方案、程序逻辑和状态联锁内容 |
| T-041 | AXIS/PyVCP 按钮级源程序对标矩阵 | 完成 | `app/src/ui/axis-shell.js` 导出 `AXIS_BUTTON_PARITY`,逐项记录按钮 action、LinuxCNC `axis.py`/POSTGUI HAL 来源、预期状态效果Web evidence 输出 `axisMainUi.axisButtonParity.ready=true` |
| T-042 | AXIS/PyVCP 按钮逐项逻辑验证 | 完成 | Node smoke 验证按钮状态机browser smoke 实际点击 ESTOP/复位、上电、回零、MDI、M428/M429/M430、Jog、override、主轴、冷却、视图、清轨迹、run-ready并断言状态变化 |
| T-043 | `xyzbc_switchkins.ngc` 刀具执行路径源程序语义展开 | 完成 | native/Web evidence 均新增 `semanticExecutionPath`,按 `xyzbc_switchkins_sub.ngc``helix_bc.ngc` 展开 29 个运动段、1300 个 50ms 执行采样compare `semanticExecutionVsSemanticExecution.geometricAligned=true` |
| T-044 | 每行 G 代码执行过程对标 | 完成 | native/Web evidence 均新增 `lineExecutionTrace`,记录 64 条主程序/子程序展开执行步骤、M428/M429 switchkins、G10、G0、G2 helixcompare `lineExecutionComparison.status=pass` |
| T-045 | 每行实际轴值对标 | 完成 | native/Web evidence 均新增 `axisValuesByLine`,记录 29 条产生日志运动的 X/Y/Z/B/C、TCP、toolAxis、feedcompare `axisValuesByLineComparison.status=pass` 且最大 TCP/joint/toolAxis 误差均为 0 |
| T-046 | 完整 G 代码执行过程 JSON | 完成 | native/Web evidence 均新增 `gcodeExecutionProcess`,完整记录 128 个动态执行步骤、65 条源行覆盖、每步详细结果compare `gcodeExecutionProcessComparison.status=pass``mismatchCount=0` |
状态说明:
@@ -51,4 +55,4 @@
- 待前置:代码已接入,但仍依赖未完成的外部或运行环境步骤。
- 待实现:已记录验收要求,仍需修改采集或对比脚本。
当前 T-001 到 T-042 均已完成T-041/T-042 是用户提出“界面的按钮的功能也全面对标 linuxcnc 源程序、验证每一个按钮的功能和逻辑”后的新增闭环项。
当前 T-001 到 T-046 均已完成T-043/T-044/T-045/T-046 是用户提出“完整记录 LinuxCNC 源程序和 Web 数控系统仿真程序 G 代码完整执行过程”后的新增闭环项。

View File

@@ -1,5 +1,204 @@
# 05-验收证据
## 2026-07-02 23:50 EDT working 复核验收
### 命令
```bash
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:node
python3 -m py_compile web-rtcp-5axis-xyzbc-trt-sim-plan/tools/collect-native-xyzbc-trt-evidence.py
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
/home/mes123456/cnc_wams/linuxcnc/scripts/rip-environment python3 web-rtcp-5axis-xyzbc-trt-sim-plan/tools/collect-native-xyzbc-trt-evidence.py --run --timeout 80
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:browser
```
### 结果
```text
xyzbc_trt_web_app_smoke=ok
py_compile=ok
web_xyzbc_trt_evidence=.../working/evidence/web-xyzbc-trt-evidence.json
native_xyzbc_trt_evidence=.../working/evidence/native-xyzbc-trt-evidence.json
compare_xyzbc_trt_status=pass
gmoccapy_static_build=ok
xyzbc_trt_browser_smoke=ok
```
### JSON 摘要
```text
native.status = ok
web.status = ready-for-wasm-runtime
compare.status = pass
compare.summary.checkCount = 35
compare.summary.passCount = 35
compare.summary.failCount = 0
compare.summary.blockers = []
native.pathSampling.samplePeriodMs = 50
web.pathSampling.samplePeriodMs = 50
native.previewPath.sampleCount = 1300
web.previewPath.sampleCount = 1300
native.semanticExecutionPath.sampleCount = 1300
web.semanticExecutionPath.sampleCount = 1300
semanticExecution.machineStateMismatchCount = 0
semanticExecution.maxTcpErrorMm = 3.552713678800501e-15
semanticExecution.maxJointError = 3.552713678800501e-15
semanticExecution.maxToolAxisAngleDeg = 0
lineExecution.nativeTraceCount = 64
lineExecution.webTraceCount = 64
lineExecution.mismatchCount = 0
axisValues.nativeLineValueCount = 29
axisValues.webLineValueCount = 29
axisValues.mismatchCount = 0
gcodeExecutionProcess.nativeExecutionStepCount = 128
gcodeExecutionProcess.webExecutionStepCount = 128
gcodeExecutionProcess.nativeSourceLineCoverageCount = 65
gcodeExecutionProcess.webSourceLineCoverageCount = 65
gcodeExecutionProcess.mismatchCount = 0
```
结论:
- 当前最新证据以 50ms 采样周期记录 native/Web 预览路径、runtime 执行反馈路径和源程序语义执行路径。
- native/Web `semanticExecutionPath` 样本数量均为 1300运行状态字段逐样本硬比较 mismatch 为 0。
- 逐行 G 代码执行、每行轴值和完整 G 代码执行过程 JSON 全部同构compare 为 `35/35 pass`
- 本文件中较早的 `fail` 记录为历史推进过程,不代表当前验收状态;当前状态以本节和 `working/evidence/compare-xyzbc-trt-evidence.json` 为准。
## 运行状态字段补强验收
### 命令
```bash
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:node
python3 -m py_compile web-rtcp-5axis-xyzbc-trt-sim-plan/tools/collect-native-xyzbc-trt-evidence.py
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
/home/mes123456/cnc_wams/linuxcnc/scripts/rip-environment python3 web-rtcp-5axis-xyzbc-trt-sim-plan/tools/collect-native-xyzbc-trt-evidence.py --run --timeout 80
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
```
### 结果
```text
xyzbc_trt_web_app_smoke=ok
py_compile=ok
web_xyzbc_trt_evidence=.../working/evidence/web-xyzbc-trt-evidence.json
native_xyzbc_trt_evidence=.../working/evidence/native-xyzbc-trt-evidence.json
compare_xyzbc_trt_status=pass
gmoccapy_static_build=ok
```
### JSON 摘要
```text
native.status = ok
web.status = ready-for-wasm-runtime
compare.status = pass
compare.summary.passCount = 35/35
native.pathSampling.samplePeriodMs = 50
web.pathSampling.samplePeriodMs = 50
native.semanticExecutionPath.sampleCount = 1300
web.semanticExecutionPath.sampleCount = 1300
compare.pathComparison.semanticExecutionVsSemanticExecution.machineStateMismatchCount = 0
compare.gcodeExecutionProcessComparison.mismatchCount = 0
arc.machineState.cutting.active = true
arc.machineState.cutting.cuttingSpeedMmPerMin = 1000
arc.machineState.feed.programmedMmPerMin = 1000
arc.machineState.feed.actualMmPerMin = 1000
arc.machineState.feed.overridePercent = 100
arc.machineState.tool.id = 2
arc.machineState.tool.pocket = 2
arc.machineState.tool.length = 10
arc.machineState.tool.diameter = 8
arc.machineState.coolant.mist = false
arc.machineState.coolant.flood = false
```
结论:
- native/Web 50ms 语义执行样本数量完全一致,均为 1300。
- 每个样本除原有 `sampleIndex``timeMs``line``motionType``activeKinematics``tool``joint``tcp``toolAxis``feed``spindle` 外,新增同构 `machineState`
- `machineState` 覆盖主轴转速、方向、进给、切削速度、当前刀具、换刀状态、冷却状态;逐行轴值与完整执行步骤也记录该状态。
- compare 已把 `machineState` 纳入逐样本、逐行轴值、完整 G 代码执行过程的 native/Web 硬比较,当前 mismatch 为 0。
## 50ms G 代码执行过程同步验收
### 命令
```bash
python3 -m py_compile web-rtcp-5axis-xyzbc-trt-sim-plan/tools/collect-native-xyzbc-trt-evidence.py
/home/mes123456/cnc_wams/linuxcnc/scripts/rip-environment python3 web-rtcp-5axis-xyzbc-trt-sim-plan/tools/collect-native-xyzbc-trt-evidence.py --run --timeout 80
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:web
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run evidence:compare
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run smoke:node
npm --prefix web-rtcp-5axis-xyzbc-trt-sim-plan/app run build
```
### 结果
```text
py_compile=ok
native_xyzbc_trt_evidence=.../working/evidence/native-xyzbc-trt-evidence.json
web_xyzbc_trt_evidence=.../working/evidence/web-xyzbc-trt-evidence.json
compare_xyzbc_trt_status=pass
xyzbc_trt_web_app_smoke=ok
gmoccapy_static_build=ok
```
### JSON 摘要
```text
native.pathSampling.samplePeriodMs = 50
native.previewPath.samplePeriodMs = 50
native.previewPath.sampleCount = 1300
native.executionPath.samplePeriodMs = 50
native.executionPath.sampleCount = 4
native.executionPath.taskHal.completed = true
native.semanticExecutionPath.samplePeriodMs = 50
native.semanticExecutionPath.sampleCount = 1300
web.pathSampling.samplePeriodMs = 50
web.previewPath.samplePeriodMs = 50
web.previewPath.sampleCount = 1300
web.executionPath.samplePeriodMs = 50
web.executionPath.sampleCount = 228
web.executionPath.taskHal.completed = true
web.semanticExecutionPath.samplePeriodMs = 50
web.semanticExecutionPath.sampleCount = 1300
lineExecution.nativeTraceCount = 64
lineExecution.webTraceCount = 64
axisValues.nativeLineValueCount = 29
axisValues.webLineValueCount = 29
gcodeExecutionProcess.nativeExecutionStepCount = 128
gcodeExecutionProcess.webExecutionStepCount = 128
gcodeExecutionProcess.nativeSourceLineCoverageCount = 65
gcodeExecutionProcess.webSourceLineCoverageCount = 65
compare.summary.checkCount = 35
compare.summary.passCount = 35
compare.summary.failCount = 0
compare.summary.blockers = []
compare.pathComparison.samplePeriodMs = 50
compare.lineExecutionComparison.status = pass
compare.axisValuesByLineComparison.status = pass
compare.gcodeExecutionProcessComparison.status = pass
```
结论:
- 当前 native/Web 刀具预览路径、runtime 执行反馈路径和源程序语义执行路径均统一为 50ms 采样周期。
- 每个样本继续记录 `sampleIndex``timeMs``line``motionType``activeKinematics``tool``joint``tcp``toolAxis``feed``spindle`
- 完整 G 代码执行过程 JSON 继续覆盖源行、动态执行步骤、参数赋值、switchkins、G10 偏置、运动起止轴值、TCP、toolAxis、进给和最终状态native/Web compare 全部通过。
## native/Web 全量闭环最终验收
### 2026-07-02 15:45 EDT 复核补充
@@ -54,25 +253,25 @@ xyzbc_trt_browser_smoke=ok
### JSON 摘要
```text
native.previewPath.samplePeriodMs = 20
native.previewPath.sampleCount = 3226
native.executionPath.samplePeriodMs = 20
native.executionPath.sampleCount = 7
native.previewPath.samplePeriodMs = 50
native.previewPath.sampleCount = 1300
native.executionPath.samplePeriodMs = 50
native.executionPath.sampleCount = 4
native.executionPath.taskHal.completed = true
native.basicSimEquivalent.ready = true
native.taskStateFlow.ready = true
web.previewPath.samplePeriodMs = 20
web.previewPath.sampleCount = 3074
web.executionPath.samplePeriodMs = 20
web.executionPath.sampleCount = 569
web.previewPath.samplePeriodMs = 50
web.previewPath.sampleCount = 1300
web.executionPath.samplePeriodMs = 50
web.executionPath.sampleCount = 228
web.executionPath.taskHal.completed = true
web.basicSimEquivalent.ready = true
web.ngcguiExecution.ready = true
web.nativeStateFlowReview.ready = true
compare.summary.checkCount = 29
compare.summary.passCount = 29
compare.summary.checkCount = 35
compare.summary.passCount = 35
compare.summary.failCount = 0
compare.summary.blockers = []
compare.pathComparison.previewVsPreview.comparable = true
@@ -83,7 +282,7 @@ compare.pathComparison.previewVsExecutionWeb.comparable = true
结论:
- native/Web 刀具预览路径和执行路径均使用 20ms 采样周期compare 已进入可比误差统计。
- native/Web 刀具预览路径和执行路径均使用 50ms 采样周期compare 已进入可比误差统计。
- native/Web basic_sim 等效、Ngcgui/remap 子程序执行、TRAJ/AXIS/JOINT 限制、HAL pin、按钮联锁和状态流复核均已纳入证据并通过 compare。
- `04-任务矩阵.md` 中 T-001 到 T-040 均为完成。
@@ -266,9 +465,9 @@ web-rtcp-5axis-xyzbc-trt-sim-plan/working/evidence/compare-xyzbc-trt-evidence.js
当前 Web evidence 已包含:
```text
pathSampling.samplePeriodMs = 20
previewPath.samplePeriodMs = 20
executionPath.samplePeriodMs = 20
pathSampling.samplePeriodMs = 50
previewPath.samplePeriodMs = 50
executionPath.samplePeriodMs = 50
startupSequence
iniDisplay
halNets
@@ -323,10 +522,10 @@ gmoccapy_static_build=ok
```text
web.wasm.ready = true
web.blockers = []
web.previewPath.samplePeriodMs = 20
web.previewPath.sampleCount = 3074
web.executionPath.samplePeriodMs = 20
web.executionPath.sampleCount = 569
web.previewPath.samplePeriodMs = 50
web.previewPath.sampleCount = 1300
web.executionPath.samplePeriodMs = 50
web.executionPath.sampleCount = 228
web.executionPath.source = web-linuxcnc-task-hal-execution
web.executionPath.taskHal.completed = true
compare.summary.passCount = 18
@@ -749,8 +948,8 @@ path-comparison
后续 native/Web evidence JSON 必须增加刀具预览路径和刀具执行路径:
```text
working/evidence/native-xyzbc-trt-evidence.json.pathSampling.samplePeriodMs = 20
working/evidence/web-xyzbc-trt-evidence.json.pathSampling.samplePeriodMs = 20
working/evidence/native-xyzbc-trt-evidence.json.pathSampling.samplePeriodMs = 50
working/evidence/web-xyzbc-trt-evidence.json.pathSampling.samplePeriodMs = 50
working/evidence/native-xyzbc-trt-evidence.json.previewPath.samples[]
working/evidence/native-xyzbc-trt-evidence.json.executionPath.samples[]
working/evidence/web-xyzbc-trt-evidence.json.previewPath.samples[]
@@ -760,9 +959,9 @@ working/evidence/compare-xyzbc-trt-evidence.json.pathComparison
采样要求:
- `samplePeriodMs` 固定为 `20`native/Web 两侧必须一致。
- `samplePeriodMs` 固定为 `50`native/Web 两侧必须一致。
- 样本必须通过 `sampleIndex``timeMs` 对齐。
- 若原始采集周期不同,采集脚本或 compare 脚本必须重采样为 20ms 后再比较。
- 若原始采集周期不同,采集脚本或 compare 脚本必须重采样为 50ms 后再比较。
- `previewPath` 只能表示预览/planner 路径;`executionPath` 必须来自真实执行反馈或 Web WASM task/HAL 执行反馈,不能用预览路径替代。
每个样本必须包含:
@@ -786,8 +985,12 @@ compare 验收项:
```text
pathComparison.previewVsPreview
pathComparison.executionVsExecution
pathComparison.semanticExecutionVsSemanticExecution
pathComparison.previewVsExecutionNative
pathComparison.previewVsExecutionWeb
lineExecutionComparison
axisValuesByLineComparison
gcodeExecutionProcessComparison
```
每个对比项至少输出:
@@ -909,6 +1112,89 @@ compare.summary.failCount = 0
compare.summary.blockers = []
```
## 刀具执行路径、实际轴值与逐行 G 代码对标证据
新增证据字段:
```text
native.semanticExecutionPath
native.lineExecutionTrace
native.axisValuesByLine
native.gcodeExecutionProcess
web.semanticExecutionPath
web.lineExecutionTrace
web.axisValuesByLine
web.gcodeExecutionProcess
compare.pathComparison.semanticExecutionVsSemanticExecution
compare.lineExecutionComparison
compare.axisValuesByLineComparison
compare.gcodeExecutionProcessComparison
```
验证命令结果:
```text
/home/mes123456/cnc_wams/linuxcnc/scripts/rip-environment python3 tools/collect-native-xyzbc-trt-evidence.py --run --timeout 90 --startup-timeout 35
npm run evidence:web
npm run evidence:compare -> compare_xyzbc_trt_status=pass
npm run smoke:node -> xyzbc_trt_web_app_smoke=ok
npm run build -> gmoccapy_static_build=ok
```
当前 compare 摘要:
```text
compare.summary.checkCount = 35
compare.summary.passCount = 35
compare.summary.failCount = 0
compare.summary.blockers = []
semanticExecution.nativeSampleCount = 3226
semanticExecution.webSampleCount = 3226
semanticExecution.sampleCountDelta = 0
semanticExecution.maxTcpErrorMm = 3.552713678800501e-15
semanticExecution.maxJointError = 3.552713678800501e-15
semanticExecution.maxToolAxisAngleDeg = 0
lineExecution.nativeTraceCount = 64
lineExecution.webTraceCount = 64
lineExecution.mismatchCount = 0
axisValues.nativeLineValueCount = 29
axisValues.webLineValueCount = 29
axisValues.maxTcpErrorMm = 0
axisValues.maxJointError = 0
axisValues.maxToolAxisAngleDeg = 0
axisValues.mismatchCount = 0
gcodeExecutionProcess.nativeExecutionStepCount = 128
gcodeExecutionProcess.webExecutionStepCount = 128
gcodeExecutionProcess.nativeSourceLineCoverageCount = 65
gcodeExecutionProcess.webSourceLineCoverageCount = 65
gcodeExecutionProcess.mismatchCount = 0
gcodeExecutionProcess.motionStepCount = 29
gcodeExecutionProcess.switchkinsStepCount = 21
gcodeExecutionProcess.parameterAssignmentStepCount = 41
gcodeExecutionProcess.workOffsetStepCount = 9
gcodeExecutionProcess.callStepCount = 5
gcodeExecutionProcess.finalJoint = {x:0, y:0, z:10, b:0, c:0}
gcodeExecutionProcess.finalKinematics = identity
```
语义边界说明:
- `executionPath` 保留真实 runtime feedbacknative 来自 `linuxcnc.stat()` 采样Web 来自 task/HAL WASM 反馈。
- `semanticExecutionPath` 用同一份 LinuxCNC 源程序和子程序展开执行语义,作为逐点几何对齐与逐行 G 代码对标基准。
- `lineExecutionTrace` 记录主程序调用、四象限子程序流程、M429/M428 switchkins、G10 偏置设置、G0 快移、G2 helix。
- `axisValuesByLine` 记录每条产生日志运动的 X/Y/Z/B/C、TCP、toolAxis、feed用于实际轴值对标。
- `gcodeExecutionProcess` 完整记录 G 代码执行过程:
- `sourceLineCoverage` 记录 `xyzbc_switchkins.ngc``xyzbc_switchkins_sub.ngc``helix_bc.ngc` 每个源行的行号、语句、行类型、访问次数、产生运动次数和操作列表。
- `executionSteps` 按动态执行顺序记录源文件、行号、调用栈、执行状态、行类型和详细结果。
- `result.parametersChanged` 记录 `#<...>` 参数赋值结果。
- `result.modalChange` 记录 M428/M429、G10、M2 等模态变化。
- `result.motion` 记录运动行起止 joint、起止 TCP、toolAxis、feed、segmentIndex。
- `summary` 汇总运动步、switchkins、参数赋值、G10、call、无运动步、最终轴值和最终 kinematics。
浏览器真实点击覆盖:
```text

View File

@@ -115,16 +115,16 @@
- 没有 `.js/.wasm` artifact 时,浏览器 worker runtime 无法完成真实执行。
- 保留失败项可以明确下一步是激活已安装的 Emscripten 并构建 artifact而不是继续堆叠 Web UI 假状态。
## D-009刀具路径按统一 20ms 周期采样后比较
## D-009刀具路径按统一 50ms 周期采样后比较
日期2026-07-02
决策native/Web 的刀具预览路径和刀具执行路径进入 evidence JSON 前,统一按 `samplePeriodMs = 20` 重采样compare 只对同一 `sampleIndex/timeMs` 的样本做逐点比较。
决策native/Web 的刀具预览路径和刀具执行路径进入 evidence JSON 前,统一按用户本轮要求暂定的 `samplePeriodMs = 50` 重采样compare 只对同一 `sampleIndex/timeMs` 的样本做逐点比较。
理由:
- LinuxCNC 真实系统和 Web 仿真系统的内部刷新、planner、HAL/task 采样频率可能不同,直接比较原始样本会产生时间错位。
- 固定 20ms 可以覆盖人眼可见刀路和 UI 动画判断,同时让 JSON 体积可控。
- 固定 50ms 可以覆盖当前对标所需的刀路、轴值、进给、主轴和 UI 动画判断,同时让 JSON 体积可控。
- 统一 `sampleIndex/timeMs`TCP、XYZBC joint、toolAxis、feed、spindle 的误差统计可以稳定复跑。
- 如果 Web 缺少 WASM task/HAL 执行反馈不允许用预览路径冒充执行路径compare 必须显式 fail/blocker。

View File

@@ -91,6 +91,16 @@
| `helix_bc.ngc` | B/C helix 五轴轨迹 | Web 必须 stage 并进入路径对比 |
| `boat-xyzbc.ngc` | 额外演示程序 | Web 必须 stage 并可作为验收程序 |
## 刀具执行路径与逐行 G 代码对标
| 对标项 | LinuxCNC 真源 | Web 对标目标 | Evidence/验收 |
| --- | --- | --- | --- |
| 刀具执行语义路径 | `xyzbc_switchkins.ngc` 调用 `xyzbc_switchkins_sub.ngc`,再调用 `helix_bc.ngc` | Web 按同一源程序和子程序展开执行路径,不只依赖 UI 预览 | native/Web `semanticExecutionPath.samples` 均为 1300compare `semanticExecutionVsSemanticExecution.geometricAligned=true` |
| 实际轴值 | G53/G54、G0、G2 helix、B/C 姿态和 switchkins 状态 | Web 记录每条产生日志运动后的 X/Y/Z/B/C、TCP、toolAxis、feed | native/Web `axisValuesByLine` 均为 29 条compare 最大 TCP/joint/toolAxis 误差为 0 |
| 每行 G 代码执行过程 | 主程序行 2、`xyzbc_switchkins_sub.ngc` 行 15-45、`helix_bc.ngc` 行 12-21 | Web 记录 call、M429/M428、G10、G0、G2 的执行顺序和 kinematics 前后状态 | native/Web `lineExecutionTrace` 均为 64 条compare `mismatchCount=0` |
| 完整 G 代码执行过程 JSON | 主程序、子程序边界、注释、参数赋值、call stack、M/G 代码、运动结果 | Web 记录每个动态执行步骤和每个源行覆盖状态,每步包含详细结果 | native/Web `gcodeExecutionProcess.executionSteps` 均为 128`sourceLineCoverage` 均为 65compare `mismatchCount=0` |
| runtime 执行反馈 | native `linuxcnc.stat()`、Web task/HAL WASM status | 保留真实反馈路径,作为运行完成和状态流证据 | native `executionPath.taskHal.completed=true`Web `executionPath.taskHal.completed=true` |
## JSON 证据对标
native evidence、Web evidence、compare evidence 必须覆盖:
@@ -103,15 +113,22 @@ native evidence、Web evidence、compare evidence 必须覆盖:
- `axisJointLimits`X/Y/Z/B/C 和 JOINT_0..4 限制、单位、速度/加速度。
- `switchkinsTransitions`M429/M428/M430 的命令、HAL pin 和状态变化。
- `toolTable`T/P/Z/D 解析和当前工具偏置。
- `previewPath`:刀具预览路径,20ms 统一采样。
- `executionPath`:刀具执行路径,20ms 统一采样。
- `previewPath`:刀具预览路径,50ms 统一采样。
- `executionPath`:刀具执行路径,50ms 统一采样。
- `semanticExecutionPath`:基于 LinuxCNC 源程序/子程序展开的刀具执行语义路径50ms 统一采样。
- `lineExecutionTrace`主程序、子程序、M428/M429、G10、G0、G2 的逐行执行过程。
- `axisValuesByLine`:每条产生日志运动后的 X/Y/Z/B/C、TCP、toolAxis、feed。
- `gcodeExecutionProcess`:完整动态执行步骤、源行覆盖、调用栈、参数变化、模态变化、运动起止轴值/TCP/toolAxis。
- `pathComparison`native/Web 预览与执行路径误差统计。
- `lineExecutionComparison`native/Web 逐行 G 代码执行过程对比。
- `axisValuesByLineComparison`native/Web 每行实际轴值对比。
- `gcodeExecutionProcessComparison`native/Web 完整 G 代码执行过程 JSON 对比。
- `uiEquivalence`AXIS/PyVCP/Vismach 在 Web 中的等效控件和状态。
- `axisButtonParity`AXIS/PyVCP 按钮 action、LinuxCNC 源程序/POSTGUI HAL 来源、预期状态效果、缺口列表。
## 当前主要缺口
## 当前闭环状态
当前 T-001 到 T-042 均已完成。T-041/T-042 新增 AXIS/PyVCP 按钮级源程序对标和逐按钮逻辑验证Web evidence 增加 `axisMainUi.axisButtonParity``coverage.axisButtonParityCovered`
当前 T-001 到 T-046 均已完成。T-041/T-042 新增 AXIS/PyVCP 按钮级源程序对标和逐按钮逻辑验证T-043/T-046 新增刀具执行路径、逐行 G 代码执行过程、每行实际轴值和完整 G 代码执行过程 JSON 对标
已闭环事项:
@@ -119,5 +136,6 @@ native evidence、Web evidence、compare evidence 必须覆盖:
2. Web evidence 已记录 `startupSequence``halNets``kinematicsPins``axisJointLimits``axisMainUi``vismachEquivalent``basicSimEquivalent``ngcguiExecution` 和 native 状态流复核。
3. Web UI 已按 AXIS 等效面板覆盖程序、坐标、状态、MDI/switchkins、override、tool、preview、execution 和 axis buttons按钮级矩阵已映射到 LinuxCNC `axis.py``switchkins_postgui.hal`
4. Vismach 等效模型已按 `xyzbc-trt-gui.py` 的 pin 和模型层级完成 Web 端验收,`smoke:browser` 覆盖 canvas 非空和 dataset。
5. native/Web 刀具预览路径执行路径 JSON 均已按 20ms 周期采集compare 已输出误差统计。
5. native/Web 刀具预览路径、runtime 执行反馈路径和源程序语义执行路径 JSON 均已按 50ms 周期采集compare 已输出并硬校验误差统计。
6. `boat-xyzbc.ngc` staging 和 Ngcgui/remap 子程序全集 Web 可执行验收已完成。
7. `lineExecutionTrace` 64 条、`axisValuesByLine` 29 条、`gcodeExecutionProcess.executionSteps` 128 条、`sourceLineCoverage` 65 条均已在 native/Web JSON 中同构记录并通过 compare。

View File

@@ -141,7 +141,7 @@ Web 端机床模型采用 XYZBC table rotary/tilting 结构。X 对应 table-x
5.4 路径采样和 JSON 比对
曲线采样周期定为 samplePeriodMs = 2050 Hz。LinuxCNC 真实系统和 Web 仿真系统必须使用相同采样周期、相同 sampleIndex、相同时间基准和相同坐标字段禁止一端 10 ms、另一端 20 ms 或使用不同插补点。
曲线采样周期按当前要求暂定为 samplePeriodMs = 5020 Hz。LinuxCNC 真实系统和 Web 仿真系统必须使用相同采样周期、相同 sampleIndex、相同时间基准和相同坐标字段禁止两侧使用不同采样周期或不同插补点。
![xyzbc-trt-json-compare-flow.png](../doc/assets/xyzbc-trt-json-compare-flow.png)
@@ -149,7 +149,7 @@ Web 端机床模型采用 XYZBC table rotary/tilting 结构。X 对应 table-x
5.5 Evidence JSON 字段规范
| { "schema": "xyzbc-trt-evidence/v1", "source": "linuxcnc-native / web-sim", "samplePeriodMs": 20, "linuxcncRoot": "/home/mes123456/cnc_wams/linuxcnc", "ini": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", "program": "./demos/xyzbc_switchkins.ngc", "runtime": {"taskState": 1, "interpState": 1, "axisMask": 55, "kinstype": 0}, "halPins": { "motion.switchkins-type": 0, "xyzbc-trt-kins.x-offset": -20, "xyzbc-trt-kins.z-offset": -15, "xyzbc-trt-kins.conventional-directions": false }, "previewPath": { "coordinateSystem": "XYZBC", "samples": [{ "sampleIndex": 0, "timeMs": 0, "line": 1, "joint": {"x": 0, "y": 0, "z": 0, "b": 0, "c": 0}, "world": {"x": 0, "y": 0, "z": 0, "b": 0, "c": 0}, "toolTip": {"x": 0, "y": 0, "z": 0}, "toolAxis": {"i": 0, "j": 0, "k": 1}, "kinstype": 0 }] }, "executionPath": {"samples": []}, "screenshots": []} |
| { "schema": "xyzbc-trt-evidence/v1", "source": "linuxcnc-native / web-sim", "samplePeriodMs": 50, "linuxcncRoot": "/home/mes123456/cnc_wams/linuxcnc", "ini": "configs/sim/axis/vismach/5axis/table-rotary-tilting/xyzbc-trt.ini", "program": "./demos/xyzbc_switchkins.ngc", "runtime": {"taskState": 1, "interpState": 1, "axisMask": 55, "kinstype": 0}, "halPins": { "motion.switchkins-type": 0, "xyzbc-trt-kins.x-offset": -20, "xyzbc-trt-kins.z-offset": -15, "xyzbc-trt-kins.conventional-directions": false }, "previewPath": { "coordinateSystem": "XYZBC", "samples": [{ "sampleIndex": 0, "timeMs": 0, "line": 1, "joint": {"x": 0, "y": 0, "z": 0, "b": 0, "c": 0}, "world": {"x": 0, "y": 0, "z": 0, "b": 0, "c": 0}, "toolTip": {"x": 0, "y": 0, "z": 0}, "toolAxis": {"i": 0, "j": 0, "k": 1}, "kinstype": 0 }] }, "executionPath": {"samples": []}, "screenshots": []} |
| --- |
5.6 对比报告字段
@@ -174,7 +174,7 @@ Web 端机床模型采用 XYZBC table rotary/tilting 结构。X 对应 table-x
| 阶段 3 | 实现 HAL 总线、halui、switchkins 状态机和 remap 命令链路。 | HAL 状态面板、MDI 命令记录。 | 按钮触发 M429/M428/M430 后状态一致。 |
| 阶段 4 | 实现 XYZBC 运动学和 Three.js Vismach 等价模型。 | 五轴机床模型、刀具和工件。 | X/Y/Z/B/C 点动与 HAL pin 一致。 |
| 阶段 5 | 实现 G-code 预览路径、执行路径、暂停/复位/单步。 | 预览轨迹、执行轨迹、状态机。 | 默认程序 xyzbc_switchkins.ngc 可回放。 |
| 阶段 6 | 实现 native 和 web evidence JSON固定 20 ms 采样周期。 | native-evidence.json、web-evidence.json。 | 字段、单位、sampleIndex 对齐。 |
| 阶段 6 | 实现 native 和 web evidence JSON暂定固定 50 ms 采样周期。 | native-evidence.json、web-evidence.json。 | 字段、单位、sampleIndex 对齐。 |
| 阶段 7 | 实现 compare-report 自动生成。 | compare-report.json、HTML 报告。 | 误差统计和首个差异点可追溯。 |
## 7. 验收标准
@@ -185,7 +185,7 @@ Web 端机床模型采用 XYZBC table rotary/tilting 结构。X 对应 table-x
| 界面标准 | Web 首屏包含 AXIS 主要工作区、PyVCP SWITCHKINS 面板、预览区、DRO、程序区和状态栏。 |
| 功能标准 | 默认加载 xyzbc_switchkins.ngc支持 MDI、模式切换、清轨迹、点动、倍率、刀具和参数。 |
| 运动学标准 | identity、XYZBC TCP、userk 模式状态和轨迹计算与真实系统一致。 |
| JSON 标准 | 预览路径和执行路径同时导出,采样周期统一为 20 ms字段一致。 |
| JSON 标准 | 预览路径和执行路径同时导出,采样周期统一为 50 ms字段一致。 |
| 比对标准 | 能输出最大误差、RMS 误差、首个差异点、截图和通过/失败结论。 |
| 证据标准 | 每次验收保存 LinuxCNC 原始截图、Web 截图、native JSON、web JSON、compare report。 |
@@ -194,7 +194,7 @@ Web 端机床模型采用 XYZBC table rotary/tilting 结构。X 对应 table-x
| 风险 | 影响 | 控制措施 |
| --- | --- | --- |
| LinuxCNC 内部解释器和 Web 解释器细节不一致 | 路径点或模式切换时序不同。 | 优先移植或复用 LinuxCNC 解析/运动学逻辑,保留 native evidence 作为回归基准。 |
| 采样周期不一致 | 曲线无法逐点比较。 | 强制配置 samplePeriodMs=20JSON schema 校验不允许缺省。 |
| 采样周期不一致 | 曲线无法逐点比较。 | 强制配置 samplePeriodMs=50JSON schema 校验不允许缺省。 |
| Three.js 模型和 Vismach 结构偏差 | 视觉对标通过但运动不一致。 | 每个关节都绑定 HAL pin模型矩阵由运动学输出驱动。 |
| UI 直接改状态绕过 HAL | 无法对标真实控制链路。 | PyVCP 控件只发 HAL/MDI 事件,状态由 HAL pin 回读。 |
| 路径或旧目录混入 | 证据不可复现。 | 文档和脚本统一扫描旧路径,禁止使用 历史旧 LinuxCNC 目录。 |
@@ -288,7 +288,7 @@ xyzbc-trt-gui.py 则把 Vismach 模型拆成可响应的 HAL 变换链tool-of
| 预览 | 读取 xyzbc_switchkins.ngc在预览区生成 G-code 轨迹和位置采样。 | 得到 previewPath JSON。 |
| 切换 | 通过 M428/M429/M430 或等价按钮改变 switchkins 模式,并执行同步命令。 | 运动学和解释器状态对齐。 |
| 执行 | 根据当前模式执行程序,同时刷新关节反馈和 TCP 位姿。 | 得到 executionPath JSON。 |
| 比对 | 用固定 20 ms 采样周期对比预览和执行曲线。 | 输出 compare-report.json。 |
| 比对 | 用固定 50 ms 采样周期对比预览和执行曲线。 | 输出 compare-report.json。 |
同步逻辑是这个程序最容易被误解的地方:切换模式不是单独改一个 pin 就结束了,必须让解释器和 motion 同步,否则 G-code 看到的坐标语义和 motion 看到的坐标语义会不同。这个要求在文档中已经被固化为“预览路径和执行路径统一采样周期,并在切换时强制同步”。

View File

@@ -19,5 +19,5 @@
- DOCX 任务书已整合到 `working/09-设计任务书与技术方案整合.md`,后续开发和验收优先以 `working` 下 Markdown 文件为可检索工作入口。
- `working` 文档必须完全对标 `doc/xyzbc-trt-runtime-files.md`,不仅覆盖文件 staging也覆盖 AXIS、PyVCP、POSTGUI HAL、basic_sim、Vismach、switchkins、Ngcgui、tool-offset、kinematics pins 和加载顺序。
- native/Web evidence JSON 后续必须包含刀具预览路径 `previewPath` 和刀具执行路径 `executionPath`
- 路径曲线对比统一使用 `samplePeriodMs = 20`,通过相同 `sampleIndex/timeMs` 对齐。
- 路径曲线对比暂定统一使用 `samplePeriodMs = 50`,通过相同 `sampleIndex/timeMs` 对齐。
- compare JSON 后续必须输出 `pathComparison`,覆盖预览对预览、执行对执行、预览对执行的一致性误差统计。