| 1 | // Numeric helpers that mirror Python semantics exactly, plus a port of
|
| 2 | // plotink's ebb_calc (EBB firmware motion math) and the cubic solver used by
|
| 3 | // the S-curve planner. Exactness matters here: the planner feeds these values
|
| 4 | // straight into T3/TD commands, so a rounding difference is a different move.
|
| 5 |
|
| 6 | export const EBB_ACCUMULATOR_MAX = 2147483648; // 2^31
|
| 7 | const ACC_MAX_BIG = 2147483648n;
|
| 8 |
|
| 9 | /** Python's round(): round half to even. */
|
| 10 | export function pyRound(x) {
|
| 11 | const f = Math.floor(x);
|
| 12 | const diff = x - f;
|
| 13 | if (diff > 0.5) return f + 1;
|
| 14 | if (diff < 0.5) return f;
|
| 15 | return f % 2 === 0 ? f : f + 1;
|
| 16 | }
|
| 17 |
|
| 18 | /** Python's int() on a float: truncate toward zero. */
|
| 19 | export const pyInt = Math.trunc;
|
| 20 |
|
| 21 | /** Python's math.isclose(a, b, rel_tol=1e-9, abs_tol=absTol). */
|
| 22 | export function isClose(a, b, absTol = 0) {
|
| 23 | if (a === b) return true;
|
| 24 | const diff = Math.abs(a - b);
|
| 25 | return diff <= Math.max(1e-9 * Math.max(Math.abs(a), Math.abs(b)), absTol);
|
| 26 | }
|
| 27 |
|
| 28 | export function distance(x, y) {
|
| 29 | return Math.sqrt(x * x + y * y);
|
| 30 | }
|
| 31 |
|
| 32 | /** plot_utils.dotProductXY: dot product clamped to [-1, 1]. */
|
| 33 | export function dotProductXY(a, b) {
|
| 34 | const t = a[0] * b[0] + a[1] * b[1];
|
| 35 | if (t > 1) return 1;
|
| 36 | if (t < -1) return -1;
|
| 37 | return t;
|
| 38 | }
|
| 39 |
|
| 40 | export function constrainLimits(value, lo, hi) {
|
| 41 | return Math.max(lo, Math.min(hi, value));
|
| 42 | }
|
| 43 |
|
| 44 | /** plot_utils.checkLimitsTol: clamp, and report if beyond tolerance. */
|
| 45 | export function checkLimitsTol(value, lo, hi, tol) {
|
| 46 | if (value > hi) return [hi, value > hi + tol];
|
| 47 | if (value < lo) return [lo, value < lo - tol];
|
| 48 | return [value, false];
|
| 49 | }
|
| 50 |
|
| 51 | function bigRoundHalfUpDiv6(n) {
|
| 52 | // Decimal ROUND_HALF_UP of n/6: ties go away from zero.
|
| 53 | return n >= 0n ? (n + 3n) / 6n : -((-n + 3n) / 6n);
|
| 54 | }
|
| 55 |
|
| 56 | /**
|
| 57 | * Final step count and accumulator after a T3 move on one axis
|
| 58 | * (ebb_calc.move_dist_t3). `accum` may be the string "clear".
|
| 59 | * Returns [steps, accumulator].
|
| 60 | */
|
| 61 | export function moveDistT3(time, rate, accel, jerk, accum = 'clear') {
|
| 62 | time = pyInt(time);
|
| 63 | rate = pyInt(rate);
|
| 64 | accel = pyInt(accel);
|
| 65 | jerk = pyInt(jerk);
|
| 66 | if (time === 0) return [0, 0];
|
| 67 |
|
| 68 | const halfAccel = pyInt(accel / 2);
|
| 69 | const jerkOverSix = pyInt(jerk / 6);
|
| 70 |
|
| 71 | if (accum === 'clear') {
|
| 72 | accum = 0;
|
| 73 | let tempRate = rate - halfAccel + jerkOverSix + accel;
|
| 74 | if (tempRate < 0) {
|
| 75 | accum = 2147483647;
|
| 76 | } else if (tempRate === 0) {
|
| 77 | tempRate = accel + jerk;
|
| 78 | if (tempRate < 0) accum = 2147483647;
|
| 79 | else if (tempRate === 0 && jerk < 0) accum = 2147483647;
|
| 80 | }
|
| 81 | } else {
|
| 82 | accum = pyInt(accum);
|
| 83 | }
|
| 84 |
|
| 85 | // 6 * (accum + T*(r_eff + T*(a/2 + T*j/6))), where
|
| 86 | // 6 * r_eff = 6r + 3a - 6*int(a/2) + 6*int(j/6) - j. All exact integers.
|
| 87 | const T = BigInt(time);
|
| 88 | const r6 = 6n * BigInt(rate) + 3n * BigInt(accel) - 6n * BigInt(halfAccel) +
|
| 89 | 6n * BigInt(jerkOverSix) - BigInt(jerk);
|
| 90 | const n6 = 6n * BigInt(accum) + T * (r6 + T * (3n * BigInt(accel) + T * BigInt(jerk)));
|
| 91 | const rounded = bigRoundHalfUpDiv6(n6);
|
| 92 | const pos = Math.floor(Number(rounded) / EBB_ACCUMULATOR_MAX);
|
| 93 | const remainder = rounded - ACC_MAX_BIG * BigInt(pos);
|
| 94 | return [pos, Number(remainder)];
|
| 95 | }
|
| 96 |
|
| 97 | /** Rate after a T3 move on one axis (ebb_calc.rate_t3). */
|
| 98 | export function rateT3(time, rate, accel, jerk) {
|
| 99 | time = pyInt(time);
|
| 100 | if (time === 0) return rate + accel + jerk;
|
| 101 | const base = pyInt(rate) - pyInt(accel / 2) + pyInt(jerk / 6);
|
| 102 | return pyRound(base + (pyInt(accel) - jerk / 2) * time + jerk * time * time / 2);
|
| 103 | }
|
| 104 |
|
| 105 | /**
|
| 106 | * Exact peak |rate| over ticks 1..time of a T3 move on one axis. The rate
|
| 107 | * at tick t is r0 + accel*t + jerk*(t^2 - t)/2, with r0 the firmware's
|
| 108 | * pre-adjusted rate; the peak is at an endpoint or next to the vertex.
|
| 109 | */
|
| 110 | export function peakRateT3(time, rate, accel, jerk) {
|
| 111 | const r0 = rate - pyInt(accel / 2) + pyInt(jerk / 6);
|
| 112 | const at = (t) => Math.abs(r0 + accel * t + jerk * (t * t - t) / 2);
|
| 113 | let peak = Math.max(at(1), at(time));
|
| 114 | if (jerk !== 0) {
|
| 115 | const vertex = 0.5 - accel / jerk;
|
| 116 | for (const t of [Math.floor(vertex), Math.ceil(vertex)]) {
|
| 117 | if (t > 1 && t < time) peak = Math.max(peak, at(t));
|
| 118 | }
|
| 119 | }
|
| 120 | return peak;
|
| 121 | }
|
| 122 |
|
| 123 | /** Peak |rate| over a T3 or TD command's parameters, both axes. */
|
| 124 | export function peakRateCommand(kind, p) {
|
| 125 | if (kind === 'T3') {
|
| 126 | const [t, r1, a1, j1, r2, a2, j2] = p;
|
| 127 | return Math.max(peakRateT3(t, r1, a1, j1), peakRateT3(t, r2, a2, j2));
|
| 128 | }
|
| 129 | const [t, r1a, r1b, a1, j1, r2a, r2b, a2, j2] = p;
|
| 130 | return Math.max(peakRateT3(t, r1a, 0, j1), peakRateT3(t, r1b, a1, -j1),
|
| 131 | peakRateT3(t, r2a, 0, j2), peakRateT3(t, r2b, a2, -j2));
|
| 132 | }
|
| 133 |
|
| 134 | const COMPLEX = Symbol('complex');
|
| 135 |
|
| 136 | /**
|
| 137 | * Roots of a*x^3 + b*x^2 + c*x + d = 0 (port of cubic_eqn.solve).
|
| 138 | * Complex roots are returned as the COMPLEX marker so callers can skip them,
|
| 139 | * as the Python planner does with isinstance(item, complex).
|
| 140 | */
|
| 141 | export function cubicSolve(a, b, c, d) {
|
| 142 | if (a === 0 && b === 0) return [(-d * 1.0) / c];
|
| 143 | if (a === 0) {
|
| 144 | const disc = c * c - 4.0 * b * d;
|
| 145 | if (disc >= 0) {
|
| 146 | const s = Math.sqrt(disc);
|
| 147 | return [(-c + s) / (2.0 * b), (-c - s) / (2.0 * b)];
|
| 148 | }
|
| 149 | return [COMPLEX, COMPLEX];
|
| 150 | }
|
| 151 | const f = ((3.0 * c / a) - ((b ** 2.0) / (a ** 2.0))) / 3.0;
|
| 152 | const g = (((2.0 * (b ** 3.0)) / (a ** 3.0)) - ((9.0 * b * c) / (a ** 2.0)) + (27.0 * d / a)) / 27.0;
|
| 153 | const h = (g ** 2.0) / 4.0 + (f ** 3.0) / 27.0;
|
| 154 |
|
| 155 | if (f === 0 && g === 0 && h === 0) {
|
| 156 | const x = (d / a) >= 0 ? -((d / (1.0 * a)) ** (1 / 3.0)) : (-d / (1.0 * a)) ** (1 / 3.0);
|
| 157 | return [x, x, x];
|
| 158 | }
|
| 159 | if (h <= 0) {
|
| 160 | const i = Math.sqrt(((g ** 2.0) / 4.0) - h);
|
| 161 | const j = i ** (1 / 3.0);
|
| 162 | const k = Math.acos(-(g / (2 * i)));
|
| 163 | const L = -j;
|
| 164 | const M = Math.cos(k / 3.0);
|
| 165 | const N = Math.sqrt(3) * Math.sin(k / 3.0);
|
| 166 | const P = -(b / (3.0 * a));
|
| 167 | return [2 * j * Math.cos(k / 3.0) - (b / (3.0 * a)), L * (M + N) + P, L * (M - N) + P];
|
| 168 | }
|
| 169 | const R = -(g / 2.0) + Math.sqrt(h);
|
| 170 | const S = R >= 0 ? R ** (1 / 3.0) : -((-R) ** (1 / 3.0));
|
| 171 | const T = -(g / 2.0) - Math.sqrt(h);
|
| 172 | const U = T >= 0 ? T ** (1 / 3.0) : -((-T) ** (1 / 3.0));
|
| 173 | // Python builds x2, x3 with a 0.5j factor, so they are always complex-typed.
|
| 174 | return [(S + U) - (b / (3.0 * a)), COMPLEX, COMPLEX];
|
| 175 | }
|
| 176 |
|
| 177 | export function isComplex(root) {
|
| 178 | return root === COMPLEX;
|
| 179 | }
|