Keep XYZAC tool motion smooth and expose rotary DRO values
This commit is contained in:
@@ -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 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<string, number> | undefined;
|
const pose = (state.snapshot?.visual?.pose ?? state.snapshot?.authoritative?.kinematics?.pose) as Record<string, number> | 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 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<string, number>;
|
||||||
|
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];
|
const relative = state.snapshot?.coordinates?.relative ?? [0, 0, 0, 0, 0];
|
||||||
return <section className="dro-view" data-axis-feature="AXIS-VIEW-04" role="tabpanel" aria-label="DRO"><div className="dro-mode-row"><strong>Position</strong><label>Coordinate<select data-axis-feature="AXIS-VIEW-18" value={coordType} onChange={(event) => setCoordType(event.target.value as "machine" | "relative")}><option value="machine">Machine</option><option value="relative">Relative</option></select></label><label>Display<select data-axis-feature="AXIS-VIEW-17" value={displayType} onChange={(event) => setDisplayType(event.target.value as "actual" | "commanded")}><option value="actual">Actual</option><option value="commanded">Commanded</option></select></label><label>Units<select data-axis-feature="AXIS-VIEW-02" value={metric ? "mm" : "inch"} onChange={(event) => setMetric(event.target.value === "mm")}><option value="mm">mm</option><option value="inch">inch</option></select></label></div><div className="dro-column-headings"><span /><span>Displayed</span><span>Actual</span><span>Relative</span></div><div className="dro-table">{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 <div className="dro-row" key={label}><strong>{label}</strong><output>{format(coordType === "relative" ? relativeValue : displayed, index)}</output><output>{format(actual, index)}</output><output>{format(relativeValue, index)}</output></div>; })}</div><div className="dro-footer"><span>Units: <b>{metric ? "mm" : "inch"}</b> / deg</span><span>{coordType} / {displayType}</span><span>Tick: {state.tick}</span><span>Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}</span></div></section>;
|
return <section className="dro-view" data-axis-feature="AXIS-VIEW-04" role="tabpanel" aria-label="DRO"><div className="dro-mode-row"><strong>Position</strong><label>Coordinate<select data-axis-feature="AXIS-VIEW-18" value={coordType} onChange={(event) => setCoordType(event.target.value as "machine" | "relative")}><option value="machine">Machine</option><option value="relative">Relative</option></select></label><label>Display<select data-axis-feature="AXIS-VIEW-17" value={displayType} onChange={(event) => setDisplayType(event.target.value as "actual" | "commanded")}><option value="actual">Actual</option><option value="commanded">Commanded</option></select></label><label>Units<select data-axis-feature="AXIS-VIEW-02" value={metric ? "mm" : "inch"} onChange={(event) => setMetric(event.target.value === "mm")}><option value="mm">mm</option><option value="inch">inch</option></select></label></div><div className="dro-column-headings"><span /><span>Displayed</span><span>Actual</span><span>Relative</span></div><div className="dro-table">{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 <div className="dro-row" key={label}><strong>{label}</strong><output>{format(coordType === "relative" ? relativeValue : displayed, index)}</output><output>{format(actual, index)}</output><output>{format(relativeValue, index)}</output></div>; })}</div><div className="dro-footer"><span>Units: <b>{metric ? "mm" : "inch"}</b> / deg</span><span>{coordType} / {displayType}</span><span>Tick: {state.tick}</span><span>Hash: {state.snapshot?.state_hash?.slice(0, 12) || "-"}</span></div></section>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROGRAM_ROW_HEIGHT = 17.4;
|
const PROGRAM_ROW_HEIGHT = 17.4;
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
|||||||
let frame = 0;
|
let frame = 0;
|
||||||
let liveTick = -1;
|
let liveTick = -1;
|
||||||
let liveTrace: THREE.Vector3[] = [];
|
let liveTrace: THREE.Vector3[] = [];
|
||||||
let pathKey = "";
|
let pathSegments: Segment[] | null = null;
|
||||||
let liveKey = "";
|
let segmentById = new Map<number, Segment>();
|
||||||
let bounds = { center: new THREE.Vector3(), radius: .12 };
|
let bounds = { center: new THREE.Vector3(), radius: .12 };
|
||||||
let playbackQueue: Array<{ tick: number; pose: Point; joints: number[]; activeSegmentId: number; activeProgress: number }> = [];
|
let playbackQueue: Array<{ tick: number; pose: Point; joints: number[]; activeSegmentId: number; activeProgress: number }> = [];
|
||||||
let playbackStart = 0;
|
let playbackStart = 0;
|
||||||
@@ -98,6 +98,10 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
|||||||
let renderedJoints: number[] = [];
|
let renderedJoints: number[] = [];
|
||||||
let renderedActiveSegment = 0;
|
let renderedActiveSegment = 0;
|
||||||
let renderedProgress = 0;
|
let renderedProgress = 0;
|
||||||
|
let liveHistoryLine: THREE.Line | null = null;
|
||||||
|
let activeLiveLine: THREE.Line | null = null;
|
||||||
|
let lastHistoryTick = -1;
|
||||||
|
let lastActiveKey = "";
|
||||||
let lastPreset = "";
|
let lastPreset = "";
|
||||||
const presetCamera = (selected: Props["view"], selectedZoom: number) => {
|
const presetCamera = (selected: Props["view"], selectedZoom: number) => {
|
||||||
const distance = bounds.radius * 2.9 / Math.max(.5, selectedZoom);
|
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 segments = state.toolpath as Segment[];
|
||||||
const layer = layersRef.current;
|
const layer = layersRef.current;
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
if (state.tick < queuedLastTick) {
|
// React can deliver a full snapshot just behind an already queued
|
||||||
playbackQueue = [];
|
// motion-delta response. Keep the rendered pose monotonic until the
|
||||||
playbackStart = 0;
|
// newer samples arrive; otherwise the tool visibly jumps backward.
|
||||||
queuedLastTick = -1;
|
const staleState = state.tick < queuedLastTick;
|
||||||
}
|
if (!staleState && !state.motionTrace.length && state.tick > queuedLastTick) {
|
||||||
if (!state.motionTrace.length && state.tick > queuedLastTick) {
|
|
||||||
playbackQueue = [];
|
playbackQueue = [];
|
||||||
playbackStart = 0;
|
playbackStart = 0;
|
||||||
queuedLastTick = state.tick;
|
queuedLastTick = state.tick;
|
||||||
@@ -146,7 +149,7 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
|||||||
renderedActiveSegment = state.activeSegmentId ?? 0;
|
renderedActiveSegment = state.activeSegmentId ?? 0;
|
||||||
renderedProgress = state.activeProgress;
|
renderedProgress = state.activeProgress;
|
||||||
}
|
}
|
||||||
for (const sample of state.motionTrace) {
|
for (const sample of staleState ? [] : state.motionTrace) {
|
||||||
if (sample.tick > queuedLastTick) {
|
if (sample.tick > queuedLastTick) {
|
||||||
playbackQueue.push(sample);
|
playbackQueue.push(sample);
|
||||||
queuedLastTick = sample.tick;
|
queuedLastTick = sample.tick;
|
||||||
@@ -156,16 +159,18 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
|||||||
if (playbackQueue.length) {
|
if (playbackQueue.length) {
|
||||||
const index = Math.min(playbackQueue.length - 1, Math.max(0, Math.floor((now - playbackStart) / 10)));
|
const index = Math.min(playbackQueue.length - 1, Math.max(0, Math.floor((now - playbackStart) / 10)));
|
||||||
const sample = playbackQueue[index];
|
const sample = playbackQueue[index];
|
||||||
renderedTick = sample.tick;
|
if (sample.tick >= renderedTick) {
|
||||||
renderedPose = sample.pose;
|
renderedTick = sample.tick;
|
||||||
renderedJoints = sample.joints;
|
renderedPose = sample.pose;
|
||||||
renderedActiveSegment = sample.activeSegmentId;
|
renderedJoints = sample.joints;
|
||||||
renderedProgress = sample.activeProgress;
|
renderedActiveSegment = sample.activeSegmentId;
|
||||||
|
renderedProgress = sample.activeProgress;
|
||||||
|
}
|
||||||
if (index > 32) {
|
if (index > 32) {
|
||||||
playbackQueue.splice(0, index);
|
playbackQueue.splice(0, index);
|
||||||
playbackStart += index * 10;
|
playbackStart += index * 10;
|
||||||
}
|
}
|
||||||
} else {
|
} else if (!staleState) {
|
||||||
renderedTick = state.tick;
|
renderedTick = state.tick;
|
||||||
renderedPose = state.livePose;
|
renderedPose = state.livePose;
|
||||||
renderedJoints = state.joints;
|
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
|
// The planned path is immutable for a loaded program. Rebuilding all
|
||||||
// geometries on every animation frame starves worker snapshots and
|
// geometries on every animation frame starves worker snapshots and
|
||||||
// makes an otherwise continuous WASM trajectory appear to stutter.
|
// 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("|");
|
// Toolpath arrays are immutable between PROGRAM_LOAD/RESTART. Rebuild
|
||||||
if (nextPathKey !== pathKey) {
|
// 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(pathGroup);
|
||||||
|
clearLines(liveGroup);
|
||||||
bounds = fit(segments);
|
bounds = fit(segments);
|
||||||
// Long programs can contain thousands of linear blocks. Batch their
|
// Long programs can contain thousands of linear blocks. Batch their
|
||||||
// vertices so preview loading does not block the UI on one WebGL
|
// 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.rapid, rapid, true);
|
||||||
addSegments(batched.completed, completed);
|
addSegments(batched.completed, completed);
|
||||||
for (const sampled of sampledLines) addLine(sampled.points, sampled.rapid ? rapid : completed, sampled.rapid);
|
for (const sampled of sampledLines) addLine(sampled.points, sampled.rapid ? rapid : completed, sampled.rapid);
|
||||||
pathKey = nextPathKey;
|
liveHistoryLine = null;
|
||||||
liveKey = "";
|
activeLiveLine = null;
|
||||||
|
lastHistoryTick = -1;
|
||||||
|
lastActiveKey = "";
|
||||||
}
|
}
|
||||||
const presetKey = `${viewRef.current}:${zoomRef.current}:${bounds.radius}:${bounds.center.x}:${bounds.center.y}:${bounds.center.z}`;
|
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 (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) {
|
if (renderedTick !== liveTick) {
|
||||||
const current = sourcePoint(renderedPose);
|
const current = sourcePoint(renderedPose);
|
||||||
const previous = liveTrace.at(-1);
|
const previous = liveTrace.at(-1);
|
||||||
@@ -222,15 +238,18 @@ export function XyzacToolPreview({ visual, view = "p", zoom = 1, rotateMode = fa
|
|||||||
liveTick = renderedTick;
|
liveTick = renderedTick;
|
||||||
}
|
}
|
||||||
element.dataset.xyzacLiveTraceCount = String(liveTrace.length);
|
element.dataset.xyzacLiveTraceCount = String(liveTrace.length);
|
||||||
const nextLiveKey = `${renderedTick}:${renderedActiveSegment}:${renderedProgress}:${liveTrace.length}`;
|
const historyDue = liveTrace.length > 1 && (lastHistoryTick < 0 || renderedTick - lastHistoryTick >= 4);
|
||||||
if (nextLiveKey !== liveKey) {
|
if (historyDue) {
|
||||||
clearLines(liveGroup);
|
if (liveHistoryLine) { liveHistoryLine.geometry.dispose(); liveGroup.remove(liveHistoryLine); }
|
||||||
if (liveTrace.length > 1) {
|
liveHistoryLine = new THREE.Line(new THREE.BufferGeometry().setFromPoints(liveTrace), liveHistory);
|
||||||
const historyLine = new THREE.Line(new THREE.BufferGeometry().setFromPoints(liveTrace), liveHistory);
|
liveHistoryLine.renderOrder = 19;
|
||||||
historyLine.renderOrder = 19;
|
liveGroup.add(liveHistoryLine);
|
||||||
liveGroup.add(historyLine);
|
lastHistoryTick = renderedTick;
|
||||||
}
|
}
|
||||||
const activeSegment = segments.find((segment) => Boolean(segment.active || segment.id === renderedActiveSegment));
|
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) {
|
if (activeSegment) {
|
||||||
const start = activeSegment.start ?? {}; const end = activeSegment.end ?? {};
|
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)];
|
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);
|
const lastPoint = travelled.at(-1);
|
||||||
if (!lastPoint || lastPoint.distanceTo(livePoint) > 1e-9) travelled.push(livePoint);
|
if (!lastPoint || lastPoint.distanceTo(livePoint) > 1e-9) travelled.push(livePoint);
|
||||||
if (travelled.length > 1) {
|
if (travelled.length > 1) {
|
||||||
const liveLine = new THREE.Line(new THREE.BufferGeometry().setFromPoints(travelled), live);
|
activeLiveLine = new THREE.Line(new THREE.BufferGeometry().setFromPoints(travelled), live);
|
||||||
liveLine.renderOrder = 20;
|
activeLiveLine.renderOrder = 20;
|
||||||
liveGroup.add(liveLine);
|
liveGroup.add(activeLiveLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
liveKey = nextLiveKey;
|
lastActiveKey = nextActiveKey;
|
||||||
}
|
}
|
||||||
pathGroup.visible = layer.showPath; liveGroup.visible = layer.showLive;
|
pathGroup.visible = layer.showPath; liveGroup.visible = layer.showLive;
|
||||||
grid.scale.setScalar(Math.max(.2, bounds.radius * 2.4));
|
grid.scale.setScalar(Math.max(.2, bounds.radius * 2.4));
|
||||||
|
|||||||
@@ -89,7 +89,16 @@ function projection(result: Record<string, unknown>, message: MessageEnvelope, p
|
|||||||
program: (result.program as Record<string, unknown>) ?? previous.snapshot.program,
|
program: (result.program as Record<string, unknown>) ?? previous.snapshot.program,
|
||||||
motion: {
|
motion: {
|
||||||
...previous.snapshot.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,
|
joints,
|
||||||
},
|
},
|
||||||
wasm_motion: (result.wasm_motion as Record<string, unknown>) ?? previous.snapshot.wasm_motion,
|
wasm_motion: (result.wasm_motion as Record<string, unknown>) ?? previous.snapshot.wasm_motion,
|
||||||
|
|||||||
39
web/react/src/test_xyzac_motion_projection.mjs
Normal file
39
web/react/src/test_xyzac_motion_projection.mjs
Normal file
@@ -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 } }));
|
||||||
Reference in New Issue
Block a user