| 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 | }
|