Log in
src/calc.js 179 lines · 6.1 KB · javascript
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
}