diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index 0dc2bb3..607ce71 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -40,7 +40,7 @@ function AxisRadio({ axis, selected, onChange }: { axis: string; selected: strin return onChange(axis)} />{axis.toUpperCase()}; } -function MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programRunning, canPower, canHome, canRun, onEstop, onRelease, onPower, onHomeAll, onRun, onSequence }: { isEstop: boolean; machineOn: boolean; allHomed: boolean; programLoaded: boolean; programRunning: boolean; canPower: boolean; canHome: boolean; canRun: boolean; onEstop: () => void; onRelease: () => void; onPower: () => void; onHomeAll: () => void; onRun: () => void; onSequence: () => void }) { +function MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programRunning, programLoading, canPower, canHome, canRun, onEstop, onRelease, onPower, onHomeAll, onRun, onSequence }: { isEstop: boolean; machineOn: boolean; allHomed: boolean; programLoaded: boolean; programRunning: boolean; programLoading: boolean; canPower: boolean; canHome: boolean; canRun: boolean; onEstop: () => void; onRelease: () => void; onPower: () => void; onHomeAll: () => void; onRun: () => void; onSequence: () => void }) { return Machine sequence{isEstop ? "ESTOP" : machineOn ? "ON" : "OFF"} / {allHomed ? "HOME ALL" : "NOT HOMED"} @@ -48,8 +48,8 @@ function MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programR Release E-stop {machineOn ? "Power Off" : "Power On"} HOME ALL - Run Program - Run Startup Sequence + Run Program + Run Startup Sequence ; } @@ -167,6 +167,7 @@ export function App() { const [commandStatus, setCommandStatus] = useState("Idle"); const [programText, setProgramText] = useState(DEFAULT_PROGRAM); const [programPath, setProgramPath] = useState(PROGRAM_PATH); + const [programLoading, setProgramLoading] = useState(false); const [selectedAxis, setSelectedAxis] = useState("x"); const [leftTab, setLeftTab] = useState<"manual" | "mdi" | "diagnostics">("manual"); const [rightTab, setRightTab] = useState<"preview" | "dro">("preview"); @@ -221,7 +222,7 @@ export function App() { }); }; const execute: Action = (kind, payload = {}) => dispatch(kind, payload).catch(() => undefined); - const runProgram = () => { void dispatch("PROGRAM_RUN").catch(() => undefined); }; + const runProgram = () => { if (programLoading) return; void dispatch("PROGRAM_RUN").catch(() => undefined); }; const stopProgram = () => { const wasRunning = Boolean(runTimer.current) || gates.abort.enabled; stopRunTimer(); if (wasRunning) void dispatch("PROGRAM_ABORT", {}, gates.abort.enabled).catch(() => undefined); }; const emergencyStop = () => { stopRunTimer(); stopJogTimer(); void dispatch("ESTOP", { active: true }, false).catch(() => undefined); }; const releaseEstop = () => dispatch("ESTOP", { active: false }, false); @@ -230,6 +231,7 @@ export function App() { const resetMachine = () => { stopRunTimer(); stopJogTimer(); if (isEstop || programRunning || !machineOn) return; void dispatch("RESET").catch(() => undefined); }; const homeAll = () => { void dispatch("HOME", { all: true }).catch(() => undefined); }; const runStartupSequence = () => { + if (programLoading) return; const release = isEstop ? releaseEstop() : Promise.resolve(); const power = () => machineOn ? Promise.resolve() : dispatch("POWER", { active: true }, false); void release.then(power) @@ -237,8 +239,63 @@ export function App() { .then(() => dispatch("PROGRAM_RUN", {}, false)) .catch(() => undefined); }; - const loadCase = async () => { setCaseStatus("loading session..."); try { await client.ensureSession(); setCaseStatus("loading exact XYZAC case..."); const bundle = await loadSimulationCase({ path: casePath, baseUrl: "." }); if (bundle.status !== "ready") { setCaseStatus(`blocked: ${bundle.missing.join(", ")}`); return; } const source = bundle.files?.[PROGRAM_PATH]; if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing"); const normalizedSource = normalizeProgramSource(source); setCaseStatus("starting exact case..."); 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; setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); setPlotCleared(false); await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: axisState, gate: false }); setOperationTrace(operationController.exportTrace()); setCaseStatus(`${bundle.entrypoint} + ${PROGRAM_PATH} -> ${resultPayload?.configuration?.state ?? "READY"}`); } catch (error) { setOperationTrace(operationController.exportTrace()); setCaseStatus(`blocked: ${error instanceof Error ? error.message : "case_load_failed"}`); } }; - const loadPackagedProgram = async () => { const path = "linuxcnc/nc_files/3D_Chips.ngc"; setCaseStatus(`${path} loading from published workspace...`); try { const response = await fetch(`./${path}`); if (!response.ok) throw new Error(`program_asset_http_${response.status}`); const source = normalizeProgramSource(await response.text()); if (!source.trim()) throw new Error("program_asset_empty"); setProgramText(source); setProgramPath(path); setPlotCleared(false); await operationController.execute("PROGRAM_LOAD", { source, path }, { state: axisState, gate: false }); setOperationTrace(operationController.exportTrace()); setCaseStatus(`${path} loaded through linuxcnc-wasm`); } catch (error) { setOperationTrace(operationController.exportTrace()); setCaseStatus(`blocked: ${error instanceof Error ? error.message : "program_asset_load_failed"}`); } }; + const loadCase = async () => { + if (programLoading || programRunning) return; + setProgramLoading(true); + setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview"); + setCaseStatus("loading session..."); + try { + await client.ensureSession(); + setCaseStatus("loading exact XYZAC case..."); + const bundle = await loadSimulationCase({ path: casePath, baseUrl: "." }); + if (bundle.status !== "ready") { setCaseStatus(`blocked: ${bundle.missing.join(", ")}`); return; } + const source = bundle.files?.[PROGRAM_PATH]; + if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing"); + const normalizedSource = normalizeProgramSource(source); + setCaseStatus("starting exact case..."); + 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; + setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); setPlotCleared(false); + await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: axisState, gate: false }); + setOperationTrace(operationController.exportTrace()); + setCommandStatus("PROGRAM_LOAD acknowledged"); + setCaseStatus(`${bundle.entrypoint} + ${PROGRAM_PATH} -> ${resultPayload?.configuration?.state ?? "READY"}`); + } catch (error) { + setOperationTrace(operationController.exportTrace()); + setCaseStatus(`blocked: ${error instanceof Error ? error.message : "case_load_failed"}`); + } finally { setProgramLoading(false); } + }; + const loadPackagedProgram = async () => { + if (programLoading || programRunning) return; + const path = "linuxcnc/nc_files/3D_Chips.ngc"; + setProgramLoading(true); + setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview"); + setCaseStatus(`${path} loading from published workspace...`); + try { + const response = await fetch(`./${path}`); + if (!response.ok) throw new Error(`program_asset_http_${response.status}`); + const source = normalizeProgramSource(await response.text()); + if (!source.trim()) throw new Error("program_asset_empty"); + setProgramText(source); setProgramPath(path); setPlotCleared(false); + await operationController.execute("PROGRAM_LOAD", { source, path }, { state: axisState, gate: false }); + setOperationTrace(operationController.exportTrace()); + setCommandStatus("PROGRAM_LOAD acknowledged"); + setCaseStatus(`${path} loaded through linuxcnc-wasm`); + } catch (error) { + setOperationTrace(operationController.exportTrace()); + setCaseStatus(`blocked: ${error instanceof Error ? error.message : "program_asset_load_failed"}`); + } finally { setProgramLoading(false); } + }; + const loadProgram = () => { + if (programLoading || programRunning) return; + setProgramLoading(true); + setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview"); + setPlotCleared(false); + void dispatch("PROGRAM_LOAD", { source: programText, path: programPath }) + .then(() => setCaseStatus(`${programPath} loaded through linuxcnc-wasm`)) + .catch(() => undefined) + .finally(() => setProgramLoading(false)); + }; const openProgramFile = () => programFileInput.current?.click(); const onProgramFile = async (event: React.ChangeEvent) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; const source = normalizeProgramSource(await file.text()); const path = file.name === "3D_Chips.ngc" ? "linuxcnc/nc_files/3D_Chips.ngc" : `browser/${file.name}`; setProgramText(source); setProgramPath(path); setPlotCleared(false); setCommandStatus(`${file.name} loaded in editor; press Load to send it to LinuxCNC`); }; const enterManualMode = () => { setLeftTab("manual"); if (String(axisState.task.mode).toUpperCase() !== "MANUAL") void dispatch("SET_MODE", { mode: "MANUAL" }, false).catch(() => undefined); }; @@ -282,14 +339,14 @@ export function App() { const position = `X${Number(xyzacView.pose?.x ?? axes[0] ?? 0).toFixed(3)} Y${Number(xyzacView.pose?.y ?? axes[1] ?? 0).toFixed(3)} Z${Number(xyzacView.pose?.z ?? axes[2] ?? 0).toFixed(3)} A${Number(xyzacView.pose?.a ?? 0).toFixed(3)} C${Number(xyzacView.pose?.c ?? 0).toFixed(3)}`; return AXISLinuxCNC Web/WASM{state.status.toUpperCase()}{taskState} - Open G-code... { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); }} disabled={programRunning}>Reload editor setCommandStatus("Tool data editor is browser-backed")}>Edit tool data... setCommandStatus("Quit is unavailable in a browser tab")}>Quit{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}{machineOn ? "Power off" : "Power on"}Reset machineRun program execute("PROGRAM_STEP")} disabled={!gates.step.enabled}>Step execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled}>Pause execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled}>ResumeStop setRightTab("preview")}>Preview setRightTab("dro")}>DROManual Control setLeftTab("mdi")} disabled={programRunning}>MDI setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS + Open G-code... { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); }} disabled={programRunning || programLoading}>Reload editor setCommandStatus("Tool data editor is browser-backed")}>Edit tool data... setCommandStatus("Quit is unavailable in a browser tab")}>Quit{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}{machineOn ? "Power off" : "Power on"}Reset machineRun program execute("PROGRAM_STEP")} disabled={!gates.step.enabled || programLoading}>Step execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled || programLoading}>Pause execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled || programLoading}>ResumeStop setRightTab("preview")}>Preview setRightTab("dro")}>DROManual Control setLeftTab("mdi")} disabled={programRunning || programLoading}>MDI setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); setPlotCleared(false); }} /> execute("PROGRAM_STEP")} /> execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /> setBlockDelete((value) => !value)} /> setOptionalStop((value) => !value)} /> setZoom((value) => Math.min(2, value + .2))} /> setZoom((value) => Math.max(.5, value - .2))} /> { setView("z"); setRightTab("preview"); }} /> { setView("z2"); setRightTab("preview"); }} /> { setView("x"); setRightTab("preview"); }} /> { setView("y"); setRightTab("preview"); }} /> { setView("y2"); setRightTab("preview"); }} /> { setView("p"); setRightTab("preview"); }} /> setRotateMode((value) => !value)} /> xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /> - Simulation case setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path === SELECTED_XYZAC_CASE).map((entry) => {entry.label} | XYZAC TRT)}Load caseLoad 3D_Chips.ngc{caseStatus}{commandStatus} - { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} /> + Simulation case setCasePath(event.target.value)}>{SIM_CASE_CATALOG.filter((entry) => entry.path === SELECTED_XYZAC_CASE).map((entry) => {entry.label} | XYZAC TRT)}Load caseLoad 3D_Chips.ngc{programLoading ? "PROGRAM_LOAD pending: LinuxCNC WASM preview in progress..." : caseStatus}{commandStatus} + { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} /> Manual Control [F3] setLeftTab("mdi")}>MDI [F5] setLeftTab("diagnostics")}>Diagnostics setRightTab("preview")}>Preview setRightTab("dro")}>DRO Jog Speed:50%Feed Override:{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}% execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} />Spindle Override:{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer})} - { setPlotCleared(false); execute("PROGRAM_LOAD", { source: programText, path: programPath }); }} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled} canRun={gates.run.enabled} canStep={gates.step.enabled} currentLine={currentLine} executionLine={executionLine} executionLineText={executionLineText} diagnostic={state.error?.code ?? null} /> + execute("PROGRAM_STEP")} canLoad={gates.load.enabled && !programLoading} canRun={gates.run.enabled && !programLoading} canStep={gates.step.enabled && !programLoading} currentLine={currentLine} executionLine={executionLine} executionLineText={executionLineText} diagnostic={state.error?.code ?? null} /> ; diff --git a/web/react/src/styles.css b/web/react/src/styles.css index f3d0a0c..6b90ff1 100644 --- a/web/react/src/styles.css +++ b/web/react/src/styles.css @@ -56,6 +56,7 @@ button { color: inherit; } .axis-case-strip > .axis-text-button { min-width: 68px; white-space: nowrap; } .case-status, .command-status { overflow: hidden; color: #4d4d4d; font: 11px ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; } .case-status { min-width: 0; flex: 1 1 auto; } +.case-status.loading { color: #805400; font-weight: 700; } .command-status { margin-left: auto; } .machine-sequence { display: flex; align-items: center; gap: 12px; min-width: 0; padding: 6px 9px; border-bottom: 1px solid #858585; background: #cfcfcf; }