Lock AXIS panel contract to source behavior

This commit is contained in:
2026-09-07 18:35:05 +08:00
parent f94f9b357e
commit 845075f4a3
7 changed files with 361 additions and 175 deletions

View File

@@ -185,7 +185,6 @@ export function App() {
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[]>([]);
const feedOverridePending = useRef<number | null>(null);
const feedOverrideTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
@@ -314,9 +313,6 @@ export function App() {
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 });
};
const stopPreviewProgress = () => {
if (previewProgressTimer.current) {
clearInterval(previewProgressTimer.current);
@@ -366,8 +362,7 @@ export function App() {
const operation = await operationController.execute("LOAD_WORKSPACE", { root_path: "/workspace", configuration: toWorkerConfiguration(bundle) }, { state: axisState, gate: false });
const resultPayload = (operation.response as { payload?: { configuration?: { state?: string } } }).payload;
markPreviewProgress(28, "wasm", "LinuxCNC WASM 配置已加载,准备解析刀具路径");
setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH); setPlotCleared(false);
await clearLoadedPreview();
setProgramText(normalizedSource); setProgramPath(PROGRAM_PATH);
markPreviewProgress(38, "wasm", "LinuxCNC WASM 正在解析刀具路径");
const programLoad = await operationController.execute("PROGRAM_LOAD", { source: normalizedSource, path: PROGRAM_PATH }, { state: axisState, gate: false });
finishPreviewProgress(PROGRAM_PATH, programLoad.response);
@@ -393,8 +388,7 @@ export function App() {
if (!response.ok) throw new Error(`program_asset_http_${response.status}`);
const source = normalizeProgramSource(await response.text());
if (!source.trim()) throw new Error("program_asset_empty");
setProgramText(source); setProgramPath(path); setPlotCleared(false);
await clearLoadedPreview();
setProgramText(source); setProgramPath(path);
markPreviewProgress(38, "wasm", "LinuxCNC WASM 正在解析刀具路径");
const programLoad = await operationController.execute("PROGRAM_LOAD", { source, path }, { state: axisState, gate: false });
finishPreviewProgress(path, programLoad.response);
@@ -412,8 +406,7 @@ export function App() {
setProgramLoading(true);
startPreviewProgress(programPath || "当前程序");
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
setPlotCleared(false);
void clearLoadedPreview()
void Promise.resolve()
.then(() => {
markPreviewProgress(38, "wasm", "LinuxCNC WASM 正在解析刀具路径");
return operationController.execute("PROGRAM_LOAD", { source: programText, path: programPath }, { state: axisState, gate: false });
@@ -444,10 +437,8 @@ export function App() {
const path = file.name === "3D_Chips.ngc" ? "linuxcnc/nc_files/3D_Chips.ngc" : `browser/${file.name}`;
setProgramText(source);
setProgramPath(path);
setPlotCleared(false);
setCommandStatus("PROGRAM_LOAD pending: waiting for LinuxCNC WASM preview");
setCaseStatus(`${file.name} loading through linuxcnc-wasm...`);
await clearLoadedPreview();
markPreviewProgress(38, "wasm", "LinuxCNC WASM 正在解析刀具路径");
const programLoad = await operationController.execute("PROGRAM_LOAD", { source, path }, { state: axisState, gate: false });
finishPreviewProgress(path, programLoad.response);
@@ -487,7 +478,7 @@ export function App() {
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 === "k") { event.preventDefault(); void dispatch("VISUAL_CLEAR", {}, true, "vismachClear").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; }
@@ -583,7 +574,7 @@ export function App() {
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 xyzacCommand = (kind: string, payload: Record<string, unknown> = {}, action?: string) => { void dispatch(kind, payload, true, action ?? null).catch(() => undefined); };
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";
useEffect(() => {
if (!uiCacheHydrated.current || !wasmSnapshotReady) return;
@@ -618,7 +609,7 @@ export function App() {
<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" data-axis-control="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" data-axis-control="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" data-axis-control="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 && idle && manualReady && Boolean(gates.coolant?.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" data-axis-control="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" data-axis-control="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" data-axis-control="override-jog-speed" 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" data-axis-control="override-feed" 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" data-axis-control="override-spindle" 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" data-axis-control="override-max-velocity" 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" data-axis-control="override-rapid" 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-control={`preview-layer-${layer}`} 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-metrics" data-axis-feature="AXIS-VIEW-03"><div className="axis-slider"><label>Jog Speed:<output>{jogScalePercent === null ? "-" : `${jogScalePercent}%`}</output><input type="range" data-axis-control="override-jog-speed" min="6" max="100" value={jogScalePercent ?? 6} onChange={(event) => void dispatch("SET_JOG_SCALE", { percent: Number(event.target.value) })} disabled={programLoading || jogScalePercent === null} aria-label="Jog speed" /></label><label>Feed Override:<output>{feedOverridePercent === null ? "-" : `${feedOverridePercent}%`}</output><input type="range" data-axis-control="override-feed" min="0" max="200" step="1" value={feedOverridePercent ?? 0} onChange={requestFeedOverride} disabled={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" data-axis-control="override-spindle" min="0" max="200" value={spindleOverridePercent ?? 0} onChange={(event) => void dispatch("SET_SPINDLE_OVERRIDE", { spindle: 0, scale: Number(event.target.value) / 100 })} disabled={programLoading || spindleOverridePercent === null} aria-label="Spindle override" /></label><label>Max Velocity:<output>{maxVelocityPercent === null ? "-" : `${maxVelocityPercent}%`}</output><input type="range" data-axis-control="override-max-velocity" min="6" max="100" value={maxVelocityPercent ?? 6} onChange={(event) => void dispatch("SET_MAX_VELOCITY", { percent: Number(event.target.value) })} disabled={programLoading || 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" data-axis-control="override-rapid" 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-control={`preview-layer-${layer}`} 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} currentLine={visibleCurrentLine} executionLine={visibleExecutionLine} executionLineText={visibleExecutionLineText} diagnostic={state.error?.code ?? null} /></XyzacProgramPane>
<input ref={programFileInput} className="visually-hidden-file-input" data-axis-control="program-file-input" type="file" accept=".ngc,.nc,.gcode,.tap,text/plain" onChange={onProgramFile} aria-label="Open G-code file" />
{aboutOpen && <section className="axis-about-backdrop" role="presentation" onMouseDown={(event) => { if (event.target === event.currentTarget) setAboutOpen(false); }}><div className="axis-about-dialog" role="dialog" aria-modal="true" aria-labelledby="axis-about-title"><img src={axisIcon("axis-24x24.png")} alt="" /><h2 id="axis-about-title">LinuxCNC AXIS Web/WASM</h2><p>Source-compatible control panel for the selected XYZAC TRT simulation.</p><TextButton controlId="about-close" onClick={() => setAboutOpen(false)}>OK</TextButton></div></section>}