CI Log in

zach/dotter

src/plan.js 166 lines · 5.8 KB · javascript
1
// S-curve motion-planning utilities (port of nextdrawcore/plan_utils.py).
2
// Units are inches and seconds unless noted; "ISR units" are the EBB's
3
// 25 kHz accumulator units (rate 2^31 == one step per 40 µs tick).
4
5
import { cubicSolve, isClose, isComplex, moveDistT3, rateT3 } from './calc.js';
6
7
/** Pen-up and pen-down jerk (in/s^3) for the current accel setting. */
8
export function calcJerk(nd) {
9
  let accelScaled = Math.min(nd.options.accel, 100) / 100.0;
10
  accelScaled = accelScaled * accelScaled * accelScaled;
11
  return [nd.params.jerkPenUp * accelScaled, nd.params.jerkPenDown * accelScaled];
12
}
13
14
/**
15
 * Plan a 1D S-curve. With dist == null, return the distance needed to go
16
 * from vIn to vMax. Otherwise return the highest speed reachable from vIn
17
 * over dist (equivalently, the highest initial speed that can decelerate to
18
 * vIn), capped at vMax. Returns null on failure.
19
 * See plan_utils.scurve_plan for the derivation.
20
 */
21
export function scurvePlan(vIn, vMax, jM, dist = null, minTime = null) {
22
  if (vIn == null || vMax == null || jM == null) return null;
23
  vIn = Math.abs(vIn);
24
  vMax = Math.abs(vMax);
25
  jM = Math.abs(jM);
26
  if (jM === 0) jM = 1000; // Avoid division by zero in constant-velocity mode.
27
28
  let tM = Math.sqrt(Math.abs(vMax - vIn) / jM);
29
  let dMax = 2 * vIn * tM + jM * tM * tM * tM;
30
31
  if (dist == null && isClose(tM, 0)) return 0;
32
33
  if (minTime != null && tM < minTime / 2) {
34
    tM = minTime / 2;
35
    const jerkNew = Math.abs(vMax - vIn) / (tM * tM);
36
    dMax = 2 * vIn * tM + jerkNew * tM * tM * tM;
37
  }
38
39
  if (dist == null) return dMax;
40
41
  dist = Math.abs(dist);
42
  if (dist >= dMax || isClose(dist, dMax)) return vMax;
43
44
  // Solve jM*t^3 + 2*vIn*t - dist = 0 for the shortest positive real t.
45
  tM = 1e100;
46
  for (const item of cubicSolve(jM, 0, 2 * vIn, -dist)) {
47
    if (isComplex(item)) continue;
48
    if (item > 0 && item <= tM) tM = item;
49
  }
50
  if (tM > 1e90) return null;
51
52
  if (minTime != null && tM < minTime / 2) {
53
    tM = minTime / 2;
54
    if (vIn !== 0 && dist / vIn < minTime) {
55
      // Actually a deceleration; forward speed can never be < 0.
56
      return Math.max((dist - vIn * tM) / tM, 0);
57
    }
58
    jM = Math.abs(dist - 2 * vIn * tM) / (tM * tM * tM);
59
  }
60
  return Math.min(vMax, vIn + jM * tM * tM);
61
}
62
63
function scurveJerkSearch(vStart, vEnd, dist, maxJerk, tMin, leniency) {
64
  const vI = Math.min(vStart, vEnd);
65
  const vF = Math.max(vStart, vEnd);
66
  let tmLower = vI === 0 ? Math.max(tMin, dist / (2 * 0.001)) : Math.max(tMin, dist / (2 * vI));
67
  let tmUpper = Math.sqrt((vF - vI) / (maxJerk * leniency));
68
  let tM = tmLower + tmUpper / 10;
69
  for (let iteration = 0; iteration < 50; iteration++) {
70
    const jTemp = Math.abs(dist - 2 * vI * tM) / (tM * tM * tM);
71
    const vFTemp = vI + jTemp * tM * tM;
72
    if (isClose(vFTemp, vF, 1e-6)) return jTemp;
73
    if (vFTemp > vF) tmUpper = tM;
74
    else tmLower = tM;
75
    tM = (tmLower + tmUpper) / 2.0;
76
  }
77
  return null;
78
}
79
80
/** Lower-jerk single S-curve covering dist from vStart to vEnd, or null. */
81
export function scurveJerk(vStart, vEnd, dist, maxJerk) {
82
  return scurveJerkSearch(vStart, vEnd, dist, maxJerk, 0.003, 1.25);
83
}
84
85
/** As scurveJerk, more lenient in jerk and move time. */
86
export function scurveJerk2(vStart, vEnd, dist, maxJerk) {
87
  return scurveJerkSearch(vStart, vEnd, dist, maxJerk, 0.002, 1.3);
88
}
89
90
/** Travel time of an S-curve from vI to vF at the given jerk. */
91
export function scurveTime(vI, vF, jerk) {
92
  if (jerk === 0) return 0;
93
  return 2 * Math.sqrt(Math.abs((vF - vI) / jerk));
94
}
95
96
/**
97
 * Peak speed reachable in an accelerate-then-decelerate move of length dist
98
 * that cannot reach vMax (found by bisection).
99
 */
100
export function striangle(vI, vF, vMax, jerk, dist) {
101
  let lower = Math.max(vI, vF);
102
  let upper = vMax;
103
  // The Python version loops until convergence; cap iterations as a safeguard.
104
  for (let i = 0; i < 10000; i++) {
105
    const testV = (lower + upper) / 2.0;
106
    const testDist = scurvePlan(vI, testV, jerk, null) + scurvePlan(vF, testV, jerk, null);
107
    if (isClose(testDist, dist, 1e-5)) return testV;
108
    if (testDist > dist) upper = testV;
109
    else lower = testV;
110
  }
111
  return (lower + upper) / 2.0;
112
}
113
114
/**
115
 * Advance xyzPos by a TD move (two chained T3 moves in firmware).
116
 * Returns [steps1, steps2, inches, finalRate1, finalRate2].
117
 */
118
export function tdSegData(td, xyzPos, stepScale) {
119
  const [moveTime, v1a, v1b, a1, j1, v2a, v2b, a2, j2] = td;
120
  const stepDivisor = stepScale * 2.0;
121
122
  let s1a, s2a;
123
  [s1a, xyzPos.accum1] = moveDistT3(moveTime, v1a, 0, j1, xyzPos.accum1);
124
  [s2a, xyzPos.accum2] = moveDistT3(moveTime, v2a, 0, j2, xyzPos.accum2);
125
  let m1 = s1a / stepDivisor;
126
  let m2 = s2a / stepDivisor;
127
  let dx = m1 + m2;
128
  let dy = m1 - m2;
129
  let inches = Math.sqrt(dx * dx + dy * dy);
130
  xyzPos.xpos += dx;
131
  xyzPos.ypos += dy;
132
133
  let s1b, s2b;
134
  [s1b, xyzPos.accum1] = moveDistT3(moveTime, v1b, a1, -j1, xyzPos.accum1);
135
  [s2b, xyzPos.accum2] = moveDistT3(moveTime, v2b, a2, -j2, xyzPos.accum2);
136
  m1 = s1b / stepDivisor;
137
  m2 = s2b / stepDivisor;
138
  dx = m1 + m2;
139
  dy = m1 - m2;
140
  inches += Math.sqrt(dx * dx + dy * dy);
141
  xyzPos.xpos += dx;
142
  xyzPos.ypos += dy;
143
144
  const rate1 = rateT3(moveTime, v1b, a1, -j1);
145
  const rate2 = rateT3(moveTime, v2b, a2, -j2);
146
  return [s1a + s1b, s2a + s2b, inches, rate1, rate2];
147
}
148
149
/**
150
 * Advance xyzPos by a T3 move.
151
 * Returns [steps1, steps2, inches, finalRate1, finalRate2].
152
 */
153
export function t3SegData(t3, xyzPos, stepScale) {
154
  const [moveTime, v1, a1, j1, v2, a2, j2] = t3;
155
  let s1, s2;
156
  [s1, xyzPos.accum1] = moveDistT3(moveTime, v1, a1, j1, xyzPos.accum1);
157
  [s2, xyzPos.accum2] = moveDistT3(moveTime, v2, a2, j2, xyzPos.accum2);
158
  const m1 = s1 / (stepScale * 2.0);
159
  const m2 = s2 / (stepScale * 2.0);
160
  const dx = m1 + m2;
161
  const dy = m1 - m2;
162
  const inches = Math.sqrt(dx * dx + dy * dy);
163
  xyzPos.xpos += dx;
164
  xyzPos.ypos += dy;
165
  return [s1, s2, inches, rateT3(moveTime, v1, a1, j1), rateT3(moveTime, v2, a2, j2)];
166
}