提交 xyzbc-trt 界面与验证更新
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createSimulationStore } from "./state/store.js";
|
||||
import { mountGmoccapyShell } from "./ui/gmoccapy-shell.js";
|
||||
import { mountAxisShell } from "./ui/axis-shell.js";
|
||||
import { createLinuxCncInterpreterRuntime } from "./runtime/linuxcnc-interpreter-runtime.js";
|
||||
import { createLinuxCncInterpreterWorkerRuntime } from "./runtime/linuxcnc-interpreter-worker-client.js";
|
||||
import { createLinuxCncKinematicsRuntime } from "./runtime/linuxcnc-kinematics-runtime.js";
|
||||
@@ -15,7 +15,7 @@ if (!app) {
|
||||
}
|
||||
|
||||
const store = createSimulationStore();
|
||||
const shell = mountGmoccapyShell(app, store);
|
||||
const shell = mountAxisShell(app, store);
|
||||
const iniConfigReady = attachProfileIniConfig(store, store.getState().profile);
|
||||
const kinematicsRuntimeReady = attachDefaultKinematicsRuntime(store, store.getState().profile.kinematicsModuleId || store.getState().machineProfile);
|
||||
const interpreterRuntimeReady = attachDefaultInterpreterRuntime(store);
|
||||
@@ -67,6 +67,7 @@ window.webRtcp5AxisSimulation = {
|
||||
saveToolDb: store.saveToolDb,
|
||||
runFullBoundaryAudit: store.runFullBoundaryAudit,
|
||||
getRegions: shell.getRegions,
|
||||
getButtonParity: shell.getButtonParity,
|
||||
iniConfigReady,
|
||||
kinematicsRuntimeReady,
|
||||
interpreterRuntimeReady,
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
export const AXIS_PREVIEW_SAMPLE_PERIOD_MS = 20;
|
||||
|
||||
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 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) => {
|
||||
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 },
|
||||
});
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
Object.assign(pose, end);
|
||||
};
|
||||
|
||||
for (const [centerX, centerY, centerLine] of [
|
||||
[distance, distance, 18],
|
||||
[-distance, distance, 25],
|
||||
[-distance, -distance, 32],
|
||||
[distance, -distance, 39],
|
||||
]) {
|
||||
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");
|
||||
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 }, 44, "rapid", "identity");
|
||||
return segments;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -46,6 +46,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 +58,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,
|
||||
@@ -164,6 +165,7 @@ const initialState = {
|
||||
manualPanel: "manual",
|
||||
allHomed: false,
|
||||
noForceHoming: false,
|
||||
selectedJoint: 0,
|
||||
jogAxis: "x",
|
||||
jogIncrement: 1,
|
||||
mdiCommand: "G0 X0 Y0 Z0",
|
||||
@@ -171,20 +173,20 @@ const initialState = {
|
||||
resetCount: 0,
|
||||
},
|
||||
runState: "idle",
|
||||
activeProgram: "../../../linuxcnc/nc_files/3D_Chips.ngc",
|
||||
programSource: "fixture",
|
||||
programStartLine: 496,
|
||||
activeLine: 501,
|
||||
lineCount: 4711,
|
||||
fileSizeBytes: 200509,
|
||||
activeProgram: "./demos/xyzbc_switchkins.ngc",
|
||||
programSource: "linuxcnc-axis-default",
|
||||
programStartLine: 1,
|
||||
activeLine: 2,
|
||||
lineCount: 3,
|
||||
fileSizeBytes: 109,
|
||||
kinsType: "identity",
|
||||
rtcpState: "off",
|
||||
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,
|
||||
},
|
||||
@@ -201,6 +203,7 @@ const initialState = {
|
||||
interpreterRuntimeReadiness: null,
|
||||
programExecution: null,
|
||||
programExecutionTiming: null,
|
||||
programAxisPreviewPath: null,
|
||||
programElapsedSeconds: 0,
|
||||
programRemainingSeconds: 0,
|
||||
programExecutionSourceMode: "fixture-line-playback",
|
||||
@@ -241,9 +244,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,
|
||||
@@ -344,17 +347,9 @@ initialState.rightSidebarEntrances = createRightSidebarEntranceState(initialStat
|
||||
initialState.linuxCncParityMatrix = createLinuxCncParityMatrix(initialState);
|
||||
|
||||
const programLines = [
|
||||
"N4860 Y[#<yscale>*-39.009]",
|
||||
"N4870 Y[#<yscale>*-32.524]",
|
||||
"N4880 Y[#<yscale>*-32.384]",
|
||||
"N4890 Y[#<yscale>*-32.267]",
|
||||
"N4900 Y[#<yscale>*-32.235] Z[#<zscale>*-11.306]",
|
||||
"N4910 Y[#<yscale>*-32.118] Z[#<zscale>*-11.312]",
|
||||
"N4920 Y[#<yscale>*-32.103] Z[#<zscale>*-11.314]",
|
||||
"N4930 Y[#<yscale>*-32.071] Z[#<zscale>*-11.316]",
|
||||
"N4940 Y[#<yscale>*-31.972] Z[#<zscale>*-11.318]",
|
||||
"N4950 Y[#<yscale>*-31.759] Z[#<zscale>*-11.320]",
|
||||
"N4960 Y[#<yscale>*-31.509] Z[#<zscale>*-11.324]",
|
||||
"; zmax zmin r frate n a b c dist",
|
||||
"o<xyzbc_switchkins_sub> call [10] [5] [10][1000][3][0][20][45][20]",
|
||||
"m2",
|
||||
];
|
||||
|
||||
export function createSimulationStore(seed = {}) {
|
||||
@@ -381,6 +376,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);
|
||||
@@ -717,7 +728,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,
|
||||
@@ -745,6 +756,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;
|
||||
@@ -947,6 +962,12 @@ 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,
|
||||
@@ -966,11 +987,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}`,
|
||||
});
|
||||
@@ -1333,6 +1355,37 @@ export function createSimulationStore(seed = {}) {
|
||||
operatorMessage: "MDI command staged",
|
||||
});
|
||||
break;
|
||||
case "SET_ACTIVE_JOINT":
|
||||
{
|
||||
const joint = Math.min(Math.max(Number(action.joint) || 0, 0), Math.max((state.profile?.joints?.length || 5) - 1, 0));
|
||||
const axis = (state.profile?.jointConfig?.[joint]?.axis || ["X", "Y", "Z", "B", "C"][joint] || "X").toLowerCase();
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
selectedJoint: joint,
|
||||
jogAxis: axis,
|
||||
},
|
||||
operatorMessage: `joint ${joint} selected`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "SET_JOG_INCREMENT":
|
||||
{
|
||||
const increment = Math.max(Number(action.increment) || 0, 0);
|
||||
setState({
|
||||
machine: {
|
||||
...state.machine,
|
||||
jogIncrement: increment,
|
||||
},
|
||||
gmoccapyGui: {
|
||||
...state.gmoccapyGui,
|
||||
jogIncrementLabel: increment === 0 ? "Continuous" : increment.toFixed(4),
|
||||
jogIncrementOutput: increment,
|
||||
},
|
||||
operatorMessage: increment === 0 ? "jog continuous" : `jog increment ${increment}`,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "JOG":
|
||||
{
|
||||
const gate = gateLinuxCncTaskAction(state, action);
|
||||
@@ -1442,6 +1495,12 @@ export function createSimulationStore(seed = {}) {
|
||||
const toolDbSimulation = state.toolDbSimulation && toolCommands.length > 0
|
||||
? applyToolCommandSequence(state.toolDbSimulation, toolCommands)
|
||||
: state.toolDbSimulation;
|
||||
const axisPreviewPath = buildAxisPreviewPathFromProgram({
|
||||
filename: loadedProgram.activeProgram,
|
||||
sourceRel: loadedProgram.programSourceRel,
|
||||
content: action.content,
|
||||
tool: currentPathTool(state),
|
||||
});
|
||||
setState({
|
||||
...loadedProgram,
|
||||
toolDbSimulation,
|
||||
@@ -1456,11 +1515,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}`,
|
||||
});
|
||||
@@ -3979,6 +4039,25 @@ function buildLoadedProgram(action) {
|
||||
};
|
||||
}
|
||||
|
||||
function currentPathTool(state) {
|
||||
const pathTool = state.toolRuntimeState?.pathTool;
|
||||
if (pathTool) 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/)
|
||||
|
||||
661
web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/axis.css
Normal file
661
web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/styles/axis.css
Normal file
@@ -0,0 +1,661 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--axis-bg: #d7d7d7;
|
||||
--axis-border: #9a9a9a;
|
||||
--axis-dark: #050505;
|
||||
--axis-tab: #dcdcdc;
|
||||
--axis-tab-active: #eeeeee;
|
||||
--axis-button: #e6e6e6;
|
||||
--axis-text: #050505;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
background: #1f1f23;
|
||||
color: var(--axis-text);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.axis-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 42% minmax(360px, 1fr) 250px;
|
||||
grid-template-rows: 54px 31px 30px minmax(0, 1fr) minmax(205px, 24vh) 32px;
|
||||
grid-template-areas:
|
||||
"title title title"
|
||||
"menu menu menu"
|
||||
"toolbar toolbar toolbar"
|
||||
"manual main pyvcp"
|
||||
"program program pyvcp"
|
||||
"status status pyvcp";
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border: 3px solid #595959;
|
||||
background: var(--axis-bg);
|
||||
}
|
||||
|
||||
.axis-window-title {
|
||||
grid-area: title;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: #242428;
|
||||
color: #a7a7ac;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.axis-menubar {
|
||||
grid-area: menu;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 2px 8px 2px;
|
||||
border-bottom: 1px solid #adadad;
|
||||
background: #d5d5d5;
|
||||
}
|
||||
|
||||
.axis-menubar > button,
|
||||
.axis-menu > button {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #111;
|
||||
background: transparent;
|
||||
color: #050505;
|
||||
font: 24px/1 Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.axis-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.axis-menu-popup {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
z-index: 30;
|
||||
display: none;
|
||||
min-width: 190px;
|
||||
padding: 3px 0;
|
||||
border: 1px solid #6e6e6e;
|
||||
background: #e8e8e8;
|
||||
box-shadow: 2px 2px 0 #777;
|
||||
}
|
||||
|
||||
.axis-menu:focus-within .axis-menu-popup,
|
||||
.axis-menu:hover .axis-menu-popup {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.axis-menu-popup button {
|
||||
min-height: 24px;
|
||||
padding: 2px 18px 2px 8px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: #050505;
|
||||
font: 14px/1.2 Arial, Helvetica, sans-serif;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.axis-menu-popup button:hover {
|
||||
background: #0a64ad;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.axis-toolbar {
|
||||
grid-area: toolbar;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
padding: 3px 6px;
|
||||
border-bottom: 2px solid #a6a6a6;
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.axis-file-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.axis-tool-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
min-width: 26px;
|
||||
padding: 1px;
|
||||
border: 1px solid #8b8b8b;
|
||||
border-radius: 0;
|
||||
background: #e9e9e9;
|
||||
box-shadow: inset 1px 1px #ffffff, inset -1px -1px #8c8c8c;
|
||||
}
|
||||
|
||||
.axis-tool-button img {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.axis-text-tool {
|
||||
font: 700 18px/1 "Times New Roman", serif;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.axis-toolbar-separator {
|
||||
width: 8px;
|
||||
height: 26px;
|
||||
border-left: 1px solid #9b9b9b;
|
||||
}
|
||||
|
||||
.axis-manual-pane {
|
||||
grid-area: manual;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(180px, 0.56fr) minmax(220px, 0.44fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-right: 2px solid var(--axis-border);
|
||||
border-bottom: 2px solid var(--axis-border);
|
||||
background: var(--axis-bg);
|
||||
}
|
||||
|
||||
.axis-tabs {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background: #d0d0d0;
|
||||
}
|
||||
|
||||
.axis-tabs button {
|
||||
min-height: 29px;
|
||||
padding: 1px 8px;
|
||||
border: 1px solid #9c9c9c;
|
||||
border-bottom: 0;
|
||||
border-radius: 0;
|
||||
background: var(--axis-tab);
|
||||
color: #050505;
|
||||
font: 22px/1 Arial, Helvetica, sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.axis-tabs button.active {
|
||||
background: var(--axis-tab-active);
|
||||
}
|
||||
|
||||
.axis-left-tabs button {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.axis-manual-box {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 5px 8px;
|
||||
border-bottom: 2px solid #9e9e9e;
|
||||
}
|
||||
|
||||
.joint-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 100px repeat(3, 72px);
|
||||
grid-auto-rows: 31px;
|
||||
align-items: center;
|
||||
max-width: 340px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.joint-grid label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.joint-grid input {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.jog-row,
|
||||
.manual-button-row,
|
||||
.spindle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 128px;
|
||||
margin-top: 7px;
|
||||
}
|
||||
|
||||
.manual-button-row.shifted {
|
||||
margin-left: 250px;
|
||||
}
|
||||
|
||||
.spindle-row {
|
||||
margin-left: 0;
|
||||
margin-top: 13px;
|
||||
gap: 6px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.spindle-jog {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.jog-row button,
|
||||
.manual-button-row button,
|
||||
.spindle-row button,
|
||||
.switchkins-panel button {
|
||||
min-height: 31px;
|
||||
padding: 1px 10px;
|
||||
border: 2px outset #f4f4f4;
|
||||
border-radius: 0;
|
||||
background: var(--axis-button);
|
||||
color: #050505;
|
||||
font: 21px/1 Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.jog-row button {
|
||||
width: 36px;
|
||||
}
|
||||
|
||||
.jog-row select {
|
||||
height: 32px;
|
||||
border: 2px inset #efefef;
|
||||
background: #eeeeee;
|
||||
color: #050505;
|
||||
font: 21px/1 Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
color: #9c9c9c;
|
||||
}
|
||||
|
||||
.axis-override-box {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(6, 27px) 23px 25px 27px minmax(48px, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 3px 0 0;
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.axis-slider-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(176px, 1fr) 100px 84px 22px 22px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 0 0 0 2px;
|
||||
}
|
||||
|
||||
.axis-slider-row button {
|
||||
width: 22px;
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
padding: 0;
|
||||
border: 2px outset #f4f4f4;
|
||||
border-radius: 0;
|
||||
background: #e6e6e6;
|
||||
font: 16px/1 Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.axis-slider-row span,
|
||||
.axis-slider-row strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.axis-slider-row strong {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.axis-slider {
|
||||
position: relative;
|
||||
height: 15px;
|
||||
margin-left: 5px;
|
||||
border: 1px solid #aaa;
|
||||
background: #bdbdbd;
|
||||
}
|
||||
|
||||
.axis-slider i {
|
||||
display: block;
|
||||
height: 100%;
|
||||
background: #cfcfcf;
|
||||
}
|
||||
|
||||
.axis-slider b {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
right: 35px;
|
||||
width: 14px;
|
||||
height: 15px;
|
||||
border: 1px solid #9e9e9e;
|
||||
background: #e7e7e7;
|
||||
}
|
||||
|
||||
.active-gcodes-label {
|
||||
padding-left: 2px;
|
||||
}
|
||||
|
||||
.active-gcodes {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
margin-right: 0;
|
||||
padding: 4px 2px;
|
||||
border: 2px inset #eeeeee;
|
||||
background: #ffffff;
|
||||
font: 20px/1.18 "Courier New", monospace;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.axis-main-pane {
|
||||
grid-area: main;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border-right: 2px solid var(--axis-border);
|
||||
border-bottom: 2px solid var(--axis-border);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.axis-main-tabs button {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.axis-mdi-box {
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
gap: 6px;
|
||||
min-height: 0;
|
||||
padding: 8px;
|
||||
border-bottom: 2px solid #9e9e9e;
|
||||
}
|
||||
|
||||
.axis-mdi-command {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) 60px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
font: 16px/1.2 Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.axis-mdi-command input {
|
||||
min-width: 0;
|
||||
height: 30px;
|
||||
border: 2px inset #efefef;
|
||||
background: #ffffff;
|
||||
color: #050505;
|
||||
font: 17px/1.2 "Courier New", monospace;
|
||||
}
|
||||
|
||||
.axis-mdi-command button,
|
||||
.axis-mdi-history button,
|
||||
.axis-toggle-row button {
|
||||
min-height: 28px;
|
||||
border: 2px outset #f4f4f4;
|
||||
border-radius: 0;
|
||||
background: #e6e6e6;
|
||||
color: #050505;
|
||||
font: 15px/1.1 Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.axis-mdi-history {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
align-content: start;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.axis-mdi-history button {
|
||||
overflow: hidden;
|
||||
padding: 2px 6px;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.axis-toggle-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 1px 4px;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.axis-toggle-row label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.axis-preview-wrap {
|
||||
position: relative;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
border: 2px inset #d9d9d9;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.axis-preview-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.axis-preview-readout {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 34px;
|
||||
z-index: 2;
|
||||
color: #ffffff;
|
||||
font: 14px/1.28 "Courier New", monospace;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.axis-pyvcp-pane {
|
||||
grid-area: pyvcp;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 5px 6px;
|
||||
background: var(--axis-bg);
|
||||
}
|
||||
|
||||
.switchkins-panel {
|
||||
display: grid;
|
||||
grid-template-rows: 38px 44px repeat(4, 44px);
|
||||
gap: 5px;
|
||||
border: 2px ridge #eeeeee;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.switchkins-panel h2 {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: 0;
|
||||
border: 2px inset #eeeeee;
|
||||
background: #eeeeee;
|
||||
font: 24px/1 Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.switchkins-active {
|
||||
overflow: hidden;
|
||||
background: #000000;
|
||||
color: #ffff00;
|
||||
font: 34px/1.1 Arial, Helvetica, sans-serif;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.switchkins-panel button {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.switchkins-panel button[data-active="true"] {
|
||||
background: #f3f3f3;
|
||||
box-shadow: inset 2px 2px #ffffff, inset -2px -2px #7f7f7f;
|
||||
}
|
||||
|
||||
.axis-program-pane {
|
||||
grid-area: program;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
border-right: 2px solid var(--axis-border);
|
||||
border-bottom: 2px solid var(--axis-border);
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.axis-program-list {
|
||||
margin: 0;
|
||||
padding: 5px 10px 8px 84px;
|
||||
list-style: none;
|
||||
font: 21px/1.16 "Courier New", monospace;
|
||||
}
|
||||
|
||||
.axis-program-list li {
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.axis-program-list span {
|
||||
color: #888888;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.axis-program-list code {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding-left: 8px;
|
||||
color: #050505;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.axis-program-list li.active {
|
||||
background: #dfeeff;
|
||||
}
|
||||
|
||||
.axis-statusbar {
|
||||
grid-area: status;
|
||||
display: grid;
|
||||
grid-template-columns: 245px minmax(260px, 1fr) minmax(260px, 1fr);
|
||||
min-width: 0;
|
||||
border-right: 2px solid var(--axis-border);
|
||||
background: #d9d9d9;
|
||||
font: 23px/1 Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.axis-statusbar div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 4px;
|
||||
border: 2px inset #ededed;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.axis-shell {
|
||||
grid-template-columns: 41% minmax(320px, 1fr) 214px;
|
||||
min-width: 860px;
|
||||
}
|
||||
|
||||
.axis-window-title {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.axis-tabs button,
|
||||
.axis-left-tabs button,
|
||||
.axis-main-tabs button,
|
||||
.joint-grid,
|
||||
.jog-row button,
|
||||
.manual-button-row button,
|
||||
.spindle-row,
|
||||
.spindle-row button,
|
||||
.jog-row select,
|
||||
.axis-override-box,
|
||||
.switchkins-panel h2,
|
||||
.switchkins-panel button {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.switchkins-active {
|
||||
font-size: 31px;
|
||||
}
|
||||
|
||||
.axis-program-list {
|
||||
font-size: 22px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 760px) {
|
||||
.axis-shell {
|
||||
grid-template-rows: 52px 32px 32px minmax(0, 1fr) 210px 30px;
|
||||
min-height: 560px;
|
||||
}
|
||||
|
||||
.axis-menubar button,
|
||||
.axis-tabs button,
|
||||
.axis-left-tabs button,
|
||||
.axis-main-tabs button,
|
||||
.joint-grid,
|
||||
.jog-row button,
|
||||
.manual-button-row button,
|
||||
.spindle-row,
|
||||
.spindle-row button,
|
||||
.jog-row select,
|
||||
.axis-override-box,
|
||||
.switchkins-panel h2,
|
||||
.switchkins-panel button,
|
||||
.axis-program-list,
|
||||
.axis-statusbar {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.switchkins-active {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.axis-override-box {
|
||||
grid-template-rows: repeat(6, 26px) 24px 54px;
|
||||
}
|
||||
}
|
||||
550
web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/axis-shell.js
Normal file
550
web-rtcp-5axis-xyzbc-trt-sim-plan/app/src/ui/axis-shell.js
Normal file
@@ -0,0 +1,550 @@
|
||||
import { renderFiveAxisScene } from "../visualization/five-axis-scene.js";
|
||||
import { getGmoccapyIcon } from "./gmoccapy-icon-registry.js";
|
||||
|
||||
export const AXIS_BUTTON_PARITY = [
|
||||
{ id: "menu-open", action: "open", sourceSymbol: "commands.open_file", sourceLines: "axis.py:2243-2262", expected: "file picker opens without changing runtime state" },
|
||||
{ id: "menu-reload", action: "reload", sourceSymbol: "commands.reload_file", sourceLines: "axis.py:2296-2297", expected: "program reload resets active playback state" },
|
||||
{ id: "menu-stage", action: "stage", sourceSymbol: "web OPFS staging for DISPLAY PROGRAM_PREFIX", sourceLines: "xyzbc-trt.ini:[DISPLAY] PROGRAM_PREFIX", expected: "LinuxCNC profile files are staged" },
|
||||
{ id: "menu-save-session", action: "save-session", sourceSymbol: "web session persistence", sourceLines: "five-axis-session.js", expected: "session snapshot is requested" },
|
||||
{ id: "menu-restore-session", action: "restore-session", sourceSymbol: "web session persistence", sourceLines: "five-axis-session.js", expected: "session restore is requested" },
|
||||
{ id: "menu-estop", action: "estop", sourceSymbol: "commands.estop_clicked", sourceLines: "axis.py:2223-2229", expected: "toggle ESTOP/ESTOP_RESET" },
|
||||
{ id: "menu-power", action: "power", sourceSymbol: "commands.onoff_clicked", sourceLines: "axis.py:2231-2241", expected: "toggle machine ON/OFF after estop reset" },
|
||||
{ id: "menu-home-all", action: "home-all", sourceSymbol: "commands.home_all_joints", sourceLines: "axis.py:2586-2596", expected: "home all joints in manual mode" },
|
||||
{ id: "menu-run-ready", action: "run-ready", sourceSymbol: "AXIS run preconditions", sourceLines: "axis.py:2308-2320", expected: "power on, home, auto mode and TCP kins ready" },
|
||||
{ id: "menu-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "AUTO_RUN from current start line" },
|
||||
{ id: "menu-pause", action: "pause", sourceSymbol: "commands.task_pause", sourceLines: "axis.py:2329-2333", expected: "AUTO_PAUSE when running" },
|
||||
{ id: "menu-resume", action: "resume", sourceSymbol: "commands.task_resume", sourceLines: "axis.py:2344-2351", expected: "AUTO_RESUME when paused" },
|
||||
{ id: "menu-step", action: "step", sourceSymbol: "commands.task_step", sourceLines: "axis.py:2321-2327", expected: "AUTO_STEP one cycle/segment" },
|
||||
{ id: "menu-stop", action: "stop", sourceSymbol: "commands.task_stop", sourceLines: "axis.py:2365-2372", expected: "abort current task" },
|
||||
{ id: "menu-view-z", action: "view-z", sourceSymbol: "commands.set_view_z", sourceLines: "axis.py:2208-2215", expected: "select top Z camera" },
|
||||
{ id: "menu-view-y", action: "view-y", sourceSymbol: "commands.set_view_y", sourceLines: "axis.py:2192-2200", expected: "select front Y camera" },
|
||||
{ id: "menu-view-x", action: "view-x", sourceSymbol: "commands.set_view_x", sourceLines: "axis.py:2182-2190", expected: "select side X camera" },
|
||||
{ id: "menu-view-p", action: "view-p", sourceSymbol: "commands.set_view_p", sourceLines: "axis.py:2220-2221", expected: "select perspective/fit camera" },
|
||||
{ id: "menu-clear-preview", action: "clear-preview", sourceSymbol: "commands.clear_live_plot", sourceLines: "axis.py:2557-2558", expected: "clear live plot buffer" },
|
||||
{ id: "menu-audit", action: "audit", sourceSymbol: "web parity audit", sourceLines: "collect-web-xyzbc-trt-evidence.mjs", expected: "request native/Web parity audit" },
|
||||
{ id: "toolbar-estop", action: "estop", sourceSymbol: "commands.estop_clicked", sourceLines: "axis.py:2223-2229", expected: "toggle ESTOP/ESTOP_RESET" },
|
||||
{ id: "toolbar-power", action: "power", sourceSymbol: "commands.onoff_clicked", sourceLines: "axis.py:2231-2241", expected: "toggle machine power" },
|
||||
{ id: "toolbar-load", action: "open", sourceSymbol: "commands.open_file", sourceLines: "axis.py:2243-2262", expected: "open local G-code" },
|
||||
{ id: "toolbar-reload", action: "reload", sourceSymbol: "commands.reload_file", sourceLines: "axis.py:2296-2297", expected: "reload active program" },
|
||||
{ id: "toolbar-run", action: "run", sourceSymbol: "commands.task_run", sourceLines: "axis.py:2308-2320", expected: "start AUTO run" },
|
||||
{ id: "toolbar-pause-resume", action: "pause", sourceSymbol: "commands.task_pauseresume", sourceLines: "axis.py:2353-2363", expected: "pause or resume based on paused state" },
|
||||
{ id: "toolbar-step", action: "step", sourceSymbol: "commands.task_step", sourceLines: "axis.py:2321-2327", expected: "single step" },
|
||||
{ id: "toolbar-stop", action: "stop", sourceSymbol: "commands.task_stop", sourceLines: "axis.py:2365-2372", expected: "abort current task" },
|
||||
{ id: "toolbar-view-z", action: "view-z", sourceSymbol: "commands.set_view_z", sourceLines: "axis.py:2208-2215", expected: "Z view" },
|
||||
{ id: "toolbar-view-y", action: "view-y", sourceSymbol: "commands.set_view_y", sourceLines: "axis.py:2192-2200", expected: "Y view" },
|
||||
{ id: "toolbar-view-x", action: "view-x", sourceSymbol: "commands.set_view_x", sourceLines: "axis.py:2182-2190", expected: "X view" },
|
||||
{ id: "toolbar-view-p", action: "view-p", sourceSymbol: "commands.set_view_p", sourceLines: "axis.py:2220-2221", expected: "perspective view" },
|
||||
{ id: "toolbar-clear-preview", action: "clear-preview", sourceSymbol: "commands.clear_live_plot", sourceLines: "axis.py:2557-2558", expected: "clear path count" },
|
||||
{ id: "manual-tab", action: "tab-manual", sourceSymbol: "commands.ensure_manual", sourceLines: "axis.py:2520-2531", expected: "manual mode tab selected" },
|
||||
{ id: "mdi-tab", action: "tab-mdi", sourceSymbol: "commands.ensure_mdi", sourceLines: "axis.py:2533-2540", expected: "MDI mode tab selected" },
|
||||
{ id: "manual-select-joint", action: "select-joint", sourceSymbol: "axis radiobuttons ja_rbutton", sourceLines: "axis.py:1369-1380", expected: "active joint/axis selected" },
|
||||
{ id: "manual-jog-increment", action: "jog-increment", sourceSymbol: "set_hal_jogincrement", sourceLines: "axis.py:1507-1515", expected: "jog increment updates HAL-equivalent state" },
|
||||
{ id: "manual-jog-minus", action: "jog-minus", sourceSymbol: "commands.jog_minus", sourceLines: "axis.py:2573-2577", expected: "negative jog of active axis/joint" },
|
||||
{ id: "manual-jog-plus", action: "jog-plus", sourceSymbol: "commands.jog_plus", sourceLines: "axis.py:2567-2571", expected: "positive jog of active axis/joint" },
|
||||
{ id: "manual-touch-off", action: "touch-off", sourceSymbol: "commands.touch_off_system", sourceLines: "axis.py:2631-2650", expected: "G10 touch off MDI equivalent" },
|
||||
{ id: "manual-tool-touch-off", action: "tool-touch-off", sourceSymbol: "commands.touch_off_tool", sourceLines: "axis.py:2651-2685", expected: "tool length offset equivalent" },
|
||||
{ id: "manual-spindle-reverse", action: "spindle-reverse", sourceSymbol: "spindle CCW control", sourceLines: "axis.py:1389-1398", expected: "spindle reverse" },
|
||||
{ id: "manual-spindle-stop", action: "spindle-stop", sourceSymbol: "spindle stop control", sourceLines: "axis.py:1389-1398", expected: "spindle stop" },
|
||||
{ id: "manual-spindle-forward", action: "spindle-forward", sourceSymbol: "spindle CW control", sourceLines: "axis.py:1389-1398", expected: "spindle forward" },
|
||||
{ id: "override-feed-down", action: "feed-override-down", sourceSymbol: "commands.set_feedrate", sourceLines: "axis.py:2131-2137", expected: "feed override down" },
|
||||
{ id: "override-feed-up", action: "feed-override-up", sourceSymbol: "commands.set_feedrate", sourceLines: "axis.py:2131-2137", expected: "feed override up" },
|
||||
{ id: "override-rapid-down", action: "rapid-override-down", sourceSymbol: "commands.set_rapidrate", sourceLines: "axis.py:2139-2145", expected: "rapid override down" },
|
||||
{ id: "override-rapid-up", action: "rapid-override-up", sourceSymbol: "commands.set_rapidrate", sourceLines: "axis.py:2139-2145", expected: "rapid override up" },
|
||||
{ id: "override-spindle-down", action: "spindle-override-down", sourceSymbol: "commands.set_spindlerate", sourceLines: "axis.py:2122-2129", expected: "spindle override down" },
|
||||
{ id: "override-spindle-up", action: "spindle-override-up", sourceSymbol: "commands.set_spindlerate", sourceLines: "axis.py:2122-2129", expected: "spindle override up" },
|
||||
{ id: "manual-ignore-limits", action: "ignore-limits", sourceSymbol: "commands.toggle_override_limits", sourceLines: "axis.py:3161", expected: "limit override checkbox state" },
|
||||
{ id: "manual-block-delete", action: "block-delete", sourceSymbol: "commands.toggle_block_delete", sourceLines: "axis.py:2051-2059", expected: "block delete state" },
|
||||
{ id: "manual-optional-stop", action: "optional-stop", sourceSymbol: "commands.toggle_optional_stop", sourceLines: "axis.py:2047-2049", expected: "optional stop state" },
|
||||
{ id: "manual-flood", action: "toggle-flood", sourceSymbol: "commands.flood_toggle", sourceLines: "axis.py:3176", expected: "flood coolant toggle" },
|
||||
{ id: "manual-mist", action: "toggle-mist", sourceSymbol: "commands.mist_toggle", sourceLines: "axis.py:3175", expected: "mist coolant toggle" },
|
||||
{ id: "mdi-submit", action: "mdi-form", sourceSymbol: "commands.send_mdi", sourceLines: "axis.py:2413-2417", expected: "submit MDI command" },
|
||||
{ id: "mdi-history", action: "mdi-history", sourceSymbol: "commands.mdi_history_butt_1", sourceLines: "axis.py:2499-2516", expected: "run/restore MDI history command" },
|
||||
{ id: "pyvcp-identity", action: "kins-identity", sourceSymbol: "pyvcp.type0-button -> halui.mdi-command-00", sourceLines: "switchkins_postgui.hal", expected: "M429 sets identity kins" },
|
||||
{ id: "pyvcp-tcp", action: "kins-tcp", sourceSymbol: "pyvcp.type1-button -> halui.mdi-command-01", sourceLines: "switchkins_postgui.hal", expected: "M428 sets tcp-xyzbc kins" },
|
||||
{ id: "pyvcp-userk", action: "kins-userk", sourceSymbol: "pyvcp.type2-button -> halui.mdi-command-02", sourceLines: "switchkins_postgui.hal", expected: "M430 sets userk kins" },
|
||||
{ id: "pyvcp-clear", action: "clear-preview", sourceSymbol: "pyvcp.vismach-clear -> vismach.plotclear", sourceLines: "switchkins_postgui.hal", expected: "clear Vismach plot" },
|
||||
];
|
||||
|
||||
const REGIONS = [
|
||||
"menubar",
|
||||
"toolbar",
|
||||
"manual",
|
||||
"main-tabs",
|
||||
"pyvcp",
|
||||
"program",
|
||||
"statusbar",
|
||||
];
|
||||
|
||||
export function mountAxisShell(root, store) {
|
||||
root.innerHTML = `
|
||||
<section class="axis-shell" data-shell="axis-xyzbc-trt">
|
||||
<header class="axis-window-title" data-region="window-title"></header>
|
||||
<nav class="axis-menubar" data-region="menubar"></nav>
|
||||
<nav class="axis-toolbar" data-region="toolbar"></nav>
|
||||
<section class="axis-manual-pane" data-region="manual"></section>
|
||||
<section class="axis-main-pane" data-region="main-tabs"></section>
|
||||
<aside class="axis-pyvcp-pane" data-region="pyvcp"></aside>
|
||||
<section class="axis-program-pane" data-region="program"></section>
|
||||
<footer class="axis-statusbar" data-region="statusbar"></footer>
|
||||
</section>
|
||||
`;
|
||||
|
||||
const regions = Object.fromEntries(
|
||||
REGIONS.map((name) => [name, root.querySelector(`[data-region="${name}"]`)]),
|
||||
);
|
||||
regions.title = root.querySelector('[data-region="window-title"]');
|
||||
|
||||
store.subscribe((state) => render(regions, state, store.dispatch));
|
||||
root.addEventListener("axis-command", (event) => {
|
||||
runAxisCommand(event.detail?.command, store.getState(), store.dispatch, root);
|
||||
});
|
||||
|
||||
return {
|
||||
getRegions() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(regions).map(([name, element]) => [name, Boolean(element)]),
|
||||
);
|
||||
},
|
||||
getButtonParity() {
|
||||
return AXIS_BUTTON_PARITY;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function render(regions, state, dispatch) {
|
||||
renderTitle(regions.title, state);
|
||||
renderMenu(regions.menubar, state, dispatch);
|
||||
renderToolbar(regions.toolbar, state, dispatch);
|
||||
renderManual(regions.manual, state, dispatch);
|
||||
renderMainTabs(regions["main-tabs"], state, dispatch);
|
||||
renderPyvcp(regions.pyvcp, state, dispatch);
|
||||
renderProgram(regions.program, state);
|
||||
renderStatusbar(regions.statusbar, state);
|
||||
}
|
||||
|
||||
function renderTitle(element, state) {
|
||||
element.textContent = `xyzbc_switchkins.ngc - AXIS 2.10.0~pre1 on sim-xyzbc-trt-kins (switchkins)`;
|
||||
element.dataset.machineProfile = state.machineProfile;
|
||||
element.dataset.axisSourceRef = "axis.py root_window title / DISPLAY=axis";
|
||||
}
|
||||
|
||||
function renderMenu(element, state, dispatch) {
|
||||
element.innerHTML = `
|
||||
${menuButton("File", [
|
||||
["open", "Open..."],
|
||||
["reload", "Reload"],
|
||||
["stage", "Stage LinuxCNC files"],
|
||||
["save-session", "Save session"],
|
||||
["restore-session", "Restore session"],
|
||||
])}
|
||||
${menuButton("Machine", [
|
||||
["estop", "Toggle Emergency Stop"],
|
||||
["power", "Toggle Machine Power"],
|
||||
["home-all", "Home All"],
|
||||
["run-ready", "Run Ready"],
|
||||
["run", "Run"],
|
||||
[state.runState === "paused" ? "resume" : "pause", state.runState === "paused" ? "Resume" : "Pause"],
|
||||
["step", "Step"],
|
||||
["stop", "Stop"],
|
||||
])}
|
||||
${menuButton("View", [
|
||||
["view-z", "Top Z"],
|
||||
["view-y", "Front Y"],
|
||||
["view-x", "Side X"],
|
||||
["view-p", "Perspective"],
|
||||
["clear-preview", "Clear Live Plot"],
|
||||
])}
|
||||
${menuButton("Help", [
|
||||
["audit", "Run parity audit"],
|
||||
])}
|
||||
`;
|
||||
element.querySelectorAll("[data-menu-command]").forEach((button) => {
|
||||
tagAxisControl(button, button.dataset.menuCommand);
|
||||
button.addEventListener("click", () => runAxisCommand(button.dataset.menuCommand, state, dispatch, element));
|
||||
});
|
||||
}
|
||||
|
||||
function renderToolbar(element, state, dispatch) {
|
||||
element.innerHTML = `
|
||||
<input type="file" class="axis-file-input" data-action="OPEN_FILE" accept=".ngc,.nc,.tap,.gcode,.txt" />
|
||||
${toolButton("tbtn_estop", "estop", "Emergency stop", state.machine.estopActive)}
|
||||
${toolButton("tbtn_on", "power", "Machine power", state.machine.powerOn)}
|
||||
${toolButton("btn_load", "open", "Open program")}
|
||||
${toolButton("btn_reload", "reload", "Reload program")}
|
||||
${toolButton("btn_run", "run", "Run program")}
|
||||
${toolButton("tbtn_pause", state.runState === "paused" ? "resume" : "pause", state.runState === "paused" ? "Resume" : "Pause", state.runState === "paused")}
|
||||
${toolButton("btn_step", "step", "Step")}
|
||||
${toolButton("btn_stop", "stop", "Stop")}
|
||||
<span class="axis-toolbar-separator"></span>
|
||||
${smallTextTool("Z", "view-z", "View Z")}
|
||||
${smallTextTool("Y", "view-y", "View Y")}
|
||||
${smallTextTool("X", "view-x", "View X")}
|
||||
${smallTextTool("P", "view-p", "Fit perspective")}
|
||||
${toolButton("tbtn_view_tool_path", "clear-preview", "Clear plot")}
|
||||
`;
|
||||
|
||||
element.querySelectorAll(".axis-tool-button[data-action]").forEach((button) => {
|
||||
tagAxisControl(button, button.dataset.action);
|
||||
if (button.dataset.action === "open") return;
|
||||
button.addEventListener("click", () => runAxisCommand(button.dataset.action, state, dispatch, element));
|
||||
});
|
||||
element.querySelector('[data-action="open"]').addEventListener("click", () => element.querySelector('[data-action="OPEN_FILE"]').click());
|
||||
element.querySelector('[data-action="OPEN_FILE"]').addEventListener("change", async (event) => {
|
||||
const [file] = event.target.files || [];
|
||||
if (!file) return;
|
||||
dispatch({ type: "LOAD_PROGRAM", filename: file.name, content: await file.text() });
|
||||
event.target.value = "";
|
||||
});
|
||||
}
|
||||
|
||||
function renderManual(element, state, dispatch) {
|
||||
const selectedJoint = Number(state.machine?.selectedJoint ?? 0);
|
||||
const manualActive = state.machine.mode !== "mdi";
|
||||
element.innerHTML = `
|
||||
<div class="axis-tabs axis-left-tabs">
|
||||
<button type="button" data-action="tab-manual" class="${manualActive ? "active" : ""}">Manual Control [F3]</button>
|
||||
<button type="button" data-action="tab-mdi" class="${manualActive ? "" : "active"}">MDI [F5]</button>
|
||||
</div>
|
||||
<section class="axis-manual-box" ${manualActive ? "" : 'hidden'}>
|
||||
<div class="joint-grid">
|
||||
<span>Joint:</span>
|
||||
${[0, 1, 2, 3, 4].map((joint) => `
|
||||
<label><input type="radio" name="axis-joint" value="${joint}" data-action="select-joint" ${selectedJoint === joint ? "checked" : ""} /> ${joint}</label>
|
||||
`).join("")}
|
||||
</div>
|
||||
<div class="jog-row">
|
||||
<button type="button" data-action="jog-minus">-</button>
|
||||
<button type="button" data-action="jog-plus">+</button>
|
||||
<select data-action="jog-increment" aria-label="Jog mode">
|
||||
${jogIncrementOptions(state).map((option) => `
|
||||
<option value="${option.value}" ${option.selected ? "selected" : ""}>${option.label}</option>
|
||||
`).join("")}
|
||||
</select>
|
||||
</div>
|
||||
<div class="manual-button-row">
|
||||
<button type="button" data-action="home-all">Home All</button>
|
||||
<button type="button" data-action="touch-off">Touch Off</button>
|
||||
</div>
|
||||
<div class="manual-button-row shifted">
|
||||
<button type="button" data-action="tool-touch-off">Tool Touch Off</button>
|
||||
</div>
|
||||
<div class="spindle-row">
|
||||
<span>Spindle:</span>
|
||||
<button type="button" data-action="spindle-reverse">Rev</button>
|
||||
<button type="button" data-action="spindle-stop">Stop</button>
|
||||
<button type="button" data-action="spindle-forward">Fwd</button>
|
||||
</div>
|
||||
<div class="jog-row spindle-jog">
|
||||
<button type="button" data-action="spindle-override-down">-</button>
|
||||
<button type="button" data-action="spindle-override-up">+</button>
|
||||
</div>
|
||||
</section>
|
||||
<section class="axis-mdi-box" ${manualActive ? 'hidden' : ""}>
|
||||
<form class="axis-mdi-command" data-action="mdi-form">
|
||||
<label>MDI Command:</label>
|
||||
<input type="text" data-action="mdi-input" value="${escapeHtml(state.machine.mdiCommand || "")}" spellcheck="false" autocomplete="off" />
|
||||
<button type="submit">Go</button>
|
||||
</form>
|
||||
<div class="axis-mdi-history">
|
||||
${mdiQuickCommands(state).map((command) => `
|
||||
<button type="button" data-action="mdi-history" data-command="${escapeHtml(command)}">${escapeHtml(command)}</button>
|
||||
`).join("")}
|
||||
</div>
|
||||
</section>
|
||||
<section class="axis-override-box">
|
||||
${sliderRow("Feed Override:", `${state.feed.feedOverride}%`, state.feed.feedOverride, "feed")}
|
||||
${sliderRow("Rapid Override:", `${state.feed.rapidOverride}%`, state.feed.rapidOverride, "rapid")}
|
||||
${sliderRow("Spindle Override:", `${state.spindle.override}%`, state.spindle.override, "spindle")}
|
||||
${sliderRow("Jog Speed:", "1098 mm/min", 42)}
|
||||
${sliderRow("Jog Speed:", "21600 deg/min", 78)}
|
||||
${sliderRow("Max Velocity:", "60000 mm/min", 92)}
|
||||
<div class="axis-toggle-row">
|
||||
<label><input type="checkbox" data-action="ignore-limits" ${state.gmoccapyGui.ignoreLimits ? "checked" : ""} /> Ignore limits</label>
|
||||
<label><input type="checkbox" data-action="block-delete" ${state.gmoccapyGui.optionalBlocks ? "checked" : ""} /> Optional block</label>
|
||||
<label><input type="checkbox" data-action="optional-stop" ${state.gmoccapyGui.optionalStop ? "checked" : ""} /> Optional stop</label>
|
||||
</div>
|
||||
<div class="axis-toggle-row">
|
||||
<button type="button" data-action="toggle-flood">Flood</button>
|
||||
<button type="button" data-action="toggle-mist">Mist</button>
|
||||
</div>
|
||||
<div class="active-gcodes-label">Active G-Codes:</div>
|
||||
<div class="active-gcodes">G80 G17 G40 G21 G90 G94 G54 G49 G99 G64<br />G97 G91.1 G8 M5 M9 M48 M53 F0 S0</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
element.querySelectorAll("[data-action]").forEach((control) => {
|
||||
tagAxisControl(control, control.dataset.action);
|
||||
});
|
||||
|
||||
element.querySelector('[data-action="tab-manual"]').addEventListener("click", () => dispatch({ type: "SET_MODE", mode: "manual" }));
|
||||
element.querySelector('[data-action="tab-mdi"]').addEventListener("click", () => dispatch({ type: "SET_MODE", mode: "mdi" }));
|
||||
element.querySelectorAll('[data-action="select-joint"]').forEach((input) => {
|
||||
input.addEventListener("change", () => dispatch({ type: "SET_ACTIVE_JOINT", joint: Number(input.value) }));
|
||||
});
|
||||
const jogIncrement = element.querySelector('[data-action="jog-increment"]');
|
||||
if (jogIncrement) {
|
||||
jogIncrement.addEventListener("change", () => dispatch({ type: "SET_JOG_INCREMENT", increment: Number(jogIncrement.value) }));
|
||||
}
|
||||
element.querySelectorAll("[data-action]").forEach((control) => {
|
||||
const action = control.dataset.action;
|
||||
if (["tab-manual", "tab-mdi", "select-joint", "jog-increment", "mdi-form", "mdi-input", "mdi-history"].includes(action)) return;
|
||||
control.addEventListener("click", () => runAxisCommand(action, state, dispatch, element));
|
||||
});
|
||||
const mdiForm = element.querySelector('[data-action="mdi-form"]');
|
||||
if (mdiForm) {
|
||||
mdiForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
const input = element.querySelector('[data-action="mdi-input"]');
|
||||
dispatch({ type: "RUN_MDI", command: input.value });
|
||||
});
|
||||
}
|
||||
element.querySelectorAll('[data-action="mdi-history"]').forEach((button) => {
|
||||
button.addEventListener("click", () => dispatch({ type: "RUN_MDI", command: button.dataset.command }));
|
||||
});
|
||||
element.querySelector('[data-action="ignore-limits"]')?.addEventListener("change", (event) => {
|
||||
dispatch({ type: "SET_IGNORE_LIMITS", enabled: event.target.checked });
|
||||
});
|
||||
element.querySelector('[data-action="block-delete"]')?.addEventListener("change", (event) => {
|
||||
dispatch({ type: "SET_BLOCK_DELETE", enabled: event.target.checked });
|
||||
});
|
||||
element.querySelector('[data-action="optional-stop"]')?.addEventListener("change", (event) => {
|
||||
dispatch({ type: "SET_OPTIONAL_STOP", enabled: event.target.checked });
|
||||
});
|
||||
}
|
||||
|
||||
function renderMainTabs(element, state, dispatch) {
|
||||
if (element.dataset.mounted !== "true") {
|
||||
element.innerHTML = `
|
||||
<div class="axis-tabs axis-main-tabs">
|
||||
<button type="button" class="active">Preview</button>
|
||||
<button type="button">DRO</button>
|
||||
<button type="button">xyzbc_switchkins_sub</button>
|
||||
<button type="button">centering</button>
|
||||
</div>
|
||||
<div class="axis-preview-wrap">
|
||||
<div class="axis-preview-readout" data-preview-readout></div>
|
||||
<canvas class="axis-preview-canvas" data-five-axis-canvas="true" aria-label="AXIS XYZBC TRT preview"></canvas>
|
||||
</div>
|
||||
`;
|
||||
element.dataset.mounted = "true";
|
||||
}
|
||||
|
||||
const readout = element.querySelector("[data-preview-readout]");
|
||||
readout.innerHTML = [0, 1, 2, 3, 4].map((joint) => `${joint}: ${formatNumber(jointValue(state, joint), 4)}`).join("<br />");
|
||||
readout.dataset.kinsType = state.kinsType;
|
||||
|
||||
renderFiveAxisScene(element.querySelector("[data-five-axis-canvas]"), state);
|
||||
}
|
||||
|
||||
function renderPyvcp(element, state, dispatch) {
|
||||
const current = switchkinsValue(state.kinsType);
|
||||
element.innerHTML = `
|
||||
<section class="switchkins-panel" data-current-kins="${current}">
|
||||
<h2>SWITCHKINS</h2>
|
||||
<div class="switchkins-active" data-active="${current}">${switchkinsLegend(current)}</div>
|
||||
<button type="button" data-action="kins-identity" ${current === 0 ? 'data-active="true"' : ""}>IDENTITY</button>
|
||||
<button type="button" data-action="kins-tcp" ${current === 1 ? 'data-active="true"' : ""}>TCP:XYZBC</button>
|
||||
<button type="button" data-action="kins-userk" ${current === 2 ? 'data-active="true"' : ""}>userk</button>
|
||||
<button type="button" data-action="clear-preview">vismach-clear</button>
|
||||
</section>
|
||||
`;
|
||||
element.querySelector('[data-action="kins-identity"]').addEventListener("click", () => dispatch({ type: "RUN_MDI", command: "M429" }));
|
||||
element.querySelector('[data-action="kins-tcp"]').addEventListener("click", () => dispatch({ type: "RUN_MDI", command: "M428" }));
|
||||
element.querySelector('[data-action="kins-userk"]').addEventListener("click", () => dispatch({ type: "RUN_MDI", command: "M430" }));
|
||||
element.querySelector('[data-action="clear-preview"]').addEventListener("click", () => dispatch({ type: "CLEAR_PREVIEW" }));
|
||||
tagAxisControl(element.querySelector('[data-action="kins-identity"]'), "kins-identity");
|
||||
tagAxisControl(element.querySelector('[data-action="kins-tcp"]'), "kins-tcp");
|
||||
tagAxisControl(element.querySelector('[data-action="kins-userk"]'), "kins-userk");
|
||||
tagAxisControl(element.querySelector('[data-action="clear-preview"]'), "clear-preview", "pyvcp-clear");
|
||||
}
|
||||
|
||||
function renderProgram(element, state) {
|
||||
const currentLine = currentGcodeExecutionLine(state);
|
||||
const lines = state.programLines?.length
|
||||
? state.programLines.slice(0, 80)
|
||||
: [";"];
|
||||
element.innerHTML = `
|
||||
<ol class="axis-program-list">
|
||||
${lines.map((line, index) => {
|
||||
const lineNo = state.programStartLine + index;
|
||||
const active = lineNo === currentLine ? " active" : "";
|
||||
return `<li class="${active}" data-program-line="${lineNo}"><span>${lineNo}:</span><code>${escapeHtml(line)}</code></li>`;
|
||||
}).join("")}
|
||||
</ol>
|
||||
`;
|
||||
const activeRow = element.querySelector(".axis-program-list .active");
|
||||
if (activeRow) activeRow.scrollIntoView({ block: "center" });
|
||||
}
|
||||
|
||||
function renderStatusbar(element, state) {
|
||||
element.innerHTML = `
|
||||
<div>${state.machine.powerOn ? "ON" : state.machine.estopActive ? "ESTOP" : "OFF"}</div>
|
||||
<div>No tool</div>
|
||||
<div>Position: Joint</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function toolButton(buttonId, action, title, active = false) {
|
||||
const icon = getGmoccapyIcon(buttonId, active ? "active" : "inactive");
|
||||
const iconMarkup = icon.path
|
||||
? `<img src="${escapeHtml(icon.path)}" alt="" />`
|
||||
: `<span>${escapeHtml(title.slice(0, 1))}</span>`;
|
||||
return `<button type="button" class="axis-tool-button" data-action="${action}" title="${escapeHtml(title)}">${iconMarkup}</button>`;
|
||||
}
|
||||
|
||||
function smallTextTool(label, action, title) {
|
||||
return `<button type="button" class="axis-tool-button axis-text-tool" data-action="${action}" title="${escapeHtml(title)}">${escapeHtml(label)}</button>`;
|
||||
}
|
||||
|
||||
function menuButton(label, entries) {
|
||||
return `
|
||||
<div class="axis-menu">
|
||||
<button type="button">${escapeHtml(label)}</button>
|
||||
<div class="axis-menu-popup">
|
||||
${entries.map(([command, text]) => `<button type="button" data-menu-command="${escapeHtml(command)}">${escapeHtml(text)}</button>`).join("")}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function tagAxisControl(element, action, parityId = null) {
|
||||
const parity = parityId
|
||||
? AXIS_BUTTON_PARITY.find((item) => item.id === parityId)
|
||||
: AXIS_BUTTON_PARITY.find((item) => item.action === action);
|
||||
if (!parity || !element) return;
|
||||
element.dataset.axisSourceRef = parity.sourceSymbol;
|
||||
element.dataset.axisSourceLines = parity.sourceLines;
|
||||
element.dataset.axisExpectedEffect = parity.expected;
|
||||
}
|
||||
|
||||
function sliderRow(label, value, percent, target = null) {
|
||||
const clamped = Math.min(Math.max(Number(percent) || 0, 0), 120);
|
||||
return `
|
||||
<div class="axis-slider-row">
|
||||
<span>${escapeHtml(label)}</span>
|
||||
<strong>${escapeHtml(value)}</strong>
|
||||
<div class="axis-slider"><i style="width:${Math.min(clamped, 100)}%"></i><b></b></div>
|
||||
${target ? `
|
||||
<button type="button" data-action="${target}-override-down">-</button>
|
||||
<button type="button" data-action="${target}-override-up">+</button>
|
||||
` : ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function currentGcodeExecutionLine(state) {
|
||||
const feedbackLine = Number(state.programRuntimeFeedback?.line);
|
||||
if (Number.isFinite(feedbackLine) && feedbackLine > 0) return feedbackLine;
|
||||
const activeLine = Number(state.activeLine);
|
||||
return Number.isFinite(activeLine) && activeLine > 0 ? activeLine : state.programStartLine;
|
||||
}
|
||||
|
||||
function jointAxis(joint) {
|
||||
return ["x", "y", "z", "b", "c"][joint] || "x";
|
||||
}
|
||||
|
||||
function jogIncrementOptions(state) {
|
||||
const current = Number(state.machine?.jogIncrement ?? 1);
|
||||
return [
|
||||
{ value: 0, label: "Continuous" },
|
||||
{ value: 0.001, label: "0.0010" },
|
||||
{ value: 0.01, label: "0.0100" },
|
||||
{ value: 0.1, label: "0.1000" },
|
||||
{ value: 1, label: "1.0000" },
|
||||
{ value: 10, label: "10.0000" },
|
||||
].map((option) => ({
|
||||
...option,
|
||||
selected: option.value === current || (option.value === 0 && current === 0),
|
||||
}));
|
||||
}
|
||||
|
||||
function mdiQuickCommands(state) {
|
||||
const profileCommands = (state.profile?.kinematicsParameters?.switchkinsTypes || [])
|
||||
.map((type) => type.mdiCommand)
|
||||
.filter(Boolean);
|
||||
return [
|
||||
...(state.mdiHistory || []),
|
||||
state.machine?.mdiCommand,
|
||||
"G0 X0 Y0 Z0",
|
||||
"G91 X1",
|
||||
"G90",
|
||||
...profileCommands,
|
||||
].filter((command, index, commands) => command && commands.indexOf(command) === index).slice(0, 8);
|
||||
}
|
||||
|
||||
function runAxisCommand(command, state, dispatch, root) {
|
||||
const selectedJoint = Number(state.machine?.selectedJoint ?? 0);
|
||||
const selectedAxis = jointAxis(selectedJoint);
|
||||
const actionMap = {
|
||||
estop: () => dispatch({ type: state.machine?.estopActive || state.machine?.taskState === "estop" ? "RESET" : "ESTOP" }),
|
||||
power: () => dispatch({ type: "TOGGLE_POWER" }),
|
||||
reload: () => dispatch({ type: "RELOAD_PROGRAM" }),
|
||||
run: () => dispatch({ type: "RUN" }),
|
||||
pause: () => dispatch({ type: "PAUSE" }),
|
||||
resume: () => dispatch({ type: "RESUME" }),
|
||||
step: () => dispatch({ type: "STEP" }),
|
||||
stop: () => dispatch({ type: "STOP" }),
|
||||
"run-ready": () => dispatch({ type: "RUN_READY" }),
|
||||
"home-all": () => dispatch({ type: "HOME" }),
|
||||
"jog-minus": () => dispatch({ type: "JOG", axis: selectedAxis, direction: -1, increment: Number(state.machine?.jogIncrement ?? 1) || 1 }),
|
||||
"jog-plus": () => dispatch({ type: "JOG", axis: selectedAxis, direction: 1, increment: Number(state.machine?.jogIncrement ?? 1) || 1 }),
|
||||
"touch-off": () => dispatch({ type: "RUN_MDI", command: `G10 L20 P0 ${selectedAxis.toUpperCase()}0` }),
|
||||
"tool-touch-off": () => dispatch({ type: "RUN_MDI", command: "G43" }),
|
||||
"spindle-stop": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "stop" }),
|
||||
"spindle-forward": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "forward" }),
|
||||
"spindle-reverse": () => dispatch({ type: "SET_SPINDLE_DIRECTION", direction: "reverse" }),
|
||||
"spindle-override-down": () => dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: -10 }),
|
||||
"spindle-override-up": () => dispatch({ type: "ADJUST_SPINDLE_OVERRIDE", delta: 10 }),
|
||||
"feed-override-down": () => dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: -10 }),
|
||||
"feed-override-up": () => dispatch({ type: "ADJUST_OVERRIDE", target: "feed", delta: 10 }),
|
||||
"rapid-override-down": () => dispatch({ type: "ADJUST_OVERRIDE", target: "rapid", delta: -10 }),
|
||||
"rapid-override-up": () => dispatch({ type: "ADJUST_OVERRIDE", target: "rapid", delta: 10 }),
|
||||
"toggle-flood": () => dispatch({ type: "TOGGLE_COOLANT", kind: "flood" }),
|
||||
"toggle-mist": () => dispatch({ type: "TOGGLE_COOLANT", kind: "mist" }),
|
||||
"clear-preview": () => dispatch({ type: "CLEAR_PREVIEW" }),
|
||||
"view-p": () => dispatch({ type: "RESET_VIEW" }),
|
||||
"view-x": () => dispatch({ type: "SET_VIEW", view: "x" }),
|
||||
"view-y": () => dispatch({ type: "SET_VIEW", view: "y" }),
|
||||
"view-z": () => dispatch({ type: "SET_VIEW", view: "z" }),
|
||||
stage: () => dispatch({ type: "STAGE_MACHINE_FILES_REQUEST" }),
|
||||
"save-session": () => dispatch({ type: "SAVE_SESSION_REQUEST" }),
|
||||
"restore-session": () => dispatch({ type: "RESTORE_SESSION_REQUEST" }),
|
||||
audit: () => dispatch({ type: "RUN_FULL_BOUNDARY_AUDIT_REQUEST" }),
|
||||
};
|
||||
if (command === "open") {
|
||||
const shellRoot = root.closest?.(".axis-shell") || root;
|
||||
shellRoot.querySelector?.('[data-action="OPEN_FILE"]')?.click();
|
||||
return;
|
||||
}
|
||||
actionMap[command]?.();
|
||||
}
|
||||
|
||||
function jointValue(state, joint) {
|
||||
const axes = ["x", "y", "z", "b", "c"];
|
||||
return Number(state.dro?.[axes[joint]] || 0);
|
||||
}
|
||||
|
||||
function switchkinsValue(kinsType) {
|
||||
if (String(kinsType).startsWith("tcp-")) return 1;
|
||||
if (kinsType === "userk") return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function switchkinsLegend(value) {
|
||||
if (value === 1) return "1:XYZBC";
|
||||
if (value === 2) return "2:USERK";
|
||||
return "0:IDENTITY";
|
||||
}
|
||||
|
||||
function formatNumber(value, digits = 3) {
|
||||
const number = Number(value);
|
||||
return Number.isFinite(number) ? number.toFixed(digits) : (0).toFixed(digits);
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value ?? "")
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user