import { describe, expect, it } from "vitest"; import { createKdlWorkerRuntime } from "../../src/kdl/runtime.js"; import { dispatchKdlRpcRequest } from "../../src/kdl/workerRpc.js"; import type { MoveJRequest, PoseTarget, TrajectoryResult } from "../../src/kdl/types.js"; const PLANAR_URDF = ` `; async function createRobot() { const runtime = createKdlWorkerRuntime(); await dispatchKdlRpcRequest(runtime, { id: 1, method: "init", payload: [{}] }); const response = await dispatchKdlRpcRequest(runtime, { id: 2, method: "loadRobotFromUrdf", payload: [ PLANAR_URDF, { robotId: "movej", baseLink: "base_link", tipLink: "tool0" } ] }); expect(response.ok).toBe(true); return { runtime, handle: response.result as number }; } function baseRequest(overrides: Partial): MoveJRequest { return { startJoints: [0, 0], target: { id: "joint_goal", joints: [0.5, 0.25] }, speed: { kind: "joint_abs", velocity: 0.5, acceleration: 1 }, zone: { kind: "fine" }, sampleTime: 0.1, ...overrides }; } describe("planMoveJ", () => { it("plans a synchronized joint trajectory to a joint target", async () => { const { runtime, handle } = await createRobot(); const response = await dispatchKdlRpcRequest(runtime, { id: 3, method: "planMoveJ", payload: [handle, baseRequest({})] }); expect(response.ok).toBe(true); const trajectory = response.result as TrajectoryResult; expect(trajectory.ok).toBe(true); expect(trajectory.motion).toBe("MOVEJ"); expect(trajectory.points.length).toBeGreaterThan(2); expect(trajectory.points[0]).toMatchObject({ index: 0, time: 0, s: 0, joints: [0, 0], motion: "MOVEJ", targetId: "joint_goal" }); expect(trajectory.points.at(-1)?.s).toBe(1); expect(trajectory.points.at(-1)?.joints[0]).toBeCloseTo(0.5); expect(trajectory.points.at(-1)?.joints[1]).toBeCloseTo(0.25); expect(trajectory.points.at(-1)?.jointVelocity[0]).toBeCloseTo(0); expect(trajectory.points.at(-1)?.tcp.position[0]).toBeCloseTo(0.25 * Math.cos(0.5)); expect(trajectory.points.at(-1)?.tcp.position[1]).toBeCloseTo(0.25 * Math.sin(0.5)); expect(trajectory.meta).toMatchObject({ targetType: "joint", qStart: [0, 0], qEnd: [0.5, 0.25] }); }); it("uses IK for pose targets and warns when zone is approximated as fine", async () => { const { runtime, handle } = await createRobot(); const target: PoseTarget = { id: "pose_goal", pose: { position: [0, 0.3, 0], quaternion: [0, 0, 0, 1] } }; const response = await dispatchKdlRpcRequest(runtime, { id: 4, method: "planMoveJ", payload: [ handle, baseRequest({ target, zone: { kind: "distance", value: 0.01 } }) ] }); const trajectory = response.result as TrajectoryResult; expect(trajectory.ok).toBe(true); expect(trajectory.points.at(-1)?.joints[0]).toBeCloseTo(Math.PI / 2); expect(trajectory.points.at(-1)?.joints[1]).toBeCloseTo(0.3); expect(trajectory.diagnostics).toContainEqual( expect.objectContaining({ severity: "warning", code: "KDL_ZONE_APPROX_FINE" }) ); expect(trajectory.meta).toMatchObject({ targetType: "pose" }); }); it("returns a failed trajectory result for endpoint joint limit violations", async () => { const { runtime, handle } = await createRobot(); const response = await dispatchKdlRpcRequest(runtime, { id: 5, method: "planMoveJ", payload: [ handle, baseRequest({ target: { id: "bad_goal", joints: [0, 2] } }) ] }); expect(response.ok).toBe(true); expect(response.result).toMatchObject({ ok: false, motion: "MOVEJ", points: [], diagnostics: [ { severity: "error", code: "KDL_JOINT_LIMIT" } ] }); }); it("keeps velocity and acceleration within joint limits", async () => { const { runtime, handle } = await createRobot(); const response = await dispatchKdlRpcRequest(runtime, { id: 6, method: "planMoveJ", payload: [ handle, baseRequest({ speed: { kind: "joint_percent", value: 1 }, target: { id: "limit_goal", joints: [1, 0.5] } }) ] }); const trajectory = response.result as TrajectoryResult; expect(trajectory.ok).toBe(true); for (const point of trajectory.points) { expect(Math.abs(point.jointVelocity[0]!)).toBeLessThanOrEqual(1 + 1e-9); expect(Math.abs(point.jointVelocity[1]!)).toBeLessThanOrEqual(0.5 + 1e-9); expect(Math.abs(point.jointAcceleration[0]!)).toBeLessThanOrEqual(2 + 1e-9); expect(Math.abs(point.jointAcceleration[1]!)).toBeLessThanOrEqual(1 + 1e-9); } }); });