|
|
|
|
@@ -40,9 +40,9 @@ 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 MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programRunning, programLoading, canPower, canHome, canRun, onEstop, onRelease, onPower, onHomeAll, onRun, onSequence }: { isEstop: boolean; machineOn: boolean; allHomed: boolean; programLoaded: boolean; programRunning: boolean; programLoading: boolean; canPower: boolean; canHome: boolean; canRun: boolean; onEstop: () => void; onRelease: () => void; onPower: () => void; onHomeAll: () => void; onRun: () => void; onSequence: () => void }) {
|
|
|
|
|
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 }) {
|
|
|
|
|
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>{isEstop ? "ESTOP" : machineOn ? "ON" : "OFF"} / {allHomed ? "HOME ALL" : "NOT HOMED"}</span></div>
|
|
|
|
|
<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>
|
|
|
|
|
@@ -54,21 +54,17 @@ function MachineSequence({ isEstop, machineOn, allHomed, programLoaded, programR
|
|
|
|
|
</section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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> }) {
|
|
|
|
|
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> }) {
|
|
|
|
|
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 [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 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 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={!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) => { const engaged = event.target.checked; setBrake(engaged); void action("SPINDLE_BRAKE", { spindle: 0, engaged }); }} /> 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-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>;
|
|
|
|
|
<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-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>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function MdiControl({ action, ready }: { action: Action; ready: boolean }) {
|
|
|
|
|
@@ -81,12 +77,13 @@ function MdiControl({ action, ready }: { action: Action; ready: boolean }) {
|
|
|
|
|
function DroView({ axes, state, coordType, displayType, metric, setCoordType, setDisplayType, setMetric }: { axes: number[]; state: typeof INITIAL_STATE; coordType: "machine" | "relative"; displayType: "actual" | "commanded"; metric: boolean; setCoordType: (value: "machine" | "relative") => void; setDisplayType: (value: "actual" | "commanded") => void; setMetric: (value: boolean) => void }) {
|
|
|
|
|
const labels = ["X", "Y", "Z", "A", "C"];
|
|
|
|
|
const scale = metric ? 1 : 1 / 25.4;
|
|
|
|
|
const format = (value: number, index: number) => (value * (index < 3 ? scale : 1)).toFixed(4);
|
|
|
|
|
const format = (value: number, index: number) => Number.isFinite(value) ? (value * (index < 3 ? scale : 1)).toFixed(4) : "-";
|
|
|
|
|
const sourceReady = state.snapshot?.motion_authority === "wasm" && Boolean(state.snapshot?.task_authority);
|
|
|
|
|
const pose = (state.snapshot?.visual?.pose ?? state.snapshot?.authoritative?.kinematics?.pose) as Record<string, number> | undefined;
|
|
|
|
|
const commanded = [pose?.x, pose?.y, pose?.z, pose?.a, pose?.c].map((value, index) => Number.isFinite(value) ? Number(value) : Number(axes[index] ?? 0));
|
|
|
|
|
const commanded = [pose?.x, pose?.y, pose?.z, pose?.a, pose?.c].map((value, index) => sourceReady && Number.isFinite(value) ? Number(value) : sourceReady ? Number(axes[index] ?? Number.NaN) : Number.NaN);
|
|
|
|
|
const actualPose = (state.snapshot?.visual?.pose ?? state.snapshot?.kinematics?.pose ?? {}) as Record<string, number>;
|
|
|
|
|
const actualAxes = [actualPose.x, actualPose.y, actualPose.z, actualPose.a, actualPose.c].map((value, index) => Number.isFinite(value) ? Number(value) : Number(axes[index] ?? 0));
|
|
|
|
|
const relative = state.snapshot?.coordinates?.relative ?? [0, 0, 0, 0, 0];
|
|
|
|
|
const actualAxes = [actualPose.x, actualPose.y, actualPose.z, actualPose.a, actualPose.c].map((value, index) => sourceReady && Number.isFinite(value) ? Number(value) : sourceReady ? Number(axes[index] ?? Number.NaN) : Number.NaN);
|
|
|
|
|
const relative = sourceReady ? state.snapshot?.coordinates?.relative ?? [Number.NaN, Number.NaN, Number.NaN, Number.NaN, Number.NaN] : [Number.NaN, Number.NaN, Number.NaN, Number.NaN, Number.NaN];
|
|
|
|
|
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.map((label, index) => { const actual = actualAxes[index]; 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, index)}</output><output>{format(actual, index)}</output><output>{format(relativeValue, index)}</output></div>; })}</div><div className="dro-footer"><span>Units: <b>{metric ? "mm" : "inch"}</b> / deg</span><span>{coordType} / {displayType}</span><span>Tick: {state.tick}</span><span>Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}</span></div></section>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -176,8 +173,6 @@ export function App() {
|
|
|
|
|
const [view, setView] = useState<"z" | "z2" | "x" | "y" | "y2" | "p">("p");
|
|
|
|
|
const [zoom, setZoom] = useState(1);
|
|
|
|
|
const [rotateMode, setRotateMode] = useState(false);
|
|
|
|
|
const [blockDelete, setBlockDelete] = useState(false);
|
|
|
|
|
const [optionalStop, setOptionalStop] = useState(false);
|
|
|
|
|
const [coordType, setCoordType] = useState<"machine" | "relative">("machine");
|
|
|
|
|
const [displayType, setDisplayType] = useState<"actual" | "commanded">("actual");
|
|
|
|
|
const [metric, setMetric] = useState(true);
|
|
|
|
|
@@ -189,30 +184,31 @@ export function App() {
|
|
|
|
|
const programFileInput = useRef<HTMLInputElement>(null);
|
|
|
|
|
const [plotCleared, setPlotCleared] = useState(false);
|
|
|
|
|
const [operationTrace, setOperationTrace] = useState<XyzacOperationTrace[]>([]);
|
|
|
|
|
const [feedOverrideDraft, setFeedOverrideDraft] = useState(100);
|
|
|
|
|
const feedOverridePending = useRef<number | null>(null);
|
|
|
|
|
const feedOverrideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
const [previewProgress, setPreviewProgress] = useState<{ percent: number; phase: "reading" | "wasm" | "ready" | "error"; detail: string } | null>(null);
|
|
|
|
|
const previewProgressTimer = useRef<ReturnType<typeof setInterval> | null>(null);
|
|
|
|
|
useEffect(() => { const unsubscribe = client.subscribe((message) => setState((previous) => reduceFrontendState(previous, message))); client.ensureSession().then(async () => { const bundle = await loadSimulationCase({ path: DEFAULT_CASE, baseUrl: "." }); if (bundle.status !== "ready") throw new Error(`xyzac_case_blocked:${bundle.missing.join(",")}`); const source = bundle.files?.[PROGRAM_PATH]; if (typeof source !== "string" || !source.trim()) throw new Error("xyzac_demo_program_missing"); const normalizedSource = normalizeProgramSource(source); setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); const configuration = toWorkerConfiguration(bundle); await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration }, { state: {}, gate: false }); await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: {}, gate: false }); setOperationTrace(operationController.exportTrace()); setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`); }).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); }); return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current); client.close(); }; }, [client, operationController]);
|
|
|
|
|
const axes = state.snapshot?.motion?.axes ?? [0, 0, 0];
|
|
|
|
|
const axes = state.snapshot?.motion?.axes ?? [];
|
|
|
|
|
const taskRecord = state.task ?? {};
|
|
|
|
|
const axisState = { task: { ...taskRecord, state: taskRecord.state ?? "OFF", mode: taskRecord.mode ?? "MANUAL", interp_state: taskRecord.interp_state ?? (state.status === "running" ? "READING" : state.status === "paused" ? "PAUSED" : "IDLE") }, program: state.program ?? {}, motion: state.snapshot?.motion ?? {}, devices: state.devices ?? {}, xyzac: state.snapshot?.xyzac ?? {}, gates: state.gates };
|
|
|
|
|
const axisState = { task: taskRecord, program: state.program ?? {}, motion: state.snapshot?.motion ?? {}, devices: state.devices ?? {}, xyzac: state.snapshot?.xyzac ?? {}, gates: state.gates };
|
|
|
|
|
const xyzacView = useMemo(() => projectXyzacPanel(state.snapshot), [state.snapshot]);
|
|
|
|
|
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);
|
|
|
|
|
// Runtime state is displayed only after a Worker panel snapshot is
|
|
|
|
|
// available. React may render before WASM boot, but it must not invent a
|
|
|
|
|
// machine/task/operation state that can enable motion.
|
|
|
|
|
const wasmSnapshotReady = Boolean(state.snapshot?.motion_authority === "wasm" && state.snapshot?.task_authority);
|
|
|
|
|
const machineState = wasmSnapshotReady ? state.axisState?.machine_state ?? "OFF" : "PENDING";
|
|
|
|
|
const operationState = wasmSnapshotReady ? state.axisState?.operation_state ?? "IDLE" : "PENDING";
|
|
|
|
|
const taskState = wasmSnapshotReady ? state.axisState?.state ?? "OFF" : "PENDING";
|
|
|
|
|
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") };
|
|
|
|
|
const planner = state.snapshot?.authoritative?.planner;
|
|
|
|
|
const trajectory = [...(planner?.queue ?? []), ...(planner?.active ? [planner.active] : [])];
|
|
|
|
|
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); };
|
|
|
|
|
@@ -426,39 +422,36 @@ export function App() {
|
|
|
|
|
if (programRunning && !programComplete) startRunTimer(); else 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 spindleState = (state.devices as Record<string, any> | null)?.spindles?.[0] as Record<string, any> | undefined;
|
|
|
|
|
const spindleOn = spindleState ? Boolean(spindleState.enabled) : false;
|
|
|
|
|
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 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 wasmFeedOverride = Number(state.snapshot?.overrides?.feed ?? 1);
|
|
|
|
|
const feedOverridePercent = Number.isFinite(wasmFeedOverride) ? Math.max(0, Math.min(200, Math.round(wasmFeedOverride * 100))) : 100;
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (feedOverridePending.current === null) setFeedOverrideDraft(feedOverridePercent);
|
|
|
|
|
else if (feedOverridePercent === feedOverridePending.current) {
|
|
|
|
|
feedOverridePending.current = null;
|
|
|
|
|
setFeedOverrideDraft(feedOverridePercent);
|
|
|
|
|
}
|
|
|
|
|
}, [feedOverridePercent]);
|
|
|
|
|
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;
|
|
|
|
|
const sourceJogScale = Number((state.snapshot?.motion as Record<string, unknown> | undefined)?.jog_scale ?? wasmMotion.jog_scale);
|
|
|
|
|
const sourceVelocityScale = Number((state.snapshot?.motion as Record<string, unknown> | undefined)?.velocity_scale ?? wasmMotion.velocity_scale);
|
|
|
|
|
const jogScalePercent = Number.isFinite(sourceJogScale) ? Math.max(6, Math.min(100, Math.round(sourceJogScale * 100))) : null;
|
|
|
|
|
const maxVelocityPercent = Number.isFinite(sourceVelocityScale) ? Math.max(6, Math.min(100, Math.round(sourceVelocityScale * 100))) : null;
|
|
|
|
|
const spindleOverrideSource = Number(state.snapshot?.overrides?.spindle?.[0] ?? (state.snapshot?.motion as Record<string, unknown> | undefined)?.spindle_override ?? spindleState?.override);
|
|
|
|
|
const spindleOverridePercent = Number.isFinite(spindleOverrideSource) ? Math.max(0, Math.min(200, Math.round(spindleOverrideSource * 100))) : null;
|
|
|
|
|
const requestFeedOverride = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
const percent = Math.max(0, Math.min(200, Number(event.target.value)));
|
|
|
|
|
if (!Number.isFinite(percent)) return;
|
|
|
|
|
setFeedOverrideDraft(percent);
|
|
|
|
|
feedOverridePending.current = percent;
|
|
|
|
|
if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current);
|
|
|
|
|
feedOverrideTimer.current = setTimeout(() => {
|
|
|
|
|
feedOverrideTimer.current = null;
|
|
|
|
|
const target = feedOverridePending.current;
|
|
|
|
|
if (target === null) return;
|
|
|
|
|
void dispatch("SET_OVERRIDE", { feed: target / 100 }).then(() => {
|
|
|
|
|
if (feedOverridePending.current === target) feedOverridePending.current = null;
|
|
|
|
|
}).catch(() => {
|
|
|
|
|
if (feedOverridePending.current !== target) return;
|
|
|
|
|
feedOverridePending.current = null;
|
|
|
|
|
setFeedOverrideDraft(feedOverridePercent);
|
|
|
|
|
});
|
|
|
|
|
void dispatch("SET_OVERRIDE", { feed: target / 100 }).then(() => { if (feedOverridePending.current === target) feedOverridePending.current = null; }).catch(() => { if (feedOverridePending.current === target) feedOverridePending.current = null; });
|
|
|
|
|
}, 80);
|
|
|
|
|
};
|
|
|
|
|
const activeGcodes = programLoading ? "" : state.program?.active_gcodes ? String(state.program.active_gcodes) : "G21 G90\nG17 G40 G49\nG54 G64\nM5 M9";
|
|
|
|
|
const activeGcodes = programLoading ? "" : state.program?.active_gcodes ? String(state.program.active_gcodes) : "-";
|
|
|
|
|
const toolNumber = String((state.devices as Record<string, any> | null)?.tool?.current ?? state.snapshot?.tool?.current ?? 0);
|
|
|
|
|
const currentLine = Number(state.program?.line ?? state.task?.current_line ?? 0) || null;
|
|
|
|
|
const executionLine = Number(state.program?.execution_line ?? currentLine ?? 0) || null;
|
|
|
|
|
@@ -467,20 +460,20 @@ export function App() {
|
|
|
|
|
const visibleCurrentLine = programLoading ? null : currentLine;
|
|
|
|
|
const visibleExecutionLine = programLoading ? null : executionLine;
|
|
|
|
|
const visibleExecutionLineText = programLoading ? "" : executionLineText;
|
|
|
|
|
const visibleProgramStatus = programLoading ? "loading" : String(state.program?.program_status ?? "idle");
|
|
|
|
|
const visibleProgramStatus = programLoading ? "loading" : String(state.program?.program_status ?? "unknown");
|
|
|
|
|
const homedAxes = Array.isArray(state.snapshot?.motion?.homed_axes) ? state.snapshot.motion.homed_axes : [];
|
|
|
|
|
const allHomed = homedAxes.length >= 5 && homedAxes.slice(0, 5).every(Boolean);
|
|
|
|
|
const xyzacCommand = (kind: string, payload: Record<string, unknown> = {}, action?: string) => { void dispatch(kind, payload, true, action ?? null).then(() => { if (kind === "VISUAL_CLEAR") setPlotCleared(true); }).catch(() => undefined); };
|
|
|
|
|
const position = `X${Number(xyzacView.pose?.x ?? axes[0] ?? 0).toFixed(3)} Y${Number(xyzacView.pose?.y ?? axes[1] ?? 0).toFixed(3)} Z${Number(xyzacView.pose?.z ?? axes[2] ?? 0).toFixed(3)} A${Number(xyzacView.pose?.a ?? 0).toFixed(3)} C${Number(xyzacView.pose?.c ?? 0).toFixed(3)}`;
|
|
|
|
|
return <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")}>
|
|
|
|
|
const position = xyzacView.pose ? `X${Number(xyzacView.pose.x).toFixed(3)} Y${Number(xyzacView.pose.y).toFixed(3)} Z${Number(xyzacView.pose.z).toFixed(3)} A${Number(xyzacView.pose.a).toFixed(3)} C${Number(xyzacView.pose.c).toFixed(3)}` : "WASM state pending";
|
|
|
|
|
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-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={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); }} disabled={programRunning || programLoading}>Reload editor</MenuAction><MenuAction onClick={() => setCommandStatus("Tool data editor is browser-backed")}>Edit tool data...</MenuAction><MenuAction onClick={() => setCommandStatus("Quit is unavailable in a browser tab")}>Quit</MenuAction></Menu><Menu label="Machine"><MenuAction shortcut="F1" onClick={toggleEstop}>{isEstop ? "Cancel Emergency Stop" : "Emergency Stop"}</MenuAction><MenuAction shortcut="F2" onClick={powerOn} disabled={!gates.power.enabled}>{machineOn ? "Power off" : "Power on"}</MenuAction><MenuAction onClick={resetMachine} disabled={!gates.reset.enabled}>Reset machine</MenuAction><MenuAction shortcut="R" onClick={runProgram} disabled={!gates.run.enabled || programLoading}>Run program</MenuAction><MenuAction shortcut="T" onClick={() => execute("PROGRAM_STEP")} disabled={!gates.step.enabled || programLoading}>Step</MenuAction><MenuAction shortcut="P" onClick={() => execute("PROGRAM_PAUSE")} disabled={!gates.pause.enabled || programLoading}>Pause</MenuAction><MenuAction shortcut="S" onClick={() => execute("PROGRAM_RESUME")} disabled={!gates.resume.enabled || programLoading}>Resume</MenuAction><MenuAction shortcut="Esc" onClick={stopProgram} disabled={!gates.abort.enabled}>Stop</MenuAction></Menu><Menu label="View"><MenuAction onClick={() => setRightTab("preview")}>Preview</MenuAction><MenuAction onClick={() => setRightTab("dro")}>DRO</MenuAction><MenuAction onClick={enterManualMode} disabled={programRunning || programLoading}>Manual Control</MenuAction><MenuAction onClick={() => setLeftTab("mdi")} disabled={programRunning || programLoading}>MDI</MenuAction></Menu><Menu label="Help"><MenuAction onClick={() => setCommandStatus("LinuxCNC AXIS source-compatible controls")}>About AXIS</MenuAction></Menu></nav>
|
|
|
|
|
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label={machineOn ? "Power off [F2]" : "Power on [F2]"} asset="tool_power.gif" disabled={!gates.power.enabled} onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" disabled={!gates.reset.enabled} onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" disabled={programRunning || programLoading} onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" pressed={blockDelete} onClick={() => setBlockDelete((value) => !value)} /><IconButton feature="AXIS-MACHINE-08" label="Toggle optional pause" asset="tool_optpause.gif" pressed={optionalStop} onClick={() => setOptionalStop((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-01" label="Zoom in" asset="tool_zoomin.gif" onClick={() => setZoom((value) => Math.min(2, value + .2))} /><IconButton feature="AXIS-VIEW-01" label="Zoom out" asset="tool_zoomout.gif" onClick={() => setZoom((value) => Math.max(.5, value - .2))} /><IconButton feature="AXIS-VIEW-01" label="Top view" asset="tool_axis_z.gif" pressed={view === "z"} onClick={() => { setView("z"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Rotated top view" asset="tool_axis_z2.gif" pressed={view === "z2"} onClick={() => { setView("z2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Side view" asset="tool_axis_x.gif" pressed={view === "x"} onClick={() => { setView("x"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Front view" asset="tool_axis_y.gif" pressed={view === "y"} onClick={() => { setView("y"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Inverted front view" asset="tool_axis_y2.png" pressed={view === "y2"} onClick={() => { setView("y2"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-01" label="Perspective view" asset="tool_axis_p.gif" pressed={view === "p"} onClick={() => { setView("p"); setRightTab("preview"); }} /><IconButton feature="AXIS-VIEW-02" label="Drag / rotate mode" asset="tool_rotate.gif" pressed={rotateMode} onClick={() => setRotateMode((value) => !value)} /><Separator /><IconButton feature="AXIS-VIEW-15" label="Clear live plot [Ctrl-K]" asset="tool_clear.gif" onClick={() => xyzacCommand("VISUAL_CLEAR", {}, "vismachClear")} /></section>
|
|
|
|
|
<section className="axis-toolbar" data-axis-feature="AXIS-UI-05" aria-label="AXIS toolbar"><IconButton feature="AXIS-MACHINE-01" label={isEstop ? "Cancel Emergency Stop [F1]" : "Emergency Stop [F1]"} asset="tool_estop.gif" danger onClick={toggleEstop} pressed={isEstop} /><IconButton feature="AXIS-MACHINE-02" label={machineOn ? "Power off [F2]" : "Power on [F2]"} asset="tool_power.gif" disabled={!gates.power.enabled} onClick={powerOn} /><IconButton feature="AXIS-MACHINE-03" label="Reset machine" asset="tool_reload.gif" disabled={!gates.reset.enabled} onClick={resetMachine} /><Separator /><IconButton feature="AXIS-FILE-01" label="Open G-Code file [O]" asset="tool_open.gif" disabled={programRunning || programLoading} onClick={openProgramFile} /><IconButton feature="AXIS-FILE-02" label="Load exact XYZAC TRT case" asset="tool_verify.gif" disabled={!gates.load.enabled} onClick={loadCase} /><IconButton feature="AXIS-FILE-04" label="Reopen editor defaults" asset="tool_reload.gif" onClick={() => { setProgramText(DEFAULT_PROGRAM); setProgramPath(PROGRAM_PATH); setPlotCleared(false); }} /><Separator /><IconButton feature="AXIS-MACHINE-03" label="Begin executing current file [R]" asset="tool_run.gif" disabled={!gates.run.enabled} onClick={runProgram} /><IconButton feature="AXIS-MACHINE-05" label="Execute next line [T]" asset="tool_step.gif" disabled={!gates.step.enabled} onClick={() => execute("PROGRAM_STEP")} /><IconButton feature="AXIS-MACHINE-06" label={programPaused ? "Resume execution [S]" : "Pause execution [P]"} asset={programPaused ? "resume_inhibit.gif" : "tool_pause.gif"} disabled={programPaused ? !gates.resume.enabled : !gates.pause.enabled} onClick={() => execute(programPaused ? "PROGRAM_RESUME" : "PROGRAM_PAUSE")} /><IconButton feature="AXIS-MACHINE-07" label="Stop program execution [ESC]" asset="tool_stop.gif" disabled={!gates.abort.enabled} onClick={stopProgram} /><Separator /><IconButton feature="AXIS-MACHINE-09" label="Toggle skip lines with slash" asset="tool_blockdelete.gif" 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 isEstop={isEstop} machineOn={machineOn} allHomed={allHomed} programLoaded={programLoaded} programRunning={programRunning} programLoading={programLoading} canPower={gates.power.enabled} canHome={gates.home.enabled} canRun={gates.run.enabled} onEstop={emergencyStop} onRelease={() => { void releaseEstop().catch(() => undefined); }} onPower={powerOn} onHomeAll={homeAll} onRun={runProgram} onSequence={runStartupSequence} />
|
|
|
|
|
<MachineSequence 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} />
|
|
|
|
|
<section className="axis-top-pane" data-axis-feature="AXIS-UI-06"><div className="axis-notebook left-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Manual, MDI and diagnostics controls"><button id="axis-tab-manual" type="button" role="tab" aria-controls="axis-panel-manual" aria-selected={leftTab === "manual"} className={leftTab === "manual" ? "active" : ""} onClick={enterManualMode}>Manual Control [F3]</button><button id="axis-tab-mdi" type="button" role="tab" aria-controls="axis-panel-mdi" aria-selected={leftTab === "mdi"} className={leftTab === "mdi" ? "active" : ""} onClick={() => setLeftTab("mdi")}>MDI [F5]</button><button id="axis-tab-diagnostics" type="button" role="tab" aria-controls="axis-panel-diagnostics" aria-selected={leftTab === "diagnostics"} className={leftTab === "diagnostics" ? "active" : ""} onClick={() => setLeftTab("diagnostics")}>Diagnostics</button></div><div id="axis-panel-manual" role="tabpanel" aria-labelledby="axis-tab-manual" hidden={leftTab !== "manual"}><ManualControl action={execute} ready={gates.jog.enabled} canHome={gates.home.enabled} onHomeAll={homeAll} selectedAxis={selectedAxis} setSelectedAxis={setSelectedAxis} spindleOn={spindleOn} coolant={coolant} onSpindle={onSpindle} onCoolant={onCoolant} devices={state.devices ?? {}} /></div><div id="axis-panel-mdi" role="tabpanel" aria-labelledby="axis-tab-mdi" hidden={leftTab !== "mdi"}><MdiControl action={execute} ready={gates.mdi.enabled} /></div><div id="axis-panel-diagnostics" role="tabpanel" aria-labelledby="axis-tab-diagnostics" hidden={leftTab !== "diagnostics"}><DiagnosticsPanel state={state} gates={gates} /><XyzacDiagnostics view={xyzacView} operations={operationTrace} alarms={state.alarms} error={state.error} /></div></div><div className="axis-notebook right-notebook" data-axis-feature="AXIS-UI-06"><div className="notebook-tabs" role="tablist" aria-label="Preview and DRO"><button id="axis-tab-preview" type="button" role="tab" aria-controls="axis-panel-preview" aria-selected={rightTab === "preview"} className={rightTab === "preview" ? "active" : ""} onClick={() => setRightTab("preview")}>Preview</button><button id="axis-tab-dro" type="button" role="tab" aria-controls="axis-panel-dro" aria-selected={rightTab === "dro"} className={rightTab === "dro" ? "active" : ""} onClick={() => setRightTab("dro")}>DRO</button></div><div id="axis-panel-preview" role="tabpanel" aria-labelledby="axis-tab-preview" className="axis-preview" hidden={rightTab !== "preview"}><XyzacToolPreviewFromSnapshot snapshot={programLoading ? null : state.snapshot} view={view} zoom={zoom} rotateMode={rotateMode} showPath={layers.path} showLive={layers.live} showLimits={layers.limits} showVelocity={layers.velocity} showOffsets={layers.offsets} showExtents={layers.extents} /></div><div id="axis-panel-dro" role="tabpanel" aria-labelledby="axis-tab-dro" hidden={rightTab !== "dro"}><DroView axes={axes} state={state} coordType={coordType} displayType={displayType} metric={metric} setCoordType={setCoordType} setDisplayType={setDisplayType} setMetric={setMetric} /></div></div></section>
|
|
|
|
|
<section className="axis-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" aria-label="Jog speed" /></label><label>Feed Override:<output>{feedOverrideDraft}%</output><input type="range" min="0" max="200" step="1" value={feedOverrideDraft} onChange={requestFeedOverride} disabled={!gates.override.enabled || programLoading} aria-label="Feed override" aria-valuetext={`${feedOverrideDraft}%`} data-axis-feed-override={feedOverrideDraft} /></label><label>Spindle Override:<output>{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%</output><input type="range" min="0" max="120" defaultValue="100" aria-label="Spindle 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>
|
|
|
|
|
<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} 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-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={!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>
|
|
|
|
|
<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>
|
|
|
|
|
|