From d87366eb7592575fc1515893d156b8d9f0c3b5fa Mon Sep 17 00:00:00 2001 From: wangdequan Date: Sat, 29 Aug 2026 22:27:01 +0800 Subject: [PATCH] Support long G-code preview and virtualized execution display --- wasm-core/include/sim_program.h | 4 +- web/react/src/App.tsx | 69 +++++++++++++++++++++++------ web/react/src/XyzacToolPreview.tsx | 30 +++++++++++-- web/react/src/store.ts | 13 +++++- web/react/src/styles.css | 22 +++++---- web/worker/test_xyzac_3d_chips.mjs | 66 +++++++++++++++++++++++++++ web/worker/wasm-program-runtime.mjs | 10 ++--- 7 files changed, 179 insertions(+), 35 deletions(-) create mode 100644 web/worker/test_xyzac_3d_chips.mjs diff --git a/wasm-core/include/sim_program.h b/wasm-core/include/sim_program.h index bc592f2..d4f9a03 100644 --- a/wasm-core/include/sim_program.h +++ b/wasm-core/include/sim_program.h @@ -7,8 +7,8 @@ extern "C" { #endif -#define SIM_PROGRAM_SOURCE_CAPACITY 131072u -#define SIM_PROGRAM_LINE_CAPACITY 4096u +#define SIM_PROGRAM_SOURCE_CAPACITY 524288u +#define SIM_PROGRAM_LINE_CAPACITY 16384u #define SIM_PROGRAM_CANONICAL_CAPACITY 4096u #define SIM_PROGRAM_SUBROUTINE_CAPACITY 16u #define SIM_PROGRAM_SUBROUTINE_NAME_CAPACITY 48u diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index 62f6492..0dc2bb3 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -13,6 +13,7 @@ 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"); type Action = (kind: string, payload?: Record) => Promise | void; type IconButtonProps = { label: string; asset: string; onClick?: () => void; disabled?: boolean; danger?: boolean; pressed?: boolean; feature?: string }; @@ -87,6 +88,32 @@ function DroView({ axes, state, coordType, displayType, metric, setCoordType, se return
Position
DisplayedActualRelative
{labels.map((label, index) => { const actual = Number(axes[index] ?? 0); const target = Number(commanded[index] ?? actual); const relativeValue = Number(relative[index] ?? actual); const displayed = displayType === "commanded" ? target : actual; return
{label}{format(coordType === "relative" ? relativeValue : displayed, index)}{format(actual, index)}{format(relativeValue, index)}
; })}
Units: {metric ? "mm" : "inch"} / deg{coordType} / {displayType}Tick: {state.tick}Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}
; } +const PROGRAM_ROW_HEIGHT = 17.4; +const TRACE_ROW_HEIGHT = 23; +const TRACE_OVERSCAN = 12; + +function VirtualizedExecutionTrace({ entries, executing }: { entries: Array<{ line: number; text: string; program_status?: string | null }>; executing: boolean }) { + const traceRef = useRef(null); + const followRef = useRef(true); + const [windowRange, setWindowRange] = useState({ start: 0, end: Math.min(entries.length, TRACE_OVERSCAN * 2 + 24) }); + const updateWindow = () => { + const node = traceRef.current; + if (!node) return; + const start = Math.max(0, Math.floor(node.scrollTop / TRACE_ROW_HEIGHT) - TRACE_OVERSCAN); + const end = Math.min(entries.length, Math.ceil((node.scrollTop + node.clientHeight) / TRACE_ROW_HEIGHT) + TRACE_OVERSCAN); + setWindowRange((previous) => previous.start === start && previous.end === end ? previous : { start, end }); + }; + useEffect(() => { + const node = traceRef.current; + if (!node) return; + if (followRef.current) node.scrollTop = node.scrollHeight; + const frame = requestAnimationFrame(updateWindow); + return () => cancelAnimationFrame(frame); + }, [entries.length]); + const visible = entries.slice(windowRange.start, windowRange.end); + return
{ const node = event.currentTarget; followRef.current = node.scrollHeight - node.scrollTop - node.clientHeight < TRACE_ROW_HEIGHT * 1.5; updateWindow(); }}>
    {visible.map((entry, offset) => { const index = windowRange.start + offset; const isCurrent = executing && index === entries.length - 1; return
  1. L{entry.line}{entry.text || "-"}
  2. ; })}
; +} + function ProgramPane({ text, setText, activeGcodes, executionTrace, programStatus, onLoad, onRun, onStep, onOpenFile, 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; onOpenFile: () => 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()); @@ -95,13 +122,27 @@ function ProgramPane({ text, setText, activeGcodes, executionTrace, programStatu const editorRef = useRef(null); const gutterRef = useRef(null); const highlightsRef = useRef(null); - const traceRef = useRef(null); - const followTraceRef = useRef(true); + const [editorWindow, setEditorWindow] = useState({ start: 0, end: Math.min(lines.length, 48) }); + const updateEditorWindow = () => { + const node = editorRef.current; + if (!node) return; + const start = Math.max(0, Math.floor(node.scrollTop / PROGRAM_ROW_HEIGHT) - 12); + const end = Math.min(lines.length, Math.ceil((node.scrollTop + node.clientHeight) / PROGRAM_ROW_HEIGHT) + 12); + setEditorWindow((previous) => previous.start === start && previous.end === end ? previous : { start, end }); + }; useEffect(() => { - const node = traceRef.current; - if (node && followTraceRef.current) node.scrollTop = node.scrollHeight; - }, [executionTrace]); - return
{lines.map((_, index) => { const line = index + 1; const className = line === mainLine ? "current-line" : executedLines.has(line) ? "completed-line" : ""; return {line}; })}