use egui::{Color32, Pos2};
use crate::core::items::LineStyle;
use crate::core::transform::Transform;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShapeKind {
Polygon,
Rectangle,
Polyline,
HLine,
VLine,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Shape {
pub kind: ShapeKind,
pub x: Vec<f64>,
pub y: Vec<f64>,
pub color: Color32,
pub fill: bool,
pub line_style: LineStyle,
pub line_width: f32,
pub gap_color: Option<Color32>,
pub is_overlay: bool,
}
impl Shape {
pub fn polygon(x: Vec<f64>, y: Vec<f64>) -> Self {
assert_eq!(
x.len(),
y.len(),
"polygon x and y must have the same length"
);
Self::with_points(ShapeKind::Polygon, x, y)
}
pub fn rectangle(x0: f64, y0: f64, x1: f64, y1: f64) -> Self {
Self::with_points(ShapeKind::Rectangle, vec![x0, x1], vec![y0, y1])
}
pub fn polyline(x: Vec<f64>, y: Vec<f64>) -> Self {
assert_eq!(
x.len(),
y.len(),
"polyline x and y must have the same length"
);
Self::with_points(ShapeKind::Polyline, x, y)
}
pub fn hlines(y: Vec<f64>) -> Self {
Self::with_points(ShapeKind::HLine, Vec::new(), y)
}
pub fn vlines(x: Vec<f64>) -> Self {
Self::with_points(ShapeKind::VLine, x, Vec::new())
}
fn with_points(kind: ShapeKind, x: Vec<f64>, y: Vec<f64>) -> Self {
Self {
kind,
x,
y,
color: Color32::WHITE,
fill: false,
line_style: LineStyle::Solid,
line_width: 1.0,
gap_color: None,
is_overlay: true,
}
}
pub fn with_color(mut self, color: Color32) -> Self {
self.color = color;
self
}
pub fn with_fill(mut self, fill: bool) -> Self {
self.fill = fill;
self
}
pub fn with_line_style(mut self, style: LineStyle) -> Self {
self.line_style = style;
self
}
pub fn with_line_width(mut self, width: f32) -> Self {
self.line_width = width;
self
}
pub fn with_gap_color(mut self, color: Color32) -> Self {
self.gap_color = Some(color);
self
}
pub fn with_overlay(mut self, overlay: bool) -> Self {
self.is_overlay = overlay;
self
}
pub fn screen_points(&self, t: &Transform) -> Vec<Pos2> {
match self.kind {
ShapeKind::Rectangle => {
if self.x.len() < 2 || self.y.len() < 2 {
return Vec::new();
}
let (x0, x1, y0, y1) = (self.x[0], self.x[1], self.y[0], self.y[1]);
vec![
t.data_to_pixel(x0, y0),
t.data_to_pixel(x1, y0),
t.data_to_pixel(x1, y1),
t.data_to_pixel(x0, y1),
]
}
ShapeKind::Polygon | ShapeKind::Polyline => self
.x
.iter()
.zip(&self.y)
.map(|(&x, &y)| t.data_to_pixel(x, y))
.collect(),
ShapeKind::HLine | ShapeKind::VLine => Vec::new(),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Line {
pub slope: f64,
pub intercept: f64,
pub color: Color32,
pub line_style: LineStyle,
pub line_width: f32,
pub gap_color: Option<Color32>,
pub is_overlay: bool,
}
impl Line {
pub fn new(slope: f64, intercept: f64) -> Self {
assert!(intercept.is_finite(), "Line intercept must be finite");
Self {
slope,
intercept,
color: Color32::WHITE,
line_style: LineStyle::Solid,
line_width: 1.0,
gap_color: None,
is_overlay: true,
}
}
pub fn from_points(point0: (f64, f64), point1: (f64, f64)) -> Self {
let (x0, y0) = point0;
let (x1, y1) = point1;
if x0 == x1 {
return Self::new(f64::INFINITY, x0);
}
let slope = (y1 - y0) / (x1 - x0);
Self::new(slope, y0 - x0 * slope)
}
pub fn with_color(mut self, color: Color32) -> Self {
self.color = color;
self
}
pub fn with_line_style(mut self, style: LineStyle) -> Self {
self.line_style = style;
self
}
pub fn with_line_width(mut self, width: f32) -> Self {
self.line_width = width;
self
}
pub fn with_gap_color(mut self, color: Color32) -> Self {
self.gap_color = Some(color);
self
}
pub fn with_overlay(mut self, overlay: bool) -> Self {
self.is_overlay = overlay;
self
}
pub fn clipped_segment(&self, bounds: egui::Rect) -> Option<(Pos2, Pos2)> {
let xmin = bounds.min.x as f64;
let ymin = bounds.min.y as f64;
let xmax = bounds.max.x as f64;
let ymax = bounds.max.y as f64;
if !self.slope.is_finite() {
if self.intercept < xmin || self.intercept > xmax {
return None;
}
let x = self.intercept as f32;
return Some((Pos2::new(x, ymin as f32), Pos2::new(x, ymax as f32)));
}
let y0 = self.slope * xmin + self.intercept;
let y1 = self.slope * xmax + self.intercept;
let (lo, hi) = if y0 <= y1 { (y0, y1) } else { (y1, y0) };
if lo < ymax && hi > ymin {
Some((
Pos2::new(xmin as f32, y0 as f32),
Pos2::new(xmax as f32, y1 as f32),
))
} else {
None
}
}
}
pub fn triangulate_simple_polygon(points: &[Pos2]) -> Vec<[u32; 3]> {
let n = points.len();
if n < 3 {
return Vec::new();
}
let mut ring: Vec<usize> = (0..n).collect();
if signed_area_2(points) < 0.0 {
ring.reverse();
}
let mut tris = Vec::with_capacity(n - 2);
let mut attempts = 0;
let max_attempts = n * n;
while ring.len() > 3 {
let m = ring.len();
let mut clipped = false;
for i in 0..m {
let a = ring[(i + m - 1) % m];
let b = ring[i];
let c = ring[(i + 1) % m];
if is_ear(points, &ring, a, b, c) {
tris.push([a as u32, b as u32, c as u32]);
ring.remove(i);
clipped = true;
break;
}
}
attempts += 1;
if !clipped || attempts > max_attempts {
for k in 1..ring.len() - 1 {
tris.push([ring[0] as u32, ring[k] as u32, ring[k + 1] as u32]);
}
return tris;
}
}
tris.push([ring[0] as u32, ring[1] as u32, ring[2] as u32]);
tris
}
fn signed_area_2(points: &[Pos2]) -> f32 {
let n = points.len();
let mut sum = 0.0;
for i in 0..n {
let p = points[i];
let q = points[(i + 1) % n];
sum += p.x * q.y - q.x * p.y;
}
sum
}
fn orient(a: Pos2, b: Pos2, c: Pos2) -> f32 {
(b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x)
}
fn is_ear(points: &[Pos2], ring: &[usize], a: usize, b: usize, c: usize) -> bool {
let (pa, pb, pc) = (points[a], points[b], points[c]);
if orient(pa, pb, pc) <= 0.0 {
return false;
}
for &v in ring {
if v == a || v == b || v == c {
continue;
}
if point_in_triangle(points[v], pa, pb, pc) {
return false;
}
}
true
}
fn point_in_triangle(p: Pos2, a: Pos2, b: Pos2, c: Pos2) -> bool {
let s1 = orient(a, b, p);
let s2 = orient(b, c, p);
let s3 = orient(c, a, p);
let has_neg = s1 < 0.0 || s2 < 0.0 || s3 < 0.0;
let has_pos = s1 > 0.0 || s2 > 0.0 || s3 > 0.0;
!(has_neg && has_pos)
}
#[cfg(test)]
mod tests {
use super::*;
use egui::{Rect, 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)),
)
}
#[test]
fn constructors_set_kind_and_defaults() {
let p = Shape::polygon(vec![0.0, 1.0, 2.0], vec![0.0, 1.0, 0.0]);
assert_eq!(p.kind, ShapeKind::Polygon);
assert_eq!(p.color, Color32::WHITE);
assert!(!p.fill);
assert_eq!(p.line_style, LineStyle::Solid);
assert_eq!(p.line_width, 1.0);
assert!(p.gap_color.is_none());
assert_eq!(
Shape::rectangle(0.0, 0.0, 1.0, 1.0).kind,
ShapeKind::Rectangle
);
assert_eq!(
Shape::polyline(vec![0.0], vec![0.0]).kind,
ShapeKind::Polyline
);
assert_eq!(Shape::hlines(vec![1.0, 2.0]).kind, ShapeKind::HLine);
assert_eq!(Shape::vlines(vec![1.0, 2.0]).kind, ShapeKind::VLine);
}
#[test]
#[should_panic(expected = "polygon x and y must have the same length")]
fn polygon_rejects_length_mismatch() {
Shape::polygon(vec![0.0, 1.0], vec![0.0]);
}
#[test]
#[should_panic(expected = "polyline x and y must have the same length")]
fn polyline_rejects_length_mismatch() {
Shape::polyline(vec![0.0], vec![0.0, 1.0]);
}
#[test]
fn builders_set_fields() {
let s = Shape::rectangle(0.0, 0.0, 1.0, 1.0)
.with_color(Color32::RED)
.with_fill(true)
.with_line_style(LineStyle::Dashed)
.with_line_width(2.0)
.with_gap_color(Color32::BLACK);
assert_eq!(s.color, Color32::RED);
assert!(s.fill);
assert_eq!(s.line_style, LineStyle::Dashed);
assert_eq!(s.line_width, 2.0);
assert_eq!(s.gap_color, Some(Color32::BLACK));
}
#[test]
fn overlay_defaults_true_and_builder_toggles() {
assert!(Shape::rectangle(0.0, 0.0, 1.0, 1.0).is_overlay);
let s = Shape::rectangle(0.0, 0.0, 1.0, 1.0).with_overlay(false);
assert!(!s.is_overlay);
}
#[test]
fn rectangle_screen_points_are_the_four_corners() {
let r = Shape::rectangle(2.0, 3.0, 8.0, 7.0);
let pts = r.screen_points(&t());
assert_eq!(
pts,
vec![
pos2(20.0, 70.0), pos2(80.0, 70.0), pos2(80.0, 30.0), pos2(20.0, 30.0), ]
);
}
#[test]
fn polygon_screen_points_map_each_vertex_and_lines_are_empty() {
let p = Shape::polygon(vec![1.0, 5.0], vec![2.0, 6.0]);
assert_eq!(
p.screen_points(&t()),
vec![pos2(10.0, 80.0), pos2(50.0, 40.0)]
);
assert!(Shape::hlines(vec![1.0]).screen_points(&t()).is_empty());
assert!(Shape::vlines(vec![1.0]).screen_points(&t()).is_empty());
}
fn bounds() -> Rect {
Rect::from_min_max(pos2(0.0, 0.0), pos2(10.0, 10.0))
}
#[test]
fn line_defaults_and_overlay_builder() {
let l = Line::new(2.0, 1.0);
assert_eq!(l.slope, 2.0);
assert_eq!(l.intercept, 1.0);
assert_eq!(l.color, Color32::WHITE);
assert_eq!(l.line_style, LineStyle::Solid);
assert!(l.is_overlay);
assert!(!Line::new(0.0, 0.0).with_overlay(false).is_overlay);
}
#[test]
#[should_panic(expected = "Line intercept must be finite")]
fn line_rejects_non_finite_intercept() {
Line::new(1.0, f64::NAN);
}
#[test]
fn sloped_line_clips_to_box_entry_and_exit() {
let seg = Line::new(1.0, 0.0).clipped_segment(bounds());
assert_eq!(seg, Some((pos2(0.0, 0.0), pos2(10.0, 10.0))));
let seg = Line::new(0.5, 2.0).clipped_segment(bounds()).unwrap();
assert_eq!(seg.0, pos2(0.0, 2.0));
assert_eq!(seg.1, pos2(10.0, 7.0));
}
#[test]
fn horizontal_line_inside_box_is_visible() {
let seg = Line::new(0.0, 5.0).clipped_segment(bounds());
assert_eq!(seg, Some((pos2(0.0, 5.0), pos2(10.0, 5.0))));
}
#[test]
fn sloped_line_entirely_above_box_is_none() {
assert_eq!(Line::new(1.0, 20.0).clipped_segment(bounds()), None);
}
#[test]
fn sloped_line_entirely_below_box_is_none() {
assert_eq!(Line::new(1.0, -20.0).clipped_segment(bounds()), None);
}
#[test]
fn vertical_line_inside_outside_and_on_edge() {
let seg = Line::new(f64::INFINITY, 5.0).clipped_segment(bounds());
assert_eq!(seg, Some((pos2(5.0, 0.0), pos2(5.0, 10.0))));
let seg = Line::new(f64::INFINITY, 0.0).clipped_segment(bounds());
assert_eq!(seg, Some((pos2(0.0, 0.0), pos2(0.0, 10.0))));
let seg = Line::new(f64::INFINITY, 10.0).clipped_segment(bounds());
assert_eq!(seg, Some((pos2(10.0, 0.0), pos2(10.0, 10.0))));
assert_eq!(
Line::new(f64::INFINITY, 15.0).clipped_segment(bounds()),
None
);
assert_eq!(
Line::new(f64::INFINITY, -1.0).clipped_segment(bounds()),
None
);
}
#[test]
fn from_points_sloped_and_vertical() {
let l = Line::from_points((0.0, 0.0), (2.0, 2.0));
assert_eq!(l.slope, 1.0);
assert_eq!(l.intercept, 0.0);
let l = Line::from_points((1.0, 3.0), (2.0, 5.0));
assert_eq!(l.slope, 2.0);
assert_eq!(l.intercept, 1.0);
let l = Line::from_points((4.0, 1.0), (4.0, 9.0));
assert!(!l.slope.is_finite());
assert_eq!(l.intercept, 4.0);
}
fn triangulation_area(points: &[Pos2], tris: &[[u32; 3]]) -> f32 {
tris.iter()
.map(|t| {
0.5 * orient(
points[t[0] as usize],
points[t[1] as usize],
points[t[2] as usize],
)
.abs()
})
.sum()
}
fn polygon_area(points: &[Pos2]) -> f32 {
0.5 * signed_area_2(points).abs()
}
#[test]
fn triangulate_too_few_points_is_empty() {
assert!(triangulate_simple_polygon(&[]).is_empty());
assert!(triangulate_simple_polygon(&[pos2(0.0, 0.0)]).is_empty());
assert!(triangulate_simple_polygon(&[pos2(0.0, 0.0), pos2(1.0, 1.0)]).is_empty());
}
#[test]
fn triangulate_triangle_is_itself() {
let p = [pos2(0.0, 0.0), pos2(2.0, 0.0), pos2(1.0, 2.0)];
let tris = triangulate_simple_polygon(&p);
assert_eq!(tris.len(), 1);
assert!((triangulation_area(&p, &tris) - polygon_area(&p)).abs() < 1e-4);
}
#[test]
fn triangulate_convex_quad_tiles_it() {
let p = [
pos2(0.0, 0.0),
pos2(4.0, 0.0),
pos2(4.0, 3.0),
pos2(0.0, 3.0),
];
let tris = triangulate_simple_polygon(&p);
assert_eq!(tris.len(), 2); assert!((triangulation_area(&p, &tris) - 12.0).abs() < 1e-4);
}
#[test]
fn triangulate_concave_polygon_tiles_exactly_not_its_hull() {
let p = [
pos2(0.0, 0.0), pos2(4.0, 1.0), pos2(0.0, 2.0), pos2(1.0, 1.0), ];
let tris = triangulate_simple_polygon(&p);
assert_eq!(tris.len(), 2); let area = triangulation_area(&p, &tris);
assert!(
(area - 3.0).abs() < 1e-4,
"expected polygon area 3.0, got {area}"
);
assert!(area < 4.0 - 1e-3, "must not cover the convex hull (4.0)");
}
#[test]
fn triangulate_normalizes_clockwise_input() {
let p = [
pos2(1.0, 1.0), pos2(0.0, 2.0), pos2(4.0, 1.0), pos2(0.0, 0.0), ];
let tris = triangulate_simple_polygon(&p);
assert_eq!(tris.len(), 2);
assert!((triangulation_area(&p, &tris) - 3.0).abs() < 1e-4);
}
}