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) },
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),
},
Arc {
center: (f64, f64),
inner_radius: f64,
outer_radius: 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,
}
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::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 } => {
let a = t.data_to_pixel(center.0 - radii.0, center.1 - radii.1);
let b = t.data_to_pixel(center.0 + radii.0, center.1 + radii.1);
Rect::from_two_pos(a, b)
}
Roi::Arc {
center,
outer_radius,
..
} => {
let a = t.data_to_pixel(center.0 - outer_radius, center.1 - outer_radius);
let b = t.data_to_pixel(center.0 + outer_radius, center.1 + outer_radius);
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::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 } => match index {
0 => *center,
1 => (center.0 + radii.0, center.1),
2 => (center.0, center.1 + radii.1),
_ => 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)
}
_ => 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()),
)
}
_ => unreachable!("outer match restricts this arm to Rect/HRange/VRange"),
};
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 } => match edge {
RoiEdge::Left => x.0 = dx.min(x.1),
RoiEdge::Right => x.1 = dx.max(x.0),
RoiEdge::Bottom => y.0 = dy.min(y.1),
RoiEdge::Top => y.1 = dy.max(y.0),
RoiEdge::BottomLeft => {
x.0 = dx.min(x.1);
y.0 = dy.min(y.1);
}
RoiEdge::BottomRight => {
x.1 = dx.max(x.0);
y.0 = dy.min(y.1);
}
RoiEdge::TopLeft => {
x.0 = dx.min(x.1);
y.1 = dy.max(y.0);
}
RoiEdge::TopRight => {
x.1 = dx.max(x.0);
y.1 = dy.max(y.0);
}
RoiEdge::Vertex(_) => {}
},
Roi::HRange { y } => match edge {
RoiEdge::Bottom => y.0 = dy.min(y.1),
RoiEdge::Top => y.1 = dy.max(y.0),
_ => {}
},
Roi::VRange { x } => match edge {
RoiEdge::Left => x.0 = dx.min(x.1),
RoiEdge::Right => x.1 = dx.max(x.0),
_ => {}
},
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 } => match edge {
RoiEdge::Vertex(0) => *center = (dx, dy),
RoiEdge::Vertex(1) => radii.0 = (dx - center.0).abs(),
RoiEdge::Vertex(2) => radii.1 = (dy - center.1).abs(),
_ => {}
},
Roi::Arc {
center,
inner_radius,
outer_radius,
start_angle,
end_angle,
} => {
let (cx, cy) = *center;
let mid = (*inner_radius + *outer_radius) * 0.5;
match edge {
RoiEdge::Vertex(0) => {
let r = (dx - cx).hypot(dy - cy);
let w = *outer_radius - *inner_radius;
*inner_radius = (r - w * 0.5).max(0.0);
*outer_radius = r + w * 0.5;
}
RoiEdge::Vertex(1) => {
let d = (dx - cx).hypot(dy - cy);
let w = 2.0 * (d - mid).abs();
*inner_radius = (mid - w * 0.5).max(0.0);
*outer_radius = mid + w * 0.5;
}
RoiEdge::Vertex(2) => *start_angle = (dy - cy).atan2(dx - cx),
RoiEdge::Vertex(3) => *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 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::Point { x: px, y: py } => x == *px && y == *py,
Roi::Cross { center } => x == center.0 || y == center.1,
Roi::Line { start, end } => 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 } => {
let major = radii.0.max(radii.1);
let minor = radii.0.min(radii.1);
if major <= 0.0 || minor <= 0.0 {
return false;
}
let (dx, dy) = (x - center.0, y - center.1);
(dx * dx) / (major * major) + (dy * dy) / (minor * minor) <= 1.0
}
Roi::Arc {
center,
inner_radius,
outer_radius,
start_angle,
end_angle,
} => arc_contains(
*center,
*inner_radius,
*outer_radius,
*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::Point { x, y } => vec![v((*x, *y))],
Roi::Cross { center: c } => vec![center(*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 } => vec![
v((c.0 + radii.0, c.1)),
v((c.0, c.1 + radii.1)),
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::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, 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 fill: bool,
}
impl ManagedRoi {
pub fn new(roi: Roi) -> Self {
Self {
roi,
color: None,
name: String::new(),
selected: false,
line_width: DEFAULT_ROI_LINE_WIDTH,
line_style: RoiLineStyle::default(),
fill: false,
}
}
}
fn arc_vertex_pos(roi: &Roi, index: usize) -> Option<(f64, f64)> {
let Roi::Arc {
center,
inner_radius,
outer_radius,
start_angle,
end_angle,
} = roi
else {
return None;
};
let radius = (inner_radius + outer_radius) * 0.5;
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(*outer_radius, 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 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 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: (1.0, 1.0),
y: (1.0, 1.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 move_edge_clamps_to_stay_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, 8.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),
};
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),
}
);
}
#[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),
inner_radius: 2.0,
outer_radius: 4.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 {
inner_radius,
outer_radius,
start_angle,
end_angle,
..
} = roi
{
approx(inner_radius, 4.0, "mid inner");
approx(outer_radius, 6.0, "mid outer");
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 {
inner_radius,
outer_radius,
..
} = roi
{
approx(inner_radius, 0.0, "weight inner");
approx(outer_radius, 6.0, "weight outer");
} else {
panic!("not an arc");
}
let mut roi = base.clone();
roi.move_edge(RoiEdge::Vertex(1), (10.0, 0.0));
if let Roi::Arc {
inner_radius,
outer_radius,
..
} = roi
{
approx(inner_radius, 0.0, "weight clamp inner");
approx(outer_radius, 10.0, "weight clamp 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 {
inner_radius,
outer_radius,
start_angle,
end_angle,
..
} = roi
{
approx(start_angle, FRAC_PI_2, "start angle");
approx(end_angle, PI, "end angle");
approx(inner_radius, 2.0, "angle keeps inner");
approx(outer_radius, 4.0, "angle keeps outer");
} 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), };
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),
};
assert!(!degenerate.contains((0.0, 0.0)));
}
#[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, 4.5)));
assert!(roi.contains((4.0, 5.0)));
assert!(!roi.contains((4.0, 6.0)));
assert!(!roi.contains((9.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![Center]
);
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),
}
.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),
},
];
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),
inner_radius: 1.0,
outer_radius: 2.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),
inner_radius: 1.0,
outer_radius: 2.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 arc_and_band_handle_counts() {
use HandleKind::*;
let arc = Roi::Arc {
center: (0.0, 0.0),
inner_radius: 1.0,
outer_radius: 2.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),
inner_radius: 1.0,
outer_radius: 2.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);
}
}