|
|
|
|
@@ -12,59 +12,58 @@ import { XYZAC_CASE, projectXyzacPanel, type XyzacOperationTrace } from "./xyzac
|
|
|
|
|
export const SELECTED_XYZAC_CASE = XYZAC_CASE;
|
|
|
|
|
const DEFAULT_CASE = SELECTED_XYZAC_CASE;
|
|
|
|
|
const PROGRAM_PATH = "linuxcnc/configs/sim/axis/vismach/5axis/table-rotary-tilting/demos/xyzac_switchkins.ngc";
|
|
|
|
|
const DEFAULT_PROGRAM = "";
|
|
|
|
|
const normalizeProgramSource = (source: string) => source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
|
|
|
// Button-to-command sequencing is defined in
|
|
|
|
|
// docs/linuxcnc-axis-web-control-panel-source-logic.md; React only projects
|
|
|
|
|
// Worker gates and emits the documented operation.
|
|
|
|
|
|
|
|
|
|
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 };
|
|
|
|
|
type IconButtonProps = { label: string; asset: string; onClick?: () => void; disabled?: boolean; disabledReason?: string; 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 IconButton({ label, asset, onClick, disabled = false, disabledReason, 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={disabled && disabledReason ? disabledReason : label} aria-label={label} aria-pressed={pressed}><img src={axisIcon(asset)} alt="" aria-hidden="true" /></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 TextButton({ children, onClick, onPointerDown, onPointerUp, onPointerCancel, onPointerLeave, ariaLabel, disabled = false, disabledReason, 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; disabledReason?: string; 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} title={disabled && disabledReason ? disabledReason : undefined}>{children}</button>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Separator() { return <span className="axis-toolbar-separator" aria-hidden="true" />; }
|
|
|
|
|
|
|
|
|
|
function Menu({ label, children }: { label: string; children: React.ReactNode }) {
|
|
|
|
|
// AXIS defines Machine/View menu bindings, but this Web surface already
|
|
|
|
|
// exposes the same commands through the toolbar and notebook tabs. Keep one
|
|
|
|
|
// visible entry point per action while retaining the source command flow.
|
|
|
|
|
if (label === "Machine" || label === "View") return null;
|
|
|
|
|
return <details className="axis-menu-item" data-axis-feature="AXIS-UI-02"><summary>{label}</summary><div className="axis-menu-popup">{children}</div></details>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MenuAction({ children, onClick, shortcut, disabled = false }: { children: React.ReactNode; onClick?: () => void; shortcut?: string; disabled?: boolean }) {
|
|
|
|
|
return <button type="button" onClick={onClick} disabled={disabled}><span>{children}</span>{shortcut && <kbd>{shortcut}</kbd>}</button>;
|
|
|
|
|
}
|
|
|
|
|
function MenuAction({ children, onClick, shortcut, disabled = false }: { children: React.ReactNode; onClick?: () => void; shortcut?: string; disabled?: boolean }) { return <button type="button" onClick={onClick} disabled={disabled}><span>{children}</span>{shortcut && <kbd>{shortcut}</kbd>}</button>; }
|
|
|
|
|
|
|
|
|
|
function AxisRadio({ axis, selected, onChange }: { axis: string; selected: string; onChange: (axis: string) => void }) {
|
|
|
|
|
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 MachineSequence({ wasmReady, isEstop, machineOn, allHomed, programLoaded, programRunning, programLoading, canPower, canHome, canRun, onEstop, onRelease, onPower, onHomeAll, onRun, onSequence }: { wasmReady: boolean; 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 }) {
|
|
|
|
|
function MachineSequence({ wasmReady, allHomed, canHome, onHomeAll }: { wasmReady: boolean; allHomed: boolean; canHome: boolean; onHomeAll: () => void }) {
|
|
|
|
|
return <section className="machine-sequence" data-axis-feature="AXIS-MACHINE-SEQUENCE" aria-label="Machine sequence">
|
|
|
|
|
<div className="machine-sequence-heading"><strong>Machine sequence</strong><span>{wasmReady ? (isEstop ? "ESTOP" : machineOn ? "ON" : "OFF") : "WASM PENDING"} / {wasmReady ? (allHomed ? "HOME ALL" : "NOT HOMED") : "WASM PENDING"}</span></div>
|
|
|
|
|
<div className="machine-sequence-actions" role="group" aria-label="Emergency stop, power, homing and program controls">
|
|
|
|
|
<TextButton danger disabled={isEstop} onClick={onEstop} ariaLabel="Emergency stop">Emergency Stop</TextButton>
|
|
|
|
|
<TextButton disabled={!isEstop} onClick={onRelease} ariaLabel="Release emergency stop">Release E-stop</TextButton>
|
|
|
|
|
<TextButton disabled={!canPower} onClick={onPower} ariaLabel={machineOn ? "Power off" : "Power on"}>{machineOn ? "Power Off" : "Power On"}</TextButton>
|
|
|
|
|
<TextButton disabled={!canHome} onClick={onHomeAll} ariaLabel="Home all axes">HOME ALL</TextButton>
|
|
|
|
|
<TextButton disabled={!canRun || programLoading} onClick={onRun} ariaLabel="Run program">Run Program</TextButton>
|
|
|
|
|
<TextButton feature="AXIS-MACHINE-SEQUENCE" disabled={!programLoaded || !canRun || programRunning || programLoading} onClick={onSequence} ariaLabel="Run startup sequence">Run Startup Sequence</TextButton>
|
|
|
|
|
<div className="machine-sequence-heading"><strong>Machine sequence</strong><span>{wasmReady ? (allHomed ? "HOMED" : "NOT HOMED") : "WASM PENDING"}</span></div>
|
|
|
|
|
<div className="machine-sequence-actions" role="group" aria-label="Homing controls">
|
|
|
|
|
<TextButton disabled={!canHome} onClick={onHomeAll} ariaLabel={allHomed ? "Re-home all axes" : "Home all axes"}>{allHomed ? "RE-HOME ALL" : "HOME ALL"}</TextButton>
|
|
|
|
|
</div>
|
|
|
|
|
</section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ManualControl({ action, canJog, canHome, canSpindle, canCoolant, canTool, canProbe, canTouchOff, canToolTouchOff, onHomeAll, selectedAxis, setSelectedAxis, spindleOn, spindleBrake, coolant, onSpindle, onCoolant, jogScale, onJogScale, devices }: { action: Action; canJog: boolean; canHome: boolean; canSpindle: boolean; canCoolant: boolean; canTool: boolean; canProbe: boolean; canTouchOff: boolean; canToolTouchOff: boolean; onHomeAll: () => void; selectedAxis: string; setSelectedAxis: (axis: string) => void; spindleOn: boolean; spindleBrake: boolean; coolant: { mist: boolean; flood: boolean }; onSpindle: (direction: number) => void; onCoolant: (key: "mist" | "flood") => void; jogScale: number | null; onJogScale: (event: React.ChangeEvent<HTMLInputElement>) => void; devices: Record<string, any> }) {
|
|
|
|
|
function ManualControl({ action, canJog, canHome, canSpindle, canCoolant, canTool, canProbe, canTouchOff, canToolTouchOff, selectedAxis, setSelectedAxis, spindleDirection, spindleBrake, coolant, onSpindle, onCoolant, onHomeAxis, onTouchOff, onToolTouchOff, jogScale, onJogScale, devices }: { action: Action; canJog: boolean; canHome: boolean; canSpindle: boolean; canCoolant: boolean; canTool: boolean; canProbe: boolean; canTouchOff: boolean; canToolTouchOff: boolean; selectedAxis: string; setSelectedAxis: (axis: string) => void; spindleDirection: number; spindleBrake: boolean; coolant: { mist: boolean; flood: boolean }; onSpindle: (direction: number) => void; onCoolant: (key: "mist" | "flood") => void; onHomeAxis: () => void; onTouchOff: () => void; onToolTouchOff: () => void; jogScale: number | null; onJogScale: (event: React.ChangeEvent<HTMLInputElement>) => void; devices: Record<string, any> }) {
|
|
|
|
|
const [increment, setIncrement] = useState("Continuous");
|
|
|
|
|
const [jogging, setJogging] = useState(false);
|
|
|
|
|
const jog = (direction: number) => { setJogging(true); const request = action("JOG_START", { axis: selectedAxis, direction, 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 jog = (direction: number) => { setJogging(true); const continuous = increment === "Continuous"; const request = action("JOG_START", { axis: selectedAxis, direction, increment: continuous ? null : Number(increment), continuous }); if (!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", "c"].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={!canJog} onPointerDown={() => jog(-1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} negative`}>-</TextButton><TextButton disabled={!canJog} 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={!canJog} 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={!canTouchOff} onClick={() => action("MDI_EXECUTE", { block: `G92 ${selectedAxis.toUpperCase()}0` })}>Touch Off</TextButton><TextButton disabled={!canToolTouchOff} onClick={() => action("MDI_EXECUTE", { block: "G43" })}>Tool Touch Off</TextButton></div><label className="check-line"><input type="checkbox" disabled title="LinuxCNC WASM override-limits command unavailable" /> Override Limits (WASM unavailable)</label></div>
|
|
|
|
|
<div className="manual-spindle-block"><h3>Spindle:</h3><div className="spindle-row"><IconButton label="Spindle counter-clockwise" asset="spindle_ccw.gif" disabled={!canSpindle} pressed={spindleOn === false} onClick={() => onSpindle(-1)} /><TextButton disabled={!canSpindle} onClick={() => onSpindle(0)}>Stop</TextButton><IconButton label="Spindle clockwise" asset="spindle_cw.gif" disabled={!canSpindle} pressed={spindleOn} onClick={() => onSpindle(1)} /></div><div className="spindle-row"><TextButton disabled={!canSpindle} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })}>-</TextButton><TextButton disabled={!canSpindle} onClick={() => action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })}>+</TextButton><label className="check-line"><input type="checkbox" checked={spindleBrake} disabled={!canSpindle} onChange={(event) => void action("SPINDLE_BRAKE", { spindle: 0, engaged: event.target.checked })} /> Brake</label></div></div>
|
|
|
|
|
<div className="manual-jog-block"><h3>Jog:</h3><div className="jog-row"><TextButton disabled={!canJog} onPointerDown={() => jog(-1)} onPointerUp={stopJog} onPointerCancel={stopJog} onPointerLeave={stopJog} ariaLabel={`Jog ${selectedAxis.toUpperCase()} negative`}>-</TextButton><TextButton disabled={!canJog} 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={!canJog} onClick={stopJog} ariaLabel="Stop jog">Stop Jog</TextButton></div><div className="manual-action-row"><TextButton disabled={!canHome} onClick={onHomeAxis}>Home Axis</TextButton><TextButton disabled={!canTouchOff} onClick={onTouchOff}>Touch Off</TextButton><TextButton disabled={!canToolTouchOff} disabledReason="blocked_by_abi_tool_offsets" onClick={onToolTouchOff}>Tool Touch Off</TextButton></div><label className="check-line"><input type="checkbox" disabled title="LinuxCNC WASM override-limits command unavailable" /> Override Limits (WASM unavailable)</label></div>
|
|
|
|
|
<div className="manual-spindle-block"><h3>Spindle:</h3><div className="spindle-row"><IconButton label="Spindle counter-clockwise" asset="spindle_ccw.gif" disabled={!canSpindle} pressed={spindleDirection === -1} onClick={() => onSpindle(-1)} /><TextButton disabled={!canSpindle} onClick={() => onSpindle(0)} ariaLabel="Stop spindle">Stop Spindle</TextButton><IconButton label="Spindle clockwise" asset="spindle_cw.gif" disabled={!canSpindle} pressed={spindleDirection === 1} onClick={() => onSpindle(1)} /></div><div className="spindle-row"><TextButton disabled={!canSpindle} onPointerDown={() => action("SPINDLE_COMMAND", { spindle: 0, delta: -100 })} onPointerUp={() => action("SPINDLE_CONSTANT", { spindle: 0 })} onPointerCancel={() => action("SPINDLE_CONSTANT", { spindle: 0 })} ariaLabel="Decrease spindle speed">-</TextButton><TextButton disabled={!canSpindle} onPointerDown={() => action("SPINDLE_COMMAND", { spindle: 0, delta: 100 })} onPointerUp={() => action("SPINDLE_CONSTANT", { spindle: 0 })} onPointerCancel={() => action("SPINDLE_CONSTANT", { spindle: 0 })} ariaLabel="Increase spindle speed">+</TextButton><label className="check-line"><input type="checkbox" checked={spindleBrake} disabled={!canSpindle} onChange={(event) => void action("SPINDLE_BRAKE", { spindle: 0, engaged: 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} disabled={!canCoolant} onChange={() => onCoolant("mist")} /> Mist</label><label className="check-line"><input type="checkbox" checked={coolant.flood} disabled={!canCoolant} onChange={() => onCoolant("flood")} /> Flood</label></div>
|
|
|
|
|
</div><div className="manual-device-block"><h3>Tool / Probe:</h3><div className="manual-action-row"><TextButton disabled={!canTool} onClick={() => action("TOOL_PREPARE", { tool: 1, pocket: 1 })}>Prepare T1</TextButton><TextButton disabled={!canTool} onClick={() => action("TOOL_CHANGE", {})}>Change</TextButton><TextButton disabled={!canProbe} onClick={() => action("PROBE_CONFIG", { action: "arm", probe_type: 0 })}>Arm Probe</TextButton><TextButton disabled={!canProbe} onClick={() => action("PROBE_CONFIG", { action: "disarm" })}>Disarm</TextButton></div><span className="device-status">{devices.tool ? `Tool ${devices.tool.current ?? 0} / ${devices.tool.state ?? "idle"}; Probe ${devices.probe?.state ?? "idle"}` : "WASM state pending"}</span></div><div className="manual-sliders"><label>Jog Speed:<output>{jogScale === null ? "-" : `${Math.round(jogScale * 100)}%`}</output><input type="range" min="6" max="100" value={jogScale === null ? 6 : Math.round(jogScale * 100)} onChange={onJogScale} disabled={!canJog || jogScale === null} /></label></div></section>;
|
|
|
|
|
</div><div className="manual-device-block"><h3>Tool / Probe:</h3><div className="manual-action-row"><TextButton disabled={!canTool} onClick={() => action("TOOL_PREPARE", { tool: 1, pocket: 1 })}>Prepare T1</TextButton><TextButton disabled={!canTool} onClick={() => action("TOOL_CHANGE", {})}>Change</TextButton><TextButton disabled={!canProbe} onClick={() => action("PROBE_CONFIG", { action: "arm", probe_type: 0 })}>Arm Probe</TextButton><TextButton disabled={!canProbe} onClick={() => action("PROBE_CONFIG", { action: "disarm" })}>Disarm</TextButton></div><span className="device-status">{devices.tool ? `Tool ${devices.tool.current ?? 0} / ${devices.tool.state ?? "idle"}; Probe ${devices.probe?.state ?? "idle"}` : "WASM state pending"}</span></div></section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MdiControl({ action, ready }: { action: Action; ready: boolean }) {
|
|
|
|
|
@@ -113,7 +112,7 @@ function VirtualizedExecutionTrace({ entries, executing }: { entries: Array<{ li
|
|
|
|
|
return <div ref={traceRef} className="program-trace-scroll" onScroll={(event) => { const node = event.currentTarget; followRef.current = node.scrollHeight - node.scrollTop - node.clientHeight < TRACE_ROW_HEIGHT * 1.5; updateWindow(); }}><ol style={{ height: `${Math.max(1, entries.length * TRACE_ROW_HEIGHT)}px` }}>{visible.map((entry, offset) => { const index = windowRange.start + offset; const isCurrent = executing && index === entries.length - 1; return <li key={`${entry.line}-${index}`} className={isCurrent ? "executing-line" : "completed-line"} style={{ position: "absolute", top: `${index * TRACE_ROW_HEIGHT}px`, left: 0, height: `${TRACE_ROW_HEIGHT}px` }} aria-current={isCurrent ? "true" : undefined}><span>L{entry.line}</span><code title={entry.text}>{entry.text || "-"}</code></li>; })}</ol></div>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ProgramPane({ text, setText, activeGcodes, executionTrace, programStatus, onLoad, onRun, onStep, canLoad, canRun, canStep, currentLine, executionLine, executionLineText, diagnostic }: { text: string; setText: (text: string) => void; activeGcodes: string; executionTrace: Array<{ line: number; text: string; program_status?: string | null }>; programStatus: string; onLoad: () => void; onRun: () => void; onStep: () => void; canLoad: boolean; canRun: boolean; canStep: boolean; currentLine: number | null; executionLine: number | null; executionLineText: string; diagnostic: string | null }) {
|
|
|
|
|
function ProgramPane({ text, setText, activeGcodes, executionTrace, programStatus, onLoad, canLoad, currentLine, executionLine, executionLineText, diagnostic }: { text: string; setText: (text: string) => void; activeGcodes: string; executionTrace: Array<{ line: number; text: string; program_status?: string | null }>; programStatus: string; onLoad: () => void; onRun?: () => void; onStep?: () => void; canLoad: boolean; canRun?: boolean; canStep?: boolean; currentLine: number | null; executionLine: number | null; executionLineText: string; diagnostic: string | null }) {
|
|
|
|
|
const lines = text.split("\n");
|
|
|
|
|
const executing = ["running", "paused"].includes(programStatus.toLowerCase());
|
|
|
|
|
const mainLine = executing && currentLine && currentLine > 0 && currentLine <= lines.length ? currentLine : null;
|
|
|
|
|
@@ -141,7 +140,7 @@ function ProgramPane({ text, setText, activeGcodes, executionTrace, programStatu
|
|
|
|
|
}, [mainLine]);
|
|
|
|
|
const visibleLines = lines.slice(editorWindow.start, editorWindow.end);
|
|
|
|
|
const lineClass = (line: number) => line === mainLine ? "current-line" : executedLines.has(line) ? "completed-line" : "";
|
|
|
|
|
return <section className="axis-program-pane" data-axis-feature="AXIS-UI-07" aria-label="G-code program"><div className="program-main-layout"><div className="program-editor-wrap"><div ref={gutterRef} className="program-gutter" aria-label="G-code line numbers"><div className="program-gutter-content" style={{ height: `${Math.max(PROGRAM_ROW_HEIGHT, lines.length * PROGRAM_ROW_HEIGHT)}px` }}>{visibleLines.map((_, offset) => { const index = editorWindow.start + offset; const line = index + 1; return <span key={index} className={lineClass(line)} style={{ top: `${index * PROGRAM_ROW_HEIGHT}px` }} aria-current={line === mainLine ? "true" : undefined}>{line}</span>; })}</div></div><div className="program-editor-input"><div ref={highlightsRef} className="program-line-highlights" style={{ height: `${Math.max(PROGRAM_ROW_HEIGHT, lines.length * PROGRAM_ROW_HEIGHT)}px` }} aria-hidden="true"><span className="program-highlights-spacer" />{visibleLines.map((_, offset) => { const index = editorWindow.start + offset; return <span key={index} className={lineClass(index + 1)} style={{ top: `${index * PROGRAM_ROW_HEIGHT}px` }} />; })}</div><textarea ref={editorRef} id="axis-program" data-axis-feature="AXIS-EDIT-01" value={text} onChange={(event) => setText(event.target.value)} onScroll={() => { if (editorRef.current) { if (gutterRef.current) gutterRef.current.scrollTop = editorRef.current.scrollTop; if (highlightsRef.current) { highlightsRef.current.scrollTop = editorRef.current.scrollTop; highlightsRef.current.scrollLeft = editorRef.current.scrollLeft; } updateEditorWindow(); } }} spellCheck={false} aria-invalid={Boolean(diagnostic)} aria-label="G-code program" /></div></div><div className="program-execution-panel"><div className="program-execution-line" role="status" aria-label="Currently executing G-code"><span>{executionLine ? `Executing source line ${executionLine}` : "Execution idle"}</span><code title={executionLineText}>{executionLineText || "-"}</code></div><div className="program-execution-trace" role="log" aria-label="G-code execution trace" aria-live="polite"><strong>Subprogram / execution trace <span>({executionTrace.length} cached)</span></strong><VirtualizedExecutionTrace entries={executionTrace} executing={executing} /></div></div></div><aside className="program-side"><div className="program-actions"><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>;
|
|
|
|
|
return <section className="axis-program-pane" data-axis-feature="AXIS-UI-07" aria-label="G-code program"><div className="program-main-layout"><div className="program-editor-wrap"><div ref={gutterRef} className="program-gutter" aria-label="G-code line numbers"><div className="program-gutter-content" style={{ height: `${Math.max(PROGRAM_ROW_HEIGHT, lines.length * PROGRAM_ROW_HEIGHT)}px` }}>{visibleLines.map((_, offset) => { const index = editorWindow.start + offset; const line = index + 1; return <span key={index} className={lineClass(line)} style={{ top: `${index * PROGRAM_ROW_HEIGHT}px` }} aria-current={line === mainLine ? "true" : undefined}>{line}</span>; })}</div></div><div className="program-editor-input"><div ref={highlightsRef} className="program-line-highlights" style={{ height: `${Math.max(PROGRAM_ROW_HEIGHT, lines.length * PROGRAM_ROW_HEIGHT)}px` }} aria-hidden="true"><span className="program-highlights-spacer" />{visibleLines.map((_, offset) => { const index = editorWindow.start + offset; return <span key={index} className={lineClass(index + 1)} style={{ top: `${index * PROGRAM_ROW_HEIGHT}px` }} />; })}</div><textarea ref={editorRef} id="axis-program" data-axis-feature="AXIS-EDIT-01" value={text} onChange={(event) => setText(event.target.value)} onScroll={() => { if (editorRef.current) { if (gutterRef.current) gutterRef.current.scrollTop = editorRef.current.scrollTop; if (highlightsRef.current) { highlightsRef.current.scrollTop = editorRef.current.scrollTop; highlightsRef.current.scrollLeft = editorRef.current.scrollLeft; } updateEditorWindow(); } }} spellCheck={false} aria-invalid={Boolean(diagnostic)} aria-label="G-code program" /></div></div><div className="program-execution-panel"><div className="program-execution-line" role="status" aria-label="Currently executing G-code"><span>{executionLine ? `Executing source line ${executionLine}` : "Execution idle"}</span><code title={executionLineText}>{executionLineText || "-"}</code></div><div className="program-execution-trace" role="log" aria-label="G-code execution trace" aria-live="polite"><strong>Subprogram / execution trace <span>({executionTrace.length} cached)</span></strong><VirtualizedExecutionTrace entries={executionTrace} executing={executing} /></div></div></div><aside className="program-side"><div className="program-actions"><TextButton feature="AXIS-EDIT-01" onClick={onLoad} disabled={!canLoad}>Load</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 }> }) {
|
|
|
|
|
@@ -162,11 +161,11 @@ export function App() {
|
|
|
|
|
const operationController = useMemo(() => new AxisOperationController(client), [client]);
|
|
|
|
|
const [state, setState] = useState(INITIAL_STATE);
|
|
|
|
|
const [casePath, setCasePath] = useState<string>(DEFAULT_CASE);
|
|
|
|
|
const [caseStatus, setCaseStatus] = useState("ready to load");
|
|
|
|
|
const [caseStatus, setCaseStatus] = useState("loading configured program...");
|
|
|
|
|
const [commandStatus, setCommandStatus] = useState("Idle");
|
|
|
|
|
const [programText, setProgramTextState] = useState(DEFAULT_PROGRAM);
|
|
|
|
|
const [programText, setProgramTextState] = useState("");
|
|
|
|
|
const [programPath, setProgramPath] = useState(PROGRAM_PATH);
|
|
|
|
|
const [programLoading, setProgramLoading] = useState(false);
|
|
|
|
|
const [programLoading, setProgramLoading] = useState(true);
|
|
|
|
|
const [selectedAxis, setSelectedAxis] = useState("x");
|
|
|
|
|
const [leftTab, setLeftTab] = useState<"manual" | "mdi" | "diagnostics">("manual");
|
|
|
|
|
const [rightTab, setRightTab] = useState<"preview" | "dro">("preview");
|
|
|
|
|
@@ -176,11 +175,13 @@ export function App() {
|
|
|
|
|
const [coordType, setCoordType] = useState<"machine" | "relative">("machine");
|
|
|
|
|
const [displayType, setDisplayType] = useState<"actual" | "commanded">("actual");
|
|
|
|
|
const [metric, setMetric] = useState(true);
|
|
|
|
|
const [touchOffSystem, setTouchOffSystem] = useState("P1 G54");
|
|
|
|
|
const [layers, setLayers] = useState({ grid: true, tool: true, path: true, live: true, limits: false, velocity: false, offsets: false, extents: false });
|
|
|
|
|
const runTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
const jogTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
const runStepPending = useRef(false);
|
|
|
|
|
const jogStepPending = useRef(false);
|
|
|
|
|
const continuousJog = useRef<{ axis: string; direction: number } | null>(null);
|
|
|
|
|
const programFileInput = useRef<HTMLInputElement>(null);
|
|
|
|
|
const [plotCleared, setPlotCleared] = useState(false);
|
|
|
|
|
const [operationTrace, setOperationTrace] = useState<XyzacOperationTrace[]>([]);
|
|
|
|
|
@@ -197,7 +198,6 @@ export function App() {
|
|
|
|
|
const cachedResponse = await client.loadUiState(UI_CACHE_KEY).catch(() => null);
|
|
|
|
|
const cachedPayload = cachedResponse?.payload && typeof cachedResponse.payload === "object" ? cachedResponse.payload as Record<string, unknown> : null;
|
|
|
|
|
const cached = cachedPayload?.status === "verified" && cachedPayload.state && typeof cachedPayload.state === "object" ? cachedPayload.state as Record<string, unknown> : null;
|
|
|
|
|
const cachedProgram = cached?.program && typeof cached.program === "object" ? cached.program as Record<string, unknown> : null;
|
|
|
|
|
if (cached) {
|
|
|
|
|
if (cached.casePath === DEFAULT_CASE) setCasePath(DEFAULT_CASE);
|
|
|
|
|
if (["x", "y", "z", "a", "c"].includes(String(cached.selectedAxis))) setSelectedAxis(String(cached.selectedAxis));
|
|
|
|
|
@@ -209,6 +209,7 @@ export function App() {
|
|
|
|
|
if (["machine", "relative"].includes(String(cached.coordType))) setCoordType(cached.coordType as "machine" | "relative");
|
|
|
|
|
if (["actual", "commanded"].includes(String(cached.displayType))) setDisplayType(cached.displayType as "actual" | "commanded");
|
|
|
|
|
if (typeof cached.metric === "boolean") setMetric(cached.metric);
|
|
|
|
|
if (typeof cached.touchOffSystem === "string" && /^(?:P[0-9]|G5[4-9](?:\.[123])?)(?:\s+.*)?$/i.test(cached.touchOffSystem)) setTouchOffSystem(cached.touchOffSystem);
|
|
|
|
|
if (cached.layers && typeof cached.layers === "object") {
|
|
|
|
|
const savedLayers = cached.layers as Record<string, unknown>;
|
|
|
|
|
setLayers((current) => {
|
|
|
|
|
@@ -227,15 +228,11 @@ export function App() {
|
|
|
|
|
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 });
|
|
|
|
|
if (typeof cachedProgram?.source === "string" && cachedProgram.source.trim() && cachedProgram.source.length <= 900000 && typeof cachedProgram.path === "string" && cachedProgram.path.length <= 512 && (cachedProgram.source !== normalizedSource || cachedProgram.path !== PROGRAM_PATH)) {
|
|
|
|
|
const cachedSource = normalizeProgramSource(cachedProgram.source);
|
|
|
|
|
setProgramTextState(cachedSource); setProgramPath(cachedProgram.path);
|
|
|
|
|
await operationController.execute("PROGRAM_LOAD", { source: cachedSource, path: cachedProgram.path }, { state: {}, gate: false });
|
|
|
|
|
}
|
|
|
|
|
uiCacheHydrated.current = true;
|
|
|
|
|
setOperationTrace(operationController.exportTrace());
|
|
|
|
|
setCaseStatus(`${DEFAULT_CASE} + ${cachedProgram?.path ?? PROGRAM_PATH} loaded`);
|
|
|
|
|
}).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); });
|
|
|
|
|
setCommandStatus("PROGRAM_LOAD acknowledged");
|
|
|
|
|
setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`);
|
|
|
|
|
}).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); }).finally(() => setProgramLoading(false));
|
|
|
|
|
return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current); if (uiCacheSaveTimer.current) clearTimeout(uiCacheSaveTimer.current); client.close(); };
|
|
|
|
|
}, [client, operationController]);
|
|
|
|
|
const axes = state.snapshot?.motion?.axes ?? [];
|
|
|
|
|
@@ -252,15 +249,26 @@ export function App() {
|
|
|
|
|
const isEstop = wasmSnapshotReady && machineState === "ESTOP";
|
|
|
|
|
const machineOn = wasmSnapshotReady && machineState === "ON";
|
|
|
|
|
const ready = wasmSnapshotReady && 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 wasmGate = (action: string) => state.gates[action] ?? { action, enabled: false, reason: "linuxcnc_wasm_snapshot_pending" };
|
|
|
|
|
const gates = { load: wasmGate("load"), run: wasmGate("run"), step: wasmGate("step"), pause: wasmGate("pause"), resume: wasmGate("resume"), abort: wasmGate("abort"), mdi: wasmGate("mdi"), jog: wasmGate("jog"), home: wasmGate("home"), power: wasmGate("power"), reset: wasmGate("reset"), override: wasmGate("override"), spindle: wasmGate("spindle"), coolant: wasmGate("coolant"), tool: wasmGate("tool"), probe: wasmGate("probe"), touchOff: wasmGate("touchOff"), toolTouchOff: wasmGate("toolTouchOff") };
|
|
|
|
|
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(() => { if (jogStepPending.current) return; jogStepPending.current = true; operationController.execute("SIM_STEP", { cycles: 1, response: "motion_delta" }, { state: axisState, gate: false }).catch(() => undefined).finally(() => { jogStepPending.current = false; }); }, 50); };
|
|
|
|
|
const stopJogTimer = () => { continuousJog.current = null; if (jogTimer.current) { clearInterval(jogTimer.current); jogTimer.current = null; } };
|
|
|
|
|
const startJogTimer = (axis: string, direction: number) => {
|
|
|
|
|
continuousJog.current = { axis, direction };
|
|
|
|
|
if (jogTimer.current) return;
|
|
|
|
|
jogTimer.current = setInterval(() => {
|
|
|
|
|
const jog = continuousJog.current;
|
|
|
|
|
if (!jog || jogStepPending.current) return;
|
|
|
|
|
jogStepPending.current = true;
|
|
|
|
|
void operationController.execute("JOG_START", { axis: jog.axis, direction: jog.direction, increment: null, continuous: true, continuousSample: true }, { state: axisState, gate: false })
|
|
|
|
|
.then(() => operationController.execute("SIM_STEP", { cycles: 1, response: "motion_delta" }, { state: axisState, gate: false }))
|
|
|
|
|
.catch(() => undefined)
|
|
|
|
|
.finally(() => { jogStepPending.current = false; });
|
|
|
|
|
}, 50);
|
|
|
|
|
};
|
|
|
|
|
const startRunTimer = () => { if (runTimer.current) return; runTimer.current = setInterval(() => { if (runStepPending.current) return; runStepPending.current = true; operationController.execute("SIM_STEP", { cycles: 5, response: "motion_delta" }, { state: axisState, gate: false }).catch(() => undefined).finally(() => { runStepPending.current = false; }); }, 50); };
|
|
|
|
|
const dispatch = async (kind: string, payload: Record<string, unknown> = {}, gate = true, action: string | null = null): Promise<void> => {
|
|
|
|
|
setCommandStatus(`${kind} pending`);
|
|
|
|
|
@@ -279,7 +287,7 @@ export function App() {
|
|
|
|
|
}
|
|
|
|
|
const pending = operationController.execute(kind, payload, { state: axisState, gate, ...(action ? { action } : {}) });
|
|
|
|
|
setOperationTrace(operationController.exportTrace());
|
|
|
|
|
return pending.then(() => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN" || kind === "PROGRAM_RESUME") startRunTimer(); if (["PROGRAM_PAUSE", "PROGRAM_ABORT", "PROGRAM_RESTART"].includes(kind)) stopRunTimer(); if (kind === "JOG_START") startJogTimer(); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => {
|
|
|
|
|
return pending.then(() => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN" || kind === "PROGRAM_RESUME") startRunTimer(); if (["PROGRAM_PAUSE", "PROGRAM_ABORT", "PROGRAM_RESTART"].includes(kind)) stopRunTimer(); if (kind === "JOG_START" && payload.continuous === true && payload.continuousSample !== true) startJogTimer(String(payload.axis ?? "x"), Number(payload.direction)); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => {
|
|
|
|
|
setOperationTrace(operationController.exportTrace());
|
|
|
|
|
const code = error.message;
|
|
|
|
|
setCommandStatus(`${kind} blocked: ${code}`);
|
|
|
|
|
@@ -288,21 +296,21 @@ export function App() {
|
|
|
|
|
};
|
|
|
|
|
const execute: Action = (kind, payload = {}) => dispatch(kind, payload).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 stopProgram = () => { const wasRunning = programRunning || programPaused; stopRunTimer(); if (wasRunning) void dispatch("PROGRAM_ABORT", {}, true).catch(() => undefined); };
|
|
|
|
|
const emergencyStop = () => { stopRunTimer(); stopJogTimer(); void dispatch("ESTOP", { active: true }, false).catch(() => undefined); };
|
|
|
|
|
const releaseEstop = () => dispatch("ESTOP", { active: false }, false);
|
|
|
|
|
const toggleEstop = () => { if (!isEstop) { emergencyStop(); return; } void releaseEstop().catch(() => undefined); };
|
|
|
|
|
const powerOn = () => { if (!gates.power.enabled) return; void dispatch("POWER", { active: isEstop ? false : !machineOn }, false).catch(() => undefined); };
|
|
|
|
|
const resetMachine = () => { stopRunTimer(); stopJogTimer(); if (!gates.reset.enabled) 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)
|
|
|
|
|
.then(() => dispatch("HOME", { all: true }, false))
|
|
|
|
|
.then(() => dispatch("PROGRAM_RUN", {}, false))
|
|
|
|
|
.catch(() => undefined);
|
|
|
|
|
const homeAll = () => {
|
|
|
|
|
const confirm = globalThis.confirm;
|
|
|
|
|
if (allHomed && typeof confirm === "function" && !confirm("All joints are already homed. Re-home all joints?")) return;
|
|
|
|
|
void dispatch("HOME", { all: true }).catch(() => undefined);
|
|
|
|
|
};
|
|
|
|
|
const homeAxis = () => {
|
|
|
|
|
const index = ["x", "y", "z", "a", "c"].indexOf(selectedAxis);
|
|
|
|
|
const confirm = globalThis.confirm;
|
|
|
|
|
if (index >= 0 && homedAxes[index] && typeof confirm === "function" && !confirm(`${selectedAxis.toUpperCase()} is already homed. Re-home this joint?`)) return;
|
|
|
|
|
void dispatch("HOME", { axis: selectedAxis }).catch(() => undefined);
|
|
|
|
|
};
|
|
|
|
|
const clearLoadedPreview = async () => {
|
|
|
|
|
await operationController.execute("VISUAL_CLEAR", {}, { state: axisState, gate: false });
|
|
|
|
|
@@ -370,17 +378,7 @@ export function App() {
|
|
|
|
|
setCaseStatus(`blocked: ${error instanceof Error ? error.message : "case_load_failed"}`);
|
|
|
|
|
} finally { setProgramLoading(false); }
|
|
|
|
|
};
|
|
|
|
|
const reopenDefaults = () => {
|
|
|
|
|
if (programLoading || programRunning || !gates.load.enabled) return;
|
|
|
|
|
void loadCase();
|
|
|
|
|
};
|
|
|
|
|
// The legacy menu/toolbar handlers call setProgramText(DEFAULT_PROGRAM).
|
|
|
|
|
// Route that action through the same WASM-backed reopen transaction instead
|
|
|
|
|
// of changing only a local editor string.
|
|
|
|
|
const setProgramText = (value: string) => {
|
|
|
|
|
if (value === DEFAULT_PROGRAM) { reopenDefaults(); return; }
|
|
|
|
|
setProgramTextState(value);
|
|
|
|
|
};
|
|
|
|
|
const setProgramText = (value: string) => { setProgramTextState(value); };
|
|
|
|
|
const loadPackagedProgram = async () => {
|
|
|
|
|
if (programLoading || programRunning) return;
|
|
|
|
|
const path = "linuxcnc/nc_files/3D_Chips.ngc";
|
|
|
|
|
@@ -463,24 +461,52 @@ export function App() {
|
|
|
|
|
setProgramLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
const enterManualMode = () => { setLeftTab("manual"); if (String(axisState.task.mode).toUpperCase() !== "MANUAL") void dispatch("SET_MODE", { mode: "MANUAL" }, false).catch(() => undefined); };
|
|
|
|
|
const enterMdiMode = () => { setLeftTab("mdi"); if (String(axisState.task.mode).toUpperCase() !== "MDI") void dispatch("SET_MODE", { mode: "MDI" }, false).catch(() => undefined); };
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!wasmSnapshotReady || programRunning || programPaused || programLoading || leftTab === "diagnostics") return;
|
|
|
|
|
const desiredMode = leftTab === "mdi" ? "MDI" : "MANUAL";
|
|
|
|
|
if (String(axisState.task.mode).toUpperCase() === desiredMode) return;
|
|
|
|
|
void dispatch("SET_MODE", { mode: desiredMode }, false).catch(() => undefined);
|
|
|
|
|
}, [leftTab, axisState.task.mode, wasmSnapshotReady, programRunning, programPaused, programLoading]);
|
|
|
|
|
const enterManualMode = () => {
|
|
|
|
|
setLeftTab("manual");
|
|
|
|
|
if (!wasmSnapshotReady || programRunning || programPaused || programLoading) return;
|
|
|
|
|
if (String(axisState.task.mode).toUpperCase() === "MANUAL") return;
|
|
|
|
|
// Send the explicit tab gesture in the same Worker queue as any following
|
|
|
|
|
// Run command. A state-sync effect can otherwise arrive after AUTO starts
|
|
|
|
|
// and be rejected as an invalid AUTO -> MANUAL transition.
|
|
|
|
|
void dispatch("SET_MODE", { mode: "MANUAL" }, false).catch(() => undefined);
|
|
|
|
|
};
|
|
|
|
|
const enterMdiMode = () => {
|
|
|
|
|
setLeftTab("mdi");
|
|
|
|
|
if (!wasmSnapshotReady || programRunning || programPaused || programLoading) return;
|
|
|
|
|
if (String(axisState.task.mode).toUpperCase() === "MDI") return;
|
|
|
|
|
void dispatch("SET_MODE", { mode: "MDI" }, false).catch(() => undefined);
|
|
|
|
|
};
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const onKeyDown = (event: KeyboardEvent) => {
|
|
|
|
|
const target = event.target as HTMLElement | null;
|
|
|
|
|
if (target && ["INPUT", "TEXTAREA", "SELECT"].includes(target.tagName)) return;
|
|
|
|
|
if (!event.ctrlKey && event.key === "R") { event.preventDefault(); setCommandStatus("PROGRAM_REVERSE blocked: LinuxCNC reverse command is not exposed by the selected WASM program ABI"); return; }
|
|
|
|
|
if (!event.ctrlKey && event.key === "F") { event.preventDefault(); setCommandStatus("PROGRAM_FORWARD blocked: LinuxCNC forward command is not exposed by the selected WASM program ABI"); return; }
|
|
|
|
|
if (!event.ctrlKey && event.key === "B") { event.preventDefault(); execute("SPINDLE_BRAKE", { spindle: 0, engaged: true }); return; }
|
|
|
|
|
if (!event.ctrlKey && event.key === "b") { event.preventDefault(); execute("SPINDLE_BRAKE", { spindle: 0, engaged: false }); return; }
|
|
|
|
|
const key = event.key.toLowerCase();
|
|
|
|
|
if (event.ctrlKey && key === "k") { event.preventDefault(); void dispatch("VISUAL_CLEAR", {}, true, "vismachClear").then(() => setPlotCleared(true)).catch(() => undefined); return; }
|
|
|
|
|
if (event.ctrlKey && key === "r") { event.preventDefault(); loadProgram(); return; }
|
|
|
|
|
if (event.ctrlKey && event.key === "Home") { event.preventDefault(); homeAll(); return; }
|
|
|
|
|
if (event.ctrlKey && event.key === "End") { event.preventDefault(); promptTouchOff(true); return; }
|
|
|
|
|
if (key === "f3") { event.preventDefault(); enterManualMode(); return; }
|
|
|
|
|
if (key === "f5") { event.preventDefault(); setLeftTab("mdi"); return; }
|
|
|
|
|
if (key === "f5") { event.preventDefault(); enterMdiMode(); return; }
|
|
|
|
|
if (key === "f1") { event.preventDefault(); toggleEstop(); return; }
|
|
|
|
|
if (key === "f2") { event.preventDefault(); powerOn(); return; }
|
|
|
|
|
if (key === "f7") { event.preventDefault(); onCoolant("mist"); return; }
|
|
|
|
|
if (key === "f8") { event.preventDefault(); onCoolant("flood"); return; }
|
|
|
|
|
if (key === "f9") { event.preventDefault(); onSpindle(spindleDirection === 1 ? 0 : 1); return; }
|
|
|
|
|
if (key === "f10") { event.preventDefault(); onSpindle(spindleDirection === -1 ? 0 : -1); return; }
|
|
|
|
|
if (key === "f11") { event.preventDefault(); execute("SPINDLE_COMMAND", { spindle: 0, delta: -100 }); return; }
|
|
|
|
|
if (key === "f12") { event.preventDefault(); execute("SPINDLE_COMMAND", { spindle: 0, delta: 100 }); return; }
|
|
|
|
|
if (key === "v") { event.preventDefault(); setView((current) => current === "x" ? "y" : current === "y" ? "p" : current === "z" ? "z2" : current === "z2" ? "x" : "z"); setRightTab("preview"); return; }
|
|
|
|
|
if (key === "d") { event.preventDefault(); setRotateMode((current) => !current); return; }
|
|
|
|
|
if (key === "!") { event.preventDefault(); setMetric((current) => !current); return; }
|
|
|
|
|
if (key === "@") { event.preventDefault(); setDisplayType((current) => current === "actual" ? "commanded" : "actual"); return; }
|
|
|
|
|
if (key === "#") { event.preventDefault(); setCoordType((current) => current === "machine" ? "relative" : "machine"); return; }
|
|
|
|
|
if (key === "f4") { event.preventDefault(); setRightTab((current) => current === "preview" ? "dro" : "preview"); return; }
|
|
|
|
|
if (event.key === "Home") { event.preventDefault(); homeAxis(); return; }
|
|
|
|
|
if (event.key === "End") { event.preventDefault(); promptTouchOff(false); 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; }
|
|
|
|
|
@@ -498,15 +524,30 @@ export function App() {
|
|
|
|
|
if (!ready) { stopRunTimer(); stopJogTimer(); }
|
|
|
|
|
}, [programRunning, ready, state.program?.program_status, state.task?.mode, state.snapshot?.wasm_motion]);
|
|
|
|
|
const spindleState = (state.devices as Record<string, any> | null)?.spindles?.[0] as Record<string, any> | undefined;
|
|
|
|
|
const spindleOn = spindleState ? Boolean(spindleState.enabled) : false;
|
|
|
|
|
const spindleDirection = spindleState?.enabled ? Number(spindleState.command_rpm ?? 0) < 0 ? -1 : 1 : 0;
|
|
|
|
|
const spindleBrake = spindleState ? Boolean(spindleState.brake) : false;
|
|
|
|
|
const ioState = (state.snapshot?.io as Record<string, any> | undefined)?.state as Record<string, any> | undefined;
|
|
|
|
|
const manualReady = wasmSnapshotReady && Boolean(ioState?.ready) && !Boolean(ioState?.estop);
|
|
|
|
|
const deviceIoEnvelope = state.deviceIo as Record<string, any> | null;
|
|
|
|
|
const deviceIoCanonical = (deviceIoEnvelope?.state as Record<string, any> | undefined) ?? deviceIoEnvelope;
|
|
|
|
|
const coolant = { mist: Boolean(deviceIoCanonical?.coolant?.mist ?? deviceIoCanonical?.coolant?.mist_requested), flood: Boolean(deviceIoCanonical?.coolant?.flood ?? deviceIoCanonical?.coolant?.flood_requested) };
|
|
|
|
|
const onSpindle = (direction: number) => { execute("SPINDLE_COMMAND", { spindle: 0, direction, rpm: direction === 0 ? 0 : 600, enabled: direction !== 0 }); };
|
|
|
|
|
const onSpindle = (direction: number) => { execute("SPINDLE_COMMAND", { spindle: 0, direction, rpm: direction === 0 ? 0 : 1, 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 promptTouchOff = (toolOnly: boolean) => {
|
|
|
|
|
if (toolOnly) { setCommandStatus("TOOL_TOUCH_OFF blocked: blocked_by_abi_tool_offsets"); return; }
|
|
|
|
|
const prompt = globalThis.prompt;
|
|
|
|
|
if (typeof prompt !== "function") return;
|
|
|
|
|
const entered = prompt(toolOnly ? `Tool ${toolNumber} Touch Off: enter ${selectedAxis.toUpperCase()} coordinate` : `Touch Off: enter ${selectedAxis.toUpperCase()} coordinate`, "0");
|
|
|
|
|
if (entered === null) return;
|
|
|
|
|
const value = Number(entered);
|
|
|
|
|
if (!Number.isFinite(value)) { setCommandStatus("TOUCH_OFF blocked: touch_off_value_invalid"); return; }
|
|
|
|
|
const system = prompt("Coordinate system (P0 Current, P1 G54 ... P9 G59.3)", touchOffSystem);
|
|
|
|
|
if (system === null) return;
|
|
|
|
|
const normalizedSystem = system.trim().toUpperCase();
|
|
|
|
|
if (!/^(?:P[0-9]|G5[4-9](?:\.[123])?)(?:\s+.*)?$/.test(normalizedSystem)) { setCommandStatus("TOUCH_OFF blocked: touch_off_system_invalid"); return; }
|
|
|
|
|
setTouchOffSystem(normalizedSystem);
|
|
|
|
|
void dispatch(toolOnly ? "TOOL_TOUCH_OFF" : "TOUCH_OFF", { axis: selectedAxis, value, system: normalizedSystem, units: metric ? "mm" : "inch" }).catch(() => undefined);
|
|
|
|
|
};
|
|
|
|
|
const wasmMotion = (state.snapshot?.wasm_motion as { state?: Record<string, unknown> } | undefined)?.state ?? {};
|
|
|
|
|
const sourceFeedOverride = Number(state.snapshot?.overrides?.feed ?? wasmMotion.feed_override);
|
|
|
|
|
const feedOverridePercent = Number.isFinite(sourceFeedOverride) ? Math.max(0, Math.min(200, Math.round(sourceFeedOverride * 100))) : null;
|
|
|
|
|
@@ -546,7 +587,7 @@ export function App() {
|
|
|
|
|
if (!uiCacheHydrated.current || !wasmSnapshotReady) return;
|
|
|
|
|
if (uiCacheSaveTimer.current) clearTimeout(uiCacheSaveTimer.current);
|
|
|
|
|
const cacheState = {
|
|
|
|
|
version: 1,
|
|
|
|
|
version: 2,
|
|
|
|
|
selectedAxis,
|
|
|
|
|
leftTab,
|
|
|
|
|
rightTab,
|
|
|
|
|
@@ -558,24 +599,24 @@ export function App() {
|
|
|
|
|
metric,
|
|
|
|
|
layers,
|
|
|
|
|
casePath,
|
|
|
|
|
program: { path: programPath, source: programText },
|
|
|
|
|
touchOffSystem,
|
|
|
|
|
};
|
|
|
|
|
uiCacheSaveTimer.current = setTimeout(() => {
|
|
|
|
|
uiCacheSaveTimer.current = null;
|
|
|
|
|
void client.saveUiState(cacheState, UI_CACHE_KEY).catch(() => undefined);
|
|
|
|
|
}, 500);
|
|
|
|
|
return () => { if (uiCacheSaveTimer.current) { clearTimeout(uiCacheSaveTimer.current); uiCacheSaveTimer.current = null; } };
|
|
|
|
|
}, [client, wasmSnapshotReady, selectedAxis, leftTab, rightTab, view, zoom, rotateMode, coordType, displayType, metric, layers, casePath, programPath, programText]);
|
|
|
|
|
return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-authority="linuxcnc-wasm-worker" data-axis-state-source={wasmSnapshotReady ? "linuxcnc-wasm" : "pending"} data-axis-motion-authority={String(state.snapshot?.motion_authority ?? "pending")} data-axis-task-authority={String(state.snapshot?.task_authority ?? "pending")} data-axis-task-mode={String(taskRecord.mode ?? "pending")} data-axis-ui-cache={uiCacheHydrated.current ? "opfs" : "pending"} data-axis-view={view} data-axis-zoom={String(zoom)} data-axis-tp-queue-depth={String((wasmMotion as { queue_depth?: number }).queue_depth ?? "")} data-axis-tp-velocity={String((wasmMotion as { velocity?: number }).velocity ?? "")} data-axis-tp-active-id={String((wasmMotion as { active_id?: number }).active_id ?? "")} data-axis-current-line={String(visibleCurrentLine ?? "")} 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")}>
|
|
|
|
|
}, [client, wasmSnapshotReady, selectedAxis, leftTab, rightTab, view, zoom, rotateMode, coordType, displayType, metric, touchOffSystem, layers, casePath]);
|
|
|
|
|
return <main className="axis-app" data-axis-feature="AXIS-UI-01" data-axis-authority="linuxcnc-wasm-worker" data-axis-state-source={wasmSnapshotReady ? "linuxcnc-wasm" : "pending"} data-axis-motion-authority={String(state.snapshot?.motion_authority ?? "pending")} data-axis-task-authority={String(state.snapshot?.task_authority ?? "pending")} data-axis-task-mode={String(taskRecord.mode ?? "pending")} data-axis-ui-cache={uiCacheHydrated.current ? "opfs" : "pending"} data-axis-view={view} data-axis-zoom={String(zoom)} data-axis-tp-queue-depth={String((wasmMotion as { queue_depth?: number }).queue_depth ?? "")} data-axis-tp-velocity={String((wasmMotion as { velocity?: number }).velocity ?? "")} data-axis-tp-active-id={String((wasmMotion as { active_id?: number }).active_id ?? "")} data-axis-current-line={String(visibleCurrentLine ?? "")} data-axis-run-enabled={String(gates.run.enabled && !programLoading)} 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")} data-axis-program-loading={String(programLoading)} data-axis-program-path={String(state.program?.path ?? "")} data-axis-program-source-hash={String(state.program?.source_hash ?? "")}>
|
|
|
|
|
<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={enterMdiMode} 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" disabled /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" disabled /><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>{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 wasmReady={wasmSnapshotReady} 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} />
|
|
|
|
|
<nav className="axis-menubar" data-axis-feature="AXIS-UI-02" aria-label="AXIS menu"><Menu label="File"><MenuAction onClick={loadProgram} 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 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={!programRunning && !programPaused}>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={enterMdiMode} 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 || programLoading} disabledReason={programLoading ? "axis_program_load_in_progress" : gates.power.reason} onClick={powerOn} /><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 || programLoading} disabledReason={programLoading ? "axis_program_load_in_progress" : gates.load.reason} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reload current G-code [Ctrl-R]" asset="tool_reload.gif" disabled={!gates.load.enabled || programLoading} disabledReason={programLoading ? "axis_program_load_in_progress" : gates.load.reason} onClick={loadProgram} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled || programLoading} disabledReason={programLoading ? "axis_program_load_in_progress" : gates.run.reason} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled || programLoading} disabledReason={programLoading ? "axis_program_load_in_progress" : gates.step.reason} 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={programLoading || (programPaused ? !gates.resume.enabled : !gates.pause.enabled)} disabledReason={programLoading ? "axis_program_load_in_progress" : programPaused ? gates.resume.reason : gates.pause.reason} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!programRunning && !programPaused} disabledReason={!programRunning && !programPaused ? "axis_stop_state_gate" : gates.abort.reason} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" disabled disabledReason="blocked_by_abi: LinuxCNC block-delete state is unavailable" /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" disabled disabledReason="blocked_by_abi: LinuxCNC optional-stop state is unavailable" /><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={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 wasmReady={wasmSnapshotReady} allHomed={allHomed} canHome={gates.home.enabled && !programLoading} onHomeAll={homeAll} />
|
|
|
|
|
<XyzacTrtPanel view={xyzacView} command={xyzacCommand} loading={programLoading} />
|
|
|
|
|
<XyzacLiveParameters snapshot={state.snapshot} />
|
|
|
|
|
<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={enterMdiMode}>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} canJog={manualReady && gates.jog.enabled} canHome={gates.home.enabled} canSpindle={manualReady && Boolean(gates.spindle?.enabled)} canCoolant={wasmSnapshotReady && Boolean(gates.coolant?.enabled)} canTool={manualReady && Boolean(gates.tool?.enabled)} canProbe={manualReady && Boolean(gates.probe?.enabled)} canTouchOff={manualReady && Boolean(gates.touchOff?.enabled)} canToolTouchOff={manualReady && Boolean(gates.toolTouchOff?.enabled)} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} spindleBrake={spindleBrake} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} jogScale={Number.isFinite(sourceJogScale) ? sourceJogScale : null} onJogScale={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={manualReady && 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-panel-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>{jogScalePercent === null ? "-" : `${jogScalePercent}%`}</output><input type="range" min="6" max="100" value={jogScalePercent ?? 6} onChange={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} disabled={!gates.jog.enabled || jogScalePercent === null} aria-label="Jog speed" /></label><label>Feed Override:<output>{feedOverridePercent === null ? "-" : `${feedOverridePercent}%`}</output><input type="range" min="0" max="200" step="1" value={feedOverridePercent ?? 0} onChange={requestFeedOverride} disabled={!gates.override.enabled || programLoading || feedOverridePercent === null} aria-label="Feed override" aria-valuetext={feedOverridePercent === null ? "WASM state pending" : `${feedOverridePercent}%`} data-axis-feed-override={feedOverridePercent ?? ""} /></label><label>Spindle Override:<output>{spindleOverridePercent === null ? "-" : `${spindleOverridePercent}%`}</output><input type="range" min="0" max="200" value={spindleOverridePercent ?? 0} onChange={(event) => void dispatch("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: Number(event.target.value) / 100 })} disabled={!gates.override.enabled || spindleOverridePercent === null} aria-label="Spindle override" /></label><label>Max Velocity:<output>{maxVelocityPercent === null ? "-" : `${maxVelocityPercent}%`}</output><input type="range" min="6" max="100" value={maxVelocityPercent ?? 6} onChange={(event) => void dispatch("SET_MAX_VELOCITY", { percent: Number(event.target.value) })} disabled={!gates.override.enabled || maxVelocityPercent === null} aria-label="Max velocity" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["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>
|
|
|
|
|
<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={enterMdiMode}>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} canJog={!programLoading && manualReady && gates.jog.enabled} canHome={!programLoading && gates.home.enabled} canSpindle={!programLoading && manualReady && Boolean(gates.spindle?.enabled)} canCoolant={!programLoading && wasmSnapshotReady && Boolean(gates.coolant?.enabled)} canTool={!programLoading && manualReady && Boolean(gates.tool?.enabled)} canProbe={!programLoading && manualReady && Boolean(gates.probe?.enabled)} canTouchOff={!programLoading && manualReady && Boolean(gates.touchOff?.enabled)} canToolTouchOff={!programLoading && manualReady && Boolean(gates.toolTouchOff?.enabled)} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleDirection={spindleDirection} spindleBrake={spindleBrake} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} onHomeAxis={homeAxis} onTouchOff={() => promptTouchOff(false)} onToolTouchOff={() => promptTouchOff(true)} jogScale={Number.isFinite(sourceJogScale) ? sourceJogScale : null} onJogScale={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={!programLoading && manualReady && 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-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>{jogScalePercent === null ? "-" : `${jogScalePercent}%`}</output><input type="range" min="6" max="100" value={jogScalePercent ?? 6} onChange={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} disabled={programLoading || !gates.jog.enabled || jogScalePercent === null} aria-label="Jog speed" /></label><label>Feed Override:<output>{feedOverridePercent === null ? "-" : `${feedOverridePercent}%`}</output><input type="range" min="0" max="200" step="1" value={feedOverridePercent ?? 0} onChange={requestFeedOverride} disabled={!gates.override.enabled || programLoading || feedOverridePercent === null} aria-label="Feed override" aria-valuetext={feedOverridePercent === null ? "WASM state pending" : `${feedOverridePercent}%`} data-axis-feed-override={feedOverridePercent ?? ""} /></label><label>Spindle Override:<output>{spindleOverridePercent === null ? "-" : `${spindleOverridePercent}%`}</output><input type="range" min="0" max="200" value={spindleOverridePercent ?? 0} onChange={(event) => void dispatch("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: Number(event.target.value) / 100 })} disabled={programLoading || !gates.override.enabled || spindleOverridePercent === null} aria-label="Spindle override" /></label><label>Max Velocity:<output>{maxVelocityPercent === null ? "-" : `${maxVelocityPercent}%`}</output><input type="range" min="6" max="100" value={maxVelocityPercent ?? 6} onChange={(event) => void dispatch("SET_MAX_VELOCITY", { percent: Number(event.target.value) })} disabled={programLoading || !gates.override.enabled || maxVelocityPercent === null} aria-label="Max velocity" /></label><label title="blocked_by_abi: LinuxCNC rapidrate is not exposed by the selected WASM TP"><span>Rapid Override</span><output>Unavailable</output><input type="range" min="0" max="120" value="0" disabled aria-label="Rapid override" /></label></div><div className="axis-layer-controls" aria-label="Preview layers">{(["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>
|
|
|
|
|
<XyzacProgramPane status={visibleProgramStatus} currentLine={visibleCurrentLine} sourceHash={programLoading ? "" : String(state.program?.source_hash ?? "")}><ProgramPane text={programText} setText={setProgramText} activeGcodes={activeGcodes} executionTrace={visibleExecutionTrace} programStatus={visibleProgramStatus} onLoad={loadProgram} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled && !programLoading} canRun={gates.run.enabled && !programLoading} canStep={gates.step.enabled && !programLoading} currentLine={visibleCurrentLine} executionLine={visibleExecutionLine} executionLineText={visibleExecutionLineText} diagnostic={state.error?.code ?? null} /></XyzacProgramPane>
|
|
|
|
|
<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>
|
|
|
|
|
|