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
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:
- horizontal swaps shrink, move, then grow without overlap

View file

@ -110,7 +110,8 @@ fn plan_forward(request: &MultiphaseRequest) -> Option<MultiphasePlan> {
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(
@ -178,12 +179,14 @@ fn plan_axis_crossing_lanes(
)
}
fn plan_horizontal_space_then_vertical_growth(
fn plan_space_then_orthogonal_growth(
request: &MultiphaseRequest,
axis: PhaseAxis,
) -> Option<MultiphasePlan> {
if request.windows.len() < 2 {
return None;
}
let orth_axis = axis.other();
let min_width = sane_min_size(request.bounds.width());
let min_height = sane_min_size(request.bounds.height());
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 {
return None;
}
let x_changes = window.from.x1() != window.to.x1() || window.from.x2() != window.to.x2();
let y_changes = window.from.y1() != window.to.y1() || window.from.y2() != window.to.y2();
if x_changes && window.from.width() == window.to.width() {
let after_move = Rect::new_sized_saturating(
window.to.x1(),
window.from.y1(),
window.to.width(),
window.from.height(),
let main_changes = main_start(window.from, axis) != main_start(window.to, axis)
|| main_end(window.from, axis) != main_end(window.to, axis);
let orth_changes = orth_start(window.from, axis) != orth_start(window.to, axis)
|| orth_end(window.from, axis) != orth_end(window.to, axis);
if main_changes && main_size(window.from, axis) == main_size(window.to, axis) {
let after_move = with_main_interval(
window.from,
axis,
main_start(window.to, axis),
main_end(window.to, axis),
);
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);
}
} else if x_changes {
let after_x_scale = Rect::new_sized_saturating(
window.to.x1(),
window.from.y1(),
window.to.width(),
window.from.height(),
} else if main_changes {
let after_main_scale = with_main_interval(
window.from,
axis,
main_start(window.to, axis),
main_end(window.to, axis),
);
push_step(&mut phase1, window.node_id, window.from, after_x_scale);
if y_changes {
push_step(&mut phase3, window.node_id, after_x_scale, window.to);
push_step(&mut phase1, window.node_id, window.from, after_main_scale);
if orth_changes {
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);
}
}
@ -227,9 +232,9 @@ fn plan_horizontal_space_then_vertical_growth(
build_validated_plan(
request,
[
(PhaseKind::Scale, PhaseAxis::Horizontal, phase1),
(PhaseKind::Move, PhaseAxis::Horizontal, phase2),
(PhaseKind::Scale, PhaseAxis::Vertical, phase3),
(PhaseKind::Scale, axis, phase1),
(PhaseKind::Move, axis, phase2),
(PhaseKind::Scale, orth_axis, phase3),
],
)
}
@ -444,10 +449,12 @@ mod tests {
}
fn request(windows: Vec<MultiphaseWindow>) -> MultiphaseRequest {
MultiphaseRequest {
bounds: rect(0, 0, 400, 100),
windows,
}
let bounds = windows
.iter()
.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> {
@ -596,6 +603,91 @@ mod tests {
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]
fn unsupported_diagonal_motion_falls_back_to_linear() {
let req = request(vec![MultiphaseWindow {