Fix web homing motion and synchronize preview state

This commit is contained in:
2026-09-09 09:31:32 +08:00
parent bade560372
commit eb5c58cdb3
11 changed files with 375 additions and 33 deletions

View File

@@ -13,6 +13,7 @@ import { XYZAC_CASE, projectXyzacPanel, type XyzacOperationTrace } from "./xyzac
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 XYZAC_AXIS_TO_JOINT = Object.freeze({ x: 0, y: 1, z: 2, a: 3, c: 4 } as const);
const normalizeProgramSource = (source: string) => source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
// Button-to-command sequencing is defined in
// docs/linuxcnc-axis-web-control-panel-source-logic.md; React only projects
@@ -239,8 +240,10 @@ export function App() {
const aboutDialog = useRef<HTMLDivElement>(null);
const [layers, setLayers] = useState({ grid: true, tool: true, path: true, live: true, limits: false, velocity: false, offsets: false, extents: false });
const runTimer = useRef<ReturnType<typeof setInterval> | null>(null);
const homeTimer = useRef<ReturnType<typeof setInterval> | null>(null);
const jogTimer = useRef<ReturnType<typeof setInterval> | null>(null);
const runStepPending = useRef(false);
const homeStepPending = useRef(false);
const jogStepPending = useRef(false);
const continuousJog = useRef<{ axis: string; direction: number } | null>(null);
const programFileInput = useRef<HTMLInputElement>(null);
@@ -315,7 +318,7 @@ export function App() {
setCommandStatus("PROGRAM_LOAD acknowledged");
setCaseStatus(`${DEFAULT_CASE} + ${PROGRAM_PATH} loaded`);
}).catch((error: Error) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`Error: ${error.message}`); setCaseStatus(`blocked: ${error.message}`); }).finally(() => setProgramLoading(false));
return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current); if (uiCacheSaveTimer.current) clearTimeout(uiCacheSaveTimer.current); client.close(); };
return () => { unsubscribe(); if (runTimer.current) clearInterval(runTimer.current); if (homeTimer.current) clearInterval(homeTimer.current); if (jogTimer.current) clearInterval(jogTimer.current); if (previewProgressTimer.current) clearInterval(previewProgressTimer.current); if (feedOverrideTimer.current) clearTimeout(feedOverrideTimer.current); if (uiCacheSaveTimer.current) clearTimeout(uiCacheSaveTimer.current); client.close(); };
}, [client, operationController]);
const axes = state.snapshot?.motion?.axes ?? [];
const taskRecord = state.task ?? {};
@@ -348,6 +351,17 @@ export function App() {
const relief = state.snapshot?.ui_relief ?? {};
const canLoad = !programLoading && gates.load.enabled;
const stopRunTimer = () => { if (runTimer.current) { clearInterval(runTimer.current); runTimer.current = null; } };
const stopHomeTimer = () => { if (homeTimer.current) { clearInterval(homeTimer.current); homeTimer.current = null; } };
const startHomeTimer = () => {
if (homeTimer.current) return;
homeTimer.current = setInterval(() => {
if (homeStepPending.current) return;
homeStepPending.current = true;
void operationController.execute("SIM_STEP", { cycles: 5, response: "motion_delta" }, { state: axisState, gate: false })
.catch(() => stopHomeTimer())
.finally(() => { homeStepPending.current = false; });
}, 50);
};
const stopJogTimer = () => { continuousJog.current = null; if (jogTimer.current) { clearInterval(jogTimer.current); jogTimer.current = null; } };
const startJogTimer = (axis: string, direction: number) => {
continuousJog.current = { axis, direction };
@@ -373,7 +387,7 @@ export function App() {
}
const pending = operationController.execute(kind, payload, { state: axisState, gate, ...(action ? { action } : {}) });
setOperationTrace(operationController.exportTrace());
return pending.then(() => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN" || kind === "PROGRAM_RESUME") startRunTimer(); if (["PROGRAM_PAUSE", "PROGRAM_ABORT", "PROGRAM_RESTART"].includes(kind)) stopRunTimer(); if (kind === "JOG_START") startJogTimer(String(payload.axis ?? "x"), Number(payload.direction)); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => {
return pending.then((operation) => { setOperationTrace(operationController.exportTrace()); setCommandStatus(`${kind} acknowledged`); if (kind === "PROGRAM_RUN" || kind === "PROGRAM_RESUME") startRunTimer(); if (["PROGRAM_PAUSE", "PROGRAM_ABORT", "PROGRAM_RESTART"].includes(kind)) stopRunTimer(); if (kind === "HOME" && (operation.response as { payload?: { homing?: { phase?: string } } })?.payload?.homing?.phase === "moving") startHomeTimer(); if (kind === "JOG_START") startJogTimer(String(payload.axis ?? "x"), Number(payload.direction)); if (kind === "JOG_STOP") stopJogTimer(); }).catch((error: Error) => {
setOperationTrace(operationController.exportTrace());
const code = error.message;
setCommandStatus(`${kind} blocked: ${code}`);
@@ -396,7 +410,13 @@ export function App() {
const homeAxis = () => {
if (programLoading || !gates.home.enabled) return;
// switchkins.c reports KINEMATICS_BOTH even when its active type is 0.
void dispatch("HOME", { axis: selectedAxis }).catch(() => undefined);
// AXIS therefore homes a numeric joint; this fixed XYZAC Web surface
// maps its visible one-to-one axis selector to that joint explicitly.
const joint = XYZAC_AXIS_TO_JOINT[selectedAxis as keyof typeof XYZAC_AXIS_TO_JOINT];
if (joint === undefined) { setCommandStatus("HOME blocked: home_axis_invalid"); return; }
const confirm = globalThis.confirm;
if (Boolean(homedAxes[joint]) && typeof confirm === "function" && !confirm(`Joint ${joint} is already homed. Re-home it?`)) return;
void dispatch("HOME", { axis: selectedAxis, joint }).catch(() => undefined);
};
const stopPreviewProgress = () => {
if (previewProgressTimer.current) {
@@ -625,7 +645,8 @@ export function App() {
const mdiMotionActive = !programRunning && String(state.task?.mode ?? "").toUpperCase() === "MDI" && wasmQueueDepth > 0;
if ((programRunning && !programComplete) || mdiMotionActive || Boolean(state.snapshot?.motion?.stepping)) startRunTimer(); else stopRunTimer();
if (wasmQueueDepth === 0 && !jogStepPending.current) stopJogTimer();
if (!ready) { stopRunTimer(); stopJogTimer(); }
if (wasmQueueDepth === 0 && !homeStepPending.current) stopHomeTimer();
if (!ready) { stopRunTimer(); stopHomeTimer(); stopJogTimer(); }
}, [programRunning, ready, state.program?.program_status, state.task?.mode, state.snapshot?.wasm_motion]);
const spindleState = (state.devices as Record<string, any> | null)?.spindles?.[0] as Record<string, any> | undefined;
const spindleDirection = Number(spindleState?.direction ?? 0);

View File

@@ -119,6 +119,7 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
let historyVertexCount = 0;
let activeLiveLine: THREE.Line | null = null;
let livePathEpoch = -1;
let poseEpoch = -1;
let lastHistoryTick = -1;
let lastActiveKey = "";
let lastPreset = "";
@@ -165,6 +166,29 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
const segments = state.toolpath as Segment[];
const layer = layersRef.current;
const now = performance.now();
// A HOME transaction can reset the canonical pose without advancing the
// simulation tick. Treat the source home sequence as an explicit pose
// barrier so stale queued jog samples cannot keep the old value on the
// HUD or tool marker. Keep the Live Plot history intact.
if (state.poseEpoch !== poseEpoch) {
poseEpoch = state.poseEpoch;
playbackQueue = [];
playbackStart = 0;
queuedLastTick = state.tick;
renderedTick = state.tick;
renderedPose = state.livePose;
renderedJoints = state.joints;
renderedActiveSegment = state.activeSegmentId ?? 0;
renderedProgress = state.activeProgress;
paintedTick = renderedTick;
paintedPose = renderedPose;
paintedJoints = renderedJoints;
paintedActiveSegment = renderedActiveSegment;
paintedProgress = renderedProgress;
liveTick = renderedTick;
if (activeLiveLine) { activeLiveLine.geometry.dispose(); liveGroup.remove(activeLiveLine); activeLiveLine = null; }
lastActiveKey = "";
}
if (state.livePathEpoch !== livePathEpoch) {
livePathEpoch = state.livePathEpoch;
clearLines(liveGroup);

View File

@@ -138,6 +138,7 @@ export type PanelSnapshotV1 = {
schema: "axis-visual.v1";
pose: Record<string, number>;
joints: number[];
pose_epoch?: number;
live_path_epoch?: number;
toolpath?: Array<Record<string, unknown>>;
active_segment_id?: number | null;

View File

@@ -9,6 +9,7 @@ export type XyzacVisualState = {
joints: [number, number, number, number, number];
livePose: XyzacKinematicsSnapshot["pose"];
activeProgress: number;
poseEpoch: number;
livePathEpoch: number;
motionTrace: Array<{ tick: number; pose: XyzacKinematicsSnapshot["pose"]; joints: number[]; activeSegmentId: number; activeProgress: number }>;
offsets: XyzacKinematicsSnapshot["offsets"];
@@ -28,7 +29,7 @@ export type XyzacVisualSource = {
state_hash?: string;
kinematics?: XyzacKinematicsSnapshot;
motion?: { velocity_mm_min?: number };
visual?: { active_segment_id?: number | null; active_progress?: number; live_path_epoch?: number; motion_trace?: Array<{ tick: number; pose: Record<string, number>; joints: number[]; active_segment_id: number; active_progress: number }>; pose?: Record<string, number>; joints?: number[]; toolpath?: Array<Record<string, unknown>> };
visual?: { active_segment_id?: number | null; active_progress?: number; pose_epoch?: number; live_path_epoch?: number; motion_trace?: Array<{ tick: number; pose: Record<string, number>; joints: number[]; active_segment_id: number; active_progress: number }>; pose?: Record<string, number>; joints?: number[]; toolpath?: Array<Record<string, unknown>> };
};
export function projectXyzacVisual(snapshot: XyzacVisualSource | null | undefined): XyzacVisualState {
@@ -53,6 +54,7 @@ export function projectXyzacVisual(snapshot: XyzacVisualSource | null | undefine
joints,
livePose: pose,
activeProgress,
poseEpoch: Number.isSafeInteger(snapshot?.visual?.pose_epoch) ? Number(snapshot?.visual?.pose_epoch) : 0,
livePathEpoch: Number.isSafeInteger(snapshot?.visual?.live_path_epoch) ? Number(snapshot?.visual?.live_path_epoch) : 0,
motionTrace,
offsets: kinematics?.offsets ?? ZERO_OFFSETS,