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

@ -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![