use egui::{Color32, Pos2, Rect};
use crate::core::items::LineStyle;
use crate::core::transform::Transform;
pub const DEFAULT_ROI_COLOR: Color32 = Color32::RED;
pub const DEFAULT_ROI_LINE_WIDTH: f32 = 1.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RoiEdge {
Left,
Right,
Bottom,
Top,
BottomLeft,
BottomRight,
TopLeft,
TopRight,
Vertex(usize),
}
#[derive(Clone, Debug, PartialEq)]
pub enum Roi {
Rect { x: (f64, f64), y: (f64, f64) },
HRange { y: (f64, f64) },
VRange { x: (f64, f64) },
HLine { y: f64 },
VLine { x: f64 },
Point { x: f64, y: f64 },
Line { start: (f64, f64), end: (f64, f64) },
Polygon { vertices: Vec<(f64, f64)> },
Cross { center: (f64, f64) },
Circle { center: (f64, f64), radius: f64 },
Ellipse {
center: (f64, f64),
radii: (f64, f64),
orientation: f64,
},
Arc {
center: (f64, f64),
radius: f64,
weight: f64,
start_angle: f64,
end_angle: f64,
},
Band {
begin: (f64, f64),
end: (f64, f64),
width: f64,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HandleKind {
Vertex,
Edge,
Center,
Translate,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RoiHandle {
pub pos: [f64; 2],
pub kind: HandleKind,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BandLines {
Sloped {
slope: f64,
middle: f64,
edges: (f64, f64),
},
Vertical {
middle: f64,
edges: (f64, f64),
},
}
impl Roi {
pub fn screen_rect(&self, t: &Transform) -> Rect {
let area = t.area;
match self {
Roi::Rect { x, y } => {
let a = t.data_to_pixel(x.0, y.0);
let b = t.data_to_pixel(x.1, y.1);
Rect::from_two_pos(a, b)
}
Roi::HRange { y } => {
let py0 = t.data_to_pixel(t.x.min, y.0).y;
let py1 = t.data_to_pixel(t.x.min, y.1).y;
Rect::from_x_y_ranges(area.left()..=area.right(), py0.min(py1)..=py0.max(py1))
}
Roi::VRange { x } => {
let px0 = t.data_to_pixel(x.0, t.y.min).x;
let px1 = t.data_to_pixel(x.1, t.y.min).x;
Rect::from_x_y_ranges(px0.min(px1)..=px0.max(px1), area.top()..=area.bottom())
}
Roi::HLine { y } => {
let py = t.data_to_pixel(t.x.min, *y).y;
Rect::from_x_y_ranges(area.left()..=area.right(), py..=py)
}
Roi::VLine { x } => {
let px = t.data_to_pixel(*x, t.y.min).x;
Rect::from_x_y_ranges(px..=px, area.top()..=area.bottom())
}
Roi::Point { x, y } => {
let p = t.data_to_pixel(*x, *y);
Rect::from_center_size(p, egui::vec2(1.0, 1.0))
}
Roi::Line { start, end } => {
let a = t.data_to_pixel(start.0, start.1);
let b = t.data_to_pixel(end.0, end.1);
Rect::from_two_pos(a, b)
}
Roi::Polygon { vertices } => {
let mut rect = Rect::NOTHING;
for &(x, y) in vertices {
let p = t.data_to_pixel(x, y);
if rect.is_negative() {
rect = Rect::from_center_size(p, egui::vec2(1.0, 1.0));
} else {
rect = rect.union(Rect::from_center_size(p, egui::vec2(1.0, 1.0)));
}
}
if rect.is_negative() { area } else { rect }
}
Roi::Cross { center } => {
let p = t.data_to_pixel(center.0, center.1);
Rect::from_center_size(p, egui::vec2(1.0, 1.0))
}
Roi::Circle { center, radius } => {
let a = t.data_to_pixel(center.0 - radius, center.1 - radius);
let b = t.data_to_pixel(center.0 + radius, center.1 + radius);
Rect::from_two_pos(a, b)
}
Roi::Ellipse {
center,
radii,
orientation,
} => {
let (hx, hy) = ellipse_aabb_half_extents(*radii, *orientation);
let a = t.data_to_pixel(center.0 - hx, center.1 - hy);
let b = t.data_to_pixel(center.0 + hx, center.1 + hy);
Rect::from_two_pos(a, b)
}
Roi::Arc {
center,
radius,
weight,
..
} => {
let outer = arc_outer_radius(*radius, *weight);
let a = t.data_to_pixel(center.0 - outer, center.1 - outer);
let b = t.data_to_pixel(center.0 + outer, center.1 + outer);
Rect::from_two_pos(a, b)
}
Roi::Band { .. } => {
let mut rect = Rect::NOTHING;
for &(x, y) in &band_corners(self).unwrap_or_default() {
let p = t.data_to_pixel(x, y);
if rect.is_negative() {
rect = Rect::from_center_size(p, egui::vec2(1.0, 1.0));
} else {
rect = rect.union(Rect::from_center_size(p, egui::vec2(1.0, 1.0)));
}
}
if rect.is_negative() { area } else { rect }
}
}
}
fn edges(&self) -> Vec<RoiEdge> {
match self {
Roi::Rect { .. } => vec![
RoiEdge::Left,
RoiEdge::Right,
RoiEdge::Bottom,
RoiEdge::Top,
RoiEdge::BottomLeft,
RoiEdge::BottomRight,
RoiEdge::TopLeft,
RoiEdge::TopRight,
],
Roi::HRange { .. } => vec![RoiEdge::Bottom, RoiEdge::Top],
Roi::VRange { .. } => vec![RoiEdge::Left, RoiEdge::Right],
Roi::HLine { .. } => vec![RoiEdge::Bottom],
Roi::VLine { .. } => vec![RoiEdge::Left],
Roi::Point { .. } => vec![RoiEdge::Vertex(0)],
Roi::Line { .. } => vec![RoiEdge::Vertex(0), RoiEdge::Vertex(1)],
Roi::Polygon { vertices } => (0..vertices.len()).map(RoiEdge::Vertex).collect(),
Roi::Cross { .. } => vec![RoiEdge::Vertex(0)],
Roi::Circle { .. } => vec![RoiEdge::Vertex(0), RoiEdge::Vertex(1)],
Roi::Ellipse { .. } => {
vec![RoiEdge::Vertex(0), RoiEdge::Vertex(1), RoiEdge::Vertex(2)]
}
Roi::Arc { .. } => (0..4).map(RoiEdge::Vertex).collect(),
Roi::Band { .. } => (0..4).map(RoiEdge::Vertex).collect(),
}
}
fn vertex_pixel(&self, t: &Transform, index: usize) -> Option<Pos2> {
let (x, y) = match self {
Roi::Point { x, y } if index == 0 => (*x, *y),
Roi::Line { start, end } => match index {
0 => *start,
1 => *end,
_ => return None,
},
Roi::Polygon { vertices } => vertices.get(index).copied()?,
Roi::Cross { center } if index == 0 => *center,
Roi::Circle { center, radius } => match index {
0 => *center,
1 => (center.0 + radius, center.1),
_ => return None,
},
Roi::Ellipse {
center,
radii,
orientation,
} => {
let (c, s) = (orientation.cos(), orientation.sin());
match index {
0 => *center,
1 => (center.0 + radii.0 * c, center.1 + radii.0 * s),
2 => (center.0 - radii.1 * s, center.1 + radii.1 * c),
_ => return None,
}
}
Roi::Arc { .. } => arc_vertex_pos(self, index)?,
Roi::Band { .. } => band_vertex_pos(self, index)?,
_ => return None,
};
Some(t.data_to_pixel(x, y))
}
pub fn handle_centers(&self, t: &Transform) -> Vec<Pos2> {
let mid = |a: f64, b: f64| (a + b) * 0.5;
let center = || self.screen_rect(t).center();
self.edges()
.iter()
.map(|edge| match self {
Roi::Rect { x, y } => {
let (dx, dy) = match edge {
RoiEdge::Left => (x.0, mid(y.0, y.1)),
RoiEdge::Right => (x.1, mid(y.0, y.1)),
RoiEdge::Bottom => (mid(x.0, x.1), y.0),
RoiEdge::Top => (mid(x.0, x.1), y.1),
RoiEdge::BottomLeft => (x.0, y.0),
RoiEdge::BottomRight => (x.1, y.0),
RoiEdge::TopLeft => (x.0, y.1),
RoiEdge::TopRight => (x.1, y.1),
RoiEdge::Vertex(_) => (mid(x.0, x.1), mid(y.0, y.1)),
};
t.data_to_pixel(dx, dy)
}
Roi::HRange { y } => {
let cx = (t.area.left() + t.area.right()) * 0.5;
let dy = match edge {
RoiEdge::Bottom => y.0,
RoiEdge::Top => y.1,
_ => mid(y.0, y.1),
};
egui::pos2(cx, t.data_to_pixel(t.x.min, dy).y)
}
Roi::VRange { x } => {
let cy = (t.area.top() + t.area.bottom()) * 0.5;
let dx = match edge {
RoiEdge::Left => x.0,
RoiEdge::Right => x.1,
_ => mid(x.0, x.1),
};
egui::pos2(t.data_to_pixel(dx, t.y.min).x, cy)
}
Roi::HLine { y } => {
let cx = (t.area.left() + t.area.right()) * 0.5;
egui::pos2(cx, t.data_to_pixel(t.x.min, *y).y)
}
Roi::VLine { x } => {
let cy = (t.area.top() + t.area.bottom()) * 0.5;
egui::pos2(t.data_to_pixel(*x, t.y.min).x, cy)
}
_ => match edge {
RoiEdge::Vertex(n) => self.vertex_pixel(t, *n).unwrap_or_else(center),
_ => center(),
},
})
.collect()
}
pub fn edge_at(&self, t: &Transform, cursor: Pos2, grab_px: f32) -> Option<RoiEdge> {
match self {
Roi::Point { .. }
| Roi::Line { .. }
| Roi::Polygon { .. }
| Roi::Cross { .. }
| Roi::Circle { .. }
| Roi::Ellipse { .. }
| Roi::Arc { .. }
| Roi::Band { .. } => {
let mut best: Option<(RoiEdge, f32)> = None;
for edge in self.edges() {
if let RoiEdge::Vertex(n) = edge
&& let Some(p) = self.vertex_pixel(t, n)
{
let dist = cursor.distance(p);
if dist <= grab_px && best.is_none_or(|(_, d)| dist < d) {
best = Some((edge, dist));
}
}
}
best.map(|(e, _)| e)
}
_ => {
let area = t.area;
let (lx, rx, by, ty, x_span, y_span) = match self {
Roi::Rect { x, y } => {
let lx = t.data_to_pixel(x.0, y.0).x;
let rx = t.data_to_pixel(x.1, y.0).x;
let by = t.data_to_pixel(x.0, y.0).y;
let ty = t.data_to_pixel(x.0, y.1).y;
(
Some(lx),
Some(rx),
Some(by),
Some(ty),
(lx.min(rx), lx.max(rx)),
(ty.min(by), ty.max(by)),
)
}
Roi::HRange { y } => {
let by = t.data_to_pixel(t.x.min, y.0).y;
let ty = t.data_to_pixel(t.x.min, y.1).y;
(
None,
None,
Some(by),
Some(ty),
(area.left(), area.right()),
(ty.min(by), ty.max(by)),
)
}
Roi::VRange { x } => {
let lx = t.data_to_pixel(x.0, t.y.min).x;
let rx = t.data_to_pixel(x.1, t.y.min).x;
(
Some(lx),
Some(rx),
None,
None,
(lx.min(rx), lx.max(rx)),
(area.top(), area.bottom()),
)
}
Roi::HLine { y } => {
let py = t.data_to_pixel(t.x.min, *y).y;
(
None,
None,
Some(py),
None,
(area.left(), area.right()),
(py, py),
)
}
Roi::VLine { x } => {
let px = t.data_to_pixel(*x, t.y.min).x;
(
Some(px),
None,
None,
None,
(px, px),
(area.top(), area.bottom()),
)
}
_ => unreachable!(
"outer match restricts this arm to Rect/HRange/VRange/HLine/VLine"
),
};
let corner_pos = |edge: RoiEdge| -> Option<Pos2> {
Some(match edge {
RoiEdge::BottomLeft => egui::pos2(lx?, by?),
RoiEdge::BottomRight => egui::pos2(rx?, by?),
RoiEdge::TopLeft => egui::pos2(lx?, ty?),
RoiEdge::TopRight => egui::pos2(rx?, ty?),
_ => return None,
})
};
let mut best_corner: Option<(RoiEdge, f32)> = None;
for edge in self.edges() {
if let Some(corner) = corner_pos(edge) {
let dist = cursor.distance(corner);
if dist <= grab_px && best_corner.is_none_or(|(_, d)| dist < d) {
best_corner = Some((edge, dist));
}
}
}
if let Some((edge, _)) = best_corner {
return Some(edge);
}
let (x_lo, x_hi) = x_span;
let (y_lo, y_hi) = y_span;
let mut best: Option<(RoiEdge, f32)> = None;
for edge in self.edges() {
let dist = match edge {
RoiEdge::Left | RoiEdge::Right => {
if cursor.y < y_lo - grab_px || cursor.y > y_hi + grab_px {
continue;
}
let ex = if edge == RoiEdge::Left { lx } else { rx };
match ex {
Some(ex) => (cursor.x - ex).abs(),
None => continue,
}
}
RoiEdge::Bottom | RoiEdge::Top => {
if cursor.x < x_lo - grab_px || cursor.x > x_hi + grab_px {
continue;
}
let ey = if edge == RoiEdge::Top { ty } else { by };
match ey {
Some(ey) => (cursor.y - ey).abs(),
None => continue,
}
}
_ => continue,
};
if dist <= grab_px && best.is_none_or(|(_, d)| dist < d) {
best = Some((edge, dist));
}
}
best.map(|(edge, _)| edge)
}
}
}
pub fn move_edge(&mut self, edge: RoiEdge, data: (f64, f64)) {
let (dx, dy) = data;
match self {
Roi::Rect { x, y } => {
let (x0, x1, y0, y1) = (x.0, x.1, y.0, y.1);
match edge {
RoiEdge::Left => *x = (dx.min(x1), dx.max(x1)),
RoiEdge::Right => *x = (dx.min(x0), dx.max(x0)),
RoiEdge::Bottom => *y = (dy.min(y1), dy.max(y1)),
RoiEdge::Top => *y = (dy.min(y0), dy.max(y0)),
RoiEdge::BottomLeft => {
*x = (dx.min(x1), dx.max(x1));
*y = (dy.min(y1), dy.max(y1));
}
RoiEdge::BottomRight => {
*x = (dx.min(x0), dx.max(x0));
*y = (dy.min(y1), dy.max(y1));
}
RoiEdge::TopLeft => {
*x = (dx.min(x1), dx.max(x1));
*y = (dy.min(y0), dy.max(y0));
}
RoiEdge::TopRight => {
*x = (dx.min(x0), dx.max(x0));
*y = (dy.min(y0), dy.max(y0));
}
RoiEdge::Vertex(_) => {}
}
}
Roi::HRange { y } => match edge {
RoiEdge::Bottom => *y = (dy.min(y.1), dy.max(y.1)),
RoiEdge::Top => *y = (dy.min(y.0), dy.max(y.0)),
_ => {}
},
Roi::VRange { x } => match edge {
RoiEdge::Left => *x = (dx.min(x.1), dx.max(x.1)),
RoiEdge::Right => *x = (dx.min(x.0), dx.max(x.0)),
_ => {}
},
Roi::HLine { y } => {
if let RoiEdge::Bottom = edge {
*y = dy;
}
}
Roi::VLine { x } => {
if let RoiEdge::Left = edge {
*x = dx;
}
}
Roi::Point { x, y } => {
if let RoiEdge::Vertex(0) = edge {
*x = dx;
*y = dy;
}
}
Roi::Line { start, end } => match edge {
RoiEdge::Vertex(0) => *start = (dx, dy),
RoiEdge::Vertex(1) => *end = (dx, dy),
_ => {}
},
Roi::Polygon { vertices } => {
if let RoiEdge::Vertex(n) = edge
&& let Some(v) = vertices.get_mut(n)
{
*v = (dx, dy);
}
}
Roi::Cross { center } => {
if let RoiEdge::Vertex(0) = edge {
*center = (dx, dy);
}
}
Roi::Circle { center, radius } => match edge {
RoiEdge::Vertex(0) => *center = (dx, dy),
RoiEdge::Vertex(1) => {
let (ex, ey) = (dx - center.0, dy - center.1);
*radius = (ex * ex + ey * ey).sqrt();
}
_ => {}
},
Roi::Ellipse {
center,
radii,
orientation,
} => match edge {
RoiEdge::Vertex(0) => *center = (dx, dy),
RoiEdge::Vertex(1) => {
let (ex, ey) = (dx - center.0, dy - center.1);
radii.0 = ex.hypot(ey);
*orientation = ey.atan2(ex);
}
RoiEdge::Vertex(2) => {
let (ex, ey) = (dx - center.0, dy - center.1);
radii.1 = ex.hypot(ey);
*orientation = ey.atan2(ex) - std::f64::consts::FRAC_PI_2;
}
_ => {}
},
Roi::Arc {
center,
radius,
weight,
start_angle,
end_angle,
} => {
let (cx, cy) = *center;
match edge {
RoiEdge::Vertex(0) => {
*radius = (dx - cx).hypot(dy - cy);
}
RoiEdge::Vertex(1) => {
let d = (dx - cx).hypot(dy - cy);
*weight = 2.0 * (d - *radius).abs();
}
RoiEdge::Vertex(2) => {
*start_angle = coherent_angle(*start_angle, (dy - cy).atan2(dx - cx));
}
RoiEdge::Vertex(3) => {
*end_angle = coherent_angle(*end_angle, (dy - cy).atan2(dx - cx));
}
_ => {}
}
}
Roi::Band { begin, end, width } => match edge {
RoiEdge::Vertex(0) => *begin = (dx, dy),
RoiEdge::Vertex(1) => *end = (dx, dy),
RoiEdge::Vertex(2) | RoiEdge::Vertex(3) => {
let center = ((begin.0 + end.0) * 0.5, (begin.1 + end.1) * 0.5);
let n = band_normal(*begin, *end);
let mut proj = n.0 * (dx - center.0) + n.1 * (dy - center.1);
if let RoiEdge::Vertex(3) = edge {
proj = -proj;
}
*width = 2.0 * proj.max(0.0);
}
_ => {}
},
}
}
pub fn band_lines(&self) -> Option<BandLines> {
let Roi::Band { begin, end, width } = self else {
return None;
};
let n = band_normal(*begin, *end);
let off = (0.5 * width * n.0, 0.5 * width * n.1);
if begin.0 == end.0 {
return Some(BandLines::Vertical {
middle: begin.0,
edges: (begin.0 - off.0, begin.0 + off.0),
});
}
let slope = (end.1 - begin.1) / (end.0 - begin.0);
let middle = begin.1 - slope * begin.0;
let edges = (
begin.1 - off.1 - slope * (begin.0 - off.0),
begin.1 + off.1 - slope * (begin.0 + off.0),
);
Some(BandLines::Sloped {
slope,
middle,
edges,
})
}
#[must_use]
pub fn band_unbounded_segments(
&self,
x_range: (f64, f64),
y_range: (f64, f64),
) -> Option<[[(f64, f64); 2]; 3]> {
Some(match self.band_lines()? {
BandLines::Sloped {
slope,
middle,
edges,
} => {
let (x0, x1) = x_range;
let seg = |c: f64| [(x0, slope * x0 + c), (x1, slope * x1 + c)];
[seg(middle), seg(edges.0), seg(edges.1)]
}
BandLines::Vertical { middle, edges } => {
let (y0, y1) = y_range;
let seg = |x: f64| [(x, y0), (x, y1)];
[seg(middle), seg(edges.0), seg(edges.1)]
}
})
}
pub fn contains(&self, pos: (f64, f64)) -> bool {
let (x, y) = pos;
match self {
Roi::Rect {
x: (x0, x1),
y: (y0, y1),
} => x >= *x0 && x <= *x1 && y >= *y0 && y <= *y1,
Roi::HRange { y: (y0, y1) } => y >= *y0 && y <= *y1,
Roi::VRange { x: (x0, x1) } => x >= *x0 && x <= *x1,
Roi::HLine { y: ly } => y == *ly,
Roi::VLine { x: lx } => x == *lx,
Roi::Point { x: px, y: py } => x == *px && y == *py,
Roi::Cross { center } => x == center.0 || y == center.1,
Roi::Line { start, end } => {
let (bx0, bx1) = (start.0.min(end.0), start.0.max(end.0));
let (by0, by1) = (start.1.min(end.1), start.1.max(end.1));
(bx0..=bx1).contains(&x)
&& (by0..=by1).contains(&y)
&& segment_intersects_unit_square(*start, *end, pos)
}
Roi::Polygon { vertices } => point_in_polygon(vertices, pos),
Roi::Circle { center, radius } => {
let (dx, dy) = (x - center.0, y - center.1);
(dx * dx + dy * dy).sqrt() <= *radius
}
Roi::Ellipse {
center,
radii,
orientation,
} => {
let (a, b) = *radii;
if a <= 0.0 || b <= 0.0 {
return false;
}
let (dx, dy) = (x - center.0, y - center.1);
let (c, s) = (orientation.cos(), orientation.sin());
let xr = dx * c + dy * s;
let yr = -dx * s + dy * c;
(xr * xr) / (a * a) + (yr * yr) / (b * b) <= 1.0
}
Roi::Arc {
center,
radius,
weight,
start_angle,
end_angle,
} => arc_contains(
*center,
arc_inner_radius(*radius, *weight),
arc_outer_radius(*radius, *weight),
*start_angle,
*end_angle,
pos,
),
Roi::Band { .. } => match band_corners(self) {
Some(corners) => point_in_polygon(&corners, pos),
None => false,
},
}
}
pub fn handles(&self) -> Vec<RoiHandle> {
let v = |p: (f64, f64)| RoiHandle {
pos: [p.0, p.1],
kind: HandleKind::Vertex,
};
let center = |p: (f64, f64)| RoiHandle {
pos: [p.0, p.1],
kind: HandleKind::Center,
};
let translate = |p: (f64, f64)| RoiHandle {
pos: [p.0, p.1],
kind: HandleKind::Translate,
};
let edge = |p: (f64, f64)| RoiHandle {
pos: [p.0, p.1],
kind: HandleKind::Edge,
};
match self {
Roi::Rect {
x: (x0, x1),
y: (y0, y1),
} => vec![
v((*x0, *y0)),
v((*x1, *y0)),
v((*x0, *y1)),
v((*x1, *y1)),
translate(((x0 + x1) * 0.5, (y0 + y1) * 0.5)),
],
Roi::HRange { y: (y0, y1) } => vec![
edge((0.0, *y0)),
edge((0.0, *y1)),
center((0.0, (y0 + y1) * 0.5)),
],
Roi::VRange { x: (x0, x1) } => vec![
edge((*x0, 0.0)),
edge((*x1, 0.0)),
center(((x0 + x1) * 0.5, 0.0)),
],
Roi::HLine { y } => vec![v((0.0, *y))],
Roi::VLine { x } => vec![v((*x, 0.0))],
Roi::Point { x, y } => vec![v((*x, *y))],
Roi::Cross { center: c } => vec![v(*c)],
Roi::Line { start, end } => vec![
v(*start),
v(*end),
translate(((start.0 + end.0) * 0.5, (start.1 + end.1) * 0.5)),
],
Roi::Polygon { vertices } => {
let mut hs: Vec<RoiHandle> = vertices.iter().map(|&p| v(p)).collect();
if let Some(&first) = vertices.first() {
hs.push(translate(first));
}
hs
}
Roi::Circle { center: c, radius } => {
vec![v((c.0 + radius, c.1)), translate(*c)]
}
Roi::Ellipse {
center: c,
radii,
orientation,
} => {
let (cs, sn) = (orientation.cos(), orientation.sin());
vec![
v((c.0 + radii.0 * cs, c.1 + radii.0 * sn)),
v((c.0 - radii.1 * sn, c.1 + radii.1 * cs)),
translate(*c),
]
}
Roi::Arc { center: c, .. } => {
let mut hs: Vec<RoiHandle> = (0..4)
.filter_map(|i| arc_vertex_pos(self, i).map(v))
.collect();
hs.push(translate(*c));
hs
}
Roi::Band { begin, end, .. } => {
let mut hs: Vec<RoiHandle> = (0..4)
.filter_map(|i| band_vertex_pos(self, i).map(v))
.collect();
hs.push(translate((
(begin.0 + end.0) * 0.5,
(begin.1 + end.1) * 0.5,
)));
hs
}
}
}
pub fn translate(&mut self, dx: f64, dy: f64) {
let shift = |p: &mut (f64, f64)| {
p.0 += dx;
p.1 += dy;
};
match self {
Roi::Rect { x, y } => {
x.0 += dx;
x.1 += dx;
y.0 += dy;
y.1 += dy;
}
Roi::HRange { y } => {
y.0 += dy;
y.1 += dy;
}
Roi::VRange { x } => {
x.0 += dx;
x.1 += dx;
}
Roi::HLine { y } => *y += dy,
Roi::VLine { x } => *x += dx,
Roi::Point { x, y } => {
*x += dx;
*y += dy;
}
Roi::Cross { center } => shift(center),
Roi::Line { start, end } => {
shift(start);
shift(end);
}
Roi::Polygon { vertices } => {
for v in vertices.iter_mut() {
shift(v);
}
}
Roi::Circle { center, .. } => shift(center),
Roi::Ellipse { center, .. } => shift(center),
Roi::Arc { center, .. } => shift(center),
Roi::Band { begin, end, .. } => {
shift(begin);
shift(end);
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum RoiLineStyle {
#[default]
Solid,
Dashed,
Dotted,
}
impl RoiLineStyle {
pub fn to_line_style(self) -> LineStyle {
match self {
RoiLineStyle::Solid => LineStyle::Solid,
RoiLineStyle::Dashed => LineStyle::Dashed,
RoiLineStyle::Dotted => LineStyle::Dotted,
}
}
pub(crate) fn label(self) -> &'static str {
match self {
RoiLineStyle::Solid => "─",
RoiLineStyle::Dashed => "╌",
RoiLineStyle::Dotted => "┈",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum RoiInteractionMode {
ArcThreePoint,
ArcPolar,
ArcTranslation,
BandBounded,
BandUnbounded,
}
impl RoiInteractionMode {
#[must_use]
pub fn label(self) -> &'static str {
match self {
RoiInteractionMode::ArcThreePoint => "3 points",
RoiInteractionMode::ArcPolar => "Polar",
RoiInteractionMode::ArcTranslation => "Translation",
RoiInteractionMode::BandBounded => "Bounded",
RoiInteractionMode::BandUnbounded => "Unbounded",
}
}
#[must_use]
pub fn description(self) -> &'static str {
match self {
RoiInteractionMode::ArcThreePoint => {
"Provides 3 points to define the main radius circle"
}
RoiInteractionMode::ArcPolar => "Provides anchors to edit the ROI in polar coords",
RoiInteractionMode::ArcTranslation => "Provides anchors to only move the ROI",
RoiInteractionMode::BandBounded => "Band is bounded on both sides",
RoiInteractionMode::BandUnbounded => "Band is unbounded on both sides",
}
}
}
impl Roi {
#[must_use]
pub fn available_interaction_modes(&self) -> &'static [RoiInteractionMode] {
match self {
Roi::Arc { .. } => &[
RoiInteractionMode::ArcThreePoint,
RoiInteractionMode::ArcPolar,
RoiInteractionMode::ArcTranslation,
],
Roi::Band { .. } => &[
RoiInteractionMode::BandBounded,
RoiInteractionMode::BandUnbounded,
],
_ => &[],
}
}
#[must_use]
pub fn default_interaction_mode(&self) -> Option<RoiInteractionMode> {
self.available_interaction_modes().first().copied()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ManagedRoi {
pub roi: Roi,
pub color: Option<Color32>,
pub name: String,
pub selected: bool,
pub line_width: f32,
pub line_style: RoiLineStyle,
pub gap_color: Option<Color32>,
pub fill: bool,
pub visible: bool,
interaction_mode: Option<RoiInteractionMode>,
}
impl ManagedRoi {
pub fn new(roi: Roi) -> Self {
let interaction_mode = roi.default_interaction_mode();
Self {
roi,
color: None,
name: String::new(),
selected: false,
line_width: DEFAULT_ROI_LINE_WIDTH,
line_style: RoiLineStyle::default(),
gap_color: None,
fill: false,
visible: true,
interaction_mode,
}
}
#[must_use]
pub fn interaction_mode(&self) -> Option<RoiInteractionMode> {
self.interaction_mode
}
pub fn set_interaction_mode(&mut self, mode: RoiInteractionMode) -> bool {
if self.roi.available_interaction_modes().contains(&mode) {
self.interaction_mode = Some(mode);
true
} else {
false
}
}
}
#[must_use]
pub fn arc_inner_radius(radius: f64, weight: f64) -> f64 {
(radius - weight * 0.5).max(0.0)
}
#[must_use]
pub fn arc_outer_radius(radius: f64, weight: f64) -> f64 {
radius + weight * 0.5
}
fn arc_vertex_pos(roi: &Roi, index: usize) -> Option<(f64, f64)> {
let Roi::Arc {
center,
radius,
weight,
start_angle,
end_angle,
} = roi
else {
return None;
};
let mid_angle = (start_angle + end_angle) * 0.5;
let at = |r: f64, a: f64| (center.0 + r * a.cos(), center.1 + r * a.sin());
Some(match index {
0 => at(*radius, mid_angle),
1 => at(arc_outer_radius(*radius, *weight), mid_angle),
2 => at(*radius, *start_angle),
3 => at(*radius, *end_angle),
_ => return None,
})
}
fn band_vertex_pos(roi: &Roi, index: usize) -> Option<(f64, f64)> {
let Roi::Band { begin, end, width } = roi else {
return None;
};
let center = ((begin.0 + end.0) * 0.5, (begin.1 + end.1) * 0.5);
let n = band_normal(*begin, *end);
let off = (0.5 * width * n.0, 0.5 * width * n.1);
Some(match index {
0 => *begin,
1 => *end,
2 => (center.0 + off.0, center.1 + off.1),
3 => (center.0 - off.0, center.1 - off.1),
_ => return None,
})
}
fn coherent_angle(previous: f64, target: f64) -> f64 {
let mut delta = target - previous;
if delta > std::f64::consts::PI {
delta -= std::f64::consts::TAU;
} else if delta < -std::f64::consts::PI {
delta += std::f64::consts::TAU;
}
previous + delta
}
fn band_normal(begin: (f64, f64), end: (f64, f64)) -> (f64, f64) {
let (vx, vy) = (end.0 - begin.0, end.1 - begin.1);
let len = (vx * vx + vy * vy).sqrt();
if len == 0.0 {
(0.0, 0.0)
} else {
(-vy / len, vx / len)
}
}
fn ellipse_aabb_half_extents(radii: (f64, f64), orientation: f64) -> (f64, f64) {
let (a, b) = radii;
let (c, s) = (orientation.cos(), orientation.sin());
let hx = ((a * c).powi(2) + (b * s).powi(2)).sqrt();
let hy = ((a * s).powi(2) + (b * c).powi(2)).sqrt();
(hx, hy)
}
fn band_corners(roi: &Roi) -> Option<Vec<(f64, f64)>> {
let Roi::Band { begin, end, width } = roi else {
return None;
};
let n = band_normal(*begin, *end);
let off = (0.5 * width * n.0, 0.5 * width * n.1);
Some(vec![
(begin.0 - off.0, begin.1 - off.1),
(begin.0 + off.0, begin.1 + off.1),
(end.0 + off.0, end.1 + off.1),
(end.0 - off.0, end.1 - off.1),
])
}
fn arc_contains(
center: (f64, f64),
inner_radius: f64,
outer_radius: f64,
start_angle: f64,
end_angle: f64,
pos: (f64, f64),
) -> bool {
let (dx, dy) = (pos.0 - center.0, pos.1 - center.1);
let distance = dx.hypot(dy);
if distance < inner_radius || distance > outer_radius {
return false;
}
let mut angle = dy.atan2(dx);
let (mut start, azim_range) = if end_angle - start_angle < 0.0 {
(end_angle, start_angle - end_angle)
} else {
(start_angle, end_angle - start_angle)
};
let two_pi = std::f64::consts::TAU;
start = (start + std::f64::consts::PI).rem_euclid(two_pi) - std::f64::consts::PI;
if angle < start {
angle += two_pi;
}
angle >= start && angle <= start + azim_range
}
fn point_in_polygon(vertices: &[(f64, f64)], pos: (f64, f64)) -> bool {
let n = vertices.len();
if n < 3 {
return false;
}
let (px, py) = pos;
let mut inside = false;
let (mut ax, mut ay) = vertices[n - 1];
for &(bx, by) in vertices {
if ((ax <= px && px < bx) || (bx <= px && px < ax)) && (py <= ay || py <= by) {
let yinters = (px - ax) * (by - ay) / (bx - ax) + ay;
if py < yinters {
inside = !inside;
}
}
ax = bx;
ay = by;
}
inside
}
fn segment_intersects_unit_square(p1: (f64, f64), p2: (f64, f64), corner: (f64, f64)) -> bool {
let (cx, cy) = corner;
let bl = (cx, cy);
let br = (cx + 1.0, cy);
let tr = (cx + 1.0, cy + 1.0);
let tl = (cx, cy + 1.0);
segments_intersect(p1, p2, bl, br)
|| segments_intersect(p1, p2, br, tr)
|| segments_intersect(p1, p2, tr, tl)
|| segments_intersect(p1, p2, tl, bl)
}
fn segments_intersect(a1: (f64, f64), a2: (f64, f64), b1: (f64, f64), b2: (f64, f64)) -> bool {
let dir_a = (a2.0 - a1.0, a2.1 - a1.1);
let dir_b = (b2.0 - b1.0, b2.1 - b1.1);
let dp = (a1.0 - b1.0, a1.1 - b1.1);
let denom = -dir_a.1 * dir_b.0 + dir_a.0 * dir_b.1;
if denom == 0.0 {
return false;
}
let num = -dir_a.1 * dp.0 + dir_a.0 * dp.1;
let s = num / denom;
let ix = s * dir_b.0 + b1.0;
let iy = s * dir_b.1 + b1.1;
let min_x = a1.0.min(a2.0).max(b1.0.min(b2.0));
let max_x = a1.0.max(a2.0).min(b1.0.max(b2.0));
let min_y = a1.1.min(a2.1).max(b1.1.min(b2.1));
let max_y = a1.1.max(a2.1).min(b1.1.max(b2.1));
(min_x..=max_x).contains(&ix) && (min_y..=max_y).contains(&iy)
}
#[cfg(test)]
mod tests {
use super::*;
use egui::pos2;
fn t() -> Transform {
Transform::new(
0.0,
10.0,
0.0,
10.0,
Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)),
)
}
fn t_inv() -> Transform {
let mut y = crate::core::transform::Axis::linear(0.0, 10.0);
y.inverted = true;
Transform::with_axes(
crate::core::transform::Axis::linear(0.0, 10.0),
y,
Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0)),
)
}
#[test]
fn edge_at_corner_keeps_data_identity_under_inverted_y() {
let roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
let t = t_inv();
assert_eq!(
roi.edge_at(&t, pos2(20.0, 30.0), 4.0),
Some(RoiEdge::BottomLeft)
);
assert_eq!(
roi.edge_at(&t, pos2(20.0, 70.0), 4.0),
Some(RoiEdge::TopLeft)
);
assert_eq!(
roi.edge_at(&t, pos2(80.0, 30.0), 4.0),
Some(RoiEdge::BottomRight)
);
assert_eq!(
roi.edge_at(&t, pos2(80.0, 70.0), 4.0),
Some(RoiEdge::TopRight)
);
}
#[test]
fn corner_drag_under_inverted_y_tracks_cursor_without_collapse() {
let roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
let t = t_inv();
let grab = pos2(20.0, 70.0);
let edge = roi.edge_at(&t, grab, 4.0).expect("corner grabbed");
assert_eq!(edge, RoiEdge::TopLeft);
let mut moved = roi.clone();
moved.move_edge(edge, t.pixel_to_data(pos2(10.0, 90.0)));
assert_eq!(
moved,
Roi::Rect {
x: (1.0, 8.0),
y: (3.0, 9.0)
},
"x.min and y.max follow the cursor; y.min untouched; no collapse"
);
}
#[test]
fn side_edge_under_inverted_y_maps_to_correct_data_edge() {
let roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
let t = t_inv();
assert_eq!(
roi.edge_at(&t, pos2(50.0, 30.0), 4.0),
Some(RoiEdge::Bottom)
);
assert_eq!(roi.edge_at(&t, pos2(50.0, 70.0), 4.0), Some(RoiEdge::Top));
let mut moved = roi.clone();
moved.move_edge(RoiEdge::Bottom, t.pixel_to_data(pos2(50.0, 10.0)));
let Roi::Rect { x, y } = moved else {
panic!("still a rect")
};
assert!((x.0 - 2.0).abs() < 1e-9 && (x.1 - 8.0).abs() < 1e-9);
assert!(
(y.0 - 1.0).abs() < 1e-9 && (y.1 - 7.0).abs() < 1e-9,
"y.min tracks the cursor to data 1; y.max untouched: {y:?}"
);
}
#[test]
fn circle_perimeter_resize_works_under_inverted_y() {
let mut circ = Roi::Circle {
center: (5.0, 5.0),
radius: 2.0,
};
let t = t_inv();
let handle = t.data_to_pixel(7.0, 5.0);
assert_eq!(circ.edge_at(&t, handle, 6.0), Some(RoiEdge::Vertex(1)));
circ.move_edge(
RoiEdge::Vertex(1),
t.pixel_to_data(t.data_to_pixel(8.0, 5.0)),
);
assert_eq!(
circ,
Roi::Circle {
center: (5.0, 5.0),
radius: 3.0
}
);
}
#[test]
fn rect_screen_rect_flips_y() {
let roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
let r = roi.screen_rect(&t());
assert!((r.left() - 20.0).abs() < 1e-3 && (r.right() - 80.0).abs() < 1e-3);
assert!((r.top() - 30.0).abs() < 1e-3 && (r.bottom() - 70.0).abs() < 1e-3);
}
#[test]
fn edge_at_grabs_nearest_edge() {
let roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
assert_eq!(
roi.edge_at(&t(), pos2(21.0, 50.0), 4.0),
Some(RoiEdge::Left)
);
assert_eq!(roi.edge_at(&t(), pos2(50.0, 31.0), 4.0), Some(RoiEdge::Top));
assert_eq!(roi.edge_at(&t(), pos2(50.0, 50.0), 4.0), None);
}
#[test]
fn edge_at_corner_takes_priority_over_edges() {
let roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
assert_eq!(
roi.edge_at(&t(), pos2(21.0, 31.0), 4.0),
Some(RoiEdge::TopLeft)
);
assert_eq!(
roi.edge_at(&t(), pos2(79.0, 31.0), 4.0),
Some(RoiEdge::TopRight)
);
assert_eq!(
roi.edge_at(&t(), pos2(21.0, 69.0), 4.0),
Some(RoiEdge::BottomLeft)
);
assert_eq!(
roi.edge_at(&t(), pos2(79.0, 69.0), 4.0),
Some(RoiEdge::BottomRight)
);
assert_eq!(
roi.edge_at(&t(), pos2(21.0, 50.0), 4.0),
Some(RoiEdge::Left)
);
assert_eq!(roi.edge_at(&t(), pos2(50.0, 31.0), 4.0), Some(RoiEdge::Top));
}
#[test]
fn move_edge_corner_resizes_both_axes() {
let mut roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
roi.move_edge(RoiEdge::TopRight, (9.0, 9.0));
assert_eq!(
roi,
Roi::Rect {
x: (2.0, 9.0),
y: (3.0, 9.0)
}
);
roi.move_edge(RoiEdge::BottomLeft, (1.0, 1.0));
assert_eq!(
roi,
Roi::Rect {
x: (1.0, 9.0),
y: (1.0, 9.0)
}
);
roi.move_edge(RoiEdge::TopRight, (-5.0, -5.0));
assert_eq!(
roi,
Roi::Rect {
x: (-5.0, 1.0),
y: (-5.0, 1.0)
}
);
}
#[test]
fn rect_side_edge_crosses_opposite_instead_of_collapsing() {
let mut roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
roi.move_edge(RoiEdge::Left, (10.0, 0.0));
assert_eq!(
roi,
Roi::Rect {
x: (8.0, 10.0), y: (3.0, 7.0), }
);
}
#[test]
fn hrange_edge_crosses_opposite_instead_of_collapsing() {
let mut roi = Roi::HRange { y: (3.0, 7.0) };
roi.move_edge(RoiEdge::Bottom, (0.0, 9.0));
assert_eq!(roi, Roi::HRange { y: (7.0, 9.0) });
}
#[test]
fn handle_centers_includes_four_corners() {
let roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
let centers = roi.handle_centers(&t());
assert_eq!(centers.len(), 8);
let corner = |c: Pos2| (c.x, c.y);
assert_eq!(corner(centers[4]), (20.0, 70.0)); assert_eq!(corner(centers[5]), (80.0, 70.0)); assert_eq!(corner(centers[6]), (20.0, 30.0)); assert_eq!(corner(centers[7]), (80.0, 30.0)); }
#[test]
fn handle_centers_keep_data_identity_under_inverted_y() {
let roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
let centers = roi.handle_centers(&t_inv());
let corner = |c: Pos2| (c.x, c.y);
assert_eq!(corner(centers[2]), (50.0, 30.0)); assert_eq!(corner(centers[3]), (50.0, 70.0)); assert_eq!(corner(centers[4]), (20.0, 30.0)); assert_eq!(corner(centers[6]), (20.0, 70.0)); }
#[test]
fn hrange_only_exposes_horizontal_edges() {
let roi = Roi::HRange { y: (3.0, 7.0) };
assert_eq!(
roi.edge_at(&t(), pos2(5.0, 70.0), 4.0),
Some(RoiEdge::Bottom)
);
assert_eq!(roi.edge_at(&t(), pos2(0.0, 50.0), 4.0), None);
}
#[test]
fn hline_is_grabbable_anywhere_along_its_span_and_moves_in_y() {
let mut roi = Roi::HLine { y: 4.0 };
assert_eq!(roi.edges(), vec![RoiEdge::Bottom]);
assert_eq!(
roi.edge_at(&t(), pos2(5.0, 61.0), 4.0),
Some(RoiEdge::Bottom)
);
assert_eq!(
roi.edge_at(&t(), pos2(95.0, 59.0), 4.0),
Some(RoiEdge::Bottom)
);
assert_eq!(roi.edge_at(&t(), pos2(50.0, 20.0), 4.0), None);
roi.move_edge(RoiEdge::Bottom, (7.0, 6.5));
assert_eq!(roi, Roi::HLine { y: 6.5 });
roi.translate(3.0, -1.5);
assert_eq!(roi, Roi::HLine { y: 5.0 });
}
#[test]
fn vline_is_grabbable_anywhere_along_its_span_and_moves_in_x() {
let mut roi = Roi::VLine { x: 2.0 };
assert_eq!(roi.edges(), vec![RoiEdge::Left]);
assert_eq!(
roi.edge_at(&t(), pos2(21.0, 10.0), 4.0),
Some(RoiEdge::Left)
);
assert_eq!(
roi.edge_at(&t(), pos2(19.0, 90.0), 4.0),
Some(RoiEdge::Left)
);
assert_eq!(roi.edge_at(&t(), pos2(60.0, 50.0), 4.0), None);
roi.move_edge(RoiEdge::Left, (8.0, 3.0));
assert_eq!(roi, Roi::VLine { x: 8.0 });
roi.translate(-1.0, 5.0);
assert_eq!(roi, Roi::VLine { x: 7.0 });
}
#[test]
fn line_roi_contains_only_on_the_line_and_has_one_handle() {
let h = Roi::HLine { y: 4.0 };
assert!(h.contains((123.0, 4.0))); assert!(!h.contains((123.0, 4.1)));
assert_eq!(h.handles().len(), 1);
assert_eq!(h.handles()[0].kind, HandleKind::Vertex);
let v = Roi::VLine { x: 2.0 };
assert!(v.contains((2.0, -50.0))); assert!(!v.contains((2.1, -50.0)));
assert_eq!(v.handles().len(), 1);
}
#[test]
fn move_edge_flips_but_stays_normalized() {
let mut roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
roi.move_edge(RoiEdge::Left, (12.0, 5.0));
assert_eq!(
roi,
Roi::Rect {
x: (8.0, 12.0),
y: (3.0, 7.0)
}
);
roi.move_edge(RoiEdge::Right, (9.0, 5.0));
assert_eq!(
roi,
Roi::Rect {
x: (8.0, 9.0),
y: (3.0, 7.0)
}
);
}
#[test]
fn point_roi_vertex_handle_moves_it() {
let mut roi = Roi::Point { x: 5.0, y: 5.0 };
roi.move_edge(RoiEdge::Vertex(0), (3.0, 4.0));
assert_eq!(roi, Roi::Point { x: 3.0, y: 4.0 });
}
#[test]
fn circle_handles_drag_center_and_radius() {
let mut roi = Roi::Circle {
center: (5.0, 5.0),
radius: 2.0,
};
roi.move_edge(RoiEdge::Vertex(1), (8.0, 9.0)); roi.move_edge(RoiEdge::Vertex(0), (1.0, 2.0));
if let Roi::Circle { center, radius } = roi {
assert_eq!(center, (1.0, 2.0));
assert!((radius - 5.0).abs() < 1e-9, "radius {radius}");
} else {
panic!("not a circle");
}
}
#[test]
fn ellipse_handles_drag_center_and_each_semi_axis() {
let mut roi = Roi::Ellipse {
center: (0.0, 0.0),
radii: (3.0, 4.0),
orientation: 0.0,
};
roi.move_edge(RoiEdge::Vertex(1), (5.0, 0.0)); roi.move_edge(RoiEdge::Vertex(2), (0.0, 7.0)); roi.move_edge(RoiEdge::Vertex(0), (2.0, 3.0)); assert_eq!(
roi,
Roi::Ellipse {
center: (2.0, 3.0),
radii: (5.0, 7.0),
orientation: 0.0,
}
);
}
#[test]
fn line_roi_endpoints_move_independently() {
let mut roi = Roi::Line {
start: (0.0, 0.0),
end: (10.0, 10.0),
};
roi.move_edge(RoiEdge::Vertex(0), (1.0, 2.0));
roi.move_edge(RoiEdge::Vertex(1), (9.0, 8.0));
assert_eq!(
roi,
Roi::Line {
start: (1.0, 2.0),
end: (9.0, 8.0)
}
);
}
#[test]
fn polygon_vertex_move_updates_specific_vertex() {
let mut roi = Roi::Polygon {
vertices: vec![(0.0, 0.0), (5.0, 0.0), (5.0, 5.0)],
};
roi.move_edge(RoiEdge::Vertex(1), (6.0, 1.0));
assert_eq!(
roi,
Roi::Polygon {
vertices: vec![(0.0, 0.0), (6.0, 1.0), (5.0, 5.0)]
}
);
}
#[test]
fn arc_handles_drag_radius_weight_and_angles() {
use std::f64::consts::{FRAC_PI_2, PI};
let base = Roi::Arc {
center: (0.0, 0.0),
radius: 3.0,
weight: 2.0,
start_angle: 0.0,
end_angle: FRAC_PI_2,
};
let approx =
|a: f64, b: f64, what: &str| assert!((a - b).abs() < 1e-9, "{what}: {a} vs {b}");
let mut roi = base.clone();
roi.move_edge(RoiEdge::Vertex(0), (5.0, 0.0));
if let Roi::Arc {
radius,
weight,
start_angle,
end_angle,
..
} = roi
{
approx(radius, 5.0, "mid radius");
approx(weight, 2.0, "mid keeps weight");
approx(start_angle, 0.0, "mid keeps start");
approx(end_angle, FRAC_PI_2, "mid keeps end");
} else {
panic!("not an arc");
}
let mut roi = base.clone();
roi.move_edge(RoiEdge::Vertex(1), (6.0, 0.0));
if let Roi::Arc { radius, weight, .. } = roi {
approx(radius, 3.0, "weight keeps radius");
approx(weight, 6.0, "weight");
approx(
arc_inner_radius(radius, weight),
0.0,
"reported inner clamps",
);
approx(arc_outer_radius(radius, weight), 6.0, "reported outer");
} else {
panic!("not an arc");
}
let mut roi = base.clone();
roi.move_edge(RoiEdge::Vertex(2), (0.0, 5.0));
roi.move_edge(RoiEdge::Vertex(3), (-5.0, 0.0));
if let Roi::Arc {
radius,
weight,
start_angle,
end_angle,
..
} = roi
{
approx(start_angle, FRAC_PI_2, "start angle");
approx(end_angle, PI, "end angle");
approx(radius, 3.0, "angle keeps radius");
approx(weight, 2.0, "angle keeps weight");
} else {
panic!("not an arc");
}
}
#[test]
fn arc_weight_survives_a_reported_inner_radius_clamp() {
use std::f64::consts::FRAC_PI_2;
let approx =
|a: f64, b: f64, what: &str| assert!((a - b).abs() < 1e-9, "{what}: {a} vs {b}");
let mut roi = Roi::Arc {
center: (0.0, 0.0),
radius: 3.0,
weight: 2.0,
start_angle: 0.0,
end_angle: FRAC_PI_2,
};
roi.move_edge(RoiEdge::Vertex(1), (10.0, 0.0));
if let Roi::Arc { radius, weight, .. } = roi {
approx(radius, 3.0, "radius unchanged by the weight drag");
approx(weight, 14.0, "true weight stored, not the clamped 10");
approx(
arc_inner_radius(radius, weight),
0.0,
"reported inner clamps at 0",
);
approx(arc_outer_radius(radius, weight), 10.0, "reported outer");
} else {
panic!("not an arc");
}
roi.move_edge(RoiEdge::Vertex(0), (8.0, 0.0));
if let Roi::Arc { radius, weight, .. } = roi {
approx(radius, 8.0, "radius moved to 8");
approx(weight, 14.0, "weight conserved across the radius drag");
approx(arc_outer_radius(radius, weight), 15.0, "outer = 8 + 7");
approx(
arc_inner_radius(radius, weight),
1.0,
"inner un-clamps to 8 − 7",
);
} else {
panic!("not an arc");
}
}
#[test]
fn arc_angle_drag_stays_coherent_across_the_branch_cut() {
let approx =
|a: f64, b: f64, what: &str| assert!((a - b).abs() < 1e-9, "{what}: {a} vs {b}");
let mut roi = Roi::Arc {
center: (0.0, 0.0),
radius: 3.0,
weight: 2.0,
start_angle: 3.0,
end_angle: std::f64::consts::FRAC_PI_2,
};
let target = -3.0f64;
roi.move_edge(RoiEdge::Vertex(2), (5.0 * target.cos(), 5.0 * target.sin()));
if let Roi::Arc { start_angle, .. } = roi {
approx(
start_angle,
3.0 + (std::f64::consts::TAU - 6.0),
"coherent start",
);
} else {
panic!("not an arc");
}
let mut roi = Roi::Arc {
center: (0.0, 0.0),
radius: 3.0,
weight: 2.0,
start_angle: 0.0,
end_angle: -3.0,
};
let target = 3.0f64;
roi.move_edge(RoiEdge::Vertex(3), (5.0 * target.cos(), 5.0 * target.sin()));
if let Roi::Arc { end_angle, .. } = roi {
approx(
end_angle,
-3.0 - (std::f64::consts::TAU - 6.0),
"coherent end",
);
} else {
panic!("not an arc");
}
let mut roi = Roi::Arc {
center: (0.0, 0.0),
radius: 3.0,
weight: 2.0,
start_angle: 3.0 + (std::f64::consts::TAU - 6.0),
end_angle: 0.0,
};
let target = 3.4 - std::f64::consts::TAU; roi.move_edge(RoiEdge::Vertex(2), (5.0 * target.cos(), 5.0 * target.sin()));
if let Roi::Arc { start_angle, .. } = roi {
approx(start_angle, 3.4, "accumulated start");
} else {
panic!("not an arc");
}
}
#[test]
fn band_handles_drag_endpoints_and_width() {
let mut roi = Roi::Band {
begin: (0.0, 0.0),
end: (10.0, 0.0),
width: 4.0,
};
roi.move_edge(RoiEdge::Vertex(0), (1.0, 1.0));
roi.move_edge(RoiEdge::Vertex(1), (9.0, 1.0));
assert_eq!(
roi,
Roi::Band {
begin: (1.0, 1.0),
end: (9.0, 1.0),
width: 4.0,
}
);
roi.move_edge(RoiEdge::Vertex(2), (5.0, 4.0));
if let Roi::Band { width, .. } = roi {
assert!((width - 6.0).abs() < 1e-9, "width {width}");
} else {
panic!("not a band");
}
roi.move_edge(RoiEdge::Vertex(3), (5.0, -2.0));
if let Roi::Band { width, .. } = roi {
assert!((width - 6.0).abs() < 1e-9, "width {width}");
} else {
panic!("not a band");
}
roi.move_edge(RoiEdge::Vertex(3), (5.0, 5.0));
if let Roi::Band { width, .. } = roi {
assert!((width - 0.0).abs() < 1e-9, "clamped width {width}");
} else {
panic!("not a band");
}
}
#[test]
fn edge_at_finds_line_endpoint() {
let roi = Roi::Line {
start: (2.0, 5.0),
end: (8.0, 5.0),
};
assert_eq!(
roi.edge_at(&t(), pos2(21.0, 50.0), 4.0),
Some(RoiEdge::Vertex(0))
);
assert_eq!(
roi.edge_at(&t(), pos2(79.0, 50.0), 4.0),
Some(RoiEdge::Vertex(1))
);
assert_eq!(roi.edge_at(&t(), pos2(50.0, 50.0), 4.0), None); }
#[test]
fn rect_contains_inside_edge_outside() {
let roi = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
assert!(roi.contains((5.0, 5.0))); assert!(roi.contains((2.0, 5.0))); assert!(roi.contains((8.0, 7.0))); assert!(!roi.contains((1.999, 5.0))); assert!(!roi.contains((5.0, 7.001))); }
#[test]
fn band_contains_ignores_spanned_axis() {
let h = Roi::HRange { y: (3.0, 7.0) };
assert!(h.contains((1e9, 5.0))); assert!(h.contains((0.0, 3.0))); assert!(!h.contains((0.0, 2.999))); let v = Roi::VRange { x: (2.0, 8.0) };
assert!(v.contains((5.0, -1e9))); assert!(!v.contains((8.001, 0.0))); }
#[test]
fn point_contains_requires_exact_match() {
let roi = Roi::Point { x: 5.0, y: 5.0 };
assert!(roi.contains((5.0, 5.0)));
assert!(!roi.contains((5.0, 5.000001)));
}
#[test]
fn cross_contains_on_either_crosshair() {
let roi = Roi::Cross { center: (5.0, 5.0) };
assert!(roi.contains((5.0, 5.0))); assert!(roi.contains((5.0, -100.0))); assert!(roi.contains((100.0, 5.0))); assert!(!roi.contains((4.999, 5.001))); }
#[test]
fn circle_contains_inside_edge_outside() {
let roi = Roi::Circle {
center: (5.0, 5.0),
radius: 2.0,
};
assert!(roi.contains((5.0, 5.0))); assert!(roi.contains((7.0, 5.0))); assert!(roi.contains((6.0, 6.0))); assert!(!roi.contains((7.001, 5.0))); }
#[test]
fn ellipse_contains_inside_edge_outside() {
let roi = Roi::Ellipse {
center: (5.0, 5.0),
radii: (4.0, 2.0), orientation: 0.0,
};
assert!(roi.contains((5.0, 5.0))); assert!(roi.contains((9.0, 5.0))); assert!(roi.contains((5.0, 7.0))); assert!(!roi.contains((5.0, 7.001))); assert!(!roi.contains((9.001, 5.0))); let degenerate = Roi::Ellipse {
center: (0.0, 0.0),
radii: (0.0, 1.0),
orientation: 0.0,
};
assert!(!degenerate.contains((0.0, 0.0)));
}
#[test]
fn ellipse_axis0_handle_drag_off_axis_rotates_and_resizes() {
use std::f64::consts::FRAC_PI_4;
let mut roi = Roi::Ellipse {
center: (0.0, 0.0),
radii: (3.0, 2.0),
orientation: 0.0,
};
let d = 5.0_f64;
roi.move_edge(
RoiEdge::Vertex(1),
(d * FRAC_PI_4.cos(), d * FRAC_PI_4.sin()),
);
match roi {
Roi::Ellipse {
center,
radii,
orientation,
} => {
assert!(center.0.abs() < 1e-9 && center.1.abs() < 1e-9, "{center:?}");
assert!((radii.0 - 5.0).abs() < 1e-9, "axis0 = cursor distance");
assert!((radii.1 - 2.0).abs() < 1e-9, "axis1 unchanged");
assert!(
(orientation - FRAC_PI_4).abs() < 1e-9,
"orientation = cursor angle: {orientation}"
);
}
other => panic!("{other:?}"),
}
}
#[test]
fn ellipse_axis1_handle_drag_sets_perpendicular_orientation() {
use std::f64::consts::FRAC_PI_2;
let mut roi = Roi::Ellipse {
center: (0.0, 0.0),
radii: (3.0, 2.0),
orientation: 0.0,
};
roi.move_edge(RoiEdge::Vertex(2), (4.0, 0.0));
match roi {
Roi::Ellipse {
radii, orientation, ..
} => {
assert!((radii.1 - 4.0).abs() < 1e-9, "axis1 = 4: {radii:?}");
assert!(
(orientation + FRAC_PI_2).abs() < 1e-9,
"axis1→+x ⟹ θ = −π/2: {orientation}"
);
}
other => panic!("{other:?}"),
}
}
#[test]
fn ellipse_contains_respects_orientation() {
use std::f64::consts::FRAC_PI_2;
let roi = Roi::Ellipse {
center: (0.0, 0.0),
radii: (4.0, 2.0),
orientation: FRAC_PI_2,
};
assert!(roi.contains((0.0, 4.0))); assert!(!roi.contains((0.0, 4.001)));
assert!(roi.contains((2.0, 0.0))); assert!(!roi.contains((2.001, 0.0)));
assert!(!roi.contains((4.0, 0.0)));
}
#[test]
fn ellipse_handles_follow_orientation() {
use std::f64::consts::FRAC_PI_2;
let roi = Roi::Ellipse {
center: (1.0, 1.0),
radii: (4.0, 2.0),
orientation: FRAC_PI_2,
};
let hs = roi.handles();
assert!(
(hs[0].pos[0] - 1.0).abs() < 1e-9 && (hs[0].pos[1] - 5.0).abs() < 1e-9,
"{:?}",
hs[0].pos
);
assert!(
(hs[1].pos[0] + 1.0).abs() < 1e-9 && (hs[1].pos[1] - 1.0).abs() < 1e-9,
"{:?}",
hs[1].pos
);
assert!((hs[2].pos[0] - 1.0).abs() < 1e-9 && (hs[2].pos[1] - 1.0).abs() < 1e-9);
}
#[test]
fn ellipse_aabb_half_extents_axis_aligned_and_rotated() {
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4};
let (hx, hy) = ellipse_aabb_half_extents((4.0, 2.0), 0.0);
assert!((hx - 4.0).abs() < 1e-9 && (hy - 2.0).abs() < 1e-9);
let (hx, hy) = ellipse_aabb_half_extents((4.0, 2.0), FRAC_PI_2);
assert!((hx - 2.0).abs() < 1e-9 && (hy - 4.0).abs() < 1e-9);
let (hx, hy) = ellipse_aabb_half_extents((3.0, 3.0), FRAC_PI_4);
assert!((hx - 3.0).abs() < 1e-9 && (hy - 3.0).abs() < 1e-9);
}
#[test]
fn polygon_contains_inside_outside() {
let roi = Roi::Polygon {
vertices: vec![(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)],
};
assert!(roi.contains((2.0, 2.0))); assert!(!roi.contains((5.0, 2.0))); assert!(!roi.contains((2.0, -1.0))); let tri = Roi::Polygon {
vertices: vec![(0.0, 0.0), (4.0, 0.0), (0.0, 4.0)],
};
assert!(tri.contains((1.0, 1.0))); assert!(!tri.contains((3.0, 3.0))); let line = Roi::Polygon {
vertices: vec![(0.0, 0.0), (4.0, 0.0)],
};
assert!(!line.contains((2.0, 0.0)));
}
#[test]
fn line_contains_unit_square_intersection() {
let roi = Roi::Line {
start: (2.0, 5.0),
end: (8.0, 5.0),
};
assert!(roi.contains((4.0, 5.0)));
assert!(!roi.contains((4.0, 4.5)));
assert!(!roi.contains((4.0, 6.0)));
assert!(!roi.contains((9.0, 4.5)));
}
#[test]
fn line_contains_bbox_gate_trims_beyond_endpoint_strip() {
let roi = Roi::Line {
start: (2.0, 2.0),
end: (8.0, 8.0),
};
assert!(!roi.contains((1.5, 1.5)));
assert!(roi.contains((5.0, 5.0)));
assert!(roi.contains((5.0, 4.5)));
}
fn kinds(handles: &[RoiHandle]) -> Vec<HandleKind> {
handles.iter().map(|h| h.kind).collect()
}
#[test]
fn handle_counts_and_roles_per_kind() {
use HandleKind::*;
let rect = Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
};
assert_eq!(
kinds(&rect.handles()),
vec![Vertex, Vertex, Vertex, Vertex, Translate]
);
let corners: Vec<[f64; 2]> = rect.handles()[..4].iter().map(|h| h.pos).collect();
assert!(corners.contains(&[2.0, 3.0]));
assert!(corners.contains(&[8.0, 7.0]));
assert_eq!(rect.handles()[4].pos, [5.0, 5.0]);
assert_eq!(
kinds(&Roi::HRange { y: (3.0, 7.0) }.handles()),
vec![Edge, Edge, Center]
);
assert_eq!(
kinds(&Roi::VRange { x: (2.0, 8.0) }.handles()),
vec![Edge, Edge, Center]
);
assert_eq!(
kinds(&Roi::Point { x: 1.0, y: 2.0 }.handles()),
vec![Vertex]
);
assert_eq!(
kinds(&Roi::Cross { center: (1.0, 2.0) }.handles()),
vec![Vertex]
);
assert_eq!(
kinds(
&Roi::Line {
start: (0.0, 0.0),
end: (4.0, 2.0),
}
.handles()
),
vec![Vertex, Vertex, Translate]
);
let poly = Roi::Polygon {
vertices: vec![(0.0, 0.0), (4.0, 0.0), (4.0, 4.0)],
};
assert_eq!(
kinds(&poly.handles()),
vec![Vertex, Vertex, Vertex, Translate]
);
assert!(
Roi::Polygon {
vertices: Vec::new()
}
.handles()
.is_empty()
);
assert_eq!(
kinds(
&Roi::Circle {
center: (5.0, 5.0),
radius: 2.0,
}
.handles()
),
vec![Vertex, Translate]
);
assert_eq!(
kinds(
&Roi::Ellipse {
center: (5.0, 5.0),
radii: (4.0, 2.0),
orientation: 0.0,
}
.handles()
),
vec![Vertex, Vertex, Translate]
);
}
#[test]
fn translate_moves_every_2d_handle_by_the_same_delta() {
let rois = [
Roi::Rect {
x: (2.0, 8.0),
y: (3.0, 7.0),
},
Roi::Point { x: 1.0, y: 2.0 },
Roi::Cross { center: (1.0, 2.0) },
Roi::Line {
start: (0.0, 0.0),
end: (4.0, 2.0),
},
Roi::Polygon {
vertices: vec![(0.0, 0.0), (4.0, 0.0), (4.0, 4.0)],
},
Roi::Circle {
center: (5.0, 5.0),
radius: 2.0,
},
Roi::Ellipse {
center: (5.0, 5.0),
radii: (4.0, 2.0),
orientation: 0.0,
},
];
let (dx, dy) = (1.5, -0.5);
for roi in rois {
let before = roi.handles();
let mut moved = roi.clone();
moved.translate(dx, dy);
let after = moved.handles();
assert_eq!(before.len(), after.len());
for (b, a) in before.iter().zip(&after) {
assert_eq!(a.kind, b.kind);
assert!((a.pos[0] - (b.pos[0] + dx)).abs() < 1e-9, "{roi:?}");
assert!((a.pos[1] - (b.pos[1] + dy)).abs() < 1e-9, "{roi:?}");
}
}
}
#[test]
fn arc_contains_inside_outside_ring_and_sweep() {
let arc = Roi::Arc {
center: (0.0, 0.0),
radius: 1.5,
weight: 1.0,
start_angle: 0.0,
end_angle: std::f64::consts::FRAC_PI_2,
};
assert!(arc.contains((1.5, 0.0))); assert!(arc.contains((0.0, 1.5))); let d = std::f64::consts::FRAC_1_SQRT_2 * 1.5;
assert!(arc.contains((d, d))); assert!(!arc.contains((0.5, 0.0))); assert!(!arc.contains((2.5, 0.0))); assert!(!arc.contains((-1.5, 0.0))); assert!(!arc.contains((0.0, -1.5))); }
#[test]
fn arc_contains_handles_the_pi_branch_wrap() {
let arc = Roi::Arc {
center: (0.0, 0.0),
radius: 1.5,
weight: 1.0,
start_angle: 3.0 * std::f64::consts::FRAC_PI_4,
end_angle: 5.0 * std::f64::consts::FRAC_PI_4,
};
assert!(arc.contains((-1.5, 0.0))); assert!(!arc.contains((1.5, 0.0))); assert!(!arc.contains((0.0, -1.5))); }
#[test]
fn band_contains_axis_aligned_inside_edge_outside() {
let band = Roi::Band {
begin: (0.0, 0.0),
end: (4.0, 0.0),
width: 2.0,
};
assert!(band.contains((2.0, 0.0))); assert!(band.contains((2.0, 0.5))); assert!(!band.contains((2.0, 1.5))); assert!(!band.contains((2.0, -1.5))); assert!(!band.contains((5.0, 0.0))); assert!(!band.contains((-0.5, 0.0))); }
#[test]
fn band_contains_rotated_band() {
let band = Roi::Band {
begin: (0.0, 0.0),
end: (0.0, 4.0),
width: 2.0,
};
assert!(band.contains((0.0, 2.0))); assert!(!band.contains((1.5, 2.0))); assert!(!band.contains((0.0, 5.0))); }
#[test]
fn band_lines_horizontal_band_centres_on_the_segment() {
let band = Roi::Band {
begin: (0.0, 0.0),
end: (4.0, 0.0),
width: 2.0,
};
match band.band_lines().expect("band") {
BandLines::Sloped {
slope,
middle,
edges,
} => {
assert_eq!(slope, 0.0);
assert_eq!(middle, 0.0);
assert_eq!(edges, (-1.0, 1.0));
}
other => panic!("{other:?}"),
}
}
#[test]
fn band_lines_vertical_band_uses_x_constants() {
let band = Roi::Band {
begin: (0.0, 0.0),
end: (0.0, 4.0),
width: 2.0,
};
match band.band_lines().expect("band") {
BandLines::Vertical { middle, edges } => {
assert_eq!(middle, 0.0);
assert_eq!(edges, (1.0, -1.0));
}
other => panic!("{other:?}"),
}
}
#[test]
fn band_lines_diagonal_band_edges_are_parallel_offsets() {
let band = Roi::Band {
begin: (0.0, 0.0),
end: (2.0, 2.0),
width: 2.0 * std::f64::consts::SQRT_2,
};
match band.band_lines().expect("band") {
BandLines::Sloped {
slope,
middle,
edges,
} => {
assert!((slope - 1.0).abs() <= 1e-12, "slope={slope}");
assert!(middle.abs() <= 1e-12, "middle={middle}");
assert!((edges.0 - (-2.0)).abs() <= 1e-12, "e0={}", edges.0);
assert!((edges.1 - 2.0).abs() <= 1e-12, "e1={}", edges.1);
}
other => panic!("{other:?}"),
}
}
#[test]
fn band_lines_is_none_for_non_band() {
let rect = Roi::Rect {
x: (0.0, 1.0),
y: (0.0, 1.0),
};
assert_eq!(rect.band_lines(), None);
}
#[test]
fn band_unbounded_segments_span_horizontal_band_across_x() {
let band = Roi::Band {
begin: (0.0, 0.0),
end: (4.0, 0.0),
width: 2.0,
};
let segs = band
.band_unbounded_segments((-5.0, 5.0), (-9.0, 9.0))
.expect("band has unbounded segments");
assert_eq!(segs[0], [(-5.0, 0.0), (5.0, 0.0)]);
assert_eq!(segs[1], [(-5.0, -1.0), (5.0, -1.0)]);
assert_eq!(segs[2], [(-5.0, 1.0), (5.0, 1.0)]);
}
#[test]
fn band_unbounded_segments_span_vertical_band_across_y() {
let band = Roi::Band {
begin: (3.0, 0.0),
end: (3.0, 5.0),
width: 2.0,
};
let segs = band
.band_unbounded_segments((-1.0, 1.0), (-8.0, 8.0))
.expect("vertical band has unbounded segments");
assert_eq!(segs[0], [(3.0, -8.0), (3.0, 8.0)]);
assert_eq!(segs[1], [(4.0, -8.0), (4.0, 8.0)]);
assert_eq!(segs[2], [(2.0, -8.0), (2.0, 8.0)]);
}
#[test]
fn band_unbounded_segments_is_none_for_non_band() {
assert_eq!(
Roi::Point { x: 1.0, y: 2.0 }.band_unbounded_segments((0.0, 1.0), (0.0, 1.0)),
None
);
}
#[test]
fn arc_and_band_handle_counts() {
use HandleKind::*;
let arc = Roi::Arc {
center: (0.0, 0.0),
radius: 1.5,
weight: 1.0,
start_angle: 0.0,
end_angle: std::f64::consts::FRAC_PI_2,
};
assert_eq!(
kinds(&arc.handles()),
vec![Vertex, Vertex, Vertex, Vertex, Translate]
);
assert_eq!(arc.handles().last().unwrap().pos, [0.0, 0.0]);
let band = Roi::Band {
begin: (0.0, 0.0),
end: (4.0, 0.0),
width: 2.0,
};
assert_eq!(
kinds(&band.handles()),
vec![Vertex, Vertex, Vertex, Vertex, Translate]
);
assert_eq!(band.handles()[0].pos, [0.0, 0.0]);
assert_eq!(band.handles()[1].pos, [4.0, 0.0]);
assert_eq!(band.handles().last().unwrap().pos, [2.0, 0.0]);
}
#[test]
fn arc_and_band_translate_move_every_handle() {
let (dx, dy) = (2.0, -1.0);
let rois = [
Roi::Arc {
center: (1.0, 1.0),
radius: 1.5,
weight: 1.0,
start_angle: 0.0,
end_angle: std::f64::consts::FRAC_PI_2,
},
Roi::Band {
begin: (0.0, 0.0),
end: (4.0, 2.0),
width: 1.5,
},
];
for roi in rois {
let before = roi.handles();
let mut moved = roi.clone();
moved.translate(dx, dy);
let after = moved.handles();
assert_eq!(before.len(), after.len());
for (b, a) in before.iter().zip(&after) {
assert_eq!(a.kind, b.kind);
assert!((a.pos[0] - (b.pos[0] + dx)).abs() < 1e-9, "{roi:?}");
assert!((a.pos[1] - (b.pos[1] + dy)).abs() < 1e-9, "{roi:?}");
}
}
}
#[test]
fn translate_band_rois_move_only_the_bounded_axis() {
let mut h = Roi::HRange { y: (3.0, 7.0) };
h.translate(1.5, -0.5);
assert_eq!(h, Roi::HRange { y: (2.5, 6.5) });
let mut v = Roi::VRange { x: (2.0, 8.0) };
v.translate(1.5, -0.5);
assert_eq!(v, Roi::VRange { x: (3.5, 9.5) });
}
#[test]
fn new_managed_roi_uses_silx_style_defaults() {
let r = ManagedRoi::new(Roi::Point { x: 0.0, y: 0.0 });
assert_eq!(r.color, None);
assert!(r.name.is_empty());
assert!(!r.selected);
assert_eq!(r.line_width, 1.0);
assert_eq!(r.line_style, RoiLineStyle::Solid);
assert!(!r.fill);
}
#[test]
fn roi_line_style_maps_to_painter_line_style() {
assert_eq!(RoiLineStyle::Solid.to_line_style(), LineStyle::Solid);
assert_eq!(RoiLineStyle::Dashed.to_line_style(), LineStyle::Dashed);
assert_eq!(RoiLineStyle::Dotted.to_line_style(), LineStyle::Dotted);
}
fn sample_arc() -> Roi {
Roi::Arc {
center: (0.0, 0.0),
radius: 1.5,
weight: 1.0,
start_angle: 0.0,
end_angle: std::f64::consts::FRAC_PI_2,
}
}
fn sample_band() -> Roi {
Roi::Band {
begin: (0.0, 0.0),
end: (4.0, 0.0),
width: 1.0,
}
}
#[test]
fn available_interaction_modes_match_silx_per_kind() {
assert_eq!(
sample_arc().available_interaction_modes(),
&[
RoiInteractionMode::ArcThreePoint,
RoiInteractionMode::ArcPolar,
RoiInteractionMode::ArcTranslation,
]
);
assert_eq!(
sample_band().available_interaction_modes(),
&[
RoiInteractionMode::BandBounded,
RoiInteractionMode::BandUnbounded,
]
);
assert!(
Roi::Point { x: 0.0, y: 0.0 }
.available_interaction_modes()
.is_empty()
);
assert!(
Roi::Rect {
x: (0.0, 1.0),
y: (0.0, 1.0),
}
.available_interaction_modes()
.is_empty()
);
}
#[test]
fn default_interaction_mode_is_the_silx_init_mode() {
assert_eq!(
sample_arc().default_interaction_mode(),
Some(RoiInteractionMode::ArcThreePoint)
);
assert_eq!(
sample_band().default_interaction_mode(),
Some(RoiInteractionMode::BandBounded)
);
assert_eq!(
Roi::Point { x: 0.0, y: 0.0 }.default_interaction_mode(),
None
);
}
#[test]
fn new_managed_roi_seeds_the_kind_default_mode() {
assert_eq!(
ManagedRoi::new(sample_arc()).interaction_mode(),
Some(RoiInteractionMode::ArcThreePoint)
);
assert_eq!(
ManagedRoi::new(sample_band()).interaction_mode(),
Some(RoiInteractionMode::BandBounded)
);
assert_eq!(
ManagedRoi::new(Roi::Point { x: 0.0, y: 0.0 }).interaction_mode(),
None
);
}
#[test]
fn set_interaction_mode_accepts_available_and_rejects_foreign() {
let mut arc = ManagedRoi::new(sample_arc());
assert!(arc.set_interaction_mode(RoiInteractionMode::ArcPolar));
assert_eq!(arc.interaction_mode(), Some(RoiInteractionMode::ArcPolar));
assert!(!arc.set_interaction_mode(RoiInteractionMode::BandBounded));
assert_eq!(arc.interaction_mode(), Some(RoiInteractionMode::ArcPolar));
let mut point = ManagedRoi::new(Roi::Point { x: 0.0, y: 0.0 });
assert!(!point.set_interaction_mode(RoiInteractionMode::ArcThreePoint));
assert_eq!(point.interaction_mode(), None);
}
#[test]
fn mode_label_and_description_match_silx_strings() {
assert_eq!(RoiInteractionMode::ArcThreePoint.label(), "3 points");
assert_eq!(
RoiInteractionMode::ArcThreePoint.description(),
"Provides 3 points to define the main radius circle"
);
assert_eq!(RoiInteractionMode::ArcPolar.label(), "Polar");
assert_eq!(
RoiInteractionMode::ArcPolar.description(),
"Provides anchors to edit the ROI in polar coords"
);
assert_eq!(RoiInteractionMode::ArcTranslation.label(), "Translation");
assert_eq!(
RoiInteractionMode::ArcTranslation.description(),
"Provides anchors to only move the ROI"
);
assert_eq!(RoiInteractionMode::BandBounded.label(), "Bounded");
assert_eq!(
RoiInteractionMode::BandBounded.description(),
"Band is bounded on both sides"
);
assert_eq!(RoiInteractionMode::BandUnbounded.label(), "Unbounded");
assert_eq!(
RoiInteractionMode::BandUnbounded.description(),
"Band is unbounded on both sides"
);
}
}