1
0
Fork 0
forked from wry/wry

Exercise planner with generated split trees

This commit is contained in:
atagen 2026-05-21 20:21:07 +10:00
parent cc898590d2
commit a770089b88
2 changed files with 329 additions and 0 deletions

View file

@ -219,6 +219,12 @@ pub fn plan_no_overlap_with_diagnostics(
{
return Ok(MultiphasePlan { phases: vec![] });
}
if let Some(failure) = target_shrink_bound_failure(request) {
return Err(MultiphasePlanDiagnostic {
forward: failure,
reverse: None,
});
}
let forward = match plan_forward(request) {
Ok(plan) => return Ok(plan),
Err(error) => error,
@ -283,8 +289,37 @@ fn validate_request(request: &MultiphaseRequest) -> Result<(), MultiphaseError>
Ok(())
}
fn target_shrink_bound_failure(request: &MultiphaseRequest) -> Option<MultiphasePlanFailure> {
let min_width = sane_min_size(request.bounds.width());
let min_height = sane_min_size(request.bounds.height());
for window in &request.windows {
if window.to.width() < min_width {
return Some(MultiphasePlanFailure::ShrinkBound {
axis: PhaseAxis::Horizontal,
available: window.to.width(),
required: min_width,
});
}
if window.to.height() < min_height {
return Some(MultiphasePlanFailure::ShrinkBound {
axis: PhaseAxis::Vertical,
available: window.to.height(),
required: min_height,
});
}
}
None
}
fn plan_forward(request: &MultiphaseRequest) -> Result<MultiphasePlan, MultiphasePlanFailure> {
let mut rejection = None;
match plan_single_action_phase(request) {
Ok(plan) => return Ok(plan),
Err(MultiphasePlanFailure::NoPattern) => {}
Err(error) => {
rejection.get_or_insert(error);
}
}
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
match plan_axis_crossing_lanes(request, axis) {
Ok(plan) => return Ok(plan),
@ -306,6 +341,48 @@ fn plan_forward(request: &MultiphaseRequest) -> Result<MultiphasePlan, Multiphas
Err(rejection.unwrap_or(MultiphasePlanFailure::NoPattern))
}
fn plan_single_action_phase(
request: &MultiphaseRequest,
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
let mut action = None;
let mut steps = vec![];
for window in &request.windows {
if window.from == window.to {
continue;
}
let step = MultiphaseStep {
node_id: window.node_id,
from: window.from,
to: window.to,
};
let Some(step_action) = classify_step(step) else {
return Err(MultiphasePlanFailure::NoPattern);
};
if step_action.kind == PhaseKind::Scale {
let (available, required) = match step_action.axis {
PhaseAxis::Horizontal => (window.to.width(), sane_min_size(request.bounds.width())),
PhaseAxis::Vertical => (window.to.height(), sane_min_size(request.bounds.height())),
};
if available < required {
return Err(MultiphasePlanFailure::ShrinkBound {
axis: step_action.axis,
available,
required,
});
}
}
if action.is_some_and(|action| action != step_action) {
return Err(MultiphasePlanFailure::NoPattern);
}
action = Some(step_action);
steps.push(step);
}
let Some(action) = action else {
return Err(MultiphasePlanFailure::NoPattern);
};
build_validated_plan(request, [(action.kind, action.axis, steps)])
}
fn plan_axis_crossing_lanes(
request: &MultiphaseRequest,
axis: PhaseAxis,
@ -842,6 +919,154 @@ mod tests {
MultiphaseWindow::new(id(raw), from, to)
}
#[derive(Clone)]
enum TestTree {
Leaf(u32),
Split {
id: u32,
axis: PhaseAxis,
weights: Vec<i32>,
children: Vec<TestTree>,
},
}
struct TestLeaf {
node_id: NodeId,
rect: Rect,
hierarchy: MultiphaseHierarchyPosition,
}
fn leaf(raw: u32) -> TestTree {
TestTree::Leaf(raw)
}
fn split(id: u32, axis: PhaseAxis, weights: &[i32], children: Vec<TestTree>) -> TestTree {
TestTree::Split {
id,
axis,
weights: weights.to_vec(),
children,
}
}
fn layout_tree(tree: &TestTree, bounds: Rect) -> Vec<TestLeaf> {
let mut leaves = vec![];
layout_tree_inner(tree, bounds, None, 0, None, None, &mut leaves);
leaves.sort_by_key(|leaf| leaf.node_id.0);
leaves
}
fn layout_tree_inner(
tree: &TestTree,
bounds: Rect,
parent: Option<NodeId>,
depth: u16,
sibling_index: Option<u16>,
split_axis: Option<PhaseAxis>,
leaves: &mut Vec<TestLeaf>,
) {
match tree {
TestTree::Leaf(raw) => leaves.push(TestLeaf {
node_id: id(*raw),
rect: bounds,
hierarchy: MultiphaseHierarchyPosition {
parent,
depth,
sibling_index,
split_axis,
..Default::default()
},
}),
TestTree::Split {
id: split_id,
axis,
weights,
children,
} => {
assert_eq!(weights.len(), children.len());
let rects = split_rect_by_weights(bounds, *axis, weights);
for (idx, (child, rect)) in children.iter().zip(rects).enumerate() {
layout_tree_inner(
child,
rect,
Some(id(*split_id)),
depth.saturating_add(1),
Some(idx.min(u16::MAX as usize) as u16),
Some(*axis),
leaves,
);
}
}
}
}
fn split_rect_by_weights(bounds: Rect, axis: PhaseAxis, weights: &[i32]) -> Vec<Rect> {
let total_weight: i32 = weights.iter().sum();
assert!(total_weight > 0);
let total_size = match axis {
PhaseAxis::Horizontal => bounds.width(),
PhaseAxis::Vertical => bounds.height(),
};
let mut pos = match axis {
PhaseAxis::Horizontal => bounds.x1(),
PhaseAxis::Vertical => bounds.y1(),
};
let mut remaining_size = total_size;
let mut remaining_weight = total_weight;
let mut rects = vec![];
for (idx, weight) in weights.iter().enumerate() {
let size = if idx + 1 == weights.len() {
remaining_size
} else {
total_size * *weight / total_weight
};
let rect = match axis {
PhaseAxis::Horizontal => {
Rect::new_sized_saturating(pos, bounds.y1(), size, bounds.height())
}
PhaseAxis::Vertical => {
Rect::new_sized_saturating(bounds.x1(), pos, bounds.width(), size)
}
};
rects.push(rect);
pos += size;
remaining_size -= size;
remaining_weight -= *weight;
if remaining_weight == 0 {
assert_eq!(remaining_size, 0);
}
}
rects
}
fn generated_request(old: &TestTree, new: &TestTree, bounds: Rect) -> MultiphaseRequest {
let old_leaves = layout_tree(old, bounds);
let new_leaves = layout_tree(new, bounds);
assert_eq!(old_leaves.len(), new_leaves.len());
let mut windows = vec![];
for old_leaf in &old_leaves {
let new_leaf = new_leaves
.iter()
.find(|leaf| leaf.node_id == old_leaf.node_id)
.unwrap();
windows.push(MultiphaseWindow::with_hierarchy(
old_leaf.node_id,
old_leaf.rect,
new_leaf.rect,
MultiphaseWindowHierarchy::new(old_leaf.hierarchy, new_leaf.hierarchy),
));
}
MultiphaseRequest { bounds, windows }
}
fn assert_generated_case_plans(old: &TestTree, new: &TestTree, bounds: Rect) {
let req = generated_request(old, new, bounds);
assert!(!overlaps(req.windows.iter().map(|window| window.from)));
assert!(!overlaps(req.windows.iter().map(|window| window.to)));
let plan = plan_no_overlap(&req).unwrap();
assert!(validate_plan_continuous(&req, &plan));
}
fn request(windows: Vec<MultiphaseWindow>) -> MultiphaseRequest {
let bounds = windows
.iter()
@ -964,6 +1189,105 @@ mod tests {
assert!(validate_plan_continuous(&req, &plan));
}
#[test]
fn generated_sibling_swaps_plan_for_both_axes() {
let bounds = rect(0, 0, 240, 240);
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
let old = split(10, axis, &[1, 1], vec![leaf(1), leaf(2)]);
let new = split(10, axis, &[1, 1], vec![leaf(2), leaf(1)]);
assert_generated_case_plans(&old, &new, bounds);
}
}
#[test]
fn generated_size_redistributions_plan_as_single_axis_scale() {
let horizontal_old = split(
10,
PhaseAxis::Horizontal,
&[1, 1, 1],
vec![leaf(1), leaf(2), leaf(3)],
);
let horizontal_new = split(
10,
PhaseAxis::Horizontal,
&[1, 2, 1],
vec![leaf(1), leaf(2), leaf(3)],
);
let horizontal_req =
generated_request(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100));
let horizontal_plan = plan_no_overlap(&horizontal_req).unwrap();
assert_eq!(
actions(&horizontal_plan),
vec![PhaseAction {
kind: PhaseKind::Scale,
axis: PhaseAxis::Horizontal,
}]
);
assert!(validate_plan_continuous(&horizontal_req, &horizontal_plan));
let vertical_old = split(
10,
PhaseAxis::Vertical,
&[1, 1, 1],
vec![leaf(1), leaf(2), leaf(3)],
);
let vertical_new = split(
10,
PhaseAxis::Vertical,
&[1, 2, 1],
vec![leaf(1), leaf(2), leaf(3)],
);
let vertical_req = generated_request(&vertical_old, &vertical_new, rect(0, 0, 100, 400));
let vertical_plan = plan_no_overlap(&vertical_req).unwrap();
assert_eq!(
actions(&vertical_plan),
vec![PhaseAction {
kind: PhaseKind::Scale,
axis: PhaseAxis::Vertical,
}]
);
assert!(validate_plan_continuous(&vertical_req, &vertical_plan));
}
#[test]
fn generated_stack_extractions_plan_for_both_axes_and_directions() {
let horizontal_old = split(
10,
PhaseAxis::Horizontal,
&[1, 1],
vec![
leaf(1),
split(11, PhaseAxis::Vertical, &[1, 1], vec![leaf(2), leaf(3)]),
],
);
let horizontal_new = split(
10,
PhaseAxis::Horizontal,
&[1, 2, 1],
vec![leaf(1), leaf(2), leaf(3)],
);
assert_generated_case_plans(&horizontal_old, &horizontal_new, rect(0, 0, 400, 100));
assert_generated_case_plans(&horizontal_new, &horizontal_old, rect(0, 0, 400, 100));
let vertical_old = split(
20,
PhaseAxis::Vertical,
&[1, 1],
vec![
leaf(1),
split(21, PhaseAxis::Horizontal, &[1, 1], vec![leaf(2), leaf(3)]),
],
);
let vertical_new = split(
20,
PhaseAxis::Vertical,
&[1, 2, 1],
vec![leaf(1), leaf(2), leaf(3)],
);
assert_generated_case_plans(&vertical_old, &vertical_new, rect(0, 0, 100, 400));
assert_generated_case_plans(&vertical_new, &vertical_old, rect(0, 0, 100, 400));
}
#[test]
fn stack_extraction_creates_space_before_moving_child() {
let req = request(vec![