Align AXIS control flow and fix pause and program rendering

Document source UI gates, explicit native live-plot clearing, and remaining parity boundaries. Fix pause/resume transport continuity, repeated stepping, coordinate previews, and spindle override state. Optimize large program snapshots and rendering, and cover the control flows with native Tcl, WASM, and browser checks.

Validation: 22 native tests and source/browser checks pass. Worker/App regression: 113 of 123 pass; 10 pre-existing failures remain documented with no new failures.
This commit is contained in:
2026-09-08 10:46:27 +08:00
parent 845075f4a3
commit bade560372
55 changed files with 2495 additions and 286 deletions

View File

@@ -72,6 +72,8 @@ export class WasmProgramRuntime {
this.samplePointer = 0;
this.samplePosePointer = 0;
this.source = "";
this.hashedSource = null;
this.cachedSourceHash = null;
this.path = null;
this.subroutines = [];
this.executionSource = "";
@@ -95,6 +97,16 @@ export class WasmProgramRuntime {
return result === 0 ? { status: "verified", command: name } : { status: "error", code: `wasm_${errorPrefix}_${result}`, result, reason: PROGRAM_ERROR_REASONS[result] ?? `wasm_${errorPrefix}_${result}`, message_key: PROGRAM_ERROR_REASONS[result] ?? `wasm_${errorPrefix}_${result}`, source: "linuxcnc-wasm-sim_program" };
}
get sourceHash() {
// Source text is immutable between file replacements. Only its identity
// needs checking on a tick; the canonical WASM state is still read fresh.
if (this.hashedSource !== this.source) {
this.hashedSource = this.source;
this.cachedSourceHash = this.source ? fnv1a64(this.source) : null;
}
return this.cachedSourceHash;
}
load(source, path = null) {
if (typeof source !== "string" || source.length === 0) return { status: "error", code: "program_source_invalid" };
const bytes = new TextEncoder().encode(source);
@@ -140,6 +152,9 @@ export class WasmProgramRuntime {
if (loaded.status !== "verified") return loaded;
const synchronized = temporary.synchronizePosition(this.synchronizedPosition);
if (synchronized.status !== "verified") return synchronized;
const state = this.snapshot();
const initialized = temporary.initializeCoordinates(state.coordinate_context, state);
if (initialized.status !== "verified") return initialized;
temporary.setRapidVelocity(this.rapidVelocity);
for (const subroutine of this.subroutines) {
const added = temporary.addSubroutine(subroutine.name, subroutine.source, subroutine.path);
@@ -156,11 +171,26 @@ export class WasmProgramRuntime {
if (step.motion) segments.push({ id: segments.length + 1, kinematics_mode: kinematicsMode, ...step.motion });
if ([4, 5, 6].includes(step.program.status)) break;
}
return { status: "verified", schema: "wasm-canonical-preview.v1", source: "linuxcnc-wasm-sim_program", path: this.path, source_hash: fnv1a64(this.source), line_count: this.snapshot().line_count, segments, preview_hash: fnv1a64(segments) };
return { status: "verified", schema: "wasm-canonical-preview.v1", source: "linuxcnc-wasm-sim_program", path: this.path, source_hash: this.sourceHash, line_count: this.snapshot().line_count, segments, preview_hash: fnv1a64(segments) };
} finally { temporary.destroy(); }
}
start() { return this.invoke("sim_program_start", [], "program_start"); }
initializeCoordinates(context, state = {}) {
if (!context) return { status: "verified" };
const previous = this.snapshot().coordinate_context;
const blocks = ["G21"];
for (const [index, offsets] of context.offsets.entries()) {
if (offsets.some((value, axis) => value !== (previous?.offsets?.[index]?.[axis] ?? 0))) blocks.push(`G10 L2 P${index + 1} ${AXES.map((axis, a) => `${axis.toUpperCase()}${offsets[a]}`).join(" ")}`);
}
const system = context.system <= 6 ? `G${53 + context.system}` : `G59.${context.system - 6}`;
blocks.push(`${system} G${state.units === 20 ? 20 : 21} G${state.distance_mode === 91 ? 91 : 90}`);
for (const block of blocks) {
const result = this.executeBlock(block);
if (result.status !== "verified") return result;
}
return { status: "verified" };
}
synchronizePosition(value) {
const position = Object.fromEntries(AXES.map((axis) => [axis, Number(value?.[axis] ?? 0)]));
if (Object.values(position).some((coordinate) => !Number.isFinite(coordinate))) return { status: "error", code: "program_position_nonfinite" };
@@ -208,10 +238,12 @@ export class WasmProgramRuntime {
restoreState(value) {
if (!value || ![SCHEMA, "sim_program.v1"].includes(value.schema) || !Number.isSafeInteger(value.current_line) || value.current_line < 0 || !this.source) throw new Error("wasm_program_restore_schema");
if (value.source_hash && value.source_hash !== fnv1a64(this.source)) throw new Error("wasm_program_restore_source_hash");
if (value.source_hash && value.source_hash !== this.sourceHash) throw new Error("wasm_program_restore_source_hash");
const subroutines = [...this.subroutines];
const loaded = this.load(this.source, this.path);
if (loaded.status !== "verified") throw new Error(loaded.code);
const initialized = this.initializeCoordinates(value.coordinate_context, value);
if (initialized.status !== "verified") throw new Error(initialized.code);
const synchronized = this.synchronizePosition(Object.fromEntries(AXES.map((axis) => [axis, Number(value[axis] ?? 0)])));
if (synchronized.status !== "verified") throw new Error(synchronized.code);
for (const subroutine of subroutines) {
@@ -232,6 +264,14 @@ export class WasmProgramRuntime {
} else if (status === "completed") {
this.start();
for (let index = 0; index < this.snapshot().line_count + 1 && this.snapshot().status === 2; index += 1) this.step();
} else if (status === "paused") {
const from = Number(value.pause_from ?? 2);
if (from !== 1) {
this.start();
if (from === 4) for (let index = 0; index < this.snapshot().line_count + 1 && this.snapshot().status === 2; index += 1) this.step();
}
const paused = this.pause();
if (paused.status !== "verified") throw new Error(paused.code);
} else if (status === "aborted") {
const aborted = this.abort();
if (aborted.status !== "verified") throw new Error(aborted.code);
@@ -305,7 +345,7 @@ export class WasmProgramRuntime {
const motionControl = { 0: "continuous", 1: "exact_path", 2: "exact_stop" }[state.motion_control_mode] ?? "continuous";
const executionLineText = state.current_line > 0 ? (this.executionLines[state.current_line - 1] ?? "").trim() : "";
const executionMeta = state.current_line > 0 ? (this.executionLineMetadata[state.current_line - 1] ?? { path: this.path, line: state.current_line }) : { path: this.path, line: 0 };
return { schema: SCHEMA, source: "linuxcnc-wasm-sim_program", ...state, motion_control: { mode: motionControl, path_tolerance: Number(state.path_tolerance ?? 0), naive_cam_tolerance: Number(state.naive_cam_tolerance ?? 0) }, 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, execution_source_file: executionMeta.path, execution_source_line: executionMeta.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 };
return { schema: SCHEMA, source: "linuxcnc-wasm-sim_program", ...state, motion_control: { mode: motionControl, path_tolerance: Number(state.path_tolerance ?? 0), naive_cam_tolerance: Number(state.naive_cam_tolerance ?? 0) }, 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, execution_source_file: executionMeta.path, execution_source_line: executionMeta.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.sourceHash };
} finally { this.core._free(buffer); }
}