| 1 | import { describe, expect, test } from 'bun:test';
|
| 2 | import { computeSegment, trajectory } from '../src/motion.js';
|
| 3 | import { scurvePlan, scurveTime, striangle } from '../src/plan.js';
|
| 4 | import { connected, rng } from './helpers.js';
|
| 5 |
|
| 6 | const STEP_HR = 1 / 2032; // one motor-axis step, inches (16x microstepping)
|
| 7 |
|
| 8 | describe('S-curve math', () => {
|
| 9 | test('distance to reach a speed, and speed reachable in a distance, are inverses', () => {
|
| 10 | const rand = rng(5);
|
| 11 | for (let i = 0; i < 200; i++) {
|
| 12 | const j = 1000 + rand() * 30000;
|
| 13 | const v0 = rand() * 3;
|
| 14 | const vMax = v0 + 0.01 + rand() * 6;
|
| 15 | const d = scurvePlan(v0, vMax, j, null);
|
| 16 | expect(scurvePlan(v0, vMax, j, d * 0.999999)).toBeLessThanOrEqual(vMax);
|
| 17 | expect(scurvePlan(v0, 1e9, j, d)).toBeCloseTo(vMax, 6);
|
| 18 | expect(scurveTime(v0, vMax, j)).toBeCloseTo(2 * Math.sqrt((vMax - v0) / j), 12);
|
| 19 | }
|
| 20 | });
|
| 21 |
|
| 22 | test('triangle peak speed covers the requested distance', () => {
|
| 23 | const j = 15000;
|
| 24 | const dist = 0.05;
|
| 25 | const v = striangle(0, 0, 8, j, dist);
|
| 26 | expect(2 * scurvePlan(0, v, j, null)).toBeCloseTo(dist, 4);
|
| 27 | });
|
| 28 | });
|
| 29 |
|
| 30 | describe('segment planning on a connected machine', () => {
|
| 31 | for (const handling of [1, 2, 3, 4]) {
|
| 32 | test(`random segments end exactly where planned (handling ${handling})`, async () => {
|
| 33 | const { nd } = await connected({}, (n) => { n.options.homing = false; n.options.handling = handling; });
|
| 34 | const rand = rng(100 + handling);
|
| 35 | const step = 1 / nd.stepScale; // one motor step, in inches of (x+y) or (x-y)
|
| 36 | for (let i = 0; i < 150; i++) {
|
| 37 | const x = rand() * 11.81;
|
| 38 | const y = rand() * 8.58;
|
| 39 | nd.pen.phys.zUp = rand() < 0.5;
|
| 40 | const [moves, data] = computeSegment(nd, [x, y, 0, 0, false]);
|
| 41 | if (moves == null) continue;
|
| 42 | const end = data[0];
|
| 43 | // Timed moves land within a couple of motor steps of the target (as in
|
| 44 | // the Python planner); the tracked end position itself is exact.
|
| 45 | expect(Math.abs((end.xpos + end.ypos) - (x + y))).toBeLessThanOrEqual(step * 2.5);
|
| 46 | expect(Math.abs((end.xpos - end.ypos) - (x - y))).toBeLessThanOrEqual(step * 2.5);
|
| 47 | for (const [kind, p] of moves) {
|
| 48 | expect(['T3', 'TD']).toContain(kind);
|
| 49 | for (const v of p) expect(Number.isInteger(v)).toBe(true);
|
| 50 | expect(p[0]).toBeGreaterThanOrEqual(1);
|
| 51 | }
|
| 52 | Object.assign(nd.pen.phys, { xpos: end.xpos, ypos: end.ypos, accum1: end.accum1, accum2: end.accum2 });
|
| 53 | }
|
| 54 | });
|
| 55 | }
|
| 56 |
|
| 57 | test('tiny moves under one step are skipped', async () => {
|
| 58 | const { nd } = await connected({}, (n) => { n.options.homing = false; });
|
| 59 | expect(computeSegment(nd, [STEP_HR / 10, 0, 0, 0, false])).toEqual([null, null]);
|
| 60 | });
|
| 61 |
|
| 62 | test('trajectory lowers, moves, and raises', async () => {
|
| 63 | const { nd } = await connected({}, (n) => { n.options.homing = false; });
|
| 64 | const t = trajectory(nd, [[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]);
|
| 65 | expect(t[0]).toEqual(['lower']);
|
| 66 | expect(t[t.length - 1]).toEqual(['raise']);
|
| 67 | const last = t[t.length - 2][2][1];
|
| 68 | expect(last.xpos).toBeCloseTo(0, 3);
|
| 69 | expect(last.ypos).toBeCloseTo(0, 3);
|
| 70 | });
|
| 71 |
|
| 72 | test('a path that trims to one usable segment is still drawn', async () => {
|
| 73 | // Vertices closer together than one step are dropped; one real segment remains.
|
| 74 | const { nd } = await connected({}, (n) => { n.options.homing = false; });
|
| 75 | const t = trajectory(nd, [[0, 0], [0.00001, 0], [1, 0]]);
|
| 76 | const motion = t.filter((m) => m[0] === 'T3' || m[0] === 'TD');
|
| 77 | expect(motion.length).toBeGreaterThan(0);
|
| 78 | expect(motion[motion.length - 1][2][1].xpos).toBeCloseTo(1, 3);
|
| 79 | });
|
| 80 |
|
| 81 | test('out-of-bounds targets are clamped and warned about', async () => {
|
| 82 | const { nd, messages } = await connected({}, (n) => { n.options.homing = false; });
|
| 83 | const [, data] = computeSegment(nd, [20, -3, 0, 0, false]);
|
| 84 | expect(data[0].xpos).toBeCloseTo(11.81, 3);
|
| 85 | expect(data[0].ypos).toBeCloseTo(0, 3);
|
| 86 | expect(messages.join(' ')).toContain('clipped');
|
| 87 | });
|
| 88 | });
|