Log in
main 0 open branches 33 files Download ZIP Browse files
test/motion.test.js 88 lines · 4.0 KB · javascript
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
});