Validate multiphase overlap analytically
This commit is contained in:
parent
4ee2c324e1
commit
b109cdf6f2
2 changed files with 244 additions and 21 deletions
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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![
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue