From 6c133018aafa9d8af2526a5efefaffa58b4b93f3 Mon Sep 17 00:00:00 2001 From: atagen Date: Wed, 27 May 2026 13:36:46 +1000 Subject: [PATCH] Handle uneven swap lanes and clearance grouping --- src/animation/multiphase.rs | 136 +++++++++++++++++++++++++++++++++--- src/state.rs | 2 +- 2 files changed, 127 insertions(+), 11 deletions(-) diff --git a/src/animation/multiphase.rs b/src/animation/multiphase.rs index 74fde909..31c406b5 100644 --- a/src/animation/multiphase.rs +++ b/src/animation/multiphase.rs @@ -448,7 +448,11 @@ pub(crate) fn validate_phase_paths( .map_err(MultiphasePlanFailure::Validation) } -pub(crate) fn partition_motion_groups(windows: &[MultiphaseWindow]) -> Vec> { +pub(crate) fn partition_motion_groups( + windows: &[MultiphaseWindow], + clearance: i32, +) -> Vec> { + let clearance = clearance.max(0); let mut groups = vec![]; let mut seen = vec![false; windows.len()]; for start in 0..windows.len() { @@ -460,9 +464,11 @@ pub(crate) fn partition_motion_groups(windows: &[MultiphaseWindow]) -> Vec 0 { @@ -875,9 +881,11 @@ fn plan_axis_crossing_lanes( main_start(window.to, axis), main_end(window.to, axis), ); + let lane_move = crossing_lane_move_rect(lane_from, window.to, axis); push_step(&mut phase1, window.node_id, window.from, lane_from); - push_step(&mut phase2, window.node_id, lane_from, lane_to); - push_step(&mut phase3, window.node_id, lane_to, window.to); + push_step(&mut phase2, window.node_id, lane_from, lane_move); + push_step(&mut phase3, window.node_id, lane_move, lane_to); + push_step(&mut phase4, window.node_id, lane_to, window.to); if idx + 1 < windows.len() { lane_start = lane_end + clearance; } @@ -902,14 +910,31 @@ fn plan_axis_crossing_lanes( ), phase_draft( PhaseKind::Scale, - axis.other(), + axis, phase3, + PhaseReason::SameAxisRedistribution, + ), + phase_draft( + PhaseKind::Scale, + axis.other(), + phase4, PhaseReason::GrowOutOfLanes, ), ], ) } +fn crossing_lane_move_rect(from: Rect, target: Rect, axis: PhaseAxis) -> Rect { + let size = main_size(from, axis); + if main_start(target, axis) > main_start(from, axis) { + let end = main_end(target, axis); + with_main_interval(from, axis, end - size, end) + } else { + let start = main_start(target, axis); + with_main_interval(from, axis, start, start + size) + } +} + fn lane_index_for_direction(window: MultiphaseWindow, axis: PhaseAxis) -> Option { let delta = main_start(window.to, axis) - main_start(window.from, axis); match delta.cmp(&0) { @@ -1535,6 +1560,16 @@ fn motion_bounds(window: MultiphaseWindow) -> Rect { window.from.union(window.to) } +fn motion_bounds_with_clearance(window: MultiphaseWindow, clearance: i32) -> Rect { + let bounds = motion_bounds(window); + Rect::new_saturating( + bounds.x1().saturating_sub(clearance), + bounds.y1().saturating_sub(clearance), + bounds.x2().saturating_add(clearance), + bounds.y2().saturating_add(clearance), + ) +} + fn interval_changed(from: Rect, to: Rect, axis: PhaseAxis) -> bool { main_start(from, axis) != main_start(to, axis) || main_end(from, axis) != main_end(to, axis) } @@ -2055,6 +2090,67 @@ mod tests { assert!(validate_plan_continuous(&req, &planned.plan)); } + #[test] + fn uneven_swap_lanes_split_move_and_same_axis_scale() { + let req = request(vec![ + window(1, rect(0, 0, 101, 100), rect(101, 0, 201, 100)), + window(2, rect(101, 0, 201, 100), rect(0, 0, 101, 100)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + let plan = &planned.plan; + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert_eq!( + actions(plan), + vec![ + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + PhaseAction { + kind: PhaseKind::Move, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Horizontal, + }, + PhaseAction { + kind: PhaseKind::Scale, + axis: PhaseAxis::Vertical, + }, + ] + ); + assert_eq!(step_to(plan, 1, id(1)), rect(100, 0, 201, 50)); + assert_eq!(step_to(plan, 1, id(2)), rect(0, 50, 100, 100)); + assert_eq!(step_to(plan, 2, id(1)), rect(101, 0, 201, 50)); + assert_eq!(step_to(plan, 2, id(2)), rect(0, 50, 101, 100)); + assert!(validate_plan_continuous(&req, plan)); + } + + #[test] + fn uneven_swap_lanes_tolerate_stationary_sibling_in_odd_layout() { + let req = request(vec![ + window(1, rect(0, 0, 101, 100), rect(101, 0, 201, 100)), + window(2, rect(101, 0, 201, 100), rect(0, 0, 101, 100)), + window(3, rect(201, 0, 300, 100), rect(201, 0, 300, 100)), + ]); + let planned = plan_no_overlap_explained(&req).unwrap(); + + assert_eq!( + planned.explanation.strategy, + PlanStrategy::SwapLanes { + axis: PhaseAxis::Horizontal, + } + ); + assert!(validate_plan_continuous(&req, &planned.plan)); + } + #[test] fn vertical_swap_lanes_follow_motion_direction_not_node_id() { let req = request(vec![ @@ -3118,7 +3214,7 @@ mod tests { hierarchy: Default::default(), }, ]; - assert_eq!(partition_motion_groups(&windows), vec![vec![0, 1], vec![2]]); + assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1], vec![2]]); } #[test] @@ -3143,6 +3239,26 @@ mod tests { hierarchy: Default::default(), }, ]; - assert_eq!(partition_motion_groups(&windows), vec![vec![0, 1, 2]]); + assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0, 1, 2]]); + } + + #[test] + fn motion_groups_join_across_animation_clearance() { + let windows = vec![ + MultiphaseWindow { + node_id: id(1), + from: rect(0, 0, 100, 100), + to: rect(0, 0, 80, 100), + hierarchy: Default::default(), + }, + MultiphaseWindow { + node_id: id(2), + from: rect(120, 0, 220, 100), + to: rect(110, 0, 210, 100), + hierarchy: Default::default(), + }, + ]; + assert_eq!(partition_motion_groups(&windows, 0), vec![vec![0], vec![1]]); + assert_eq!(partition_motion_groups(&windows, 10), vec![vec![0, 1]]); } } diff --git a/src/state.rs b/src/state.rs index c0dbb837..b32cfd30 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1640,7 +1640,7 @@ impl State { ) }) .collect(); - for group in partition_motion_groups(&windows) { + for group in partition_motion_groups(&windows, self.layout_animation_clearance()) { if self.start_multiphase_layout_animation(&candidates, &windows, &group, now) { continue; }