153 lines
5.1 KiB
TypeScript
153 lines
5.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { createKdlWorkerRuntime } from "../../src/kdl/runtime.js";
|
|
import { dispatchKdlRpcRequest } from "../../src/kdl/workerRpc.js";
|
|
import type { Pose, PoseTarget } from "../../src/kdl/types.js";
|
|
|
|
async function createRuntime() {
|
|
const runtime = createKdlWorkerRuntime();
|
|
await dispatchKdlRpcRequest(runtime, { id: 1, method: "init", payload: [{}] });
|
|
return runtime;
|
|
}
|
|
|
|
function pose(x: number, y: number, z: number): Pose {
|
|
return {
|
|
position: [x, y, z],
|
|
quaternion: [0, 0, 0, 1]
|
|
};
|
|
}
|
|
|
|
describe("pose transform and offset API", () => {
|
|
it("normalizes pose inputs from rpy and quaternion forms", async () => {
|
|
const runtime = await createRuntime();
|
|
const rpyResponse = await dispatchKdlRpcRequest(runtime, {
|
|
id: 2,
|
|
method: "normalizePose",
|
|
payload: [{ xyz: [1, 2, 3], rpy: [0, 0, Math.PI / 2] }]
|
|
});
|
|
const quatResponse = await dispatchKdlRpcRequest(runtime, {
|
|
id: 3,
|
|
method: "normalizePose",
|
|
payload: [{ xyz: [0, 0, 0], quat: [0, 0, 0, 2] }]
|
|
});
|
|
|
|
expect(rpyResponse.ok).toBe(true);
|
|
expect((rpyResponse.result as Pose).position).toEqual([1, 2, 3]);
|
|
expect((rpyResponse.result as Pose).quaternion[2]).toBeCloseTo(Math.SQRT1_2);
|
|
expect((rpyResponse.result as Pose).quaternion[3]).toBeCloseTo(Math.SQRT1_2);
|
|
expect(quatResponse.result).toMatchObject({
|
|
position: [0, 0, 0],
|
|
quaternion: [0, 0, 0, 1]
|
|
});
|
|
});
|
|
|
|
it("composes poses and computes an inverse pose", async () => {
|
|
const runtime = await createRuntime();
|
|
const composeResponse = await dispatchKdlRpcRequest(runtime, {
|
|
id: 4,
|
|
method: "composePose",
|
|
payload: [pose(1, 0, 0), pose(0, 2, 0)]
|
|
});
|
|
expect(composeResponse.result).toMatchObject({
|
|
position: [1, 2, 0],
|
|
quaternion: [0, 0, 0, 1]
|
|
});
|
|
|
|
const inverseResponse = await dispatchKdlRpcRequest(runtime, {
|
|
id: 5,
|
|
method: "inversePose",
|
|
payload: [pose(1, 2, 3)]
|
|
});
|
|
expect(inverseResponse.result).toMatchObject({
|
|
position: [-1, -2, -3],
|
|
quaternion: [0, 0, 0, 1]
|
|
});
|
|
|
|
const identityResponse = await dispatchKdlRpcRequest(runtime, {
|
|
id: 6,
|
|
method: "composePose",
|
|
payload: [pose(1, 2, 3), inverseResponse.result]
|
|
});
|
|
expect((identityResponse.result as Pose).position[0]).toBeCloseTo(0);
|
|
expect((identityResponse.result as Pose).position[1]).toBeCloseTo(0);
|
|
expect((identityResponse.result as Pose).position[2]).toBeCloseTo(0);
|
|
});
|
|
|
|
it("applies frame, target, and tool using the same order as FK", async () => {
|
|
const runtime = await createRuntime();
|
|
const target: PoseTarget = {
|
|
id: "pick",
|
|
pose: pose(0.5, 0.1, 0.2)
|
|
};
|
|
const response = await dispatchKdlRpcRequest(runtime, {
|
|
id: 7,
|
|
method: "applyToolAndFrame",
|
|
payload: [target, pose(0, 0, 0.18), pose(0.8, 0, 0.2)]
|
|
});
|
|
|
|
const result = response.result as Pose;
|
|
expect(result.position[0]).toBeCloseTo(1.3);
|
|
expect(result.position[1]).toBeCloseTo(0.1);
|
|
expect(result.position[2]).toBeCloseTo(0.58);
|
|
expect(result.quaternion).toEqual([0, 0, 0, 1]);
|
|
});
|
|
|
|
it("applies offset in frame/world by left composition and tool by right composition", async () => {
|
|
const runtime = await createRuntime();
|
|
const target: PoseTarget = {
|
|
id: "pick",
|
|
pose: {
|
|
position: [1, 2, 3],
|
|
quaternion: [0, 0, Math.SQRT1_2, Math.SQRT1_2]
|
|
},
|
|
frame: pose(10, 0, 0)
|
|
};
|
|
|
|
const frameOffset = await dispatchKdlRpcRequest(runtime, {
|
|
id: 8,
|
|
method: "applyOffset",
|
|
payload: [target, { mode: "frame", xyz: [0.1, 0, 0] }]
|
|
});
|
|
const worldOffset = await dispatchKdlRpcRequest(runtime, {
|
|
id: 9,
|
|
method: "applyOffset",
|
|
payload: [target, { mode: "world", xyz: [0, 0.2, 0] }]
|
|
});
|
|
const toolOffset = await dispatchKdlRpcRequest(runtime, {
|
|
id: 10,
|
|
method: "applyOffset",
|
|
payload: [target, { mode: "tool", xyz: [0.1, 0, 0] }]
|
|
});
|
|
|
|
expect((frameOffset.result as PoseTarget).id).toBe("pick");
|
|
expect((frameOffset.result as PoseTarget).frame).toEqual(target.frame);
|
|
expect((frameOffset.result as PoseTarget).pose.position[0]).toBeCloseTo(1.1);
|
|
expect((frameOffset.result as PoseTarget).pose.position[1]).toBeCloseTo(2);
|
|
expect((worldOffset.result as PoseTarget).pose.position[0]).toBeCloseTo(1);
|
|
expect((worldOffset.result as PoseTarget).pose.position[1]).toBeCloseTo(2.2);
|
|
expect((toolOffset.result as PoseTarget).pose.position[0]).toBeCloseTo(1);
|
|
expect((toolOffset.result as PoseTarget).pose.position[1]).toBeCloseTo(2.1);
|
|
});
|
|
|
|
it("returns structured diagnostics for invalid pose inputs", async () => {
|
|
const runtime = await createRuntime();
|
|
const response = await dispatchKdlRpcRequest(runtime, {
|
|
id: 11,
|
|
method: "normalizePose",
|
|
payload: [{ xyz: [1, 2], rpy: [0, 0, 0] }]
|
|
});
|
|
|
|
expect(response).toMatchObject({
|
|
ok: false,
|
|
error: {
|
|
code: "KDL_INVALID_POSE",
|
|
diagnostics: [
|
|
{
|
|
severity: "error",
|
|
code: "KDL_INVALID_POSE"
|
|
}
|
|
]
|
|
}
|
|
});
|
|
});
|
|
});
|