1
0
Fork 0
forked from wry/wry

Validate multiphase overlap analytically

This commit is contained in:
atagen 2026-05-21 19:43:39 +10:00
parent 4ee2c324e1
commit b109cdf6f2
2 changed files with 244 additions and 21 deletions

View file

@ -220,7 +220,9 @@ Current pure planner status:
- Stack extraction/return patterns are covered in both horizontal and vertical - Stack extraction/return patterns are covered in both horizontal and vertical
orientations: peer/container space scales first, the extracted child moves orientations: peer/container space scales first, the extracted child moves
only after space exists, and orthogonal growth happens in the final phase. 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. - 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.
- Live layout batches are partitioned by overlapping motion bounds, so unrelated - Live layout batches are partitioned by overlapping motion bounds, so unrelated
groups can still use multiphase animation when another group falls back to groups can still use multiphase animation when another group falls back to
linear motion. linear motion.

View file

@ -1,7 +1,6 @@
use {crate::rect::Rect, crate::tree::NodeId}; use {crate::rect::Rect, crate::tree::NodeId};
const MIN_SHRINK_DENOMINATOR: i32 = 4; const MIN_SHRINK_DENOMINATOR: i32 = 4;
const PHASE_VALIDATION_SAMPLES: [f64; 5] = [0.0, 0.25, 0.5, 0.75, 1.0];
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct MultiphaseRequest { pub struct MultiphaseRequest {
@ -288,10 +287,10 @@ fn build_validated_plan<const N: usize>(
return None; return None;
} }
let plan = MultiphasePlan { phases }; let plan = MultiphasePlan { phases };
validate_plan_samples(request, &plan).then_some(plan) validate_plan_continuous(request, &plan).then_some(plan)
} }
fn validate_plan_samples(request: &MultiphaseRequest, plan: &MultiphasePlan) -> bool { fn validate_plan_continuous(request: &MultiphaseRequest, plan: &MultiphasePlan) -> bool {
let mut current: Vec<_> = request let mut current: Vec<_> = request
.windows .windows
.iter() .iter()
@ -301,26 +300,46 @@ fn validate_plan_samples(request: &MultiphaseRequest, plan: &MultiphasePlan) ->
return false; return false;
} }
for phase in &plan.phases { for phase in &plan.phases {
for t in PHASE_VALIDATION_SAMPLES { for (idx, step) in phase.steps.iter().enumerate() {
let rects = current.iter().map(|(node_id, rect)| { if phase.steps[..idx]
phase .iter()
.any(|prev| prev.node_id == step.node_id)
{
return false;
}
let Some((_, rect)) = current.iter().find(|(node_id, _)| *node_id == step.node_id)
else {
return false;
};
if *rect != step.from {
return false;
}
}
let motions: Vec<_> = current
.iter()
.map(|(node_id, rect)| {
let to = phase
.steps .steps
.iter() .iter()
.find(|step| step.node_id == *node_id) .find(|step| step.node_id == *node_id)
.map(|step| super::lerp_rect(step.from, step.to, t)) .map(|step| step.to)
.unwrap_or(*rect) .unwrap_or(*rect);
}); RectMotion { from: *rect, to }
if overlaps(rects) { })
.collect();
for (idx, motion) in motions.iter().enumerate() {
if motions[idx + 1..]
.iter()
.any(|other| motions_overlap_during_phase(*motion, *other))
{
return false; return false;
} }
} }
for step in &phase.steps { for step in &phase.steps {
let Some((_, rect)) = current let (_, rect) = current
.iter_mut() .iter_mut()
.find(|(node_id, _)| *node_id == step.node_id) .find(|(node_id, _)| *node_id == step.node_id)
else { .unwrap();
return false;
};
*rect = step.to; *rect = step.to;
} }
if overlaps(current.iter().map(|(_, rect)| *rect)) { if overlaps(current.iter().map(|(_, rect)| *rect)) {
@ -335,6 +354,122 @@ fn validate_plan_samples(request: &MultiphaseRequest, plan: &MultiphasePlan) ->
}) })
} }
#[derive(Copy, Clone)]
struct RectMotion {
from: Rect,
to: Rect,
}
fn motions_overlap_during_phase(a: RectMotion, b: RectMotion) -> bool {
let mut interval = TimeInterval::unit();
interval.intersect_less_than(edge_delta(a.from.x1(), a.to.x1(), b.from.x2(), b.to.x2()))
&& interval.intersect_less_than(edge_delta(b.from.x1(), b.to.x1(), a.from.x2(), a.to.x2()))
&& interval.intersect_less_than(edge_delta(a.from.y1(), a.to.y1(), b.from.y2(), b.to.y2()))
&& interval.intersect_less_than(edge_delta(b.from.y1(), b.to.y1(), a.from.y2(), a.to.y2()))
&& interval.is_non_empty()
}
fn edge_delta(a0: i32, a1: i32, b0: i32, b1: i32) -> LinearDelta {
let from = a0 as i64 - b0 as i64;
let to = a1 as i64 - b1 as i64;
LinearDelta {
start: from,
velocity: to - from,
}
}
#[derive(Copy, Clone)]
struct LinearDelta {
start: i64,
velocity: i64,
}
#[derive(Copy, Clone)]
struct TimeInterval {
lower: Rational,
lower_open: bool,
upper: Rational,
upper_open: bool,
}
impl TimeInterval {
fn unit() -> Self {
Self {
lower: Rational::new(0, 1),
lower_open: false,
upper: Rational::new(1, 1),
upper_open: false,
}
}
fn intersect_less_than(&mut self, delta: LinearDelta) -> bool {
if delta.velocity == 0 {
return delta.start < 0;
}
let boundary = Rational::new(-delta.start, delta.velocity);
if delta.velocity > 0 {
self.tighten_upper(boundary, true);
} else {
self.tighten_lower(boundary, true);
}
self.is_non_empty()
}
fn tighten_lower(&mut self, value: Rational, open: bool) {
match value.cmp(&self.lower) {
std::cmp::Ordering::Greater => {
self.lower = value;
self.lower_open = open;
}
std::cmp::Ordering::Equal => {
self.lower_open |= open;
}
std::cmp::Ordering::Less => {}
}
}
fn tighten_upper(&mut self, value: Rational, open: bool) {
match value.cmp(&self.upper) {
std::cmp::Ordering::Less => {
self.upper = value;
self.upper_open = open;
}
std::cmp::Ordering::Equal => {
self.upper_open |= open;
}
std::cmp::Ordering::Greater => {}
}
}
fn is_non_empty(&self) -> bool {
match self.lower.cmp(&self.upper) {
std::cmp::Ordering::Less => true,
std::cmp::Ordering::Equal => !self.lower_open && !self.upper_open,
std::cmp::Ordering::Greater => false,
}
}
}
#[derive(Copy, Clone, Eq, PartialEq)]
struct Rational {
num: i64,
den: i64,
}
impl Rational {
fn new(mut num: i64, mut den: i64) -> Self {
if den < 0 {
num = -num;
den = -den;
}
Self { num, den }
}
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
(self.num as i128 * other.den as i128).cmp(&(other.num as i128 * self.den as i128))
}
}
fn classify_step(step: MultiphaseStep) -> Option<PhaseAction> { fn classify_step(step: MultiphaseStep) -> Option<PhaseAction> {
let same_x = step.from.x1() == step.to.x1() && step.from.x2() == step.to.x2(); let same_x = step.from.x1() == step.to.x1() && step.from.x2() == step.to.x2();
let same_y = step.from.y1() == step.to.y1() && step.from.y2() == step.to.y2(); let same_y = step.from.y1() == step.to.y1() && step.from.y2() == step.to.y2();
@ -526,7 +661,7 @@ mod tests {
); );
assert_eq!(plan.phases[0].steps[0].to, rect(0, 0, 100, 50)); 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_eq!(plan.phases[0].steps[1].to, rect(100, 50, 200, 100));
assert!(validate_plan_samples(&req, &plan)); assert!(validate_plan_continuous(&req, &plan));
} }
#[test] #[test]
@ -546,7 +681,7 @@ mod tests {
let plan = plan_no_overlap(&req).unwrap(); let plan = plan_no_overlap(&req).unwrap();
assert_eq!(plan.phases[0].steps[0].to, rect(100, 0, 200, 50)); assert_eq!(plan.phases[0].steps[0].to, rect(100, 0, 200, 50));
assert_eq!(plan.phases[0].steps[1].to, rect(0, 50, 100, 100)); assert_eq!(plan.phases[0].steps[1].to, rect(0, 50, 100, 100));
assert!(validate_plan_samples(&req, &plan)); assert!(validate_plan_continuous(&req, &plan));
} }
#[test] #[test]
@ -591,7 +726,7 @@ mod tests {
assert_eq!(plan.phases[1].steps[0].to, rect(100, 0, 300, 50)); assert_eq!(plan.phases[1].steps[0].to, rect(100, 0, 300, 50));
assert_eq!(plan.phases[2].steps[0].to, rect(100, 0, 300, 100)); assert_eq!(plan.phases[2].steps[0].to, rect(100, 0, 300, 100));
assert_eq!(plan.phases[2].steps[1].to, rect(300, 0, 400, 100)); assert_eq!(plan.phases[2].steps[1].to, rect(300, 0, 400, 100));
assert!(validate_plan_samples(&req, &plan)); assert!(validate_plan_continuous(&req, &plan));
} }
#[test] #[test]
@ -631,7 +766,7 @@ mod tests {
}, },
] ]
); );
assert!(validate_plan_samples(&req, &plan)); assert!(validate_plan_continuous(&req, &plan));
} }
#[test] #[test]
@ -676,7 +811,7 @@ mod tests {
assert_eq!(plan.phases[1].steps[0].to, rect(0, 100, 50, 300)); 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[0].to, rect(0, 100, 100, 300));
assert_eq!(plan.phases[2].steps[1].to, rect(0, 300, 100, 400)); assert_eq!(plan.phases[2].steps[1].to, rect(0, 300, 100, 400));
assert!(validate_plan_samples(&req, &plan)); assert!(validate_plan_continuous(&req, &plan));
} }
#[test] #[test]
@ -716,7 +851,7 @@ mod tests {
}, },
] ]
); );
assert!(validate_plan_samples(&req, &plan)); assert!(validate_plan_continuous(&req, &plan));
} }
#[test] #[test]
@ -729,6 +864,92 @@ mod tests {
assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan)); assert_eq!(plan_no_overlap(&req), Err(MultiphaseError::NoPlan));
} }
#[test]
fn continuous_validation_rejects_narrow_mid_phase_overlap() {
let req = request(vec![
MultiphaseWindow {
node_id: id(1),
from: rect(0, 0, 10, 10),
to: rect(100, 0, 110, 10),
},
MultiphaseWindow {
node_id: id(2),
from: rect(13, 0, 14, 10),
to: rect(13, 0, 14, 10),
},
]);
let plan = MultiphasePlan {
phases: vec![MultiphasePhase {
action: PhaseAction {
kind: PhaseKind::Move,
axis: PhaseAxis::Horizontal,
},
steps: vec![MultiphaseStep {
node_id: id(1),
from: rect(0, 0, 10, 10),
to: rect(100, 0, 110, 10),
}],
}],
};
assert!(!validate_plan_continuous(&req, &plan));
}
#[test]
fn continuous_validation_allows_edge_touching_motion() {
let req = request(vec![
MultiphaseWindow {
node_id: id(1),
from: rect(0, 0, 10, 10),
to: rect(10, 0, 20, 10),
},
MultiphaseWindow {
node_id: id(2),
from: rect(20, 0, 30, 10),
to: rect(20, 0, 30, 10),
},
]);
let plan = MultiphasePlan {
phases: vec![MultiphasePhase {
action: PhaseAction {
kind: PhaseKind::Move,
axis: PhaseAxis::Horizontal,
},
steps: vec![MultiphaseStep {
node_id: id(1),
from: rect(0, 0, 10, 10),
to: rect(10, 0, 20, 10),
}],
}],
};
assert!(validate_plan_continuous(&req, &plan));
}
#[test]
fn continuous_validation_rejects_stale_step_start_rect() {
let req = request(vec![MultiphaseWindow {
node_id: id(1),
from: rect(0, 0, 10, 10),
to: rect(20, 0, 30, 10),
}]);
let plan = MultiphasePlan {
phases: vec![MultiphasePhase {
action: PhaseAction {
kind: PhaseKind::Move,
axis: PhaseAxis::Horizontal,
},
steps: vec![MultiphaseStep {
node_id: id(1),
from: rect(5, 0, 15, 10),
to: rect(20, 0, 30, 10),
}],
}],
};
assert!(!validate_plan_continuous(&req, &plan));
}
#[test] #[test]
fn motion_groups_split_disjoint_layout_changes() { fn motion_groups_split_disjoint_layout_changes() {
let windows = vec![ let windows = vec![