|
|
|
|
@@ -13,6 +13,7 @@ import { XYZAC_CASE, projectXyzacPanel, type XyzacOperationTrace } from "./xyzac
|
|
|
|
|
export const SELECTED_XYZAC_CASE = XYZAC_CASE;
|
|
|
|
|
const DEFAULT_CASE = SELECTED_XYZAC_CASE;
|
|
|
|
|
const PROGRAM_PATH = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc";
|
|
|
|
|
const XYZAC_AXIS_TO_JOINT = Object.freeze({ x: 0, y: 1, z: 2, a: 3, c: 4 } as const);
|
|
|
|
|
const normalizeProgramSource = (source: string) => source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
|
|
|
// Button-to-command sequencing is defined in
|
|
|
|
|
// docs/linuxcnc-axis-web-control-panel-source-logic.md; React only projects
|
|
|
|
|
@@ -239,8 +240,10 @@ export function App() {
|
|
|
|
|
const aboutDialog = useRef<HTMLDivElement>(null);
|
|
|
|
|
const [layers, setLayers] = useState({ grid: true, tool: true, path: true, live: true, limits: false, velocity: false, offsets: false, extents: false });
|
|
|
|
|
const runTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
const homeTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
const jogTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
const runStepPending = useRef(false);
|
|
|
|
|
const homeStepPending = useRef(false);
|
|
|
|
|
const jogStepPending = useRef(false);
|
|
|
|
|
const continuousJog = useRef<{ axis: string; direction: number } | null>(null);
|
|
|
|
|
const programFileInput = useRef<HTMLInputElement>(null);
|
|
|
|
|
@@ -315,7 +318,7 @@ export function App() {
|
|
|
|
|
setCommandStatus("PROGRAM_LOAD acknowledged");
|
|
|
|
|
setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`);
|
|
|
|
|
}).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); }).finally(() => setProgramLoading(false));
|
|
|
|
|
return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current); if (uiCacheSaveTimer.current) clearTimeout(uiCacheSaveTimer.current); client.close(); };
|
|
|
|
|
return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (homeTimer.current) clearInterval(homeTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current); if (uiCacheSaveTimer.current) clearTimeout(uiCacheSaveTimer.current); client.close(); };
|
|
|
|
|
}, [client, operationController]);
|
|
|
|
|
const axes = state.snapshot?.motion?.axes ?? [];
|
|
|
|
|
const taskRecord = state.task ?? {};
|
|
|
|
|
@@ -348,6 +351,17 @@ export function App() {
|
|
|
|
|
const relief = state.snapshot?.ui_relief ?? {};
|
|
|
|
|
const canLoad = !programLoading && gates.load.enabled;
|
|
|
|
|
const stopRunTimer = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } };
|
|
|
|
|
const stopHomeTimer = () => { if (homeTimer.current) { clearInterval(homeTimer.current); homeTimer.current = null; } };
|
|
|
|
|
const startHomeTimer = () => {
|
|
|
|
|
if (homeTimer.current) return;
|
|
|
|
|
homeTimer.current = setInterval(() => {
|
|
|
|
|
if (homeStepPending.current) return;
|
|
|
|
|
homeStepPending.current = true;
|
|
|
|
|
void operationController.execute("SIM_STEP", { cycles: 5, response: "motion_delta" }, { state: axisState, gate: false })
|
|
|
|
|
.catch(() => stopHomeTimer())
|
|
|
|
|
.finally(() => { homeStepPending.current = false; });
|
|
|
|
|
}, 50);
|
|
|
|
|
};
|
|
|
|
|
const stopJogTimer = () => { continuousJog.current = null; if (jogTimer.current) { clearInterval(jogTimer.current); jogTimer.current = null; } };
|
|
|
|
|
const startJogTimer = (axis: string, direction: number) => {
|
|
|
|
|
continuousJog.current = { axis, direction };
|
|
|
|
|
@@ -373,7 +387,7 @@ export function App() {
|
|
|
|
|
}
|
|
|
|
|
const pending = operationController.execute(kind, payload, { state: axisState, gate, ...(action ? { action } : {}) });
|
|
|
|
|
setOperationTrace(operationController.exportTrace());
|
|
|
|
|
return pending.then(() => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN" || kind === "PROGRAM_RESUME") startRunTimer(); if (["PROGRAM_PAUSE", "PROGRAM_ABORT", "PROGRAM_RESTART"].includes(kind)) stopRunTimer(); if (kind === "JOG_START") startJogTimer(String(payload.axis ?? "x"), Number(payload.direction)); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => {
|
|
|
|
|
return pending.then((operation) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN" || kind === "PROGRAM_RESUME") startRunTimer(); if (["PROGRAM_PAUSE", "PROGRAM_ABORT", "PROGRAM_RESTART"].includes(kind)) stopRunTimer(); if (kind === "HOME" && (operation.response as { payload?: { homing?: { phase?: string } } })?.payload?.homing?.phase === "moving") startHomeTimer(); if (kind === "JOG_START") startJogTimer(String(payload.axis ?? "x"), Number(payload.direction)); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => {
|
|
|
|
|
setOperationTrace(operationController.exportTrace());
|
|
|
|
|
const code = error.message;
|
|
|
|
|
setCommandStatus(`${kind} blocked: ${code}`);
|
|
|
|
|
@@ -396,7 +410,13 @@ export function App() {
|
|
|
|
|
const homeAxis = () => {
|
|
|
|
|
if (programLoading || !gates.home.enabled) return;
|
|
|
|
|
// switchkins.c reports KINEMATICS_BOTH even when its active type is 0.
|
|
|
|
|
void dispatch("HOME", { axis: selectedAxis }).catch(() => undefined);
|
|
|
|
|
// AXIS therefore homes a numeric joint; this fixed XYZAC Web surface
|
|
|
|
|
// maps its visible one-to-one axis selector to that joint explicitly.
|
|
|
|
|
const joint = XYZAC_AXIS_TO_JOINT[selectedAxis as keyof typeof XYZAC_AXIS_TO_JOINT];
|
|
|
|
|
if (joint === undefined) { setCommandStatus("HOME blocked: home_axis_invalid"); return; }
|
|
|
|
|
const confirm = globalThis.confirm;
|
|
|
|
|
if (Boolean(homedAxes[joint]) && typeof confirm === "function" && !confirm(`Joint ${joint} is already homed. Re-home it?`)) return;
|
|
|
|
|
void dispatch("HOME", { axis: selectedAxis, joint }).catch(() => undefined);
|
|
|
|
|
};
|
|
|
|
|
const stopPreviewProgress = () => {
|
|
|
|
|
if (previewProgressTimer.current) {
|
|
|
|
|
@@ -625,7 +645,8 @@ export function App() {
|
|
|
|
|
const mdiMotionActive = !programRunning && String(state.task?.mode ?? "").toUpperCase() === "MDI" && wasmQueueDepth > 0;
|
|
|
|
|
if ((programRunning && !programComplete) || mdiMotionActive || Boolean(state.snapshot?.motion?.stepping)) startRunTimer(); else stopRunTimer();
|
|
|
|
|
if (wasmQueueDepth === 0 && !jogStepPending.current) stopJogTimer();
|
|
|
|
|
if (!ready) { stopRunTimer(); stopJogTimer(); }
|
|
|
|
|
if (wasmQueueDepth === 0 && !homeStepPending.current) stopHomeTimer();
|
|
|
|
|
if (!ready) { stopRunTimer(); stopHomeTimer(); stopJogTimer(); }
|
|
|
|
|
}, [programRunning, ready, state.program?.program_status, state.task?.mode, state.snapshot?.wasm_motion]);
|
|
|
|
|
const spindleState = (state.devices as Record<string, any> | null)?.spindles?.[0] as Record<string, any> | undefined;
|
|
|
|
|
const spindleDirection = Number(spindleState?.direction ?? 0);
|
|
|
|
|
|