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
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.
- 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
groups can still use multiphase animation when another group falls back to
linear motion.

View file

@ -1,7 +1,6 @@
use {crate::rect::Rect, crate::tree::NodeId};
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)]
pub struct MultiphaseRequest {
@ -288,10 +287,10 @@ fn build_validated_plan<const N: usize>(
return None;
}
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
.windows
.iter()
@ -301,26 +300,46 @@ fn validate_plan_samples(request: &MultiphaseRequest, plan: &MultiphasePlan) ->
return false;
}
for phase in &plan.phases {
for t in PHASE_VALIDATION_SAMPLES {
let rects = current.iter().map(|(node_id, rect)| {
phase
for (idx, step) in phase.steps.iter().enumerate() {
if phase.steps[..idx]
.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
.iter()
.find(|step| step.node_id == *node_id)
.map(|step| super::lerp_rect(step.from, step.to, t))
.unwrap_or(*rect)
});
if overlaps(rects) {
.map(|step| step.to)
.unwrap_or(*rect);
RectMotion { from: *rect, to }
})
.collect();
for (idx, motion) in motions.iter().enumerate() {
if motions[idx + 1..]
.iter()
.any(|other| motions_overlap_during_phase(*motion, *other))
{
return false;
}
}
for step in &phase.steps {
let Some((_, rect)) = current
let (_, rect) = current
.iter_mut()
.find(|(node_id, _)| *node_id == step.node_id)
else {
return false;
};
.unwrap();
*rect = step.to;
}
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> {
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();
@ -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[1].to, rect(100, 50, 200, 100));
assert!(validate_plan_samples(&req, &plan));
assert!(validate_plan_continuous(&req, &plan));
}
#[test]
@ -546,7 +681,7 @@ mod tests {
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[1].to, rect(0, 50, 100, 100));
assert!(validate_plan_samples(&req, &plan));
assert!(validate_plan_continuous(&req, &plan));
}
#[test]
@ -591,7 +726,7 @@ mod tests {
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[1].to, rect(300, 0, 400, 100));
assert!(validate_plan_samples(&req, &plan));
assert!(validate_plan_continuous(&req, &plan));
}
#[test]
@ -631,7 +766,7 @@ mod tests {
},
]
);
assert!(validate_plan_samples(&req, &plan));
assert!(validate_plan_continuous(&req, &plan));
}
#[test]
@ -676,7 +811,7 @@ mod tests {
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));
assert!(validate_plan_continuous(&req, &plan));
}
#[test]
@ -716,7 +851,7 @@ mod tests {
},
]
);
assert!(validate_plan_samples(&req, &plan));
assert!(validate_plan_continuous(&req, &plan));
}
#[test]
@ -729,6 +864,92 @@ mod tests {
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]
fn motion_groups_split_disjoint_layout_changes() {
let windows = vec![