1
0
Fork 0
forked from wry/wry

Mirror stack extraction multiphase planning

This commit is contained in:
atagen 2026-05-21 18:42:45 +10:00
parent 13722429b4
commit a516b2e721
2 changed files with 128 additions and 28 deletions

View file

@ -214,6 +214,14 @@ Preferred approach:
- If a legal no-overlap sequence cannot be found for a group, fall back to the - If a legal no-overlap sequence cannot be found for a group, fall back to the
linear animator for that group only. Float windows are outside this invariant. linear animator for that group only. Float windows are outside this invariant.
Current pure planner status:
- Two-window same-axis swaps use shrink lanes, move, then grow.
- Stack extraction/return patterns are covered in both horizontal and vertical
orientations: peer/container space scales first, the extracted child moves
only after space exists, and orthogonal growth happens in the final phase.
- Every produced plan is sampled for overlap at each phase before it is accepted.
Tests: Tests:
- horizontal swaps shrink, move, then grow without overlap - horizontal swaps shrink, move, then grow without overlap

View file

@ -110,7 +110,8 @@ fn plan_forward(request: &MultiphaseRequest) -> Option<MultiphasePlan> {
return Some(plan); return Some(plan);
} }
} }
plan_horizontal_space_then_vertical_growth(request) plan_space_then_orthogonal_growth(request, PhaseAxis::Horizontal)
.or_else(|| plan_space_then_orthogonal_growth(request, PhaseAxis::Vertical))
} }
fn plan_axis_crossing_lanes( fn plan_axis_crossing_lanes(
@ -178,12 +179,14 @@ fn plan_axis_crossing_lanes(
) )
} }
fn plan_horizontal_space_then_vertical_growth( fn plan_space_then_orthogonal_growth(
request: &MultiphaseRequest, request: &MultiphaseRequest,
axis: PhaseAxis,
) -> Option<MultiphasePlan> { ) -> Option<MultiphasePlan> {
if request.windows.len() < 2 { if request.windows.len() < 2 {
return None; return None;
} }
let orth_axis = axis.other();
let min_width = sane_min_size(request.bounds.width()); let min_width = sane_min_size(request.bounds.width());
let min_height = sane_min_size(request.bounds.height()); let min_height = sane_min_size(request.bounds.height());
let mut phase1 = vec![]; let mut phase1 = vec![];
@ -193,31 +196,33 @@ fn plan_horizontal_space_then_vertical_growth(
if window.to.width() < min_width || window.to.height() < min_height { if window.to.width() < min_width || window.to.height() < min_height {
return None; return None;
} }
let x_changes = window.from.x1() != window.to.x1() || window.from.x2() != window.to.x2(); let main_changes = main_start(window.from, axis) != main_start(window.to, axis)
let y_changes = window.from.y1() != window.to.y1() || window.from.y2() != window.to.y2(); || main_end(window.from, axis) != main_end(window.to, axis);
if x_changes && window.from.width() == window.to.width() { let orth_changes = orth_start(window.from, axis) != orth_start(window.to, axis)
let after_move = Rect::new_sized_saturating( || orth_end(window.from, axis) != orth_end(window.to, axis);
window.to.x1(), if main_changes && main_size(window.from, axis) == main_size(window.to, axis) {
window.from.y1(), let after_move = with_main_interval(
window.to.width(), window.from,
window.from.height(), axis,
main_start(window.to, axis),
main_end(window.to, axis),
); );
push_step(&mut phase2, window.node_id, window.from, after_move); push_step(&mut phase2, window.node_id, window.from, after_move);
if y_changes { if orth_changes {
push_step(&mut phase3, window.node_id, after_move, window.to); push_step(&mut phase3, window.node_id, after_move, window.to);
} }
} else if x_changes { } else if main_changes {
let after_x_scale = Rect::new_sized_saturating( let after_main_scale = with_main_interval(
window.to.x1(), window.from,
window.from.y1(), axis,
window.to.width(), main_start(window.to, axis),
window.from.height(), main_end(window.to, axis),
); );
push_step(&mut phase1, window.node_id, window.from, after_x_scale); push_step(&mut phase1, window.node_id, window.from, after_main_scale);
if y_changes { if orth_changes {
push_step(&mut phase3, window.node_id, after_x_scale, window.to); push_step(&mut phase3, window.node_id, after_main_scale, window.to);
} }
} else if y_changes { } else if orth_changes {
push_step(&mut phase3, window.node_id, window.from, window.to); push_step(&mut phase3, window.node_id, window.from, window.to);
} }
} }
@ -227,9 +232,9 @@ fn plan_horizontal_space_then_vertical_growth(
build_validated_plan( build_validated_plan(
request, request,
[ [
(PhaseKind::Scale, PhaseAxis::Horizontal, phase1), (PhaseKind::Scale, axis, phase1),
(PhaseKind::Move, PhaseAxis::Horizontal, phase2), (PhaseKind::Move, axis, phase2),
(PhaseKind::Scale, PhaseAxis::Vertical, phase3), (PhaseKind::Scale, orth_axis, phase3),
], ],
) )
} }
@ -444,10 +449,12 @@ mod tests {
} }
fn request(windows: Vec<MultiphaseWindow>) -> MultiphaseRequest { fn request(windows: Vec<MultiphaseWindow>) -> MultiphaseRequest {
MultiphaseRequest { let bounds = windows
bounds: rect(0, 0, 400, 100), .iter()
windows, .map(|window| window.from.union(window.to))
} .reduce(|bounds, rect| bounds.union(rect))
.unwrap_or_else(|| rect(0, 0, 1, 1));
MultiphaseRequest { bounds, windows }
} }
fn actions(plan: &MultiphasePlan) -> Vec<PhaseAction> { fn actions(plan: &MultiphasePlan) -> Vec<PhaseAction> {
@ -596,6 +603,91 @@ mod tests {
assert!(validate_plan_samples(&req, &plan)); assert!(validate_plan_samples(&req, &plan));
} }
#[test]
fn vertical_stack_extraction_creates_space_before_moving_child() {
let req = request(vec![
MultiphaseWindow {
node_id: id(1),
from: rect(0, 0, 100, 200),
to: rect(0, 0, 100, 100),
},
MultiphaseWindow {
node_id: id(2),
from: rect(0, 200, 50, 400),
to: rect(0, 100, 100, 300),
},
MultiphaseWindow {
node_id: id(3),
from: rect(50, 200, 100, 400),
to: rect(0, 300, 100, 400),
},
]);
let plan = plan_no_overlap(&req).unwrap();
assert_eq!(
actions(&plan),
vec![
PhaseAction {
kind: PhaseKind::Scale,
axis: PhaseAxis::Vertical
},
PhaseAction {
kind: PhaseKind::Move,
axis: PhaseAxis::Vertical
},
PhaseAction {
kind: PhaseKind::Scale,
axis: PhaseAxis::Horizontal
},
]
);
assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 100));
assert_eq!(plan.phases[0].steps[1].to, rect(50, 300, 100, 400));
assert_eq!(plan.phases[1].steps[0].to, rect(0, 100, 50, 300));
assert_eq!(plan.phases[2].steps[0].to, rect(0, 100, 100, 300));
assert_eq!(plan.phases[2].steps[1].to, rect(0, 300, 100, 400));
assert!(validate_plan_samples(&req, &plan));
}
#[test]
fn vertical_stack_extraction_reverse_replays_phases_in_reverse() {
let req = request(vec![
MultiphaseWindow {
node_id: id(1),
from: rect(0, 0, 100, 100),
to: rect(0, 0, 100, 200),
},
MultiphaseWindow {
node_id: id(2),
from: rect(0, 100, 100, 300),
to: rect(0, 200, 50, 400),
},
MultiphaseWindow {
node_id: id(3),
from: rect(0, 300, 100, 400),
to: rect(50, 200, 100, 400),
},
]);
let plan = plan_no_overlap(&req).unwrap();
assert_eq!(
actions(&plan),
vec![
PhaseAction {
kind: PhaseKind::Scale,
axis: PhaseAxis::Horizontal
},
PhaseAction {
kind: PhaseKind::Move,
axis: PhaseAxis::Vertical
},
PhaseAction {
kind: PhaseKind::Scale,
axis: PhaseAxis::Vertical
},
]
);
assert!(validate_plan_samples(&req, &plan));
}
#[test] #[test]
fn unsupported_diagonal_motion_falls_back_to_linear() { fn unsupported_diagonal_motion_falls_back_to_linear() {
let req = request(vec![MultiphaseWindow { let req = request(vec![MultiphaseWindow {