atagen

monolith

public
Star0
Watch0
7de49c2aa70b317e35783f54619e08eff5326c7f
1
ssh://brewery@pls.lobotomise.me:33/atagen/monolith.git

Blame: crates/planner/src/tests.rs

b563d2da1c80atagen2026-10-16 08:25 UTC1
use crate::geom::Rect;
b563d2da1c80atagen2026-10-16 08:25 UTC2
use crate::plan::{Config, Plan, Strategy, Style, plan, plan_canonical};
b563d2da1c80atagen2026-10-16 08:25 UTC3
use crate::{plan_blocks, validate};
b563d2da1c80atagen2026-10-16 08:25 UTC4
use proptest::prelude::*;
b563d2da1c80atagen2026-10-16 08:25 UTC5
b563d2da1c80atagen2026-10-16 08:25 UTC6
const G: f64 = 10.0;
b563d2da1c80atagen2026-10-16 08:25 UTC7
b563d2da1c80atagen2026-10-16 08:25 UTC8
fn region() -> Rect {
b563d2da1c80atagen2026-10-16 08:25 UTC9
    Rect::new(G, G, 1180.0, 680.0)
b563d2da1c80atagen2026-10-16 08:25 UTC10
}
b563d2da1c80atagen2026-10-16 08:25 UTC11
b563d2da1c80atagen2026-10-16 08:25 UTC12
fn split(rect: Rect, ratios: &[f64], horizontal: bool) -> Vec<Rect> {
b563d2da1c80atagen2026-10-16 08:25 UTC13
    let total: f64 = ratios.iter().sum();
b563d2da1c80atagen2026-10-16 08:25 UTC14
    let len = if horizontal { rect.w } else { rect.h };
b563d2da1c80atagen2026-10-16 08:25 UTC15
    let avail = len - G * (ratios.len() - 1) as f64;
b563d2da1c80atagen2026-10-16 08:25 UTC16
    let mut pos = if horizontal { rect.x } else { rect.y };
b563d2da1c80atagen2026-10-16 08:25 UTC17
    ratios
b563d2da1c80atagen2026-10-16 08:25 UTC18
        .iter()
b563d2da1c80atagen2026-10-16 08:25 UTC19
        .map(|r| {
b563d2da1c80atagen2026-10-16 08:25 UTC20
            let size = avail * r / total;
b563d2da1c80atagen2026-10-16 08:25 UTC21
            let out = if horizontal {
b563d2da1c80atagen2026-10-16 08:25 UTC22
                Rect::new(pos, rect.y, size, rect.h)
b563d2da1c80atagen2026-10-16 08:25 UTC23
            } else {
b563d2da1c80atagen2026-10-16 08:25 UTC24
                Rect::new(rect.x, pos, rect.w, size)
b563d2da1c80atagen2026-10-16 08:25 UTC25
            };
b563d2da1c80atagen2026-10-16 08:25 UTC26
            pos += size + G;
b563d2da1c80atagen2026-10-16 08:25 UTC27
            out
b563d2da1c80atagen2026-10-16 08:25 UTC28
        })
b563d2da1c80atagen2026-10-16 08:25 UTC29
        .collect()
b563d2da1c80atagen2026-10-16 08:25 UTC30
}
b563d2da1c80atagen2026-10-16 08:25 UTC31
b563d2da1c80atagen2026-10-16 08:25 UTC32
fn cols(rect: Rect, n: usize) -> Vec<Rect> {
b563d2da1c80atagen2026-10-16 08:25 UTC33
    split(rect, &vec![1.0; n], true)
b563d2da1c80atagen2026-10-16 08:25 UTC34
}
b563d2da1c80atagen2026-10-16 08:25 UTC35
b563d2da1c80atagen2026-10-16 08:25 UTC36
fn rows(rect: Rect, n: usize) -> Vec<Rect> {
b563d2da1c80atagen2026-10-16 08:25 UTC37
    split(rect, &vec![1.0; n], false)
b563d2da1c80atagen2026-10-16 08:25 UTC38
}
b563d2da1c80atagen2026-10-16 08:25 UTC39
b563d2da1c80atagen2026-10-16 08:25 UTC40
fn cfg(style: Style) -> Config {
b563d2da1c80atagen2026-10-16 08:25 UTC41
    Config {
b563d2da1c80atagen2026-10-16 08:25 UTC42
        style,
b563d2da1c80atagen2026-10-16 08:25 UTC43
        ..Config::default()
b563d2da1c80atagen2026-10-16 08:25 UTC44
    }
b563d2da1c80atagen2026-10-16 08:25 UTC45
}
b563d2da1c80atagen2026-10-16 08:25 UTC46
b563d2da1c80atagen2026-10-16 08:25 UTC47
fn assert_clean(p: &Plan, c: &Config) {
b563d2da1c80atagen2026-10-16 08:25 UTC48
    let v = validate(p, c);
b563d2da1c80atagen2026-10-16 08:25 UTC49
    assert!(v.is_empty(), "violations {v:?} in {p:?}");
b563d2da1c80atagen2026-10-16 08:25 UTC50
    assert!(p.phases() <= 4, "{} phases", p.phases());
b563d2da1c80atagen2026-10-16 08:25 UTC51
    assert!(
b563d2da1c80atagen2026-10-16 08:25 UTC52
        p.groups.iter().all(|g| g.strategy != Strategy::Pinch),
b563d2da1c80atagen2026-10-16 08:25 UTC53
        "fell back to pinch: {p:?}"
b563d2da1c80atagen2026-10-16 08:25 UTC54
    );
b563d2da1c80atagen2026-10-16 08:25 UTC55
}
b563d2da1c80atagen2026-10-16 08:25 UTC56
b563d2da1c80atagen2026-10-16 08:25 UTC57
fn strategy(p: &Plan) -> Strategy {
b563d2da1c80atagen2026-10-16 08:25 UTC58
    p.groups
b563d2da1c80atagen2026-10-16 08:25 UTC59
        .iter()
b563d2da1c80atagen2026-10-16 08:25 UTC60
        .map(|g| g.strategy)
b563d2da1c80atagen2026-10-16 08:25 UTC61
        .find(|s| *s != Strategy::Unchanged)
b563d2da1c80atagen2026-10-16 08:25 UTC62
        .unwrap_or(Strategy::Unchanged)
b563d2da1c80atagen2026-10-16 08:25 UTC63
}
b563d2da1c80atagen2026-10-16 08:25 UTC64
b563d2da1c80atagen2026-10-16 08:25 UTC65
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC66
fn swap_uses_two_lanes() {
b563d2da1c80atagen2026-10-16 08:25 UTC67
    let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC68
    let ab = cols(region(), 2);
b563d2da1c80atagen2026-10-16 08:25 UTC69
    let ba = vec![ab[1], ab[0]];
b563d2da1c80atagen2026-10-16 08:25 UTC70
    let p = plan(&ab, &ba, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC71
    assert_clean(&p, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC72
    assert_eq!(strategy(&p), Strategy::Lanes { lanes: 2 });
b563d2da1c80atagen2026-10-16 08:25 UTC73
}
b563d2da1c80atagen2026-10-16 08:25 UTC74
b563d2da1c80atagen2026-10-16 08:25 UTC75
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC76
fn full_reversal_needs_one_lane_per_window() {
b563d2da1c80atagen2026-10-16 08:25 UTC77
    let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC78
    let a = cols(region(), 4);
b563d2da1c80atagen2026-10-16 08:25 UTC79
    let b: Vec<Rect> = (0..4).map(|i| a[3 - i]).collect();
b563d2da1c80atagen2026-10-16 08:25 UTC80
    let p = plan(&a, &b, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC81
    assert_clean(&p, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC82
    assert_eq!(strategy(&p), Strategy::Lanes { lanes: 4 });
b563d2da1c80atagen2026-10-16 08:25 UTC83
}
b563d2da1c80atagen2026-10-16 08:25 UTC84
b563d2da1c80atagen2026-10-16 08:25 UTC85
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC86
fn orientation_flip_is_two_phases() {
b563d2da1c80atagen2026-10-16 08:25 UTC87
    let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC88
    let p = plan(&cols(region(), 3), &rows(region(), 3), &c);
b563d2da1c80atagen2026-10-16 08:25 UTC89
    assert_clean(&p, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC90
    assert_eq!(strategy(&p), Strategy::AxisOrder);
b563d2da1c80atagen2026-10-16 08:25 UTC91
}
b563d2da1c80atagen2026-10-16 08:25 UTC92
b563d2da1c80atagen2026-10-16 08:25 UTC93
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC94
fn growing_both_axes_depends_on_style() {
b563d2da1c80atagen2026-10-16 08:25 UTC95
    let grid = |wide: bool| {
b563d2da1c80atagen2026-10-16 08:25 UTC96
        let r = if wide { [7.0, 3.0] } else { [1.0, 1.0] };
b563d2da1c80atagen2026-10-16 08:25 UTC97
        let rs = split(region(), &r, false);
b563d2da1c80atagen2026-10-16 08:25 UTC98
        let mut out = split(rs[0], &r, true);
b563d2da1c80atagen2026-10-16 08:25 UTC99
        out.extend(split(rs[1], &[1.0, 1.0], true));
b563d2da1c80atagen2026-10-16 08:25 UTC100
        out
b563d2da1c80atagen2026-10-16 08:25 UTC101
    };
b563d2da1c80atagen2026-10-16 08:25 UTC102
    let o = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC103
    let d = cfg(Style::Direct);
b563d2da1c80atagen2026-10-16 08:25 UTC104
    let po = plan(&grid(false), &grid(true), &o);
b563d2da1c80atagen2026-10-16 08:25 UTC105
    let pd = plan(&grid(false), &grid(true), &d);
b563d2da1c80atagen2026-10-16 08:25 UTC106
    assert_clean(&po, &o);
b563d2da1c80atagen2026-10-16 08:25 UTC107
    assert_clean(&pd, &d);
b563d2da1c80atagen2026-10-16 08:25 UTC108
    assert_eq!((po.phases(), pd.phases()), (2, 1));
b563d2da1c80atagen2026-10-16 08:25 UTC109
}
b563d2da1c80atagen2026-10-16 08:25 UTC110
b563d2da1c80atagen2026-10-16 08:25 UTC111
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC112
fn insert_and_split_rewinds() {
b563d2da1c80atagen2026-10-16 08:25 UTC113
    let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC114
    let layout = |on: bool| {
b563d2da1c80atagen2026-10-16 08:25 UTC115
        let n = if on { 4 } else { 3 };
b563d2da1c80atagen2026-10-16 08:25 UTC116
        let ratios: Vec<f64> = (0..n)
b563d2da1c80atagen2026-10-16 08:25 UTC117
            .map(|i| if i == 0 && on { 2.0 } else { 1.0 })
b563d2da1c80atagen2026-10-16 08:25 UTC118
            .collect();
b563d2da1c80atagen2026-10-16 08:25 UTC119
        let cs = split(region(), &ratios, true);
b563d2da1c80atagen2026-10-16 08:25 UTC120
        let (a, mut b, c3) = (cs[0], cs[1], cs[n - 1]);
b563d2da1c80atagen2026-10-16 08:25 UTC121
        let d = if on {
b563d2da1c80atagen2026-10-16 08:25 UTC122
            cs[2]
b563d2da1c80atagen2026-10-16 08:25 UTC123
        } else {
b563d2da1c80atagen2026-10-16 08:25 UTC124
            Rect::new((b.right() + c3.x) / 2.0, b.y, 0.0, b.h)
b563d2da1c80atagen2026-10-16 08:25 UTC125
        };
b563d2da1c80atagen2026-10-16 08:25 UTC126
        let e = if on {
b563d2da1c80atagen2026-10-16 08:25 UTC127
            let s = rows(b, 2);
b563d2da1c80atagen2026-10-16 08:25 UTC128
            b = s[0];
b563d2da1c80atagen2026-10-16 08:25 UTC129
            s[1]
b563d2da1c80atagen2026-10-16 08:25 UTC130
        } else {
b563d2da1c80atagen2026-10-16 08:25 UTC131
            Rect::new(b.x, b.bottom(), b.w, 0.0)
b563d2da1c80atagen2026-10-16 08:25 UTC132
        };
b563d2da1c80atagen2026-10-16 08:25 UTC133
        vec![a, b, c3, d, e]
b563d2da1c80atagen2026-10-16 08:25 UTC134
    };
b563d2da1c80atagen2026-10-16 08:25 UTC135
    let on = plan(&layout(false), &layout(true), &c);
b563d2da1c80atagen2026-10-16 08:25 UTC136
    let off = plan(&layout(true), &layout(false), &c);
b563d2da1c80atagen2026-10-16 08:25 UTC137
    assert_clean(&on, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC138
    assert_clean(&off, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC139
    assert!(on.groups.iter().all(|g| !g.opens_hole));
b563d2da1c80atagen2026-10-16 08:25 UTC140
    let mut rewound = off.keyframes.clone();
b563d2da1c80atagen2026-10-16 08:25 UTC141
    rewound.reverse();
b563d2da1c80atagen2026-10-16 08:25 UTC142
    assert_eq!(on.keyframes, rewound);
b563d2da1c80atagen2026-10-16 08:25 UTC143
}
b563d2da1c80atagen2026-10-16 08:25 UTC144
b563d2da1c80atagen2026-10-16 08:25 UTC145
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC146
fn inserting_a_seed_is_one_phase() {
b563d2da1c80atagen2026-10-16 08:25 UTC147
    let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC148
    let three = cols(region(), 3);
b563d2da1c80atagen2026-10-16 08:25 UTC149
    let four = cols(region(), 4);
b563d2da1c80atagen2026-10-16 08:25 UTC150
    let seed = Rect::new(
b563d2da1c80atagen2026-10-16 08:25 UTC151
        (three[1].right() + three[2].x) / 2.0,
b563d2da1c80atagen2026-10-16 08:25 UTC152
        three[1].y,
b563d2da1c80atagen2026-10-16 08:25 UTC153
        0.0,
b563d2da1c80atagen2026-10-16 08:25 UTC154
        three[1].h,
b563d2da1c80atagen2026-10-16 08:25 UTC155
    );
b563d2da1c80atagen2026-10-16 08:25 UTC156
    let from = vec![three[0], three[1], three[2], seed];
b563d2da1c80atagen2026-10-16 08:25 UTC157
    let to = vec![four[0], four[1], four[3], four[2]];
b563d2da1c80atagen2026-10-16 08:25 UTC158
    let p = plan(&from, &to, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC159
    assert_clean(&p, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC160
    assert_eq!(strategy(&p), Strategy::Direct);
b563d2da1c80atagen2026-10-16 08:25 UTC161
}
b563d2da1c80atagen2026-10-16 08:25 UTC162
b563d2da1c80atagen2026-10-16 08:25 UTC163
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC164
fn tag_rows_swap_rigidly_with_two_lanes() {
b563d2da1c80atagen2026-10-16 08:25 UTC165
    let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC166
    let layout = |first_top: bool| {
b563d2da1c80atagen2026-10-16 08:25 UTC167
        let rs = rows(region(), 2);
b563d2da1c80atagen2026-10-16 08:25 UTC168
        let (t1, t2) = if first_top {
b563d2da1c80atagen2026-10-16 08:25 UTC169
            (rs[0], rs[1])
b563d2da1c80atagen2026-10-16 08:25 UTC170
        } else {
b563d2da1c80atagen2026-10-16 08:25 UTC171
            (rs[1], rs[0])
b563d2da1c80atagen2026-10-16 08:25 UTC172
        };
b563d2da1c80atagen2026-10-16 08:25 UTC173
        let mut out = cols(t1, 3);
b563d2da1c80atagen2026-10-16 08:25 UTC174
        out.extend(cols(t2, 2));
b563d2da1c80atagen2026-10-16 08:25 UTC175
        out
b563d2da1c80atagen2026-10-16 08:25 UTC176
    };
b563d2da1c80atagen2026-10-16 08:25 UTC177
    let blocks = vec![vec![0, 1, 2], vec![3, 4]];
b563d2da1c80atagen2026-10-16 08:25 UTC178
    let p = plan_blocks(&layout(true), &layout(false), &blocks, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC179
    assert_clean(&p, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC180
    assert_eq!(strategy(&p), Strategy::Lanes { lanes: 2 });
b563d2da1c80atagen2026-10-16 08:25 UTC181
}
b563d2da1c80atagen2026-10-16 08:25 UTC182
b563d2da1c80atagen2026-10-16 08:25 UTC183
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC184
fn simultaneous_reversals_on_both_axes_use_the_grid() {
b563d2da1c80atagen2026-10-16 08:25 UTC185
    let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC186
    let halves = cols(region(), 2);
b563d2da1c80atagen2026-10-16 08:25 UTC187
    let (l, r) = (rows(halves[0], 2), rows(halves[1], 2));
b563d2da1c80atagen2026-10-16 08:25 UTC188
    let from = vec![l[0], r[1], r[0], l[1]];
b563d2da1c80atagen2026-10-16 08:25 UTC189
    let left3 = rows(halves[0], 3);
b563d2da1c80atagen2026-10-16 08:25 UTC190
    let to = vec![left3[0], left3[1], left3[2], halves[1]];
b563d2da1c80atagen2026-10-16 08:25 UTC191
    let p = plan(&from, &to, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC192
    assert_clean(&p, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC193
    assert!(matches!(strategy(&p), Strategy::Grid { .. }));
b563d2da1c80atagen2026-10-16 08:25 UTC194
}
b563d2da1c80atagen2026-10-16 08:25 UTC195
b563d2da1c80atagen2026-10-16 08:25 UTC196
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC197
fn distant_groups_plan_independently() {
b563d2da1c80atagen2026-10-16 08:25 UTC198
    let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC199
    let thirds = cols(region(), 3);
b563d2da1c80atagen2026-10-16 08:25 UTC200
    let l = rows(thirds[0], 2);
b563d2da1c80atagen2026-10-16 08:25 UTC201
    let from = vec![l[0], l[1], thirds[1], thirds[2]];
b563d2da1c80atagen2026-10-16 08:25 UTC202
    let to = vec![l[1], l[0], thirds[1], thirds[2]];
b563d2da1c80atagen2026-10-16 08:25 UTC203
    let p = plan(&from, &to, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC204
    assert_clean(&p, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC205
    assert!(
b563d2da1c80atagen2026-10-16 08:25 UTC206
        p.groups
b563d2da1c80atagen2026-10-16 08:25 UTC207
            .iter()
b563d2da1c80atagen2026-10-16 08:25 UTC208
            .any(|g| g.members == [3] && g.strategy == Strategy::Unchanged)
b563d2da1c80atagen2026-10-16 08:25 UTC209
    );
b563d2da1c80atagen2026-10-16 08:25 UTC210
}
b563d2da1c80atagen2026-10-16 08:25 UTC211
b563d2da1c80atagen2026-10-16 08:25 UTC212
struct Rng(u64);
b563d2da1c80atagen2026-10-16 08:25 UTC213
b563d2da1c80atagen2026-10-16 08:25 UTC214
impl Rng {
b563d2da1c80atagen2026-10-16 08:25 UTC215
    fn next(&mut self) -> u64 {
b563d2da1c80atagen2026-10-16 08:25 UTC216
        self.0 ^= self.0 << 13;
b563d2da1c80atagen2026-10-16 08:25 UTC217
        self.0 ^= self.0 >> 7;
b563d2da1c80atagen2026-10-16 08:25 UTC218
        self.0 ^= self.0 << 17;
b563d2da1c80atagen2026-10-16 08:25 UTC219
        self.0
b563d2da1c80atagen2026-10-16 08:25 UTC220
    }
b563d2da1c80atagen2026-10-16 08:25 UTC221
b563d2da1c80atagen2026-10-16 08:25 UTC222
    fn below(&mut self, n: usize) -> usize {
b563d2da1c80atagen2026-10-16 08:25 UTC223
        (self.next() % n as u64) as usize
b563d2da1c80atagen2026-10-16 08:25 UTC224
    }
b563d2da1c80atagen2026-10-16 08:25 UTC225
b563d2da1c80atagen2026-10-16 08:25 UTC226
    fn unit(&mut self) -> f64 {
b563d2da1c80atagen2026-10-16 08:25 UTC227
        (self.next() >> 11) as f64 / (1u64 << 53) as f64
b563d2da1c80atagen2026-10-16 08:25 UTC228
    }
b563d2da1c80atagen2026-10-16 08:25 UTC229
}
b563d2da1c80atagen2026-10-16 08:25 UTC230
b563d2da1c80atagen2026-10-16 08:25 UTC231
fn random_layout(rng: &mut Rng, n: usize) -> Vec<Rect> {
b563d2da1c80atagen2026-10-16 08:25 UTC232
    let mut ids: Vec<usize> = (0..n).collect();
b563d2da1c80atagen2026-10-16 08:25 UTC233
    for i in (1..n).rev() {
b563d2da1c80atagen2026-10-16 08:25 UTC234
        ids.swap(i, rng.below(i + 1));
b563d2da1c80atagen2026-10-16 08:25 UTC235
    }
b563d2da1c80atagen2026-10-16 08:25 UTC236
    let mut out = vec![Rect::default(); n];
b563d2da1c80atagen2026-10-16 08:25 UTC237
    let horizontal = rng.below(2) == 0;
b563d2da1c80atagen2026-10-16 08:25 UTC238
    build(rng, &ids, region(), horizontal, &mut out);
b563d2da1c80atagen2026-10-16 08:25 UTC239
    out
b563d2da1c80atagen2026-10-16 08:25 UTC240
}
b563d2da1c80atagen2026-10-16 08:25 UTC241
b563d2da1c80atagen2026-10-16 08:25 UTC242
fn build(rng: &mut Rng, ids: &[usize], rect: Rect, horizontal: bool, out: &mut [Rect]) {
b563d2da1c80atagen2026-10-16 08:25 UTC243
    if let [only] = ids {
b563d2da1c80atagen2026-10-16 08:25 UTC244
        out[*only] = rect;
b563d2da1c80atagen2026-10-16 08:25 UTC245
        return;
b563d2da1c80atagen2026-10-16 08:25 UTC246
    }
b563d2da1c80atagen2026-10-16 08:25 UTC247
    let k = ids.len().min(2 + rng.below(2));
b563d2da1c80atagen2026-10-16 08:25 UTC248
    let mut cuts = Vec::with_capacity(k);
b563d2da1c80atagen2026-10-16 08:25 UTC249
    let mut rest = ids;
b563d2da1c80atagen2026-10-16 08:25 UTC250
    for i in 0..k {
b563d2da1c80atagen2026-10-16 08:25 UTC251
        let take = if i == k - 1 {
b563d2da1c80atagen2026-10-16 08:25 UTC252
            rest.len()
b563d2da1c80atagen2026-10-16 08:25 UTC253
        } else {
b563d2da1c80atagen2026-10-16 08:25 UTC254
            1 + rng.below(rest.len() - (k - 1 - i))
b563d2da1c80atagen2026-10-16 08:25 UTC255
        };
b563d2da1c80atagen2026-10-16 08:25 UTC256
        let (head, tail) = rest.split_at(take);
b563d2da1c80atagen2026-10-16 08:25 UTC257
        cuts.push(head);
b563d2da1c80atagen2026-10-16 08:25 UTC258
        rest = tail;
b563d2da1c80atagen2026-10-16 08:25 UTC259
    }
b563d2da1c80atagen2026-10-16 08:25 UTC260
    let ratios: Vec<f64> = cuts.iter().map(|_| 0.5 + rng.unit()).collect();
b563d2da1c80atagen2026-10-16 08:25 UTC261
    for (part, ids) in split(rect, &ratios, horizontal).into_iter().zip(cuts) {
b563d2da1c80atagen2026-10-16 08:25 UTC262
        build(rng, ids, part, !horizontal, out);
b563d2da1c80atagen2026-10-16 08:25 UTC263
    }
b563d2da1c80atagen2026-10-16 08:25 UTC264
}
b563d2da1c80atagen2026-10-16 08:25 UTC265
b563d2da1c80atagen2026-10-16 08:25 UTC266
proptest! {
b563d2da1c80atagen2026-10-16 08:25 UTC267
    #![proptest_config(ProptestConfig::with_cases(512))]
b563d2da1c80atagen2026-10-16 08:25 UTC268
b563d2da1c80atagen2026-10-16 08:25 UTC269
    #[test]
b563d2da1c80atagen2026-10-16 08:25 UTC270
    fn random_transitions_plan_cleanly(seed in 1u64.., n in 2usize..9, direct: bool) {
b563d2da1c80atagen2026-10-16 08:25 UTC271
        let mut rng = Rng(seed);
b563d2da1c80atagen2026-10-16 08:25 UTC272
        let c = cfg(if direct { Style::Direct } else { Style::Orthogonal });
b563d2da1c80atagen2026-10-16 08:25 UTC273
        let (a, b) = (random_layout(&mut rng, n), random_layout(&mut rng, n));
b563d2da1c80atagen2026-10-16 08:25 UTC274
        let p = plan(&a, &b, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC275
        assert_clean(&p, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC276
        prop_assert_eq!(&p.keyframes[0], &a);
b563d2da1c80atagen2026-10-16 08:25 UTC277
        prop_assert_eq!(p.keyframes.last().unwrap(), &b);
b563d2da1c80atagen2026-10-16 08:25 UTC278
    }
b563d2da1c80atagen2026-10-16 08:25 UTC279
b563d2da1c80atagen2026-10-16 08:25 UTC280
    #[test]
b563d2da1c80atagen2026-10-16 08:25 UTC281
    fn planning_back_rewinds(seed in 1u64.., n in 2usize..9) {
b563d2da1c80atagen2026-10-16 08:25 UTC282
        let mut rng = Rng(seed);
b563d2da1c80atagen2026-10-16 08:25 UTC283
        let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC284
        let (a, b) = (random_layout(&mut rng, n), random_layout(&mut rng, n));
b563d2da1c80atagen2026-10-16 08:25 UTC285
        let mut back = plan_canonical(&b, &a, &c).keys;
b563d2da1c80atagen2026-10-16 08:25 UTC286
        back.reverse();
b563d2da1c80atagen2026-10-16 08:25 UTC287
        prop_assert_eq!(plan_canonical(&a, &b, &c).keys, back);
b563d2da1c80atagen2026-10-16 08:25 UTC288
    }
b563d2da1c80atagen2026-10-16 08:25 UTC289
b563d2da1c80atagen2026-10-16 08:25 UTC290
    #[test]
b563d2da1c80atagen2026-10-16 08:25 UTC291
    fn interrupts_replan_cleanly(seed in 1u64.., n in 2usize..9, at in 0.0f64..1.0, s in 0.0f64..1.0) {
b563d2da1c80atagen2026-10-16 08:25 UTC292
        let mut rng = Rng(seed);
b563d2da1c80atagen2026-10-16 08:25 UTC293
        let c = cfg(Style::Orthogonal);
b563d2da1c80atagen2026-10-16 08:25 UTC294
        let (a, b, d) = (random_layout(&mut rng, n), random_layout(&mut rng, n), random_layout(&mut rng, n));
b563d2da1c80atagen2026-10-16 08:25 UTC295
        let first = plan(&a, &b, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC296
        if first.phases() == 0 {
b563d2da1c80atagen2026-10-16 08:25 UTC297
            return Ok(());
b563d2da1c80atagen2026-10-16 08:25 UTC298
        }
b563d2da1c80atagen2026-10-16 08:25 UTC299
        let phase = ((at * first.phases() as f64) as usize).min(first.phases() - 1);
b563d2da1c80atagen2026-10-16 08:25 UTC300
        let mid = first.sample(phase, s);
b563d2da1c80atagen2026-10-16 08:25 UTC301
        let second = plan(&mid, &d, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC302
        assert_clean(&second, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC303
        prop_assert_eq!(&second.keyframes[0], &mid);
b563d2da1c80atagen2026-10-16 08:25 UTC304
    }
b563d2da1c80atagen2026-10-16 08:25 UTC305
}
b563d2da1c80atagen2026-10-16 08:25 UTC306
b563d2da1c80atagen2026-10-16 08:25 UTC307
#[test]
b563d2da1c80atagen2026-10-16 08:25 UTC308
#[ignore = "statistics, run with --ignored --nocapture"]
b563d2da1c80atagen2026-10-16 08:25 UTC309
fn strategy_distribution() {
b563d2da1c80atagen2026-10-16 08:25 UTC310
    use std::collections::BTreeMap;
b563d2da1c80atagen2026-10-16 08:25 UTC311
    for style in [Style::Orthogonal, Style::Direct] {
b563d2da1c80atagen2026-10-16 08:25 UTC312
        let c = cfg(style);
b563d2da1c80atagen2026-10-16 08:25 UTC313
        let mut rng = Rng(0x9e37_79b9_7f4a_7c15);
b563d2da1c80atagen2026-10-16 08:25 UTC314
        let mut counts: BTreeMap<String, usize> = BTreeMap::new();
b563d2da1c80atagen2026-10-16 08:25 UTC315
        let mut holes = 0;
b563d2da1c80atagen2026-10-16 08:25 UTC316
        for _ in 0..20_000 {
b563d2da1c80atagen2026-10-16 08:25 UTC317
            let n = 2 + rng.below(7);
b563d2da1c80atagen2026-10-16 08:25 UTC318
            let (a, b) = (random_layout(&mut rng, n), random_layout(&mut rng, n));
b563d2da1c80atagen2026-10-16 08:25 UTC319
            let p = plan(&a, &b, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC320
            assert_clean(&p, &c);
b563d2da1c80atagen2026-10-16 08:25 UTC321
            holes += p.groups.iter().filter(|g| g.opens_hole).count();
b563d2da1c80atagen2026-10-16 08:25 UTC322
            let key = match strategy(&p) {
b563d2da1c80atagen2026-10-16 08:25 UTC323
                Strategy::Lanes { .. } => "Lanes".to_string(),
b563d2da1c80atagen2026-10-16 08:25 UTC324
                Strategy::Grid { .. } => "Grid".to_string(),
b563d2da1c80atagen2026-10-16 08:25 UTC325
                s => format!("{s:?}"),
b563d2da1c80atagen2026-10-16 08:25 UTC326
            };
b563d2da1c80atagen2026-10-16 08:25 UTC327
            *counts.entry(key).or_default() += 1;
b563d2da1c80atagen2026-10-16 08:25 UTC328
        }
b563d2da1c80atagen2026-10-16 08:25 UTC329
        println!("{style:?}: {counts:?}, plans opening a hole: {holes}");
b563d2da1c80atagen2026-10-16 08:25 UTC330
    }
b563d2da1c80atagen2026-10-16 08:25 UTC331
}