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

@@ -175,7 +175,7 @@ export class SimSession {
const task = statePayload.task ?? this.task.export();
// The loaded toolpath is immutable during execution. Avoid cloning all
// WASM pose samples into every 10 ms step response; the frontend retains
// the canonical path from PROGRAM_LOAD/PROGRAM_RESTART.
// the canonical path published by the latest program transaction.
const xyzacProgramActive = Boolean(this.xyzacProgram);
const lightweightXyzacStep = input.kind === "SIM_STEP" && xyzacProgramActive;
const traceOnlyStep = lightweightXyzacStep && input.payload?.trace_only === true;
@@ -1997,7 +1997,6 @@ export class SimSession {
case "PROGRAM_LOAD": {
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_load_while_active"), "failed");
const source = input.payload?.source ?? input.payload?.text ?? input.payload?.program_source;
this.livePathEpoch += 1;
const savedMode = this.task.state.mode;
const restoreMode = () => {
if (savedMode !== "MANUAL" && this.task.state.mode !== savedMode) this.task.setMode(savedMode);
@@ -2077,12 +2076,11 @@ export class SimSession {
if (this.xyzacProgram) {
const previousProgramStatus = this.xyzacProgram.snapshot().program_status;
const result = this.xyzacProgram.start();
if (result.status === "verified") { this.livePathEpoch += 1; this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
if (result.status === "verified") { this.wasmMotion.setPaused(false); this.task.setMode("AUTO"); if (this.task.state.state === "STOPPED") this.task.recover(); this.task.transition("RUNNING"); const program = this.xyzacProgram.snapshot(); this.task.setProgram("running", program.execution_source_line ?? program.current_line); }
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, reopened_taskfile: ["completed", "aborted"].includes(previousProgramStatus), start_line: 0, program: this.xyzacProgram.snapshot(), task: this.task.export(), wasm_motion: this.wasmMotion.read(), motion: this.wasmMotionProjection(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
const result = this.program.start();
if (result.status === "verified") {
this.livePathEpoch += 1;
this.task.setMode("AUTO");
if (this.task.state.state === "STOPPED") this.task.recover();
if (this.motionAuthority === "wasm") this.wasmMotion.setPaused(false);
@@ -2182,12 +2180,11 @@ export class SimSession {
this.xyzacDeferredPhysicalPoseApplied = false;
this.xyzacAppendDeferredOnActive = false;
const result = this.resetXyzacProgramToLoaded();
if (result.status === "verified") { this.livePathEpoch += 1; this.wasmMotion.abort(); this.wasmMotion.setPaused(false); this.task.recover(); this.task.setMode("AUTO"); this.task.setProgram("loaded", 0); }
if (result.status === "verified") { this.wasmMotion.abort(); this.wasmMotion.setPaused(false); this.task.recover(); this.task.setMode("AUTO"); this.task.setProgram("loaded", 0); }
return finish(this.envelope(result.status === "verified" ? "COMMAND_RESULT" : "ERROR", input, { ...result, program: this.xyzacProgram.snapshot(), task: this.task.export(), motion_authority: "wasm", command: input.kind, operation_id: input.operation_id ?? null }), result.status === "verified" ? "verified" : "failed");
}
const result = this.program.restart();
if (result.status === "verified") {
this.livePathEpoch += 1;
if (["ESTOP", "FAULT"].includes(this.task.state.state)) return finish(this.error(input, "task_state", "program_restart_faulted"), "failed");
if (["RUNNING", "PAUSED"].includes(this.task.state.state)) this.task.transition("STOPPED");
if (this.task.state.state === "STOPPED") this.task.recover();

View File

@@ -30,13 +30,14 @@ const loaded = send("PROGRAM_LOAD", { source, path: "live-path.ngc" });
assert.equal(loaded.kind, "COMMAND_RESULT");
const loadEpoch = epoch(loaded);
const previewCount = loaded.payload.panel_snapshot.visual.toolpath.length;
assert.ok(loadEpoch > 0 && previewCount > 0);
assert.equal(loadEpoch, 0);
assert.ok(previewCount > 0);
send("POWER", { active: true });
send("HOME", { all: true });
const run = send("PROGRAM_RUN");
const runEpoch = epoch(run);
assert.equal(runEpoch, loadEpoch + 1);
assert.equal(runEpoch, loadEpoch);
send("SIM_STEP", { cycles: 5, response: "motion_delta" });
const pause = send("PROGRAM_PAUSE");
assert.equal(epoch(pause), runEpoch);
@@ -55,15 +56,17 @@ assert.equal(epoch(completed), runEpoch);
assert.equal(Object.hasOwn(completed.payload.panel_snapshot.visual, "toolpath"), false);
const rerun = send("PROGRAM_RUN");
assert.equal(epoch(rerun), runEpoch + 1);
assert.equal(epoch(rerun), runEpoch);
const abort = send("PROGRAM_ABORT");
assert.equal(epoch(abort), runEpoch + 1);
assert.equal(epoch(abort), runEpoch);
const replacement = send("PROGRAM_LOAD", { source: "G21\nG90\nG1 X1 Y1 Z1 F300\nM2\n", path: "replacement-live-path.ngc" });
assert.equal(epoch(replacement), runEpoch + 2);
assert.equal(epoch(replacement), runEpoch);
assert.equal(replacement.payload.panel_snapshot.visual.toolpath.length, 1);
const restart = send("PROGRAM_RESTART");
assert.equal(epoch(restart), runEpoch);
const clear = send("VISUAL_CLEAR");
assert.equal(epoch(clear), runEpoch + 3);
assert.equal(epoch(clear), runEpoch + 1);
assert.equal(clear.payload.panel_snapshot.visual.toolpath.length, 1);
assert.equal(session.previewState.segments.length, 1);
@@ -71,6 +74,6 @@ console.log(JSON.stringify({
schema: "xyzac-live-path-lifecycle.v1",
status: "verified",
source: "axis.py:LivePlotter/clear_live_plot/task_pause/task_resume/task_stop",
epochs: { load: loadEpoch, run: runEpoch, pause: epoch(pause), resume: epoch(resume), completed: epoch(completed), rerun: epoch(rerun), abort: epoch(abort), replacement: epoch(replacement), clear: epoch(clear) },
epochs: { load: loadEpoch, run: runEpoch, pause: epoch(pause), resume: epoch(resume), completed: epoch(completed), rerun: epoch(rerun), abort: epoch(abort), replacement: epoch(replacement), restart: epoch(restart), clear: epoch(clear) },
static_preview: { initial_segments: previewCount, clear_plot_segments: clear.payload.panel_snapshot.visual.toolpath.length },
}));