|
|
|
|
@@ -6,20 +6,22 @@ import { SIM_CASE_CATALOG } from "../../workspace/sim-case-catalog.mjs";
|
|
|
|
|
import { loadSimulationCase, toWorkerConfiguration } from "../../workspace/sim-case-loader.mjs";
|
|
|
|
|
import { axisIcon } from "./sourceAssets";
|
|
|
|
|
import { AxisGateError, AxisOperationController } from "../../app/axis-operation-controller.mjs";
|
|
|
|
|
import { axisGate } from "../../app/axis-contract.mjs";
|
|
|
|
|
|
|
|
|
|
export const SELECTED_AXIS3_CASE = "linuxcnc/configs/sim/axis/vismach/3axis-tutorial/3axis-tutorial.ini";
|
|
|
|
|
const DEFAULT_CASE = SELECTED_AXIS3_CASE;
|
|
|
|
|
const DEFAULT_PROGRAM = ["G21 G90", "G0 X0 Y0 Z0", "G1 X10 Y0 F120", "G1 X10 Y10", "G1 X0 Y10", "G1 X0 Y0", "M2"].join("\n");
|
|
|
|
|
const PROGRAM_PATH = "linuxcnc/configs/nc_files/3dtest.ngc";
|
|
|
|
|
const DEFAULT_PROGRAM = "";
|
|
|
|
|
|
|
|
|
|
type Action = (kind: string, payload?: Record<string, unknown>) => void;
|
|
|
|
|
type Action = (kind: string, payload?: Record<string, unknown>) => Promise<void> | void;
|
|
|
|
|
type IconButtonProps = { label: string; asset: string; onClick?: () => void; disabled?: boolean; danger?: boolean; pressed?: boolean; feature?: string };
|
|
|
|
|
|
|
|
|
|
function IconButton({ label, asset, onClick, disabled = false, danger = false, pressed = false, feature = "AXIS-UI-05" }: IconButtonProps) {
|
|
|
|
|
return <button type="button" data-axis-feature={feature} className={`axis-tool-button${danger ? " danger" : ""}${pressed ? " pressed" : ""}`} onClick={onClick} disabled={disabled} title={label} aria-label={label} aria-pressed={pressed}><img src={axisIcon(asset)} alt="" aria-hidden="true" /></button>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TextButton({ children, onClick, disabled = false, danger = false, feature = "AXIS-UI-07" }: { children: React.ReactNode; onClick?: () => void; disabled?: boolean; danger?: boolean; feature?: string }) {
|
|
|
|
|
return <button type="button" data-axis-feature={feature} className={`axis-text-button${danger ? " danger" : ""}`} onClick={onClick} disabled={disabled}>{children}</button>;
|
|
|
|
|
function TextButton({ children, onClick, onPointerDown, onPointerUp, onPointerCancel, onPointerLeave, ariaLabel, disabled = false, danger = false, feature = "AXIS-UI-07" }: { children: React.ReactNode; onClick?: () => void; onPointerDown?: React.PointerEventHandler<HTMLButtonElement>; onPointerUp?: React.PointerEventHandler<HTMLButtonElement>; onPointerCancel?: React.PointerEventHandler<HTMLButtonElement>; onPointerLeave?: React.PointerEventHandler<HTMLButtonElement>; ariaLabel?: string; disabled?: boolean; danger?: boolean; feature?: string }) {
|
|
|
|
|
return <button type="button" data-axis-feature={feature} className={`axis-text-button${danger ? " danger" : ""}`} onClick={onClick} onPointerDown={onPointerDown} onPointerUp={onPointerUp} onPointerCancel={onPointerCancel} onPointerLeave={onPointerLeave} aria-label={ariaLabel} disabled={disabled}>{children}</button>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Separator() { return <span className="axis-toolbar-separator" aria-hidden="true" />; }
|
|
|
|
|
@@ -36,25 +38,27 @@ function AxisRadio({ axis, selected, onChange }: { axis: string; selected: strin
|
|
|
|
|
return <label className={`axis-radio${selected === axis ? " selected" : ""}`}><input type="radio" name="axis" value={axis} checked={selected === axis} onChange={() => onChange(axis)} /><span>{axis.toUpperCase()}</span></label>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ManualControl({ action, ready, selectedAxis, setSelectedAxis, spindleOn, coolant, onSpindle, onCoolant }: { action: Action; ready: boolean; selectedAxis: string; setSelectedAxis: (axis: string) => void; spindleOn: boolean; coolant: { mist: boolean; flood: boolean }; onSpindle: (direction: number) => void; onCoolant: (key: "mist" | "flood") => void }) {
|
|
|
|
|
function ManualControl({ action, ready, canHome, onHomeAll, selectedAxis, setSelectedAxis, spindleOn, coolant, onSpindle, onCoolant, devices }: { action: Action; ready: boolean; canHome: boolean; onHomeAll: () => void; selectedAxis: string; setSelectedAxis: (axis: string) => void; spindleOn: boolean; coolant: { mist: boolean; flood: boolean }; onSpindle: (direction: number) => void; onCoolant: (key: "mist" | "flood") => void; devices: Record<string, any> }) {
|
|
|
|
|
const [increment, setIncrement] = useState("Continuous");
|
|
|
|
|
const [jogSpeed, setJogSpeed] = useState(50);
|
|
|
|
|
const [maxVelocity, setMaxVelocity] = useState(100);
|
|
|
|
|
const [overrideLimits, setOverrideLimits] = useState(false);
|
|
|
|
|
const [brake, setBrake] = useState(false);
|
|
|
|
|
const jog = (direction: number) => action("JOG_START", { axis: selectedAxis, direction, speed: increment === "Continuous" ? 1 : Number(increment) });
|
|
|
|
|
const [jogging, setJogging] = useState(false);
|
|
|
|
|
const jog = (direction: number) => { setJogging(true); const request = action("JOG_START", { axis: selectedAxis, direction, speed: increment === "Continuous" ? 1 : 1, increment: increment === "Continuous" ? null : Number(increment) }); if (increment !== "Continuous" && request && typeof (request as Promise<void>).then === "function") void (request as Promise<void>).then(() => action("SIM_STEP", { cycles: 1 })); };
|
|
|
|
|
const stopJog = () => { if (!jogging) return; setJogging(false); void action("JOG_STOP"); };
|
|
|
|
|
return <section className="manual-control" data-axis-feature="AXIS-MANUAL-01" aria-label="Manual Control"><div className="manual-grid">
|
|
|
|
|
<div className="manual-axis-block"><h3>Axis:</h3><div className="axis-radio-grid">{["x", "y", "z", "a", "b", "c", "u", "v", "w"].map((axis) => <AxisRadio key={axis} axis={axis} selected={selectedAxis} onChange={setSelectedAxis} />)}</div></div>
|
|
|
|
|
<div className="manual-jog-block"><h3>Jog:</h3><div className="jog-row"><TextButton disabled={!ready} onClick={() => jog(-1)}>-</TextButton><TextButton disabled={!ready} onClick={() => jog(1)}>+</TextButton><select value={increment} onChange={(event) => setIncrement(event.target.value)} aria-label="Jog increment"><option>Continuous</option><option value="0.1000">0.1000</option><option value="0.0100">0.0100</option><option value="0.0010">0.0010</option><option value="0.0001">0.0001</option></select><TextButton disabled={!ready} onClick={() => action("JOG_STOP")}>Stop</TextButton></div><div className="manual-action-row"><TextButton disabled={!ready} onClick={() => action("HOME", { axis: selectedAxis })}>Home Axis</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: `G92 ${selectedAxis.toUpperCase()}0` })}>Touch Off</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: "G43" })}>Tool Touch Off</TextButton></div><label className="check-line"><input type="checkbox" checked={overrideLimits} onChange={(event) => setOverrideLimits(event.target.checked)} /> Override Limits</label></div>
|
|
|
|
|
<div className="manual-jog-block"><h3>Jog:</h3><div className="jog-row"><TextButton disabled={!ready} onPointerDown={() => jog(-1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} negative`}>-</TextButton><TextButton disabled={!ready} onPointerDown={() => jog(1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} positive`}>+</TextButton><select value={increment} onChange={(event) => setIncrement(event.target.value)} aria-label="Jog increment"><option>Continuous</option><option value="0.1000">0.1000</option><option value="0.0100">0.0100</option><option value="0.0010">0.0010</option><option value="0.0001">0.0001</option></select><TextButton disabled={!ready} onClick={stopJog}>Stop</TextButton></div><div className="manual-action-row"><TextButton disabled={!canHome} onClick={() => action("HOME", { axis: selectedAxis })}>Home Axis</TextButton><TextButton disabled={!canHome} onClick={onHomeAll}>Home All</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: `G92 ${selectedAxis.toUpperCase()}0` })}>Touch Off</TextButton><TextButton disabled={!ready} onClick={() => action("MDI_EXECUTE", { block: "G43" })}>Tool Touch Off</TextButton></div><label className="check-line"><input type="checkbox" checked={overrideLimits} onChange={(event) => setOverrideLimits(event.target.checked)} /> Override Limits</label></div>
|
|
|
|
|
<div className="manual-spindle-block"><h3>Spindle:</h3><div className="spindle-row"><IconButton label="Spindle counter-clockwise" asset="spindle_ccw.gif" disabled={!ready} pressed={spindleOn === false} onClick={() => onSpindle(-1)} /><TextButton disabled={!ready} onClick={() => onSpindle(0)}>Stop</TextButton><IconButton label="Spindle clockwise" asset="spindle_cw.gif" disabled={!ready} pressed={spindleOn} onClick={() => onSpindle(1)} /></div><div className="spindle-row"><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>-</TextButton><TextButton disabled={!ready} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+</TextButton><label className="check-line"><input type="checkbox" checked={brake} onChange={(event) => setBrake(event.target.checked)} /> Brake</label></div></div>
|
|
|
|
|
<div className="manual-coolant-block"><h3>Coolant:</h3><label className="check-line"><input type="checkbox" checked={coolant.mist} onChange={() => onCoolant("mist")} /> Mist</label><label className="check-line"><input type="checkbox" checked={coolant.flood} onChange={() => onCoolant("flood")} /> Flood</label></div>
|
|
|
|
|
</div><div className="manual-sliders"><label>Jog Speed:<output>{jogSpeed}%</output><input type="range" min="6" max="100" value={jogSpeed} onChange={(event) => setJogSpeed(Number(event.target.value))} /></label><label>Max Velocity:<output>{maxVelocity}%</output><input type="range" min="6" max="100" value={maxVelocity} onChange={(event) => setMaxVelocity(Number(event.target.value))} /></label></div></section>;
|
|
|
|
|
</div><div className="manual-device-block"><h3>Tool / Probe:</h3><div className="manual-action-row"><TextButton disabled={!ready} onClick={() => action("TOOL_PREPARE", { tool: 1, pocket: 1 })}>Prepare T1</TextButton><TextButton disabled={!ready} onClick={() => action("TOOL_CHANGE", {})}>Change</TextButton><TextButton disabled={!ready} onClick={() => action("PROBE_CONFIG", { action: "arm", probe_type: 0 })}>Arm Probe</TextButton><TextButton disabled={!ready} onClick={() => action("PROBE_CONFIG", { action: "disarm" })}>Disarm</TextButton></div><span className="device-status">Tool {devices.tool?.current ?? 0} / {devices.tool?.state ?? "idle"}; Probe {devices.probe?.state ?? "idle"}</span></div><div className="manual-sliders"><label>Jog Speed:<output>{jogSpeed}%</output><input type="range" min="6" max="100" value={jogSpeed} onChange={(event) => setJogSpeed(Number(event.target.value))} /></label><label>Max Velocity:<output>{maxVelocity}%</output><input type="range" min="6" max="100" value={maxVelocity} onChange={(event) => setMaxVelocity(Number(event.target.value))} /></label></div></section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MdiControl({ action, ready }: { action: Action; ready: boolean }) {
|
|
|
|
|
const [command, setCommand] = useState("G0 X0");
|
|
|
|
|
const [history, setHistory] = useState<string[]>([]);
|
|
|
|
|
const submit = () => { if (!command.trim()) return; action("MDI_EXECUTE", { block: command }); setHistory((items) => [command, ...items].slice(0, 20)); };
|
|
|
|
|
const submit = () => { if (!command.trim() || !ready) return; void action("MDI_EXECUTE", { block: command }); setHistory((items) => [command, ...items].slice(0, 20)); };
|
|
|
|
|
return <section className="mdi-control" data-axis-feature="AXIS-CTRL-03" role="tabpanel" aria-label="MDI Control"><label className="mdi-label">MDI Command<input value={command} onChange={(event) => setCommand(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") submit(); }} /></label><TextButton feature="AXIS-CTRL-03" disabled={!ready} onClick={submit}>Go</TextButton><h3>MDI History</h3><ol>{history.length ? history.map((line, index) => <li key={`${line}-${index}`}><button type="button" data-axis-feature="AXIS-CTRL-03" onClick={() => setCommand(line)}>{line}</button></li>) : <li className="empty-history">No commands</li>}</ol></section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -62,12 +66,28 @@ function DroView({ axes, state, coordType, displayType, metric, setCoordType, se
|
|
|
|
|
const labels = ["X", "Y", "Z", "A", "B", "C", "U", "V", "W"];
|
|
|
|
|
const scale = metric ? 1 : 1 / 25.4;
|
|
|
|
|
const format = (value: number) => (value * scale).toFixed(4);
|
|
|
|
|
return <section className="dro-view" data-axis-feature="AXIS-VIEW-04" role="tabpanel" aria-label="DRO"><div className="dro-mode-row"><strong>Position</strong><label>Coordinate<select data-axis-feature="AXIS-VIEW-18" value={coordType} onChange={(event) => setCoordType(event.target.value as "machine" | "relative")}><option value="machine">Machine</option><option value="relative">Relative</option></select></label><label>Display<select data-axis-feature="AXIS-VIEW-17" value={displayType} onChange={(event) => setDisplayType(event.target.value as "actual" | "commanded")}><option value="actual">Actual</option><option value="commanded">Commanded</option></select></label><label>Units<select data-axis-feature="AXIS-VIEW-02" value={metric ? "mm" : "inch"} onChange={(event) => setMetric(event.target.value === "mm")}><option value="mm">mm</option><option value="inch">inch</option></select></label></div><div className="dro-table">{labels.slice(0, Math.max(3, axes.length)).map((label, index) => { const value = Number(axes[index] ?? 0); return <div className="dro-row" key={label}><strong>{label}</strong><output>{format(value)}</output><output>{format(value)}</output><output>{index < 3 ? format(10 - value) : "0.0000"}</output></div>; })}</div><div className="dro-footer"><span>Units: <b>{metric ? "mm" : "inch"}</b></span><span>{coordType} / {displayType}</span><span>Tick: {state.tick}</span><span>Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}</span></div></section>;
|
|
|
|
|
const pose = (state.snapshot?.visual?.pose ?? state.snapshot?.authoritative?.kinematics?.pose) as Record<string, number> | undefined;
|
|
|
|
|
const commanded = [pose?.x, pose?.y, pose?.z].map((value, index) => Number.isFinite(value) ? Number(value) : Number(axes[index] ?? 0));
|
|
|
|
|
const relative = state.snapshot?.coordinates?.relative ?? [0, 0, 0];
|
|
|
|
|
return <section className="dro-view" data-axis-feature="AXIS-VIEW-04" role="tabpanel" aria-label="DRO"><div className="dro-mode-row"><strong>Position</strong><label>Coordinate<select data-axis-feature="AXIS-VIEW-18" value={coordType} onChange={(event) => setCoordType(event.target.value as "machine" | "relative")}><option value="machine">Machine</option><option value="relative">Relative</option></select></label><label>Display<select data-axis-feature="AXIS-VIEW-17" value={displayType} onChange={(event) => setDisplayType(event.target.value as "actual" | "commanded")}><option value="actual">Actual</option><option value="commanded">Commanded</option></select></label><label>Units<select data-axis-feature="AXIS-VIEW-02" value={metric ? "mm" : "inch"} onChange={(event) => setMetric(event.target.value === "mm")}><option value="mm">mm</option><option value="inch">inch</option></select></label></div><div className="dro-column-headings"><span /><span>Displayed</span><span>Actual</span><span>Relative</span></div><div className="dro-table">{labels.slice(0, Math.max(3, axes.length)).map((label, index) => { const actual = Number(axes[index] ?? 0); const target = Number(commanded[index] ?? actual); const relativeValue = Number(relative[index] ?? actual); const displayed = displayType === "commanded" ? target : actual; return <div className="dro-row" key={label}><strong>{label}</strong><output>{format(coordType === "relative" ? relativeValue : displayed)}</output><output>{format(actual)}</output><output>{format(relativeValue)}</output></div>; })}</div><div className="dro-footer"><span>Units: <b>{metric ? "mm" : "inch"}</b></span><span>{coordType} / {displayType}</span><span>Tick: {state.tick}</span><span>Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}</span></div></section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ProgramPane({ text, setText, activeGcodes, onLoad, onRun, onStep, ready, currentLine, diagnostic }: { text: string; setText: (text: string) => void; activeGcodes: string; onLoad: () => void; onRun: () => void; onStep: () => void; ready: boolean; currentLine: number | null; diagnostic: string | null }) {
|
|
|
|
|
function ProgramPane({ text, setText, activeGcodes, onLoad, onRun, onStep, onOpenFile, canLoad, canRun, canStep, currentLine, diagnostic }: { text: string; setText: (text: string) => void; activeGcodes: string; onLoad: () => void; onRun: () => void; onStep: () => void; onOpenFile: () => void; canLoad: boolean; canRun: boolean; canStep: boolean; currentLine: number | null; diagnostic: string | null }) {
|
|
|
|
|
const lines = text.split("\n");
|
|
|
|
|
return <section className="axis-program-pane" data-axis-feature="AXIS-UI-07" aria-label="G-code program"><div className="program-editor-wrap"><div className="program-gutter" aria-label="G-code line numbers">{lines.map((_, index) => <span key={index} className={index + 1 === currentLine ? "current-line" : ""} aria-current={index + 1 === currentLine ? "true" : undefined}>{index + 1}</span>)}</div><textarea id="axis-program" data-axis-feature="AXIS-EDIT-01" value={text} onChange={(event) => setText(event.target.value)} spellCheck={false} aria-invalid={Boolean(diagnostic)} aria-label="G-code program" /></div><aside className="program-side"><div className="program-actions"><TextButton feature="AXIS-EDIT-01" onClick={onLoad} disabled={!ready}>Load</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onRun} disabled={!ready}>Run</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onStep} disabled={!ready}>Step</TextButton></div><label className="active-gcodes-label">Active G-Codes:<textarea data-axis-feature="AXIS-UI-07" readOnly value={activeGcodes} aria-label="Active G-Codes" /></label>{diagnostic && <output className="program-diagnostic" role="alert" data-axis-feature="AXIS-EDIT-01">{diagnostic}</output>}</aside></section>;
|
|
|
|
|
return <section className="axis-program-pane" data-axis-feature="AXIS-UI-07" aria-label="G-code program"><div className="program-editor-wrap"><div className="program-gutter" aria-label="G-code line numbers">{lines.map((_, index) => <span key={index} className={index + 1 === currentLine ? "current-line" : ""} aria-current={index + 1 === currentLine ? "true" : undefined}>{index + 1}</span>)}</div><textarea id="axis-program" data-axis-feature="AXIS-EDIT-01" value={text} onChange={(event) => setText(event.target.value)} spellCheck={false} aria-invalid={Boolean(diagnostic)} aria-label="G-code program" /></div><aside className="program-side"><div className="program-actions"><TextButton feature="AXIS-EDIT-01" onClick={onOpenFile}>Open file</TextButton><TextButton feature="AXIS-EDIT-01" onClick={onLoad} disabled={!canLoad}>Load</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onRun} disabled={!canRun}>Run</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onStep} disabled={!canStep}>Step</TextButton></div><label className="active-gcodes-label">Active G-Codes:<textarea data-axis-feature="AXIS-UI-07" readOnly value={activeGcodes} aria-label="Active G-Codes" /></label>{diagnostic && <output className="program-diagnostic" role="alert" data-axis-feature="AXIS-EDIT-01">{diagnostic}</output>}</aside></section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function DiagnosticsPanel({ state, gates }: { state: typeof INITIAL_STATE; gates: Record<string, { enabled: boolean; reason: string }> }) {
|
|
|
|
|
const configuration = state.snapshot?.configuration as Record<string, any> | null | undefined;
|
|
|
|
|
const diagnostics = Array.isArray(configuration?.diagnostics) ? configuration.diagnostics : [];
|
|
|
|
|
const gateRows = Object.entries(gates).filter(([, value]) => !value.enabled);
|
|
|
|
|
return <section className="axis-diagnostics" data-axis-feature="AXIS-UI-07" aria-label="Diagnostics">
|
|
|
|
|
<div className="diagnostic-summary"><strong>{configuration?.state ?? "NOT_LOADED"}</strong><span>Tick {state.tick}</span><span>Hash {state.snapshot?.state_hash ?? "-"}</span></div>
|
|
|
|
|
{state.error && <div className="diagnostic-error" role="alert"><b>{state.error.code}</b><span>{state.error.message ?? "Worker rejected the operation"}</span></div>}
|
|
|
|
|
<div className="diagnostic-section"><h3>Startup resources</h3>{configuration ? <><span>Entrypoint: {configuration.entrypoint ?? "-"}</span><span>Resources: {configuration.resources?.resolved_count ?? 0} resolved / {configuration.resources?.blocked_count ?? 0} blocked</span><span>HAL: {configuration.hal?.record_count ?? 0} records / {configuration.hal?.blocked_count ?? 0} blocked</span></> : <span>No configuration snapshot</span>}</div>
|
|
|
|
|
<div className="diagnostic-section"><h3>Disabled operations</h3>{gateRows.length ? gateRows.map(([name, value]) => <div className="diagnostic-row" key={name}><code>{name}</code><span>{value.reason}</span></div>) : <span>All panel operations are enabled</span>}</div>
|
|
|
|
|
<div className="diagnostic-section"><h3>Source diagnostics</h3>{diagnostics.length ? diagnostics.map((item: any, index: number) => <div className="diagnostic-row" key={`${item.code ?? "diagnostic"}-${index}`}><code>{item.code ?? "diagnostic"}</code><span>{item.path ?? item.source ?? "-"}{item.line ? `:${item.line}` : ""}</span></div>) : <span>No startup diagnostics</span>}</div>
|
|
|
|
|
</section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function App() {
|
|
|
|
|
@@ -79,10 +99,8 @@ export function App() {
|
|
|
|
|
const [commandStatus, setCommandStatus] = useState("Idle");
|
|
|
|
|
const [programText, setProgramText] = useState(DEFAULT_PROGRAM);
|
|
|
|
|
const [selectedAxis, setSelectedAxis] = useState("x");
|
|
|
|
|
const [leftTab, setLeftTab] = useState<"manual" | "mdi">("manual");
|
|
|
|
|
const [leftTab, setLeftTab] = useState<"manual" | "mdi" | "diagnostics">("manual");
|
|
|
|
|
const [rightTab, setRightTab] = useState<"preview" | "dro">("preview");
|
|
|
|
|
const [spindleOn, setSpindleOn] = useState(false);
|
|
|
|
|
const [coolant, setCoolant] = useState({ mist: false, flood: false });
|
|
|
|
|
const [view, setView] = useState<"z" | "z2" | "x" | "y" | "y2" | "p">("p");
|
|
|
|
|
const [zoom, setZoom] = useState(1);
|
|
|
|
|
const [rotateMode, setRotateMode] = useState(false);
|
|
|
|
|
@@ -93,32 +111,59 @@ export function App() {
|
|
|
|
|
const [metric, setMetric] = useState(true);
|
|
|
|
|
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);
|
|
|
|
|
useEffect(() => { const unsubscribe = client.subscribe((message) => setState((previous) => reduceFrontendState(previous, message))); client.ensureSession().catch((error: Error) => setCommandStatus(`Error: ${error.message}`)); return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); client.close(); }; }, [client]);
|
|
|
|
|
const jogTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
const programFileInput = useRef<HTMLInputElement>(null);
|
|
|
|
|
const [plotCleared, setPlotCleared] = useState(false);
|
|
|
|
|
useEffect(() => { const unsubscribe = client.subscribe((message) => setState((previous) => reduceFrontendState(previous, message))); client.ensureSession().then(async () => { const bundle = await loadSimulationCase({ path: DEFAULT_CASE, baseUrl: "." }); const source = bundle.files?.[PROGRAM_PATH]; if (typeof source !== "string" || !source.trim()) throw new Error("tutorial_program_missing"); setProgramText(source.replace(/\r\n/g, "\n").trimEnd()); const configuration = toWorkerConfiguration(bundle); await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration }, { state: {}, gate: false }); await operationController.execute("PROGRAM_LOAD", { source, path: PROGRAM_PATH }, { state: {}, gate: false }); setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`); }).catch((error: Error) => { 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 axes = state.snapshot?.motion?.axes ?? [0, 0, 0];
|
|
|
|
|
const ready = ["ready", "running", "paused"].includes(state.status);
|
|
|
|
|
const axisState = { task: { ...(state.task ?? {}), state: state.task?.state ?? (ready ? "ON" : "OFF"), mode: state.task?.mode ?? "MANUAL", interp_state: state.task?.interp_state ?? (state.status === "running" ? "READING" : state.status === "paused" ? "PAUSED" : "IDLE") }, program: state.program ?? {}, motion: state.snapshot?.motion ?? state.snapshot?.motion ?? {}, devices: state.devices ?? {} };
|
|
|
|
|
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 ?? {}, gates: state.gates };
|
|
|
|
|
const machineState = state.axisState?.machine_state ?? (String(taskRecord.state ?? "OFF").toUpperCase() === "ESTOP" ? "ESTOP" : ["READY", "RUNNING", "PAUSED", "STOPPED"].includes(String(taskRecord.state ?? "").toUpperCase()) ? "ON" : "OFF");
|
|
|
|
|
const operationState = state.axisState?.operation_state ?? (state.status === "running" ? "RUNNING" : state.status === "paused" ? "PAUSED" : "IDLE");
|
|
|
|
|
const taskState = state.axisState?.state ?? (machineState === "ESTOP" ? "ESTOP" : operationState === "RUNNING" ? "RUNNING" : operationState === "PAUSED" ? "PAUSED" : machineState === "ON" ? "IDLE" : "OFF");
|
|
|
|
|
const isEstop = machineState === "ESTOP";
|
|
|
|
|
const machineOn = machineState === "ON";
|
|
|
|
|
const ready = machineOn && !["error", "recovering"].includes(state.status);
|
|
|
|
|
const programLoaded = Boolean(state.program?.source_hash || state.program?.path || state.program?.loaded || state.program?.program_status && state.program.program_status !== "idle");
|
|
|
|
|
const programRunning = operationState === "RUNNING";
|
|
|
|
|
const programPaused = operationState === "PAUSED";
|
|
|
|
|
const idle = operationState === "IDLE";
|
|
|
|
|
const gates = Object.keys(state.gates).length ? state.gates : { load: axisGate("load", axisState), run: axisGate("run", axisState), step: axisGate("step", axisState), pause: axisGate("pause", axisState), resume: axisGate("resume", axisState), abort: axisGate("abort", axisState), mdi: axisGate("mdi", axisState), jog: axisGate("jogStart", axisState), home: axisGate("home", axisState) };
|
|
|
|
|
const planner = state.snapshot?.authoritative?.planner;
|
|
|
|
|
const trajectory = [...(planner?.queue ?? []), ...(planner?.active ? [planner.active] : [])];
|
|
|
|
|
const execute: Action = (kind, payload = {}) => {
|
|
|
|
|
const stopRunTimer = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } };
|
|
|
|
|
const stopJogTimer = () => { if (jogTimer.current) { clearInterval(jogTimer.current); jogTimer.current = null; } };
|
|
|
|
|
const startJogTimer = () => { if (jogTimer.current) return; jogTimer.current = setInterval(() => { operationController.execute("SIM_STEP", { cycles: 1 }, { state: axisState, gate: false }).catch(() => undefined); }, 50); };
|
|
|
|
|
const startRunTimer = () => { if (runTimer.current) return; runTimer.current = setInterval(() => { operationController.execute("SIM_STEP", { cycles: 1 }, { state: axisState, gate: false }).catch(() => undefined); }, 50); };
|
|
|
|
|
const dispatch = (kind: string, payload: Record<string, unknown> = {}, gate = true): Promise<void> => {
|
|
|
|
|
setCommandStatus(`${kind} pending`);
|
|
|
|
|
operationController.execute(kind, payload, { state: axisState }).then(() => setCommandStatus(`${kind} acknowledged`)).catch((error: Error) => {
|
|
|
|
|
return operationController.execute(kind, payload, { state: axisState, gate }).then(() => { setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN") startRunTimer(); if (kind === "PROGRAM_ABORT" || kind === "PROGRAM_RESTART") stopRunTimer(); if (kind === "JOG_START") startJogTimer(); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => {
|
|
|
|
|
const code = error instanceof AxisGateError ? error.code : error.message;
|
|
|
|
|
setCommandStatus(`${kind} blocked: ${code}`);
|
|
|
|
|
throw error;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const runProgram = () => { execute("PROGRAM_RUN"); if (runTimer.current) clearInterval(runTimer.current); runTimer.current = setInterval(() => { operationController.execute("SIM_STEP", { cycles: 1 }, { state: axisState, gate: false }).catch(() => undefined); }, 50); };
|
|
|
|
|
const stopProgram = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } execute("PROGRAM_ABORT"); };
|
|
|
|
|
const loadCase = async () => { setCaseStatus("loading session..."); try { await client.ensureSession(); setCaseStatus("loading exact case..."); const bundle = await loadSimulationCase({ path: casePath, baseUrl: "." }); if (bundle.status !== "ready") { setCaseStatus(`blocked: ${bundle.missing.join(", ")}`); return; } 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; setCaseStatus(`${bundle.entrypoint} -> ${resultPayload?.configuration?.state ?? "READY"}`); } catch (error) { setCaseStatus(`blocked: ${error instanceof Error ? error.message : "case_load_failed"}`); } };
|
|
|
|
|
const execute: Action = (kind, payload = {}) => dispatch(kind, payload).catch(() => undefined);
|
|
|
|
|
const runProgram = () => { 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 toggleEstop = () => { if (!isEstop) { void dispatch("ESTOP", { active: true }).catch(() => undefined); return; } void dispatch("ESTOP", { active: false }).then(() => dispatch("IO_RECOVER")).catch(() => undefined); };
|
|
|
|
|
const powerOn = () => { const recover = () => dispatch("IO_RECOVER"); if (isEstop) void dispatch("ESTOP", { active: false }).then(recover).catch(() => undefined); else void recover().catch(() => undefined); };
|
|
|
|
|
const resetMachine = () => { stopRunTimer(); stopJogTimer(); const reset = () => dispatch("RESET", {}, false); if (programRunning) void dispatch("PROGRAM_ABORT").then(() => isEstop ? dispatch("ESTOP", { active: false }) : Promise.resolve()).then(reset).catch(() => undefined); else if (isEstop) void dispatch("ESTOP", { active: false }).then(reset).catch(() => undefined); else void reset().catch(() => undefined); };
|
|
|
|
|
const homeAll = () => { void dispatch("HOME", { axis: "z" }).then(() => dispatch("HOME", { axis: "x" })).then(() => dispatch("HOME", { axis: "y" })).catch(() => undefined); };
|
|
|
|
|
const loadCase = async () => { setCaseStatus("loading session..."); try { await client.ensureSession(); setCaseStatus("loading exact case..."); const bundle = await loadSimulationCase({ path: casePath, baseUrl: "." }); if (bundle.status !== "ready") { setCaseStatus(`blocked: ${bundle.missing.join(", ")}`); return; } 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; const source = bundle.files?.[PROGRAM_PATH]; if (typeof source === "string" && source.trim()) { setProgramText(source.replace(/\r\n/g, "\n").trimEnd()); setPlotCleared(false); await operationController.execute("PROGRAM_LOAD", { source, path: PROGRAM_PATH }, { state: axisState, gate: false }); setCaseStatus(`${bundle.entrypoint} + ${PROGRAM_PATH} -> READY`); } else { setCaseStatus(`${bundle.entrypoint} -> ${resultPayload?.configuration?.state ?? "READY"} (no program)`); } } catch (error) { setCaseStatus(`blocked: ${error instanceof Error ? error.message : "case_load_failed"}`); } };
|
|
|
|
|
const openProgramFile = () => programFileInput.current?.click();
|
|
|
|
|
const onProgramFile = async (event: React.ChangeEvent<HTMLInputElement>) => { const file = event.target.files?.[0]; event.target.value = ""; if (!file) return; const source = await file.text(); setProgramText(source); 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); };
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
const target = event.target as HTMLElement | null;
|
|
|
|
|
if (target && ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName)) return;
|
|
|
|
|
const key = event.key.toLowerCase();
|
|
|
|
|
if (event.ctrlKey && key === "k") { event.preventDefault(); setCommandStatus("Live plot cleared"); return; }
|
|
|
|
|
if (key === "f3") { event.preventDefault(); setLeftTab("manual"); return; }
|
|
|
|
|
if (key === "f3") { event.preventDefault(); enterManualMode(); return; }
|
|
|
|
|
if (key === "f5") { event.preventDefault(); setLeftTab("mdi"); return; }
|
|
|
|
|
if (key === "f1") { event.preventDefault(); execute("ESTOP", { active: true }); return; }
|
|
|
|
|
if (key === "f2") { event.preventDefault(); execute("IO_RECOVER"); return; }
|
|
|
|
|
if (key === "f1") { event.preventDefault(); toggleEstop(); return; }
|
|
|
|
|
if (key === "f2") { event.preventDefault(); powerOn(); return; }
|
|
|
|
|
if (key === "r") { event.preventDefault(); runProgram(); return; }
|
|
|
|
|
if (key === "t") { event.preventDefault(); execute("PROGRAM_STEP"); return; }
|
|
|
|
|
if (key === "p") { event.preventDefault(); execute("PROGRAM_PAUSE"); return; }
|
|
|
|
|
@@ -127,23 +172,29 @@ export function App() {
|
|
|
|
|
};
|
|
|
|
|
window.addEventListener("keydown", onKeyDown);
|
|
|
|
|
return () => window.removeEventListener("keydown", onKeyDown);
|
|
|
|
|
}, [axisState, execute, runProgram, stopProgram]);
|
|
|
|
|
const onSpindle = (direction: number) => { setSpindleOn(direction !== 0); execute("SPINDLE_COMMAND", { spindle: 0, direction, rpm: direction === 0 ? 0 : 600, enabled: direction !== 0 }); };
|
|
|
|
|
const onCoolant = (key: "mist" | "flood") => { const next = { ...coolant, [key]: !coolant[key] }; setCoolant(next); execute("COOLANT_COMMAND", { mist: next.mist, flood: next.flood, lube: false }); };
|
|
|
|
|
}, [axisState, enterManualMode, powerOn, runProgram, stopProgram, toggleEstop]);
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const wasmQueueDepth = Number((state.snapshot?.wasm_motion as { state?: { queue_depth?: number } } | undefined)?.state?.queue_depth ?? 0);
|
|
|
|
|
const programComplete = String(state.program?.program_status ?? "") === "completed" && wasmQueueDepth === 0;
|
|
|
|
|
if (!programRunning || programComplete) stopRunTimer();
|
|
|
|
|
if (!ready) { stopRunTimer(); stopJogTimer(); }
|
|
|
|
|
}, [programRunning, ready, state.program?.program_status, state.snapshot?.wasm_motion]);
|
|
|
|
|
const spindleOn = Boolean((state.devices as Record<string, any> | null)?.spindles?.[0]?.enabled);
|
|
|
|
|
const coolant = { mist: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.mist_requested), flood: Boolean((state.deviceIo as Record<string, any> | null)?.coolant?.flood_requested) };
|
|
|
|
|
const onSpindle = (direction: number) => { execute("SPINDLE_COMMAND", { spindle: 0, direction, rpm: direction === 0 ? 0 : 600, enabled: direction !== 0 }); };
|
|
|
|
|
const onCoolant = (key: "mist" | "flood") => { const next = { ...coolant, [key]: !coolant[key] }; execute("COOLANT_COMMAND", { mist: next.mist, flood: next.flood, lube: false }); };
|
|
|
|
|
const activeGcodes = state.program?.active_gcodes ? String(state.program.active_gcodes) : "G21 G90\nG17 G40 G49\nG54 G64\nM5 M9";
|
|
|
|
|
const taskState = state.axisState?.state ?? String(state.task?.state ?? state.status).toUpperCase();
|
|
|
|
|
const machineState = state.axisState?.machine_state ?? "OFF";
|
|
|
|
|
const operationState = state.axisState?.operation_state ?? "IDLE";
|
|
|
|
|
const toolNumber = String((state.devices as Record<string, any> | null)?.tool?.current ?? state.snapshot?.tool?.current ?? 0);
|
|
|
|
|
const position = `X${Number(axes[0] ?? 0).toFixed(3)} Y${Number(axes[1] ?? 0).toFixed(3)} Z${Number(axes[2] ?? 0).toFixed(3)}`;
|
|
|
|
|
return <main className="axis-app" data-axis-feature="AXIS-UI-01">
|
|
|
|
|
return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-run-enabled={String(gates.run.enabled)} data-axis-homed={JSON.stringify(state.snapshot?.motion?.homed_axes ?? state.snapshot?.motion?.homed ?? null)} data-axis-program-status={String(state.program?.program_status ?? "idle")}>
|
|
|
|
|
<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={() => document.getElementById("axis-program")?.focus()}>Open...</MenuAction><MenuAction onClick={() => setProgramText(DEFAULT_PROGRAM)}>Reload</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={() => execute("ESTOP", { active: true })}>Toggle Emergency Stop</MenuAction><MenuAction shortcut="F2" onClick={() => execute("IO_RECOVER")}>Toggle Machine Power</MenuAction><MenuAction shortcut="R" onClick={runProgram}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={() => setLeftTab("manual")}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")}>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="Toggle Emergency Stop [F1]" asset="tool_estop.gif" danger onClick={() => execute("ESTOP", { active: true })} pressed={state.status === "blocked"} /><IconButton feature="AXIS-MACHINE-02" label="Toggle Machine power [F2]" asset="tool_power.gif" onClick={() => execute("IO_RECOVER")} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen current file [Ctrl-R]" asset="tool_reload.gif" onClick={() => setProgramText(DEFAULT_PROGRAM)} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!ready} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!ready} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label="Pause / resume execution [P/S]" asset={state.status === "paused" ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={!ready} onClick={() => execute(state.status === "paused" ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!ready} 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={() => setCommandStatus("Live plot cleared")} /></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_AXIS3_CASE).map((entry) => <option key={entry.path} value={entry.path}>{entry.label} | 3-axis AXIS</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase}>Load case</TextButton><span className="case-status" role="status">{caseStatus}</span><span className="command-status" role="status">{commandStatus}</span></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 and MDI controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={() => setLeftTab("manual")}>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></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={ready} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={ready} /></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"}><ThreeMachineView axes={axes} segments={trajectory} view={view} zoom={zoom} rotateMode={rotateMode} showGrid={layers.grid} showTool={layers.tool} 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>
|
|
|
|
|
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={openProgramFile}>Open G-code...</MenuAction><MenuAction onClick={() => setProgramText(DEFAULT_PROGRAM)}>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}>Power on</MenuAction><MenuAction onClick={resetMachine}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")}>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="Power on / recover [F2]" asset="tool_power.gif" onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact 3axis tutorial 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); 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={() => { setPlotCleared(true); setCommandStatus("Live plot cleared"); }} /></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_AXIS3_CASE).map((entry) => <option key={entry.path} value={entry.path}>{entry.label} | 3-axis AXIS</option>)}</select></label><TextButton feature="AXIS-EDIT-03" onClick={loadCase} disabled={!gates.load.enabled}>Load case</TextButton><span className="case-status" role="status">{caseStatus}</span><span className="command-status" role="status">{commandStatus}</span></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} /></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"}><ThreeMachineView axes={axes} segments={plotCleared ? [] : (state.toolpath.length ? state.toolpath : trajectory.map((segment) => ({ ...segment, active: true })))} view={view} zoom={zoom} rotateMode={rotateMode} showGrid={layers.grid} showTool={layers.tool} 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-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>50%</output><input type="range" min="6" max="100" defaultValue="50" /></label><label>Feed Override:<output>{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" onChange={(event) => execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["grid", "tool", "path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => <label key={layer}><input type="checkbox" data-axis-feature="AXIS-VIEW-03" checked={layers[layer]} onChange={(event) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer}</label>)}</div></section>
|
|
|
|
|
<ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} onLoad={() => execute("PROGRAM_LOAD", { source: programText, path: "main.ngc" })} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} ready={ready} currentLine={Number(state.program?.line ?? state.task?.current_line ?? 0) || null} diagnostic={state.error?.code ?? null} />
|
|
|
|
|
<ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} onOpenFile={openProgramFile} onLoad={() => { setPlotCleared(false); execute("PROGRAM_LOAD", { source: programText, path: "main.ngc" }); }} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled} canRun={gates.run.enabled} canStep={gates.step.enabled} currentLine={Number(state.program?.line ?? state.task?.current_line ?? 0) || null} diagnostic={state.error?.code ?? null} />
|
|
|
|
|
<input ref={programFileInput} className="visually-hidden-file-input" type="file" accept=".ngc,.nc,.gcode,.tap,text/plain" onChange={onProgramFile} aria-label="Open G-code file" />
|
|
|
|
|
<footer className="axis-info-bar" data-axis-feature="AXIS-UI-07"><span className={`task-state ${state.status}`} data-axis-feature="AXIS-SHELL-01">{taskState}</span><span className="machine-state" data-axis-state-machine={machineState}>Machine {machineState}</span><span className="operation-state" data-axis-state-operation={operationState}>Operation {operationState}</span><span className="tool-state" data-axis-feature="AXIS-UI-07">Tool {toolNumber}</span><span className="position-state" data-axis-feature="AXIS-VIEW-17">{position}</span><span className="footer-status" role="status">{state.error ? `Error: ${state.error.code}` : commandStatus}</span></footer>
|
|
|
|
|
</main>;
|
|
|
|
|
}
|
|
|
|
|
|