From 99fe5a4151d2c7deaabfd4118960aa5e31e3891d Mon Sep 17 00:00:00 2001 From: wangdequan Date: Sun, 30 Aug 2026 18:51:53 +0800 Subject: [PATCH] Keep XYZAC tool motion smooth and expose rotary DRO values --- web/react/src/App.tsx | 4 +- web/react/src/XyzacToolPreview.tsx | 85 ++++++++++++------- web/react/src/store.ts | 11 ++- .../src/test_xyzac_motion_projection.mjs | 39 +++++++++ 4 files changed, 104 insertions(+), 35 deletions(-) create mode 100644 web/react/src/test_xyzac_motion_projection.mjs diff --git a/web/react/src/App.tsx b/web/react/src/App.tsx index 61ca0f0..18b33ce 100644 --- a/web/react/src/App.tsx +++ b/web/react/src/App.tsx @@ -84,8 +84,10 @@ function DroView({ axes, state, coordType, displayType, metric, setCoordType, se const format = (value: number, index: number) => (value * (index < 3 ? scale : 1)).toFixed(4); const pose = (state.snapshot?.visual?.pose ?? state.snapshot?.authoritative?.kinematics?.pose) as Record | undefined; const commanded = [pose?.x, pose?.y, pose?.z, pose?.a, pose?.c].map((value, index) => Number.isFinite(value) ? Number(value) : Number(axes[index] ?? 0)); + const actualPose = (state.snapshot?.visual?.pose ?? state.snapshot?.kinematics?.pose ?? {}) as Record; + const actualAxes = [actualPose.x, actualPose.y, actualPose.z, actualPose.a, actualPose.c].map((value, index) => Number.isFinite(value) ? Number(value) : Number(axes[index] ?? 0)); const relative = state.snapshot?.coordinates?.relative ?? [0, 0, 0, 0, 0]; - 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) || "-"}
; + return
Position
DisplayedActualRelative
{labels.map((label, index) => { const actual = actualAxes[index]; 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; diff --git a/web/react/src/XyzacToolPreview.tsx b/web/react/src/XyzacToolPreview.tsx index 6fa7aef..3251991 100644 --- a/web/react/src/XyzacToolPreview.tsx +++ b/web/react/src/XyzacToolPreview.tsx @@ -87,8 +87,8 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa let frame = 0; let liveTick = -1; let liveTrace: THREE.Vector3[] = []; - let pathKey = ""; - let liveKey = ""; + let pathSegments: Segment[] | null = null; + let segmentById = new Map(); let bounds = { center: new THREE.Vector3(), radius: .12 }; let playbackQueue: Array<{ tick: number; pose: Point; joints: number[]; activeSegmentId: number; activeProgress: number }> = []; let playbackStart = 0; @@ -98,6 +98,10 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa let renderedJoints: number[] = []; let renderedActiveSegment = 0; let renderedProgress = 0; + let liveHistoryLine: THREE.Line | null = null; + let activeLiveLine: THREE.Line | null = null; + let lastHistoryTick = -1; + let lastActiveKey = ""; let lastPreset = ""; const presetCamera = (selected: Props["view"], selectedZoom: number) => { const distance = bounds.radius * 2.9 / Math.max(.5, selectedZoom); @@ -131,12 +135,11 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa const segments = state.toolpath as Segment[]; const layer = layersRef.current; const now = performance.now(); - if (state.tick < queuedLastTick) { - playbackQueue = []; - playbackStart = 0; - queuedLastTick = -1; - } - if (!state.motionTrace.length && state.tick > queuedLastTick) { + // React can deliver a full snapshot just behind an already queued + // motion-delta response. Keep the rendered pose monotonic until the + // newer samples arrive; otherwise the tool visibly jumps backward. + const staleState = state.tick < queuedLastTick; + if (!staleState && !state.motionTrace.length && state.tick > queuedLastTick) { playbackQueue = []; playbackStart = 0; queuedLastTick = state.tick; @@ -146,7 +149,7 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa renderedActiveSegment = state.activeSegmentId ?? 0; renderedProgress = state.activeProgress; } - for (const sample of state.motionTrace) { + for (const sample of staleState ? [] : state.motionTrace) { if (sample.tick > queuedLastTick) { playbackQueue.push(sample); queuedLastTick = sample.tick; @@ -156,16 +159,18 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa if (playbackQueue.length) { const index = Math.min(playbackQueue.length - 1, Math.max(0, Math.floor((now - playbackStart) / 10))); const sample = playbackQueue[index]; - renderedTick = sample.tick; - renderedPose = sample.pose; - renderedJoints = sample.joints; - renderedActiveSegment = sample.activeSegmentId; - renderedProgress = sample.activeProgress; + if (sample.tick >= renderedTick) { + renderedTick = sample.tick; + renderedPose = sample.pose; + renderedJoints = sample.joints; + renderedActiveSegment = sample.activeSegmentId; + renderedProgress = sample.activeProgress; + } if (index > 32) { playbackQueue.splice(0, index); playbackStart += index * 10; } - } else { + } else if (!staleState) { renderedTick = state.tick; renderedPose = state.livePose; renderedJoints = state.joints; @@ -175,9 +180,14 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa // The planned path is immutable for a loaded program. Rebuilding all // geometries on every animation frame starves worker snapshots and // makes an otherwise continuous WASM trajectory appear to stutter. - const nextPathKey = segments.map((segment) => `${segment.id}:${segment.kind ?? ""}:${segment.samples?.length ?? 0}:${segment.start?.x ?? 0},${segment.start?.y ?? 0},${segment.start?.z ?? 0}:${segment.end?.x ?? 0},${segment.end?.y ?? 0},${segment.end?.z ?? 0}`).join("|"); - if (nextPathKey !== pathKey) { + // Toolpath arrays are immutable between PROGRAM_LOAD/RESTART. Rebuild + // geometry only when WASM publishes a new array; scanning thousands of + // segments on every animation frame causes visible execution stalls. + if (segments !== pathSegments) { + pathSegments = segments; + segmentById = new Map(segments.flatMap((segment) => Number.isSafeInteger(segment.id) ? [[Number(segment.id), segment] as [number, Segment]] : [])); clearLines(pathGroup); + clearLines(liveGroup); bounds = fit(segments); // Long programs can contain thousands of linear blocks. Batch their // vertices so preview loading does not block the UI on one WebGL @@ -209,12 +219,18 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa addSegments(batched.rapid, rapid, true); addSegments(batched.completed, completed); for (const sampled of sampledLines) addLine(sampled.points, sampled.rapid ? rapid : completed, sampled.rapid); - pathKey = nextPathKey; - liveKey = ""; + liveHistoryLine = null; + activeLiveLine = null; + lastHistoryTick = -1; + lastActiveKey = ""; } const presetKey = `${viewRef.current}:${zoomRef.current}:${bounds.radius}:${bounds.center.x}:${bounds.center.y}:${bounds.center.z}`; if (presetKey !== lastPreset) { presetCamera(viewRef.current, zoomRef.current); lastPreset = presetKey; } - if (renderedTick < liveTick) liveTrace = []; + if (renderedTick < liveTick) { + liveTrace = []; + lastHistoryTick = -1; + if (liveHistoryLine) { liveHistoryLine.geometry.dispose(); liveGroup.remove(liveHistoryLine); liveHistoryLine = null; } + } if (renderedTick !== liveTick) { const current = sourcePoint(renderedPose); const previous = liveTrace.at(-1); @@ -222,15 +238,18 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa liveTick = renderedTick; } element.dataset.xyzacLiveTraceCount = String(liveTrace.length); - const nextLiveKey = `${renderedTick}:${renderedActiveSegment}:${renderedProgress}:${liveTrace.length}`; - if (nextLiveKey !== liveKey) { - clearLines(liveGroup); - if (liveTrace.length > 1) { - const historyLine = new THREE.Line(new THREE.BufferGeometry().setFromPoints(liveTrace), liveHistory); - historyLine.renderOrder = 19; - liveGroup.add(historyLine); - } - const activeSegment = segments.find((segment) => Boolean(segment.active || segment.id === renderedActiveSegment)); + const historyDue = liveTrace.length > 1 && (lastHistoryTick < 0 || renderedTick - lastHistoryTick >= 4); + if (historyDue) { + if (liveHistoryLine) { liveHistoryLine.geometry.dispose(); liveGroup.remove(liveHistoryLine); } + liveHistoryLine = new THREE.Line(new THREE.BufferGeometry().setFromPoints(liveTrace), liveHistory); + liveHistoryLine.renderOrder = 19; + liveGroup.add(liveHistoryLine); + lastHistoryTick = renderedTick; + } + const nextActiveKey = `${renderedTick}:${renderedActiveSegment}:${renderedProgress}`; + if (nextActiveKey !== lastActiveKey) { + if (activeLiveLine) { activeLiveLine.geometry.dispose(); liveGroup.remove(activeLiveLine); activeLiveLine = null; } + const activeSegment = segmentById.get(renderedActiveSegment) ?? segments.find((segment) => Boolean(segment.active)); if (activeSegment) { const start = activeSegment.start ?? {}; const end = activeSegment.end ?? {}; const points = Array.isArray(activeSegment.samples) && activeSegment.samples.length > 1 ? activeSegment.samples.map(sourcePoint) : [sourcePoint(start), sourcePoint(end)]; @@ -242,13 +261,13 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa const lastPoint = travelled.at(-1); if (!lastPoint || lastPoint.distanceTo(livePoint) > 1e-9) travelled.push(livePoint); if (travelled.length > 1) { - const liveLine = new THREE.Line(new THREE.BufferGeometry().setFromPoints(travelled), live); - liveLine.renderOrder = 20; - liveGroup.add(liveLine); + activeLiveLine = new THREE.Line(new THREE.BufferGeometry().setFromPoints(travelled), live); + activeLiveLine.renderOrder = 20; + liveGroup.add(activeLiveLine); } } } - liveKey = nextLiveKey; + lastActiveKey = nextActiveKey; } pathGroup.visible = layer.showPath; liveGroup.visible = layer.showLive; grid.scale.setScalar(Math.max(.2, bounds.radius * 2.4)); diff --git a/web/react/src/store.ts b/web/react/src/store.ts index 4fd5e45..ce1d13d 100644 --- a/web/react/src/store.ts +++ b/web/react/src/store.ts @@ -89,7 +89,16 @@ function projection(result: Record, message: MessageEnvelope, p program: (result.program as Record) ?? previous.snapshot.program, motion: { ...previous.snapshot.motion, - axes: [Number(pose.x ?? previous.snapshot.motion?.axes?.[0] ?? 0), Number(pose.y ?? previous.snapshot.motion?.axes?.[1] ?? 0), Number(pose.z ?? previous.snapshot.motion?.axes?.[2] ?? 0)], + // Keep the XYZAC axis vector intact on every motion delta. Older + // projection code rebuilt only XYZ, which made A/C disappear from + // consumers that read the canonical motion array. + axes: [ + Number(pose.x ?? previous.snapshot.motion?.axes?.[0] ?? 0), + Number(pose.y ?? previous.snapshot.motion?.axes?.[1] ?? 0), + Number(pose.z ?? previous.snapshot.motion?.axes?.[2] ?? 0), + Number(pose.a ?? previous.snapshot.motion?.axes?.[3] ?? 0), + Number(pose.c ?? previous.snapshot.motion?.axes?.[4] ?? 0), + ], joints, }, wasm_motion: (result.wasm_motion as Record) ?? previous.snapshot.wasm_motion, diff --git a/web/react/src/test_xyzac_motion_projection.mjs b/web/react/src/test_xyzac_motion_projection.mjs new file mode 100644 index 0000000..42f2913 --- /dev/null +++ b/web/react/src/test_xyzac_motion_projection.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import { INITIAL_STATE, reduceFrontendState } from "./store.ts"; + +const envelope = (kind, seq, payload, tick) => ({ schema_version: 1, kind, context_id: INITIAL_STATE.contextId, session_id: "motion-projection", seq, simulation_tick: tick, payload }); +const panel = { + schema: "axis-panel-snapshot.v1", + context_id: INITIAL_STATE.contextId, + session_id: "motion-projection", + simulation_tick: 1, + state_hash: "fnv1a64:panel", + task: { state: "RUNNING", mode: "AUTO", program_status: "running", current_line: 1 }, + program: { program_status: "running", current_line: 1 }, + motion: { axes: [10, 20, 30, 40, 50], joints: [10, 20, 30, 40, 50] }, + coordinates: { machine: [10, 20, 30, 40, 50], relative: [10, 20, 30, 40, 50] }, + overrides: { feed: 1, spindle: [1] }, + wasm_motion: { status: "verified", state: { position: { x: 10, y: 20, z: 30, a: 40, c: 50 }, queue_depth: 1 } }, + visual: { schema: "axis-visual.v1", pose: { x: 10, y: 20, z: 30, a: 40, c: 50 }, joints: [10, 20, 30, 40, 50], toolpath: [], active_segment_id: 1, active_progress: 0.1, source: "wasm_canonical_motion" }, + kinematics: { coordinates: "XYZAC", joint_count: 5, pose: { x: 10, y: 20, z: 30, a: 40, c: 50 }, joints: [10, 20, 30, 40, 50], offsets: {} }, + xyzac: { coordinates: "XYZAC", joint_count: 5 }, + gates: {}, +}; + +const initial = reduceFrontendState(INITIAL_STATE, envelope("STATE_SNAPSHOT", 1, { panel_snapshot: panel }, 1)); +const next = reduceFrontendState(initial, envelope("COMMAND_RESULT", 2, { + status: "verified", + snapshot_mode: "motion_delta", + motion_sample: { tick: 2, pose: { x: 11, y: 21, z: 31, a: 41, c: 51 }, joints: [11, 21, 31, 41, 51], active_segment_id: 1, active_progress: 0.2 }, + motion_trace: [], + wasm_motion: { status: "verified", state: { position: { x: 11, y: 21, z: 31, a: 41, c: 51 }, queue_depth: 1 } }, + motion: { axes: [11, 21, 31, 41, 51], joints: [11, 21, 31, 41, 51] }, + task: panel.task, + program: panel.program, + state_hash: "fnv1a64:delta", +}, 2)); + +assert.deepEqual(next.snapshot.motion.axes, [11, 21, 31, 41, 51]); +assert.equal(next.snapshot.visual.pose.a, 41); +assert.equal(next.snapshot.visual.pose.c, 51); +console.log(JSON.stringify({ schema: "xyzac-motion-projection.v1", status: "verified", axes: next.snapshot.motion.axes, pose: { a: next.snapshot.visual.pose.a, c: next.snapshot.visual.pose.c } }));