b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 1 | use crate::geom::{Change, Rect}; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 2 | use crate::plan::{Config, Plan, Style}; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 3 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 4 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 5 | pub enum Violation { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 6 | /// Windows `a` and `b` overlap at some point during `phase`. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 7 | Overlap { phase: usize, a: usize, b: usize }, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 8 | /// In orthogonal style, `window` changes both axes during `phase`. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 9 | TwoAxes { phase: usize, window: usize }, |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 10 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 11 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 12 | /// Exact check of a plan under linear interpolation. Every window in a phase |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 13 | /// shares one curve, so overlap depends only on the keyframes, not the easing. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 14 | pub fn validate(plan: &Plan, cfg: &Config) -> Vec<Violation> { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 15 | let mut out = Vec::new(); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 16 | for (phase, pair) in plan.keyframes.windows(2).enumerate() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 17 | let (a, b) = (&pair[0], &pair[1]); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 18 | for i in 0..a.len() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 19 | if cfg.style == Style::Orthogonal && Change::between(&a[i], &b[i]).both() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 20 | out.push(Violation::TwoAxes { phase, window: i }); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 21 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 22 | for j in i + 1..a.len() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 23 | if overlaps_during(&a[i], &b[i], &a[j], &b[j], cfg.eps) { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 24 | out.push(Violation::Overlap { phase, a: i, b: j }); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 25 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 26 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 27 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 28 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 29 | out |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 30 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 31 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 32 | /// Whether two linearly moving rectangles share more than `tol` of width and |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 33 | /// height at any instant. Each condition is linear in time, so the set of |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 34 | /// instants satisfying all of them is an interval. |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 35 | fn overlaps_during(a0: &Rect, a1: &Rect, b0: &Rect, b1: &Rect, tol: f64) -> bool { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 36 | let conditions = [ |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 37 | (a0.w, a1.w), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 38 | (b0.w, b1.w), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 39 | (a0.right() - b0.x, a1.right() - b1.x), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 40 | (b0.right() - a0.x, b1.right() - a1.x), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 41 | (a0.h, a1.h), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 42 | (b0.h, b1.h), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 43 | (a0.bottom() - b0.y, a1.bottom() - b1.y), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 44 | (b0.bottom() - a0.y, b1.bottom() - a1.y), |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 45 | ]; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 46 | let (mut lo, mut hi) = (0.0f64, 1.0f64); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 47 | for (v0, v1) in conditions { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 48 | let slope = v1 - v0; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 49 | if slope.abs() < 1e-12 { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 50 | if v0 <= tol { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 51 | return false; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 52 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 53 | continue; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 54 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 55 | let root = (tol - v0) / slope; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 56 | if slope > 0.0 { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 57 | lo = lo.max(root); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 58 | } else { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 59 | hi = hi.min(root); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 60 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 61 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 62 | lo < hi - 1e-9 |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 63 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 64 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 65 | #[cfg(test)] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 66 | mod tests { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 67 | use super::*; |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 68 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 69 | #[test] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 70 | fn crossing_windows_overlap() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 71 | let a0 = Rect::new(0.0, 0.0, 100.0, 100.0); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 72 | let b0 = Rect::new(110.0, 0.0, 100.0, 100.0); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 73 | assert!(overlaps_during(&a0, &b0, &b0, &a0, 0.5)); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 74 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 75 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 76 | #[test] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 77 | fn neighbours_moving_together_do_not() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 78 | let a0 = Rect::new(0.0, 0.0, 100.0, 100.0); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 79 | let a1 = Rect::new(0.0, 0.0, 150.0, 100.0); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 80 | let b0 = Rect::new(110.0, 0.0, 100.0, 100.0); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 81 | let b1 = Rect::new(160.0, 0.0, 50.0, 100.0); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 82 | assert!(!overlaps_during(&a0, &a1, &b0, &b1, 0.5)); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 83 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 84 | |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 85 | #[test] |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 86 | fn zero_size_seed_inside_a_window_is_not_an_overlap() { |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 87 | let a = Rect::new(0.0, 0.0, 100.0, 100.0); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 88 | let seed = Rect::new(50.0, 0.0, 0.0, 100.0); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 89 | assert!(!overlaps_during(&a, &a, &seed, &seed, 0.5)); |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 90 | } |
b563d2da1c80 | atagen | 2026-10-16 08:25 UTC | 91 | } |