Keep XYZAC tool motion smooth and expose rotary DRO values

This commit is contained in:
2026-08-30 18:51:53 +08:00
parent c015709d26
commit 99fe5a4151
4 changed files with 104 additions and 35 deletions

View File

@@ -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<number, Segment>();
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));