Files
KDL_WORK/kdl-wasm/web/tests/kdl/poseApi.test.ts
2026-06-27 08:45:38 -04:00

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"
}
]
}
});
});
});