Show toolpath preview loading progress
This commit is contained in:
@@ -189,7 +189,9 @@ export function App() {
|
|||||||
const programFileInput = useRef<HTMLInputElement>(null);
|
const programFileInput = useRef<HTMLInputElement>(null);
|
||||||
const [plotCleared, setPlotCleared] = useState(false);
|
const [plotCleared, setPlotCleared] = useState(false);
|
||||||
const [operationTrace, setOperationTrace] = useState<XyzacOperationTrace[]>([]);
|
const [operationTrace, setOperationTrace] = useState<XyzacOperationTrace[]>([]);
|
||||||
useEffect(() => { const unsubscribe = client.subscribe((message) => setState((previous) => reduceFrontendState(previous, message))); client.ensureSession().then(async () => { const bundle = await loadSimulationCase({ path: DEFAULT_CASE, baseUrl: "." }); if (bundle.status !== "ready") throw new Error(`xyzac_case_blocked:${bundle.missing.join(",")}`); const source = bundle.files?.[PROGRAM_PATH]; if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing"); const normalizedSource = normalizeProgramSource(source); setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); const configuration = toWorkerConfiguration(bundle); await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration }, { state: {}, gate: false }); await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: {}, gate: false }); setOperationTrace(operationController.exportTrace()); setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`); }).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); }); return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); client.close(); }; }, [client, operationController]);
|
const [previewProgress, setPreviewProgress] = useState<{ percent: number; phase: "reading" | "wasm" | "ready" | "error"; detail: string } | null>(null);
|
||||||
|
const previewProgressTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
useEffect(() => { const unsubscribe = client.subscribe((message) => setState((previous) => reduceFrontendState(previous, message))); client.ensureSession().then(async () => { const bundle = await loadSimulationCase({ path: DEFAULT_CASE, baseUrl: "." }); if (bundle.status !== "ready") throw new Error(`xyzac_case_blocked:${bundle.missing.join(",")}`); const source = bundle.files?.[PROGRAM_PATH]; if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing"); const normalizedSource = normalizeProgramSource(source); setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); const configuration = toWorkerConfiguration(bundle); await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration }, { state: {}, gate: false }); await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: {}, gate: false }); setOperationTrace(operationController.exportTrace()); setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`); }).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); }); return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); client.close(); }; }, [client, operationController]);
|
||||||
const axes = state.snapshot?.motion?.axes ?? [0, 0, 0];
|
const axes = state.snapshot?.motion?.axes ?? [0, 0, 0];
|
||||||
const taskRecord = state.task ?? {};
|
const taskRecord = state.task ?? {};
|
||||||
const axisState = { task: { ...taskRecord, state: taskRecord.state ?? "OFF", mode: taskRecord.mode ?? "MANUAL", interp_state: taskRecord.interp_state ?? (state.status === "running" ? "READING" : state.status === "paused" ? "PAUSED" : "IDLE") }, program: state.program ?? {}, motion: state.snapshot?.motion ?? {}, devices: state.devices ?? {}, xyzac: state.snapshot?.xyzac ?? {}, gates: state.gates };
|
const axisState = { task: { ...taskRecord, state: taskRecord.state ?? "OFF", mode: taskRecord.mode ?? "MANUAL", interp_state: taskRecord.interp_state ?? (state.status === "running" ? "READING" : state.status === "paused" ? "PAUSED" : "IDLE") }, program: state.program ?? {}, motion: state.snapshot?.motion ?? {}, devices: state.devices ?? {}, xyzac: state.snapshot?.xyzac ?? {}, gates: state.gates };
|
||||||
@@ -244,30 +246,66 @@ export function App() {
|
|||||||
const clearLoadedPreview = async () => {
|
const clearLoadedPreview = async () => {
|
||||||
await operationController.execute("VISUAL_CLEAR", {}, { state: axisState, gate: false });
|
await operationController.execute("VISUAL_CLEAR", {}, { state: axisState, gate: false });
|
||||||
};
|
};
|
||||||
|
const stopPreviewProgress = () => {
|
||||||
|
if (previewProgressTimer.current) {
|
||||||
|
clearInterval(previewProgressTimer.current);
|
||||||
|
previewProgressTimer.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const startPreviewProgress = (label: string) => {
|
||||||
|
stopPreviewProgress();
|
||||||
|
setPreviewProgress({ percent: 8, phase: "reading", detail: `${label}: 读取 G-code` });
|
||||||
|
previewProgressTimer.current = setInterval(() => {
|
||||||
|
setPreviewProgress((previous) => {
|
||||||
|
if (!previous || previous.percent >= 88) return previous;
|
||||||
|
return { percent: Math.min(88, previous.percent + 4), phase: "wasm", detail: `${label}: LinuxCNC WASM 解析刀具路径` };
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
const markPreviewProgress = (percent: number, phase: "reading" | "wasm", detail: string) => {
|
||||||
|
setPreviewProgress({ percent, phase, detail });
|
||||||
|
};
|
||||||
|
const finishPreviewProgress = (path: string, response: unknown) => {
|
||||||
|
stopPreviewProgress();
|
||||||
|
const payload = (response && typeof response === "object" ? (response as { payload?: unknown }).payload : null) as { preview?: { segments?: unknown[] } } | null;
|
||||||
|
const segments = Array.isArray(payload?.preview?.segments) ? payload.preview.segments : [];
|
||||||
|
const samples = segments.reduce<number>((count, segment) => count + (segment && typeof segment === "object" && Array.isArray((segment as { samples?: unknown[] }).samples) ? (segment as { samples: unknown[] }).samples.length : 0), 0);
|
||||||
|
const detail = segments.length ? `WASM 刀具路径完成: ${segments.length} 段 / ${samples} 点` : "WASM 刀具路径完成";
|
||||||
|
setPreviewProgress({ percent: 100, phase: "ready", detail: `${path}: ${detail}` });
|
||||||
|
};
|
||||||
|
const failPreviewProgress = (message: string) => {
|
||||||
|
stopPreviewProgress();
|
||||||
|
setPreviewProgress({ percent: 100, phase: "error", detail: `刀具路径加载失败: ${message}` });
|
||||||
|
};
|
||||||
const loadCase = async () => {
|
const loadCase = async () => {
|
||||||
if (programLoading || programRunning) return;
|
if (programLoading || programRunning) return;
|
||||||
setProgramLoading(true);
|
setProgramLoading(true);
|
||||||
|
startPreviewProgress("XYZAC 程序");
|
||||||
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
|
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
|
||||||
setCaseStatus("loading session...");
|
setCaseStatus("loading session...");
|
||||||
try {
|
try {
|
||||||
await client.ensureSession();
|
await client.ensureSession();
|
||||||
setCaseStatus("loading exact XYZAC case...");
|
setCaseStatus("loading exact XYZAC case...");
|
||||||
const bundle = await loadSimulationCase({ path: casePath, baseUrl: "." });
|
const bundle = await loadSimulationCase({ path: casePath, baseUrl: "." });
|
||||||
if (bundle.status !== "ready") { setCaseStatus(`blocked: ${bundle.missing.join(", ")}`); return; }
|
if (bundle.status !== "ready") throw new Error(`xyzac_case_blocked:${bundle.missing.join(",")}`);
|
||||||
const source = bundle.files?.[PROGRAM_PATH];
|
const source = bundle.files?.[PROGRAM_PATH];
|
||||||
if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing");
|
if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing");
|
||||||
const normalizedSource = normalizeProgramSource(source);
|
const normalizedSource = normalizeProgramSource(source);
|
||||||
setCaseStatus("starting exact case...");
|
setCaseStatus("starting exact case...");
|
||||||
const operation = await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }, { state: axisState, gate: false });
|
const operation = await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }, { state: axisState, gate: false });
|
||||||
const resultPayload = (operation.response as { payload?: { configuration?: { state?: string } } }).payload;
|
const resultPayload = (operation.response as { payload?: { configuration?: { state?: string } } }).payload;
|
||||||
|
markPreviewProgress(28, "wasm", "LinuxCNC WASM 配置已加载,准备解析刀具路径");
|
||||||
setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); setPlotCleared(false);
|
setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); setPlotCleared(false);
|
||||||
await clearLoadedPreview();
|
await clearLoadedPreview();
|
||||||
await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: axisState, gate: false });
|
markPreviewProgress(38, "wasm", "LinuxCNC WASM 正在解析刀具路径");
|
||||||
|
const programLoad = await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: axisState, gate: false });
|
||||||
|
finishPreviewProgress(PROGRAM_PATH, programLoad.response);
|
||||||
setOperationTrace(operationController.exportTrace());
|
setOperationTrace(operationController.exportTrace());
|
||||||
setCommandStatus("PROGRAM_LOAD acknowledged");
|
setCommandStatus("PROGRAM_LOAD acknowledged");
|
||||||
setCaseStatus(`${bundle.entrypoint} + ${PROGRAM_PATH} -> ${resultPayload?.configuration?.state ?? "READY"}`);
|
setCaseStatus(`${bundle.entrypoint} + ${PROGRAM_PATH} -> ${resultPayload?.configuration?.state ?? "READY"}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setOperationTrace(operationController.exportTrace());
|
setOperationTrace(operationController.exportTrace());
|
||||||
|
failPreviewProgress(error instanceof Error ? error.message : "case_load_failed");
|
||||||
setCaseStatus(`blocked: ${error instanceof Error ? error.message : "case_load_failed"}`);
|
setCaseStatus(`blocked: ${error instanceof Error ? error.message : "case_load_failed"}`);
|
||||||
} finally { setProgramLoading(false); }
|
} finally { setProgramLoading(false); }
|
||||||
};
|
};
|
||||||
@@ -275,6 +313,7 @@ export function App() {
|
|||||||
if (programLoading || programRunning) return;
|
if (programLoading || programRunning) return;
|
||||||
const path = "linuxcnc/nc_files/3D_Chips.ngc";
|
const path = "linuxcnc/nc_files/3D_Chips.ngc";
|
||||||
setProgramLoading(true);
|
setProgramLoading(true);
|
||||||
|
startPreviewProgress("3D_Chips.ngc");
|
||||||
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
|
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
|
||||||
setCaseStatus(`${path} loading from published workspace...`);
|
setCaseStatus(`${path} loading from published workspace...`);
|
||||||
try {
|
try {
|
||||||
@@ -284,23 +323,40 @@ export function App() {
|
|||||||
if (!source.trim()) throw new Error("program_asset_empty");
|
if (!source.trim()) throw new Error("program_asset_empty");
|
||||||
setProgramText(source); setProgramPath(path); setPlotCleared(false);
|
setProgramText(source); setProgramPath(path); setPlotCleared(false);
|
||||||
await clearLoadedPreview();
|
await clearLoadedPreview();
|
||||||
await operationController.execute("PROGRAM_LOAD", { source, path }, { state: axisState, gate: false });
|
markPreviewProgress(38, "wasm", "LinuxCNC WASM 正在解析刀具路径");
|
||||||
|
const programLoad = await operationController.execute("PROGRAM_LOAD", { source, path }, { state: axisState, gate: false });
|
||||||
|
finishPreviewProgress(path, programLoad.response);
|
||||||
setOperationTrace(operationController.exportTrace());
|
setOperationTrace(operationController.exportTrace());
|
||||||
setCommandStatus("PROGRAM_LOAD acknowledged");
|
setCommandStatus("PROGRAM_LOAD acknowledged");
|
||||||
setCaseStatus(`${path} loaded through linuxcnc-wasm`);
|
setCaseStatus(`${path} loaded through linuxcnc-wasm`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setOperationTrace(operationController.exportTrace());
|
setOperationTrace(operationController.exportTrace());
|
||||||
|
failPreviewProgress(error instanceof Error ? error.message : "program_asset_load_failed");
|
||||||
setCaseStatus(`blocked: ${error instanceof Error ? error.message : "program_asset_load_failed"}`);
|
setCaseStatus(`blocked: ${error instanceof Error ? error.message : "program_asset_load_failed"}`);
|
||||||
} finally { setProgramLoading(false); }
|
} finally { setProgramLoading(false); }
|
||||||
};
|
};
|
||||||
const loadProgram = () => {
|
const loadProgram = () => {
|
||||||
if (programLoading || programRunning) return;
|
if (programLoading || programRunning) return;
|
||||||
setProgramLoading(true);
|
setProgramLoading(true);
|
||||||
|
startPreviewProgress(programPath || "当前程序");
|
||||||
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
|
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
|
||||||
setPlotCleared(false);
|
setPlotCleared(false);
|
||||||
void clearLoadedPreview().then(() => dispatch("PROGRAM_LOAD", { source: programText, path: programPath }))
|
void clearLoadedPreview()
|
||||||
.then(() => setCaseStatus(`${programPath} loaded through linuxcnc-wasm`))
|
.then(() => {
|
||||||
.catch(() => undefined)
|
markPreviewProgress(38, "wasm", "LinuxCNC WASM 正在解析刀具路径");
|
||||||
|
return operationController.execute("PROGRAM_LOAD", { source: programText, path: programPath }, { state: axisState, gate: false });
|
||||||
|
})
|
||||||
|
.then((operation) => {
|
||||||
|
finishPreviewProgress(programPath, operation.response);
|
||||||
|
setOperationTrace(operationController.exportTrace());
|
||||||
|
setCommandStatus("PROGRAM_LOAD acknowledged");
|
||||||
|
setCaseStatus(`${programPath} loaded through linuxcnc-wasm`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
setOperationTrace(operationController.exportTrace());
|
||||||
|
failPreviewProgress(error instanceof Error ? error.message : "program_load_failed");
|
||||||
|
setCommandStatus(`PROGRAM_LOAD blocked: ${error instanceof Error ? error.message : "program_load_failed"}`);
|
||||||
|
})
|
||||||
.finally(() => setProgramLoading(false));
|
.finally(() => setProgramLoading(false));
|
||||||
};
|
};
|
||||||
const openProgramFile = () => programFileInput.current?.click();
|
const openProgramFile = () => programFileInput.current?.click();
|
||||||
@@ -308,23 +364,27 @@ export function App() {
|
|||||||
const file = event.target.files?.[0];
|
const file = event.target.files?.[0];
|
||||||
event.target.value = "";
|
event.target.value = "";
|
||||||
if (!file || programLoading || programRunning) return;
|
if (!file || programLoading || programRunning) return;
|
||||||
|
setProgramLoading(true);
|
||||||
|
startPreviewProgress(file.name);
|
||||||
try {
|
try {
|
||||||
const source = normalizeProgramSource(await file.text());
|
const source = normalizeProgramSource(await file.text());
|
||||||
if (!source.trim()) throw new Error("program_file_empty");
|
if (!source.trim()) throw new Error("program_file_empty");
|
||||||
const path = file.name === "3D_Chips.ngc" ? "linuxcnc/nc_files/3D_Chips.ngc" : `browser/${file.name}`;
|
const path = file.name === "3D_Chips.ngc" ? "linuxcnc/nc_files/3D_Chips.ngc" : `browser/${file.name}`;
|
||||||
setProgramText(source);
|
setProgramText(source);
|
||||||
setProgramPath(path);
|
setProgramPath(path);
|
||||||
setProgramLoading(true);
|
|
||||||
setPlotCleared(false);
|
setPlotCleared(false);
|
||||||
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
|
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
|
||||||
setCaseStatus(`${file.name} loading through linuxcnc-wasm...`);
|
setCaseStatus(`${file.name} loading through linuxcnc-wasm...`);
|
||||||
await clearLoadedPreview();
|
await clearLoadedPreview();
|
||||||
await operationController.execute("PROGRAM_LOAD", { source, path }, { state: axisState, gate: false });
|
markPreviewProgress(38, "wasm", "LinuxCNC WASM 正在解析刀具路径");
|
||||||
|
const programLoad = await operationController.execute("PROGRAM_LOAD", { source, path }, { state: axisState, gate: false });
|
||||||
|
finishPreviewProgress(path, programLoad.response);
|
||||||
setOperationTrace(operationController.exportTrace());
|
setOperationTrace(operationController.exportTrace());
|
||||||
setCommandStatus("PROGRAM_LOAD acknowledged");
|
setCommandStatus("PROGRAM_LOAD acknowledged");
|
||||||
setCaseStatus(`${path} loaded through linuxcnc-wasm`);
|
setCaseStatus(`${path} loaded through linuxcnc-wasm`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setOperationTrace(operationController.exportTrace());
|
setOperationTrace(operationController.exportTrace());
|
||||||
|
failPreviewProgress(error instanceof Error ? error.message : "program_file_load_failed");
|
||||||
setCommandStatus(`PROGRAM_LOAD blocked: ${error instanceof Error ? error.message : "program_file_load_failed"}`);
|
setCommandStatus(`PROGRAM_LOAD blocked: ${error instanceof Error ? error.message : "program_file_load_failed"}`);
|
||||||
setCaseStatus(`blocked: ${error instanceof Error ? error.message : "program_file_load_failed"}`);
|
setCaseStatus(`blocked: ${error instanceof Error ? error.message : "program_file_load_failed"}`);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -379,7 +439,7 @@ export function App() {
|
|||||||
<header className="axis-window-header" data-axis-feature="AXIS-SHELL-01"><div className="axis-brand"><img src={axisIcon("axis-24x24.png")} alt="AXIS" /> <strong>AXIS</strong><span>LinuxCNC Web/WASM</span></div><span className={`connection-state ${state.status}`} data-axis-feature="AXIS-SHELL-01" role="status">{state.status.toUpperCase()}</span><span className="axis-state-badge" data-axis-state={taskState} aria-label={`Machine ${machineState}, operation ${operationState}`}>{taskState}</span></header>
|
<header className="axis-window-header" data-axis-feature="AXIS-SHELL-01"><div className="axis-brand"><img src={axisIcon("axis-24x24.png")} alt="AXIS" /> <strong>AXIS</strong><span>LinuxCNC Web/WASM</span></div><span className={`connection-state ${state.status}`} data-axis-feature="AXIS-SHELL-01" role="status">{state.status.toUpperCase()}</span><span className="axis-state-badge" data-axis-state={taskState} aria-label={`Machine ${machineState}, operation ${operationState}`}>{taskState}</span></header>
|
||||||
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); }} disabled={programRunning || programLoading}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn} disabled={!gates.power.enabled}>{machineOn ? "Power off" : "Power on"}</MenuAction><MenuAction onClick={resetMachine} disabled={!gates.reset.enabled}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram} disabled={!gates.run.enabled || programLoading}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled || programLoading}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled || programLoading}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled || programLoading}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram} disabled={!gates.abort.enabled}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode} disabled={programRunning || programLoading}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")} disabled={programRunning || programLoading}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
|
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); }} disabled={programRunning || programLoading}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn} disabled={!gates.power.enabled}>{machineOn ? "Power off" : "Power on"}</MenuAction><MenuAction onClick={resetMachine} disabled={!gates.reset.enabled}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram} disabled={!gates.run.enabled || programLoading}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled || programLoading}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled || programLoading}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled || programLoading}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram} disabled={!gates.abort.enabled}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode} disabled={programRunning || programLoading}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")} disabled={programRunning || programLoading}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
|
||||||
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label={machineOn ? "Power off [F2]" : "Power on [F2]"} asset="tool_power.gif" disabled={!gates.power.enabled} onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" disabled={!gates.reset.enabled} onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" disabled={programRunning || programLoading} onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" pressed={blockDelete} onClick={() => setBlockDelete((value) => !value)} /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" pressed={optionalStop} onClick={() => setOptionalStop((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /></section>
|
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label={machineOn ? "Power off [F2]" : "Power on [F2]"} asset="tool_power.gif" disabled={!gates.power.enabled} onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" disabled={!gates.reset.enabled} onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" disabled={programRunning || programLoading} onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" pressed={blockDelete} onClick={() => setBlockDelete((value) => !value)} /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" pressed={optionalStop} onClick={() => setOptionalStop((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /></section>
|
||||||
<section className="axis-case-strip" data-axis-feature="AXIS-EDIT-03"><label>Simulation case<select data-axis-feature="AXIS-EDIT-03" value={casePath} onChange={(event) => setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path === SELECTED_XYZAC_CASE).map((entry) => <option key={entry.path} value={entry.path}>{entry.label} | XYZAC TRT</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase} disabled={!gates.load.enabled || programLoading}>Load case</TextButton><TextButton feature="AXIS-EDIT-03" onClick={loadPackagedProgram} disabled={!gates.load.enabled || programLoading}>Load 3D_Chips.ngc</TextButton><span className={`case-status${programLoading ? " loading" : ""}`} role="status" aria-live="polite" aria-busy={programLoading}>{programLoading ? "PROGRAM_LOAD pending: LinuxCNC WASM preview in progress..." : caseStatus}</span><span className="command-status" role="status">{commandStatus}</span></section>
|
<section className="axis-case-strip" data-axis-feature="AXIS-EDIT-03"><label>Simulation case<select data-axis-feature="AXIS-EDIT-03" value={casePath} onChange={(event) => setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path === SELECTED_XYZAC_CASE).map((entry) => <option key={entry.path} value={entry.path}>{entry.label} | XYZAC TRT</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase} disabled={!gates.load.enabled || programLoading}>Load case</TextButton><TextButton feature="AXIS-EDIT-03" onClick={loadPackagedProgram} disabled={!gates.load.enabled || programLoading}>Load 3D_Chips.ngc</TextButton><span className={`case-status${programLoading ? " loading" : ""}`} role="status" aria-live="polite" aria-busy={programLoading}>{programLoading ? "PROGRAM_LOAD pending: LinuxCNC WASM preview in progress..." : caseStatus}</span>{previewProgress && <div className={`preview-load-indicator ${previewProgress.phase}`} role="status" aria-live="polite" aria-label="Toolpath preview loading progress"><span title={previewProgress.detail}>{previewProgress.detail}</span><progress max="100" value={previewProgress.percent}>{previewProgress.percent}%</progress><output>{previewProgress.percent}%</output></div>}<span className="command-status" role="status">{commandStatus}</span></section>
|
||||||
<MachineSequence isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} programLoading={programLoading} canPower={gates.power.enabled} canHome={gates.home.enabled} canRun={gates.run.enabled} onEstop={emergencyStop} onRelease={() => { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} />
|
<MachineSequence isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} programLoading={programLoading} canPower={gates.power.enabled} canHome={gates.home.enabled} canRun={gates.run.enabled} onEstop={emergencyStop} onRelease={() => { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} />
|
||||||
<XyzacTrtPanel view={xyzacView} command={xyzacCommand} />
|
<XyzacTrtPanel view={xyzacView} command={xyzacCommand} />
|
||||||
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={programLoading ? null : state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
|
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={programLoading ? null : state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
|
||||||
|
|||||||
@@ -58,6 +58,16 @@ button { color: inherit; }
|
|||||||
.case-status { min-width: 0; flex: 1 1 auto; }
|
.case-status { min-width: 0; flex: 1 1 auto; }
|
||||||
.case-status.loading { color: #805400; font-weight: 700; }
|
.case-status.loading { color: #805400; font-weight: 700; }
|
||||||
.command-status { margin-left: auto; }
|
.command-status { margin-left: auto; }
|
||||||
|
.preview-load-indicator { display: flex; align-items: center; min-width: min(360px, 30vw); max-width: 420px; flex: 1 1 260px; gap: 6px; color: #805400; font: 10px ui-monospace, monospace; }
|
||||||
|
.preview-load-indicator span { min-width: 0; overflow: hidden; flex: 1 1 auto; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.preview-load-indicator progress { width: 124px; height: 12px; flex: 0 0 124px; accent-color: #9a6a13; }
|
||||||
|
.preview-load-indicator output { min-width: 4ch; color: #5a430f; text-align: right; }
|
||||||
|
.preview-load-indicator.ready { color: #185c1d; }
|
||||||
|
.preview-load-indicator.ready progress { accent-color: #3d7d45; }
|
||||||
|
.preview-load-indicator.ready output { color: #185c1d; }
|
||||||
|
.preview-load-indicator.error { color: #8b1010; }
|
||||||
|
.preview-load-indicator.error progress { accent-color: #a83232; }
|
||||||
|
.preview-load-indicator.error output { color: #8b1010; }
|
||||||
|
|
||||||
.machine-sequence { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 6px 9px; border-bottom: 1px solid #858585; background: #cfcfcf; }
|
.machine-sequence { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 6px 9px; border-bottom: 1px solid #858585; background: #cfcfcf; }
|
||||||
.machine-sequence-heading { display: grid; gap: 2px; min-width: 148px; }
|
.machine-sequence-heading { display: grid; gap: 2px; min-width: 148px; }
|
||||||
@@ -245,6 +255,7 @@ output { font: 12px ui-monospace, monospace; text-align: right; }
|
|||||||
.axis-brand span { display: none; }
|
.axis-brand span { display: none; }
|
||||||
.axis-case-strip { flex-wrap: wrap; }
|
.axis-case-strip { flex-wrap: wrap; }
|
||||||
.axis-case-strip select { max-width: 62vw; }
|
.axis-case-strip select { max-width: 62vw; }
|
||||||
|
.preview-load-indicator { min-width: min(100%, 360px); flex-basis: 100%; }
|
||||||
.command-status { margin-left: 0; width: 100%; }
|
.command-status { margin-left: 0; width: 100%; }
|
||||||
.machine-sequence { align-items: flex-start; flex-direction: column; gap: 5px; }
|
.machine-sequence { align-items: flex-start; flex-direction: column; gap: 5px; }
|
||||||
.machine-sequence-heading { min-width: 0; }
|
.machine-sequence-heading { min-width: 0; }
|
||||||
|
|||||||
Reference in New Issue
Block a user