diff --git a/wasm-core/src/sim_program.c b/wasm-core/src/sim_program.c index 6e48d3a..8c7ca20 100644 --- a/wasm-core/src/sim_program.c +++ b/wasm-core/src/sim_program.c @@ -397,8 +397,8 @@ uint32_t sim_program_canonical(const sim_program *program, char *output, uint32_ int written; if (program == NULL) return 0; written = snprintf(destination, destination_capacity, - "{\"call_depth\":%u,\"control_code\":%d,\"current_line\":%u,\"error_line\":%u,\"has_control\":%u,\"has_motion\":%u,\"last_error\":%d,\"line_count\":%u,\"main_line_count\":%u,\"motion_mode\":%d,\"rapid_velocity\":%.17g,\"schema\":\"sim_program.v1\",\"status\":%u,\"subroutine_count\":%u,\"source_length\":%u,\"interpreter_state_hash\":\"%llu\"}", - program->call_depth, program->control_code, program->current_line, program->error_line, program->has_control, program->has_motion, program->last_error, program->line_count, program->main_line_count, program->interpreter.state.motion_mode, program->rapid_velocity, program->status, program->subroutine_count, program->source_length, + "{\"call_depth\":%u,\"control_code\":%d,\"current_line\":%u,\"error_line\":%u,\"feed\":%.17g,\"has_control\":%u,\"has_motion\":%u,\"last_error\":%d,\"line_count\":%u,\"main_line_count\":%u,\"motion_mode\":%d,\"plane\":%d,\"rapid_velocity\":%.17g,\"schema\":\"sim_program.v1\",\"speed\":%.17g,\"spindle_mode\":%d,\"status\":%u,\"subroutine_count\":%u,\"source_length\":%u,\"units\":%d,\"distance_mode\":%d,\"interpreter_state_hash\":\"%llu\"}", + program->call_depth, program->control_code, program->current_line, program->error_line, program->interpreter.state.feed, program->has_control, program->has_motion, program->last_error, program->line_count, program->main_line_count, program->interpreter.state.motion_mode, program->interpreter.state.plane, program->rapid_velocity, program->interpreter.state.speed, program->interpreter.state.spindle_mode, program->status, program->subroutine_count, program->source_length, program->interpreter.state.units, program->interpreter.state.distance_mode, (unsigned long long)sim_interpreter_state_hash(&program->interpreter)); return written < 0 ? 0 : (uint32_t)written + 1u; } diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index e4a6aa1..f2913f7 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -87,9 +87,10 @@ function DroView({ axes, state, coordType, displayType, metric, setCoordType, se return PositionCoordinate setCoordType(event.target.value as "machine" | "relative")}>MachineRelativeDisplay setDisplayType(event.target.value as "actual" | "commanded")}>ActualCommandedUnits setMetric(event.target.value === "mm")}>mminchDisplayedActualRelative{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) || "-"}; } -function ProgramPane({ text, setText, activeGcodes, onLoad, onRun, onStep, onOpenFile, canLoad, canRun, canStep, currentLine, diagnostic }: { text: string; setText: (text: string) => void; activeGcodes: string; onLoad: () => void; onRun: () => void; onStep: () => void; onOpenFile: () => void; canLoad: boolean; canRun: boolean; canStep: boolean; currentLine: number | null; diagnostic: string | null }) { +function ProgramPane({ text, setText, activeGcodes, onLoad, onRun, onStep, onOpenFile, canLoad, canRun, canStep, currentLine, executionLine, executionLineText, diagnostic }: { text: string; setText: (text: string) => void; activeGcodes: 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"); - return {lines.map((_, index) => {index + 1})} setText(event.target.value)} spellCheck={false} aria-invalid={Boolean(diagnostic)} aria-label="G-code program" />; + const mainLine = currentLine && currentLine > 0 && currentLine <= lines.length ? currentLine : null; + return {lines.map((_, index) => {index + 1})} setText(event.target.value)} spellCheck={false} aria-invalid={Boolean(diagnostic)} aria-label="G-code program" />{executionLine ? `Executing source line ${executionLine}` : "Execution idle"}{executionLineText || "-"}; } function DiagnosticsPanel({ state, gates }: { state: typeof INITIAL_STATE; gates: Record }) { @@ -219,6 +220,8 @@ export function App() { const activeGcodes = state.program?.active_gcodes ? String(state.program.active_gcodes) : "G21 G90\nG17 G40 G49\nG54 G64\nM5 M9"; const toolNumber = String((state.devices as Record | 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; + const executionLineText = String(state.program?.execution_line_text ?? ""); 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 = {}, action?: string) => { void dispatch(kind, payload, true, action ?? null).then(() => { if (kind === "VISUAL_CLEAR") setPlotCleared(true); }).catch(() => undefined); }; @@ -232,7 +235,7 @@ export function App() { Manual Control [F3] setLeftTab("mdi")}>MDI [F5] setLeftTab("diagnostics")}>Diagnostics setRightTab("preview")}>Preview setRightTab("dro")}>DRO Jog Speed:50%Feed Override:{Math.round((state.snapshot?.overrides?.feed ?? 1) * 100)}% execute("SET_OVERRIDE", { feed: Number(event.target.value) / 100 })} />Spindle Override:{Math.round((state.snapshot?.overrides?.spindle?.[0] ?? 1) * 100)}%{(["path", "live", "limits", "velocity", "offsets", "extents"] as const).map((layer) => setLayers((current) => ({ ...current, [layer]: event.target.checked }))} /> {layer})} - { setPlotCleared(false); execute("PROGRAM_LOAD", { source: programText, path: PROGRAM_PATH }); }} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled} canRun={gates.run.enabled} canStep={gates.step.enabled} currentLine={currentLine} diagnostic={state.error?.code ?? null} /> + { setPlotCleared(false); execute("PROGRAM_LOAD", { source: programText, path: PROGRAM_PATH }); }} onRun={runProgram} onStep={() => execute("PROGRAM_STEP")} canLoad={gates.load.enabled} canRun={gates.run.enabled} canStep={gates.step.enabled} currentLine={currentLine} executionLine={executionLine} executionLineText={executionLineText} diagnostic={state.error?.code ?? null} /> ; diff --git a/web/react/src/styles.css b/web/react/src/styles.css index 0203add..3cd6bbb 100644 --- a/web/react/src/styles.css +++ b/web/react/src/styles.css @@ -183,6 +183,9 @@ output { font: 12px ui-monospace, monospace; text-align: right; } .program-gutter span { height: 17px; } .program-gutter span.current-line { color: #174d84; background: #c8d7e8; font-weight: 700; } .program-editor-wrap textarea { width: 100%; height: 100%; min-height: 180px; padding: 6px 8px; resize: none; border: 0; outline: none; background: #fff; color: #111; font: 12px/1.45 ui-monospace, monospace; tab-size: 4; } +.program-execution-line { display: grid; grid-template-columns: max-content minmax(0, 1fr); align-items: baseline; gap: 10px; min-width: 0; padding: 5px 8px; border-top: 1px solid #9aa8b4; border-bottom: 1px solid #9aa8b4; background: #e9f1f8; color: #173b5f; font: 12px/1.35 ui-monospace, monospace; } +.program-execution-line span { font-weight: 700; white-space: nowrap; } +.program-execution-line code { min-width: 0; overflow: hidden; color: #111; text-overflow: ellipsis; white-space: nowrap; } .program-side { display: flex; flex-direction: column; gap: 12px; padding: 8px; border-left: 1px solid #aaa; background: #d9d9d9; } .program-actions { display: flex; flex-wrap: wrap; gap: 5px; } .active-gcodes-label { display: grid; gap: 5px; color: #333; } diff --git a/web/worker/sim-session.mjs b/web/worker/sim-session.mjs index f91f2a5..080a9c3 100644 --- a/web/worker/sim-session.mjs +++ b/web/worker/sim-session.mjs @@ -161,9 +161,12 @@ export class SimSession { loaded: program.loaded, current_line: program.current_line, line: program.line, + execution_line: program.execution_line, + execution_line_text: program.execution_line_text, path: program.path, source_hash: program.source_hash, state_hash: program.state_hash, + active_gcodes: program.active_gcodes, } : null; const trace = Array.isArray(statePayload.motion_trace) ? statePayload.motion_trace : []; const motionSample = motionDelta ? (trace.at(-1) ?? { diff --git a/web/worker/wasm-program-runtime.mjs b/web/worker/wasm-program-runtime.mjs index a437ad2..f38823c 100644 --- a/web/worker/wasm-program-runtime.mjs +++ b/web/worker/wasm-program-runtime.mjs @@ -15,6 +15,16 @@ function programError(errorPrefix, result) { return { status: "error", code: `wasm_${errorPrefix}_${result}`, result, reason, message_key: reason, source: "linuxcnc-wasm-sim_program" }; } +function modalCode(prefix, value, fallback) { return `${prefix}${Number.isFinite(Number(value)) ? Number(value) : fallback}`; } + +function activeGcodes(state) { + const motion = { 0: "G0", 1: "G1", 2: "G2", 3: "G3" }[state.motion_mode] ?? "G1"; + const spindle = { 3: "M3", 4: "M4", 5: "M5" }[state.spindle_mode] ?? `M${state.spindle_mode ?? 5}`; + const feed = Number(state.feed ?? 0); + const speed = Number(state.speed ?? 0); + return [state.units === 20 ? "G20" : "G21", state.distance_mode === 91 ? "G91" : "G90", modalCode("G", state.plane, 17), motion, spindle, `F${Number.isFinite(feed) ? feed : 0}`, `S${Number.isFinite(speed) ? speed : 0}`].join(" "); +} + function call(core, name, returnType, argTypes, args) { const fn = core?.[`_${name}`] ?? core?.[name]; if (typeof fn === "function") return fn(...args); @@ -64,6 +74,7 @@ export class WasmProgramRuntime { this.source = ""; this.path = null; this.subroutines = []; + this.executionSource = ""; this.rapidVelocity = 35; if (!this.available || typeof core?._malloc !== "function") return; this.pointer = core._malloc(call(core, "sim_program_size", "number", [], [])); @@ -94,6 +105,7 @@ export class WasmProgramRuntime { this.source = source; this.path = path; this.subroutines = []; + this.executionSource = source; return { status: "verified", program: this.snapshot() }; } finally { this.core._free(pointer); } } @@ -154,6 +166,8 @@ export class WasmProgramRuntime { const code = call(this.core, "sim_program_add_subroutine", "number", ["number", "number", "number"], [this.pointer, pointer, bytes.byteLength]); if (code !== 0) return { status: "error", code: `wasm_program_add_subroutine_${code}`, result: code }; this.subroutines.push({ name: String(name ?? "subroutine"), source }); + if (this.executionSource && !this.executionSource.endsWith("\n")) this.executionSource += "\n"; + this.executionSource += source; return { status: "verified", command: "sim_program_add_subroutine", name: String(name ?? "subroutine") }; } finally { this.core._free(pointer); } } @@ -247,9 +261,11 @@ export class WasmProgramRuntime { const state = JSON.parse(readString(this.core, buffer, required)); const hash = call(this.core, "sim_program_state_hash", "number", ["number"], [this.pointer]); const programStatus = { 0: "idle", 1: "loaded", 2: "running", 3: "paused", 4: "completed", 5: "aborted", 6: "fault" }[state.status] ?? "idle"; - return { schema: SCHEMA, source: "linuxcnc-wasm-sim_program", ...state, loaded: state.status !== 0, line: state.current_line, currentLine: state.current_line, program_status: programStatus, path: this.path, subroutines: this.subroutines.map((subroutine) => subroutine.name), state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, source_hash: this.source ? fnv1a64(this.source) : null }; + const executionLines = this.executionSource ? this.executionSource.split(/\r?\n/) : []; + const executionLineText = state.current_line > 0 ? (executionLines[state.current_line - 1] ?? "").trim() : ""; + return { schema: SCHEMA, source: "linuxcnc-wasm-sim_program", ...state, active_gcodes: activeGcodes(state), loaded: state.status !== 0, line: state.current_line, currentLine: state.current_line, execution_line: state.current_line, execution_line_text: executionLineText, program_status: programStatus, path: this.path, subroutines: this.subroutines.map((subroutine) => subroutine.name), state_hash: `fnv1a64:${BigInt.asUintN(64, BigInt(hash >>> 0)).toString(16).padStart(16, "0")}`, source_hash: this.source ? fnv1a64(this.source) : null }; } finally { this.core._free(buffer); } } - destroy() { for (const pointer of [this.pointer, this.motionPointer, this.samplePointer, this.samplePosePointer]) if (pointer && typeof this.core?._free === "function") this.core._free(pointer); this.pointer = this.motionPointer = this.samplePointer = this.samplePosePointer = 0; } + destroy() { for (const pointer of [this.pointer, this.motionPointer, this.samplePointer, this.samplePosePointer]) if (pointer && typeof this.core?._free === "function") this.core._free(pointer); this.pointer = this.motionPointer = this.samplePointer = this.samplePosePointer = 0; this.executionSource = ""; } }
{executionLineText || "-"}