完成 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);