Stream long G-code execution history with scrollable panes

This commit is contained in:
2026-08-29 15:52:06 +08:00
parent 9cd70990e9
commit dfe657e736
4 changed files with 23 additions and 12 deletions

View File

@@ -93,13 +93,15 @@ function ProgramPane({ text, setText, activeGcodes, executionTrace, programStatu
const mainLine = executing && currentLine && currentLine > 0 && currentLine <= lines.length ? currentLine : null;
const executedLines = new Set(executionTrace.map((entry) => entry.line));
const editorRef = useRef<HTMLTextAreaElement>(null);
const gutterRef = useRef<HTMLDivElement>(null);
const highlightsRef = useRef<HTMLDivElement>(null);
const traceRef = useRef<HTMLOListElement>(null);
const followTraceRef = useRef(true);
useEffect(() => {
const node = traceRef.current;
if (node) node.scrollTop = node.scrollHeight;
if (node && followTraceRef.current) node.scrollTop = node.scrollHeight;
}, [executionTrace]);
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 className="program-gutter" aria-label="G-code line numbers">{lines.map((_, index) => { const line = index + 1; const className = line === mainLine ? "current-line" : executedLines.has(line) ? "completed-line" : ""; return <span key={index} className={className} aria-current={line === mainLine ? "true" : undefined}>{line}</span>; })}</div><div className="program-editor-input"><div ref={highlightsRef} className="program-line-highlights" aria-hidden="true">{lines.map((_, index) => { const line = index + 1; const className = line === mainLine ? "current-line" : executedLines.has(line) ? "completed-line" : ""; return <span key={index} className={className} />; })}</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 && highlightsRef.current) { highlightsRef.current.scrollTop = editorRef.current.scrollTop; highlightsRef.current.scrollLeft = editorRef.current.scrollLeft; } }} 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>{executionLineText || "-"}</code></div><div className="program-execution-trace" role="log" aria-label="G-code execution trace" aria-live="polite"><strong>Subprogram / execution trace</strong><ol ref={traceRef}>{executionTrace.slice(-16).map((entry, index, entries) => { const isCurrent = executing && index === entries.length - 1; return <li key={`${entry.line}-${index}`} className={isCurrent ? "executing-line" : "completed-line"} aria-current={isCurrent ? "true" : undefined}><span>L{entry.line}</span><code>{entry.text || "-"}</code></li>; })}</ol></div></div></div><aside className="program-side"><div className="program-actions"><TextButton feature="AXIS-EDIT-01" onClick={onOpenFile}>Open file</TextButton><TextButton feature="AXIS-EDIT-01" onClick={onLoad} disabled={!canLoad}>Load</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onRun} disabled={!canRun}>Run</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onStep} disabled={!canStep}>Step</TextButton></div><label className="active-gcodes-label">Active G-Codes:<textarea data-axis-feature="AXIS-UI-07" readOnly value={activeGcodes} aria-label="Active G-Codes" /></label>{diagnostic && <output className="program-diagnostic" role="alert" data-axis-feature="AXIS-EDIT-01">{diagnostic}</output>}</aside></section>;
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">{lines.map((_, index) => { const line = index + 1; const className = line === mainLine ? "current-line" : executedLines.has(line) ? "completed-line" : ""; return <span key={index} className={className} aria-current={line === mainLine ? "true" : undefined}>{line}</span>; })}</div><div className="program-editor-input"><div ref={highlightsRef} className="program-line-highlights" aria-hidden="true">{lines.map((_, index) => { const line = index + 1; const className = line === mainLine ? "current-line" : executedLines.has(line) ? "completed-line" : ""; return <span key={index} className={className} />; })}</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; } } }} 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><ol ref={traceRef} onScroll={(event) => { const node = event.currentTarget; followTraceRef.current = node.scrollHeight - node.scrollTop - node.clientHeight < 28; }}>{executionTrace.map((entry, index, entries) => { const isCurrent = executing && index === entries.length - 1; return <li key={`${entry.line}-${index}`} className={isCurrent ? "executing-line" : "completed-line"} aria-current={isCurrent ? "true" : undefined}><span>L{entry.line}</span><code title={entry.text}>{entry.text || "-"}</code></li>; })}</ol></div></div></div><aside className="program-side"><div className="program-actions"><TextButton feature="AXIS-EDIT-01" onClick={onOpenFile}>Open file</TextButton><TextButton feature="AXIS-EDIT-01" onClick={onLoad} disabled={!canLoad}>Load</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onRun} disabled={!canRun}>Run</TextButton><TextButton feature="AXIS-CTRL-02" onClick={onStep} disabled={!canStep}>Step</TextButton></div><label className="active-gcodes-label">Active G-Codes:<textarea data-axis-feature="AXIS-UI-07" readOnly value={activeGcodes} aria-label="Active G-Codes" /></label>{diagnostic && <output className="program-diagnostic" role="alert" data-axis-feature="AXIS-EDIT-01">{diagnostic}</output>}</aside></section>;
}
function DiagnosticsPanel({ state, gates }: { state: typeof INITIAL_STATE; gates: Record<string, { enabled: boolean; reason: string }> }) {