Explain multiphase planning decisions
This commit is contained in:
parent
0b6da9d8e0
commit
511e188d16
2 changed files with 398 additions and 62 deletions
|
|
@ -242,6 +242,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.
|
||||
- Multiphase planning also has an explained-plan entry point. Accepted plans
|
||||
report the deterministic strategy, phase reasons, participating nodes, and
|
||||
validation result; rejected plans report every attempted strategy and failure.
|
||||
- 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.
|
||||
|
|
@ -254,6 +257,7 @@ Tests:
|
|||
- nested containers do not produce simultaneous cross-axis motion
|
||||
- interruption restarts only affected phase groups
|
||||
- reversing direction produces equivalent motion in reverse
|
||||
- accepted and rejected plans expose deterministic strategy explanations
|
||||
- child waits for parent/container-space phases when moving upward into a
|
||||
toplevel peer position
|
||||
- mono-mode tab switches do not animate, while entering/exiting mono can animate
|
||||
|
|
|
|||
|
|
@ -120,6 +120,75 @@ pub struct MultiphasePlan {
|
|||
pub phases: Vec<MultiphasePhase>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MultiphasePlanned {
|
||||
pub plan: MultiphasePlan,
|
||||
pub explanation: MultiphasePlanExplanation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MultiphasePlanExplanation {
|
||||
pub strategy: PlanStrategy,
|
||||
pub phases: Vec<PhaseExplanation>,
|
||||
pub validation: ValidationExplanation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct PhaseExplanation {
|
||||
pub action: PhaseAction,
|
||||
pub reason: PhaseReason,
|
||||
pub nodes: Vec<NodeId>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ValidationExplanation {
|
||||
pub continuous_overlap_passed: bool,
|
||||
pub final_rects_matched: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PlanStrategy {
|
||||
NoOp,
|
||||
SingleAction,
|
||||
HierarchyOrderedScales,
|
||||
SwapLanes { axis: PhaseAxis },
|
||||
SpaceThenOrthogonalGrowth { axis: PhaseAxis },
|
||||
ReversedForwardPlan { original: Box<PlanStrategy> },
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PlanDirection {
|
||||
Forward,
|
||||
Reverse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct RejectedStrategy {
|
||||
pub direction: PlanDirection,
|
||||
pub strategy: PlanStrategy,
|
||||
pub reason: MultiphasePlanFailure,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum PhaseReason {
|
||||
SingleAction,
|
||||
SameAxisRedistribution,
|
||||
ShrinkIntoLanes {
|
||||
lane_axis: PhaseAxis,
|
||||
},
|
||||
MoveThroughFreedSpace,
|
||||
GrowOutOfLanes,
|
||||
CreateSpaceForAscendingChild,
|
||||
MoveAscendingChildAfterSpaceExists,
|
||||
OrthogonalGrowthAfterMove,
|
||||
ParentAxisBeforeChildAxis {
|
||||
parent_axis: PhaseAxis,
|
||||
parent_depth: u16,
|
||||
child_axis: PhaseAxis,
|
||||
child_depth: u16,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MultiphasePhase {
|
||||
pub action: PhaseAction,
|
||||
|
|
@ -161,10 +230,11 @@ pub enum MultiphaseError {
|
|||
NoPlan,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct MultiphasePlanDiagnostic {
|
||||
pub forward: MultiphasePlanFailure,
|
||||
pub reverse: Option<MultiphasePlanFailure>,
|
||||
pub attempted: Vec<RejectedStrategy>,
|
||||
}
|
||||
|
||||
impl MultiphasePlanDiagnostic {
|
||||
|
|
@ -176,6 +246,15 @@ impl MultiphasePlanDiagnostic {
|
|||
}
|
||||
}
|
||||
|
||||
impl ValidationExplanation {
|
||||
fn passed() -> Self {
|
||||
Self {
|
||||
continuous_overlap_passed: true,
|
||||
final_rects_matched: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum MultiphasePlanFailure {
|
||||
Request(MultiphaseError),
|
||||
|
|
@ -201,6 +280,12 @@ pub enum MultiphaseValidationError {
|
|||
FinalMismatch { node_id: NodeId },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct PlanForwardFailure {
|
||||
reason: MultiphasePlanFailure,
|
||||
attempted: Vec<RejectedStrategy>,
|
||||
}
|
||||
|
||||
pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result<MultiphasePlan, MultiphaseError> {
|
||||
plan_no_overlap_with_diagnostics(request).map_err(|diagnostic| diagnostic.legacy_error())
|
||||
}
|
||||
|
|
@ -208,10 +293,17 @@ pub fn plan_no_overlap(request: &MultiphaseRequest) -> Result<MultiphasePlan, Mu
|
|||
pub fn plan_no_overlap_with_diagnostics(
|
||||
request: &MultiphaseRequest,
|
||||
) -> Result<MultiphasePlan, MultiphasePlanDiagnostic> {
|
||||
plan_no_overlap_explained(request).map(|planned| planned.plan)
|
||||
}
|
||||
|
||||
pub fn plan_no_overlap_explained(
|
||||
request: &MultiphaseRequest,
|
||||
) -> Result<MultiphasePlanned, MultiphasePlanDiagnostic> {
|
||||
if let Err(error) = validate_request(request) {
|
||||
return Err(MultiphasePlanDiagnostic {
|
||||
forward: MultiphasePlanFailure::Request(error),
|
||||
reverse: None,
|
||||
attempted: vec![],
|
||||
});
|
||||
}
|
||||
if request
|
||||
|
|
@ -219,25 +311,38 @@ pub fn plan_no_overlap_with_diagnostics(
|
|||
.iter()
|
||||
.all(|window| window.from == window.to)
|
||||
{
|
||||
return Ok(MultiphasePlan { phases: vec![] });
|
||||
return Ok(MultiphasePlanned {
|
||||
plan: MultiphasePlan { phases: vec![] },
|
||||
explanation: MultiphasePlanExplanation {
|
||||
strategy: PlanStrategy::NoOp,
|
||||
phases: vec![],
|
||||
validation: ValidationExplanation::passed(),
|
||||
},
|
||||
});
|
||||
}
|
||||
if let Some(failure) = target_shrink_bound_failure(request) {
|
||||
return Err(MultiphasePlanDiagnostic {
|
||||
forward: failure,
|
||||
reverse: None,
|
||||
attempted: vec![],
|
||||
});
|
||||
}
|
||||
let forward = match plan_forward(request) {
|
||||
let forward = match plan_forward(request, PlanDirection::Forward) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
Err(error) => error,
|
||||
};
|
||||
let reversed = reverse_request(request);
|
||||
match plan_forward(&reversed) {
|
||||
Ok(plan) => Ok(reverse_plan(plan)),
|
||||
Err(reverse) => Err(MultiphasePlanDiagnostic {
|
||||
forward,
|
||||
reverse: Some(reverse),
|
||||
}),
|
||||
match plan_forward(&reversed, PlanDirection::Reverse) {
|
||||
Ok(plan) => Ok(reverse_planned(plan)),
|
||||
Err(reverse) => {
|
||||
let mut attempted = forward.attempted;
|
||||
attempted.extend(reverse.attempted);
|
||||
Err(MultiphasePlanDiagnostic {
|
||||
forward: forward.reason,
|
||||
reverse: Some(reverse.reason),
|
||||
attempted,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -313,46 +418,89 @@ fn target_shrink_bound_failure(request: &MultiphaseRequest) -> Option<Multiphase
|
|||
None
|
||||
}
|
||||
|
||||
fn plan_forward(request: &MultiphaseRequest) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
||||
fn plan_forward(
|
||||
request: &MultiphaseRequest,
|
||||
direction: PlanDirection,
|
||||
) -> Result<MultiphasePlanned, PlanForwardFailure> {
|
||||
let mut rejection = None;
|
||||
let mut attempted = vec![];
|
||||
match plan_single_action_phase(request) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
Err(MultiphasePlanFailure::NoPattern) => {}
|
||||
Err(error) => {
|
||||
rejection.get_or_insert(error);
|
||||
record_rejection(&mut attempted, direction, PlanStrategy::SingleAction, error);
|
||||
if error != MultiphasePlanFailure::NoPattern {
|
||||
rejection.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
match plan_hierarchy_ordered_axis_scales(request) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
Err(MultiphasePlanFailure::NoPattern) => {}
|
||||
Err(error) => {
|
||||
rejection.get_or_insert(error);
|
||||
record_rejection(
|
||||
&mut attempted,
|
||||
direction,
|
||||
PlanStrategy::HierarchyOrderedScales,
|
||||
error,
|
||||
);
|
||||
if error != MultiphasePlanFailure::NoPattern {
|
||||
rejection.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
||||
match plan_axis_crossing_lanes(request, axis) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
Err(MultiphasePlanFailure::NoPattern) => {}
|
||||
Err(error) => {
|
||||
rejection.get_or_insert(error);
|
||||
record_rejection(
|
||||
&mut attempted,
|
||||
direction,
|
||||
PlanStrategy::SwapLanes { axis },
|
||||
error,
|
||||
);
|
||||
if error != MultiphasePlanFailure::NoPattern {
|
||||
rejection.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
||||
match plan_space_then_orthogonal_growth(request, axis) {
|
||||
Ok(plan) => return Ok(plan),
|
||||
Err(MultiphasePlanFailure::NoPattern) => {}
|
||||
Err(error) => {
|
||||
rejection.get_or_insert(error);
|
||||
record_rejection(
|
||||
&mut attempted,
|
||||
direction,
|
||||
PlanStrategy::SpaceThenOrthogonalGrowth { axis },
|
||||
error,
|
||||
);
|
||||
if error != MultiphasePlanFailure::NoPattern {
|
||||
rejection.get_or_insert(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(rejection.unwrap_or(MultiphasePlanFailure::NoPattern))
|
||||
Err(PlanForwardFailure {
|
||||
reason: rejection.unwrap_or(MultiphasePlanFailure::NoPattern),
|
||||
attempted,
|
||||
})
|
||||
}
|
||||
|
||||
fn record_rejection(
|
||||
attempted: &mut Vec<RejectedStrategy>,
|
||||
direction: PlanDirection,
|
||||
strategy: PlanStrategy,
|
||||
reason: MultiphasePlanFailure,
|
||||
) {
|
||||
attempted.push(RejectedStrategy {
|
||||
direction,
|
||||
strategy,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
|
||||
fn plan_single_action_phase(
|
||||
request: &MultiphaseRequest,
|
||||
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
||||
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
||||
let mut action = None;
|
||||
let mut steps = vec![];
|
||||
for window in &request.windows {
|
||||
|
|
@ -389,12 +537,21 @@ fn plan_single_action_phase(
|
|||
let Some(action) = action else {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
};
|
||||
build_validated_plan(request, [(action.kind, action.axis, steps)])
|
||||
build_validated_plan(
|
||||
request,
|
||||
PlanStrategy::SingleAction,
|
||||
[phase_draft(
|
||||
action.kind,
|
||||
action.axis,
|
||||
steps,
|
||||
single_action_reason(action),
|
||||
)],
|
||||
)
|
||||
}
|
||||
|
||||
fn plan_hierarchy_ordered_axis_scales(
|
||||
request: &MultiphaseRequest,
|
||||
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
||||
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
||||
let mut changed_axes = vec![];
|
||||
for axis in [PhaseAxis::Horizontal, PhaseAxis::Vertical] {
|
||||
if request
|
||||
|
|
@ -408,7 +565,7 @@ fn plan_hierarchy_ordered_axis_scales(
|
|||
let [first_axis, second_axis] = changed_axes
|
||||
.try_into()
|
||||
.map_err(|_| MultiphasePlanFailure::NoPattern)?;
|
||||
let axes = hierarchy_scale_axis_order(request, first_axis, second_axis)
|
||||
let order = hierarchy_scale_axis_order(request, first_axis, second_axis)
|
||||
.ok_or(MultiphasePlanFailure::NoPattern)?;
|
||||
let mut current: Vec<_> = request
|
||||
.windows
|
||||
|
|
@ -416,7 +573,13 @@ fn plan_hierarchy_ordered_axis_scales(
|
|||
.map(|window| (window.node_id, window.from))
|
||||
.collect();
|
||||
let mut phases = vec![];
|
||||
for axis in axes {
|
||||
let reason = PhaseReason::ParentAxisBeforeChildAxis {
|
||||
parent_axis: order.axes[0],
|
||||
parent_depth: order.depths[0],
|
||||
child_axis: order.axes[1],
|
||||
child_depth: order.depths[1],
|
||||
};
|
||||
for axis in order.axes {
|
||||
let mut steps = vec![];
|
||||
for window in &request.windows {
|
||||
let (_, rect) = current
|
||||
|
|
@ -445,28 +608,44 @@ fn plan_hierarchy_ordered_axis_scales(
|
|||
if steps.is_empty() {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
phases.push((PhaseKind::Scale, axis, steps));
|
||||
phases.push(phase_draft(PhaseKind::Scale, axis, steps, reason));
|
||||
}
|
||||
let [first, second] = phases
|
||||
.try_into()
|
||||
.map_err(|_| MultiphasePlanFailure::NoPattern)?;
|
||||
build_validated_plan(request, [first, second])
|
||||
build_validated_plan(
|
||||
request,
|
||||
PlanStrategy::HierarchyOrderedScales,
|
||||
[first, second],
|
||||
)
|
||||
}
|
||||
|
||||
fn hierarchy_scale_axis_order(
|
||||
request: &MultiphaseRequest,
|
||||
first_axis: PhaseAxis,
|
||||
second_axis: PhaseAxis,
|
||||
) -> Option<[PhaseAxis; 2]> {
|
||||
) -> Option<HierarchyScaleAxisOrder> {
|
||||
let first_priority = hierarchy_axis_priority(request, first_axis)?;
|
||||
let second_priority = hierarchy_axis_priority(request, second_axis)?;
|
||||
match first_priority.cmp(&second_priority) {
|
||||
std::cmp::Ordering::Less => Some([first_axis, second_axis]),
|
||||
std::cmp::Ordering::Greater => Some([second_axis, first_axis]),
|
||||
std::cmp::Ordering::Less => Some(HierarchyScaleAxisOrder {
|
||||
axes: [first_axis, second_axis],
|
||||
depths: [first_priority, second_priority],
|
||||
}),
|
||||
std::cmp::Ordering::Greater => Some(HierarchyScaleAxisOrder {
|
||||
axes: [second_axis, first_axis],
|
||||
depths: [second_priority, first_priority],
|
||||
}),
|
||||
std::cmp::Ordering::Equal => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
struct HierarchyScaleAxisOrder {
|
||||
axes: [PhaseAxis; 2],
|
||||
depths: [u16; 2],
|
||||
}
|
||||
|
||||
fn hierarchy_axis_priority(request: &MultiphaseRequest, axis: PhaseAxis) -> Option<u16> {
|
||||
request
|
||||
.windows
|
||||
|
|
@ -485,7 +664,7 @@ fn hierarchy_axis_priority(request: &MultiphaseRequest, axis: PhaseAxis) -> Opti
|
|||
fn plan_axis_crossing_lanes(
|
||||
request: &MultiphaseRequest,
|
||||
axis: PhaseAxis,
|
||||
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
||||
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
||||
if request.windows.len() != 2 {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
|
|
@ -551,10 +730,28 @@ fn plan_axis_crossing_lanes(
|
|||
}
|
||||
build_validated_plan(
|
||||
request,
|
||||
PlanStrategy::SwapLanes { axis },
|
||||
[
|
||||
(PhaseKind::Scale, axis.other(), phase1),
|
||||
(PhaseKind::Move, axis, phase2),
|
||||
(PhaseKind::Scale, axis.other(), phase3),
|
||||
phase_draft(
|
||||
PhaseKind::Scale,
|
||||
axis.other(),
|
||||
phase1,
|
||||
PhaseReason::ShrinkIntoLanes {
|
||||
lane_axis: axis.other(),
|
||||
},
|
||||
),
|
||||
phase_draft(
|
||||
PhaseKind::Move,
|
||||
axis,
|
||||
phase2,
|
||||
PhaseReason::MoveThroughFreedSpace,
|
||||
),
|
||||
phase_draft(
|
||||
PhaseKind::Scale,
|
||||
axis.other(),
|
||||
phase3,
|
||||
PhaseReason::GrowOutOfLanes,
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
|
@ -571,7 +768,7 @@ fn lane_index_for_direction(window: MultiphaseWindow, axis: PhaseAxis) -> Option
|
|||
fn plan_space_then_orthogonal_growth(
|
||||
request: &MultiphaseRequest,
|
||||
axis: PhaseAxis,
|
||||
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
||||
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
||||
if request.windows.len() < 2 {
|
||||
return Err(MultiphasePlanFailure::NoPattern);
|
||||
}
|
||||
|
|
@ -631,24 +828,71 @@ fn plan_space_then_orthogonal_growth(
|
|||
}
|
||||
build_validated_plan(
|
||||
request,
|
||||
PlanStrategy::SpaceThenOrthogonalGrowth { axis },
|
||||
[
|
||||
(PhaseKind::Scale, axis, phase1),
|
||||
(PhaseKind::Move, axis, phase2),
|
||||
(PhaseKind::Scale, orth_axis, phase3),
|
||||
phase_draft(
|
||||
PhaseKind::Scale,
|
||||
axis,
|
||||
phase1,
|
||||
PhaseReason::CreateSpaceForAscendingChild,
|
||||
),
|
||||
phase_draft(
|
||||
PhaseKind::Move,
|
||||
axis,
|
||||
phase2,
|
||||
PhaseReason::MoveAscendingChildAfterSpaceExists,
|
||||
),
|
||||
phase_draft(
|
||||
PhaseKind::Scale,
|
||||
orth_axis,
|
||||
phase3,
|
||||
PhaseReason::OrthogonalGrowthAfterMove,
|
||||
),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
struct MultiphasePhaseDraft {
|
||||
action: PhaseAction,
|
||||
steps: Vec<MultiphaseStep>,
|
||||
reason: PhaseReason,
|
||||
}
|
||||
|
||||
fn phase_draft(
|
||||
kind: PhaseKind,
|
||||
axis: PhaseAxis,
|
||||
steps: Vec<MultiphaseStep>,
|
||||
reason: PhaseReason,
|
||||
) -> MultiphasePhaseDraft {
|
||||
MultiphasePhaseDraft {
|
||||
action: PhaseAction { kind, axis },
|
||||
steps,
|
||||
reason,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_validated_plan<const N: usize>(
|
||||
request: &MultiphaseRequest,
|
||||
phases: [(PhaseKind, PhaseAxis, Vec<MultiphaseStep>); N],
|
||||
) -> Result<MultiphasePlan, MultiphasePlanFailure> {
|
||||
strategy: PlanStrategy,
|
||||
phases: [MultiphasePhaseDraft; N],
|
||||
) -> Result<MultiphasePlanned, MultiphasePlanFailure> {
|
||||
let mut explanations = vec![];
|
||||
let phases: Vec<_> = phases
|
||||
.into_iter()
|
||||
.filter_map(|(kind, axis, steps)| {
|
||||
(!steps.is_empty()).then_some(MultiphasePhase {
|
||||
action: PhaseAction { kind, axis },
|
||||
steps,
|
||||
.filter_map(|draft| {
|
||||
if draft.steps.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut nodes: Vec<_> = draft.steps.iter().map(|step| step.node_id).collect();
|
||||
nodes.sort_by_key(|node_id| node_id.0);
|
||||
explanations.push(PhaseExplanation {
|
||||
action: draft.action,
|
||||
reason: draft.reason,
|
||||
nodes,
|
||||
});
|
||||
Some(MultiphasePhase {
|
||||
action: draft.action,
|
||||
steps: draft.steps,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -664,7 +908,14 @@ fn build_validated_plan<const N: usize>(
|
|||
}
|
||||
let plan = MultiphasePlan { phases };
|
||||
validate_plan_continuous_diagnostic(request, &plan)
|
||||
.map(|_| plan)
|
||||
.map(|_| MultiphasePlanned {
|
||||
plan,
|
||||
explanation: MultiphasePlanExplanation {
|
||||
strategy,
|
||||
phases: explanations,
|
||||
validation: ValidationExplanation::passed(),
|
||||
},
|
||||
})
|
||||
.map_err(MultiphasePlanFailure::Validation)
|
||||
}
|
||||
|
||||
|
|
@ -894,6 +1145,13 @@ fn classify_step(step: MultiphaseStep) -> Option<PhaseAction> {
|
|||
}
|
||||
}
|
||||
|
||||
fn single_action_reason(action: PhaseAction) -> PhaseReason {
|
||||
match action.kind {
|
||||
PhaseKind::Move => PhaseReason::SingleAction,
|
||||
PhaseKind::Scale => PhaseReason::SameAxisRedistribution,
|
||||
}
|
||||
}
|
||||
|
||||
fn reverse_request(request: &MultiphaseRequest) -> MultiphaseRequest {
|
||||
MultiphaseRequest {
|
||||
bounds: request.bounds,
|
||||
|
|
@ -932,6 +1190,21 @@ fn reverse_plan(plan: MultiphasePlan) -> MultiphasePlan {
|
|||
}
|
||||
}
|
||||
|
||||
fn reverse_planned(planned: MultiphasePlanned) -> MultiphasePlanned {
|
||||
let mut phases = planned.explanation.phases;
|
||||
phases.reverse();
|
||||
MultiphasePlanned {
|
||||
plan: reverse_plan(planned.plan),
|
||||
explanation: MultiphasePlanExplanation {
|
||||
strategy: PlanStrategy::ReversedForwardPlan {
|
||||
original: Box::new(planned.explanation.strategy),
|
||||
},
|
||||
phases,
|
||||
validation: planned.explanation.validation,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn overlaps(rects: impl IntoIterator<Item = Rect>) -> bool {
|
||||
let rects: Vec<_> = rects.into_iter().collect();
|
||||
for (idx, rect) in rects.iter().enumerate() {
|
||||
|
|
@ -1240,9 +1513,10 @@ mod tests {
|
|||
hierarchy: Default::default(),
|
||||
},
|
||||
]);
|
||||
let plan = plan_no_overlap(&req).unwrap();
|
||||
let planned = plan_no_overlap_explained(&req).unwrap();
|
||||
let plan = &planned.plan;
|
||||
assert_eq!(
|
||||
actions(&plan),
|
||||
actions(plan),
|
||||
vec![
|
||||
PhaseAction {
|
||||
kind: PhaseKind::Scale,
|
||||
|
|
@ -1260,7 +1534,29 @@ mod tests {
|
|||
);
|
||||
assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 50));
|
||||
assert_eq!(plan.phases[0].steps[1].to, rect(100, 50, 200, 100));
|
||||
assert!(validate_plan_continuous(&req, &plan));
|
||||
assert_eq!(
|
||||
planned.explanation.strategy,
|
||||
PlanStrategy::SwapLanes {
|
||||
axis: PhaseAxis::Horizontal
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
planned
|
||||
.explanation
|
||||
.phases
|
||||
.iter()
|
||||
.map(|phase| phase.reason)
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
PhaseReason::ShrinkIntoLanes {
|
||||
lane_axis: PhaseAxis::Vertical
|
||||
},
|
||||
PhaseReason::MoveThroughFreedSpace,
|
||||
PhaseReason::GrowOutOfLanes,
|
||||
]
|
||||
);
|
||||
assert_eq!(planned.explanation.phases[0].nodes, vec![id(1), id(2)]);
|
||||
assert!(validate_plan_continuous(&req, plan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1410,9 +1706,10 @@ mod tests {
|
|||
],
|
||||
);
|
||||
let req = generated_request(&old, &new, rect(0, 0, 400, 100));
|
||||
let plan = plan_no_overlap(&req).unwrap();
|
||||
let planned = plan_no_overlap_explained(&req).unwrap();
|
||||
let plan = &planned.plan;
|
||||
assert_eq!(
|
||||
actions(&plan),
|
||||
actions(plan),
|
||||
vec![
|
||||
PhaseAction {
|
||||
kind: PhaseKind::Scale,
|
||||
|
|
@ -1424,10 +1721,32 @@ mod tests {
|
|||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(step_to(&plan, 0, id(1)), rect(0, 0, 100, 100));
|
||||
assert_eq!(step_to(&plan, 0, id(2)), rect(100, 0, 400, 50));
|
||||
assert_eq!(step_to(&plan, 0, id(3)), rect(100, 50, 400, 100));
|
||||
assert!(validate_plan_continuous(&req, &plan));
|
||||
assert_eq!(step_to(plan, 0, id(1)), rect(0, 0, 100, 100));
|
||||
assert_eq!(step_to(plan, 0, id(2)), rect(100, 0, 400, 50));
|
||||
assert_eq!(step_to(plan, 0, id(3)), rect(100, 50, 400, 100));
|
||||
assert_eq!(
|
||||
planned.explanation.strategy,
|
||||
PlanStrategy::HierarchyOrderedScales
|
||||
);
|
||||
assert_eq!(
|
||||
planned.explanation.phases[0].reason,
|
||||
PhaseReason::ParentAxisBeforeChildAxis {
|
||||
parent_axis: PhaseAxis::Horizontal,
|
||||
parent_depth: 1,
|
||||
child_axis: PhaseAxis::Vertical,
|
||||
child_depth: 2,
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
planned.explanation.phases[0].nodes,
|
||||
vec![id(1), id(2), id(3)]
|
||||
);
|
||||
assert_eq!(planned.explanation.phases[1].nodes, vec![id(2), id(3)]);
|
||||
assert_eq!(
|
||||
planned.explanation.validation,
|
||||
ValidationExplanation::passed()
|
||||
);
|
||||
assert!(validate_plan_continuous(&req, plan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1549,9 +1868,10 @@ mod tests {
|
|||
hierarchy: Default::default(),
|
||||
},
|
||||
]);
|
||||
let plan = plan_no_overlap(&req).unwrap();
|
||||
let planned = plan_no_overlap_explained(&req).unwrap();
|
||||
let plan = &planned.plan;
|
||||
assert_eq!(
|
||||
actions(&plan),
|
||||
actions(plan),
|
||||
vec![
|
||||
PhaseAction {
|
||||
kind: PhaseKind::Scale,
|
||||
|
|
@ -1567,7 +1887,15 @@ mod tests {
|
|||
},
|
||||
]
|
||||
);
|
||||
assert!(validate_plan_continuous(&req, &plan));
|
||||
assert_eq!(
|
||||
planned.explanation.strategy,
|
||||
PlanStrategy::ReversedForwardPlan {
|
||||
original: Box::new(PlanStrategy::SpaceThenOrthogonalGrowth {
|
||||
axis: PhaseAxis::Horizontal
|
||||
})
|
||||
}
|
||||
);
|
||||
assert!(validate_plan_continuous(&req, plan));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -1670,12 +1998,16 @@ mod tests {
|
|||
hierarchy: Default::default(),
|
||||
}]);
|
||||
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
|
||||
assert_eq!(
|
||||
plan_no_overlap_with_diagnostics(&req).unwrap_err(),
|
||||
MultiphasePlanDiagnostic {
|
||||
forward: MultiphasePlanFailure::NoPattern,
|
||||
reverse: Some(MultiphasePlanFailure::NoPattern),
|
||||
}
|
||||
let diagnostic = plan_no_overlap_with_diagnostics(&req).unwrap_err();
|
||||
assert_eq!(diagnostic.forward, MultiphasePlanFailure::NoPattern);
|
||||
assert_eq!(diagnostic.reverse, Some(MultiphasePlanFailure::NoPattern));
|
||||
assert!(
|
||||
diagnostic
|
||||
.attempted
|
||||
.iter()
|
||||
.any(|attempt| attempt.direction == PlanDirection::Forward
|
||||
&& attempt.strategy == PlanStrategy::SingleAction
|
||||
&& attempt.reason == MultiphasePlanFailure::NoPattern)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue