Exercise planner with generated split trees
This commit is contained in:
parent
cc898590d2
commit
a770089b88
2 changed files with 329 additions and 0 deletions
|
|
@ -223,6 +223,8 @@ Current pure planner status:
|
|||
- 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.
|
||||
- Same-axis size redistribution is handled as a single scale phase when the
|
||||
exact validator proves adjacent windows stay non-overlapping.
|
||||
- Every produced plan is checked analytically for overlap over the full duration
|
||||
of each phase before it is accepted. This solves the linear edge inequalities
|
||||
for each pair of moving rectangles instead of relying on sampled frames.
|
||||
|
|
@ -237,6 +239,9 @@ Current pure planner status:
|
|||
It distinguishes request validation errors, missing patterns, shrink-bound
|
||||
rejections, invalid phase steps, and exact validation failures such as stale
|
||||
starts or phase overlap.
|
||||
- Planner tests now include a deterministic split-tree generator. It builds
|
||||
valid weighted tiling layouts, derives leaf hierarchy metadata, mutates them
|
||||
through supported transitions, and runs the real planner plus exact validator.
|
||||
|
||||
Tests:
|
||||
|
||||
|
|
|
|||
|
|
@ -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![
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue