b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 1 | /// Rectangle edges as (left, right, top, bottom). |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 2 | pub type Edges = [f64; 4]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 3 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 4 | /// Initial progress slope for the first phase of a replanned motion group. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 5 | /// |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 6 | /// Each item is a window's current edge velocity (units per second) and its |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 7 | /// edge displacement over the new first phase. The fastest mover with a real |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 8 | /// displacement decides the slope, because windows sharing an edge must share |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 9 | /// a curve. Returns `None` when that mover would have to reverse, so the group |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 10 | /// starts from rest instead. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 11 | pub fn group_initial_slope( |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 12 | movers: impl IntoIterator<Item = (Edges, Edges)>, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 13 | phase_secs: f64, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 14 | ) -> Option<f64> { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 15 | let mut best: Option<(f64, Edges, Edges)> = None; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 16 | for (velocity, displacement) in movers { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 17 | if dot(&displacement, &displacement) < 1.0 { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 18 | continue; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 19 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 20 | let speed = dot(&velocity, &velocity); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 21 | if best.is_none_or(|(s, _, _)| speed > s) { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 22 | best = Some((speed, velocity, displacement)); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 23 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 24 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 25 | let (_, velocity, displacement) = best?; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 26 | let slope = dot(&velocity, &displacement) / dot(&displacement, &displacement) * phase_secs; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 27 | (slope > 0.05).then_some(slope) |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 28 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 29 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 30 | fn dot(a: &Edges, b: &Edges) -> f64 { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 31 | a.iter().zip(b).map(|(x, y)| x * y).sum() |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 32 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 33 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 34 | #[cfg(test)] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 35 | mod tests { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 36 | use super::*; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 37 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 38 | #[test] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 39 | fn continues_same_direction() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 40 | let slope = group_initial_slope([([100.0, 100.0, 0.0, 0.0], [50.0, 50.0, 0.0, 0.0])], 0.5); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 41 | assert_eq!(slope, Some(1.0)); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 42 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 43 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 44 | #[test] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 45 | fn reversal_starts_from_rest() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 46 | let slope = |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 47 | group_initial_slope([([100.0, 100.0, 0.0, 0.0], [-50.0, -50.0, 0.0, 0.0])], 0.5); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 48 | assert_eq!(slope, None); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 49 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 50 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 51 | #[test] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 52 | fn ignores_windows_that_do_not_move_this_phase() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 53 | let movers = [ |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 54 | ([500.0, 500.0, 0.0, 0.0], [0.0; 4]), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 55 | ([10.0, 10.0, 0.0, 0.0], [20.0, 20.0, 0.0, 0.0]), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 56 | ]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 57 | assert_eq!(group_initial_slope(movers, 1.0), Some(0.5)); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 58 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 59 | } |