import { describe, expect, it } from "vitest"; import { createKdlWorkerRuntime } from "../../src/kdl/runtime.js"; import { dispatchKdlRpcRequest } from "../../src/kdl/workerRpc.js"; import type { IkResult, Pose } from "../../src/kdl/types.js"; const PLANAR_URDF = ` `; const UNSUPPORTED_URDF = ` `; function pose(x: number, y: number, z = 0): Pose { return { position: [x, y, z], quaternion: [0, 0, 0, 1] }; } async function createRobot(urdf = PLANAR_URDF) { const runtime = createKdlWorkerRuntime(); await dispatchKdlRpcRequest(runtime, { id: 1, method: "init", payload: [{}] }); const response = await dispatchKdlRpcRequest(runtime, { id: 2, method: "loadRobotFromUrdf", payload: [ urdf, { robotId: "ik", baseLink: "base_link", tipLink: "tool0" } ] }); expect(response.ok).toBe(true); return { runtime, handle: response.result as number }; } describe("IK and ikBatch", () => { it("solves a reachable planar target and FK back-substitution is within tolerance", async () => { const { runtime, handle } = await createRobot(); const response = await dispatchKdlRpcRequest(runtime, { id: 3, method: "ik", payload: [handle, [0, 0], pose(0, 0.4), { positionTolerance: 1e-9 }] }); expect(response.ok).toBe(true); const result = response.result as IkResult; expect(result.ok).toBe(true); expect(result.joints?.[0]).toBeCloseTo(Math.PI / 2); expect(result.joints?.[1]).toBeCloseTo(0.4); expect(result.residualPosition).toBeLessThan(1e-9); const fk = await dispatchKdlRpcRequest(runtime, { id: 4, method: "fk", payload: [handle, result.joints] }); const fkResult = fk.result as { tcp: { position: number[] } }; expect(fkResult.tcp.position[0]).toBeCloseTo(0); expect(fkResult.tcp.position[1]).toBeCloseTo(0.4); }); it("keeps ikBatch results in input order", async () => { const { runtime, handle } = await createRobot(); const response = await dispatchKdlRpcRequest(runtime, { id: 5, method: "ikBatch", payload: [ handle, [ [0, 0], [0, 0] ], [pose(0.2, 0), pose(0, 0.3)], {} ] }); expect(response.ok).toBe(true); const results = response.result as IkResult[]; expect(results).toHaveLength(2); expect(results[0]?.joints?.[0]).toBeCloseTo(0); expect(results[0]?.joints?.[1]).toBeCloseTo(0.2); expect(results[1]?.joints?.[0]).toBeCloseTo(Math.PI / 2); expect(results[1]?.joints?.[1]).toBeCloseTo(0.3); }); it("returns joint_limit reason when the candidate exceeds limits", async () => { const { runtime, handle } = await createRobot(); const response = await dispatchKdlRpcRequest(runtime, { id: 6, method: "ik", payload: [handle, [0, 0], pose(2, 0), {}] }); expect(response.ok).toBe(true); expect(response.result).toMatchObject({ ok: false, reason: "joint_limit", diagnostics: [ { severity: "error", code: "KDL_JOINT_LIMIT" } ] }); }); it("returns invalid_model reason for unsupported IK chains", async () => { const { runtime, handle } = await createRobot(UNSUPPORTED_URDF); const response = await dispatchKdlRpcRequest(runtime, { id: 7, method: "ik", payload: [handle, [0, 0], pose(0.2, 0), {}] }); expect(response.ok).toBe(true); expect(response.result).toMatchObject({ ok: false, reason: "invalid_model", diagnostics: [ { severity: "error", code: "KDL_IK_UNSUPPORTED_MODEL" } ] }); }); });