use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use eframe::egui::{self, Color32, Mesh, Pos2, Shape, Stroke};
const MAX_POINTS: usize = 1000;
pub fn fill(painter: &egui::Painter, display: &[(f64, f64)], screen: Vec<Pos2>, color: Color32) {
if screen.len() < 3 || color.a() == 0 {
return;
}
let mut hasher = std::collections::hash_map::DefaultHasher::new();
for (x, y) in display {
x.to_bits().hash(&mut hasher);
y.to_bits().hash(&mut hasher);
}
let key = hasher.finish();
let id = egui::Id::new("bobview-polygons");
let triangles = painter.ctx().memory_mut(|m| {
m.data
.get_temp_mut_or_default::<Cache>(id)
.get_or_insert(key, || {
let pts: Vec<Pos2> = display
.iter()
.map(|(x, y)| Pos2::new(*x as f32, *y as f32))
.collect();
triangulate(&pts)
})
});
match triangles.as_ref() {
Some(indices) => {
let mut mesh = Mesh::default();
for p in &screen {
mesh.colored_vertex(*p, color);
}
mesh.indices = indices.clone();
painter.add(Shape::mesh(mesh));
}
_ => {
painter.add(Shape::convex_polygon(screen, color, Stroke::NONE));
}
}
}
type Triangles = Arc<Option<Vec<u32>>>;
const CACHE_SIZE: usize = 4096;
#[derive(Clone, Default)]
struct Cache {
newer: HashMap<u64, Triangles>,
older: HashMap<u64, Triangles>,
}
impl Cache {
fn get_or_insert(&mut self, key: u64, make: impl FnOnce() -> Option<Vec<u32>>) -> Triangles {
if let Some(t) = self.newer.get(&key) {
return t.clone();
}
let t = self.older.remove(&key).unwrap_or_else(|| Arc::new(make()));
if self.newer.len() >= CACHE_SIZE {
self.older = std::mem::take(&mut self.newer);
}
self.newer.insert(key, t.clone());
t
}
}
fn area2(pts: &[Pos2]) -> f32 {
let n = pts.len();
(0..n)
.map(|i| {
let (a, b) = (pts[i], pts[(i + 1) % n]);
a.x * b.y - b.x * a.y
})
.sum()
}
fn cross(o: Pos2, a: Pos2, b: Pos2) -> f32 {
(a - o).x * (b - o).y - (a - o).y * (b - o).x
}
fn in_triangle(p: Pos2, a: Pos2, b: Pos2, c: Pos2, sign: f32) -> bool {
cross(a, b, p) * sign >= 0.0 && cross(b, c, p) * sign >= 0.0 && cross(c, a, p) * sign >= 0.0
}
pub fn triangulate(pts: &[Pos2]) -> Option<Vec<u32>> {
let n = pts.len();
if !(4..=MAX_POINTS).contains(&n) {
return None;
}
let area = area2(pts);
if area == 0.0 {
return None;
}
let sign = area.signum();
let turns = |i: usize| cross(pts[(i + n - 1) % n], pts[i], pts[(i + 1) % n]) * sign;
if (0..n).all(|i| turns(i) >= 0.0) {
return None;
}
let mut left: Vec<usize> = (0..n).collect();
let mut out = Vec::with_capacity(3 * (n - 2));
while left.len() > 3 {
let m = left.len();
let mut cut = None;
for i in 0..m {
let (a, b, c) = (left[(i + m - 1) % m], left[i], left[(i + 1) % m]);
let turn = cross(pts[a], pts[b], pts[c]) * sign;
if turn == 0.0 {
cut = Some((i, None));
break;
}
if turn < 0.0 {
continue;
}
let blocked = left.iter().any(|&k| {
k != a
&& k != b
&& k != c
&& pts[k] != pts[a]
&& pts[k] != pts[b]
&& pts[k] != pts[c]
&& in_triangle(pts[k], pts[a], pts[b], pts[c], sign)
});
if !blocked {
cut = Some((i, Some([a, b, c])));
break;
}
}
match cut {
Some((i, tri)) => {
out.extend(tri.into_iter().flatten().map(|k| k as u32));
left.remove(i);
}
None => {
for k in 1..left.len() - 1 {
out.extend([left[0], left[k], left[k + 1]].map(|k| k as u32));
}
return Some(out);
}
}
}
out.extend(left.iter().map(|&k| k as u32));
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_cache_stays_bounded_and_keeps_outlines_in_use() {
let mut cache = Cache::default();
let mut cuts = 0;
let mut get = |cache: &mut Cache, key| {
cache.get_or_insert(key, || {
cuts += 1;
None
});
};
for key in 1..=10 * CACHE_SIZE as u64 {
get(&mut cache, 0);
get(&mut cache, key);
assert!(cache.newer.len() + cache.older.len() <= 2 * CACHE_SIZE);
}
drop(get);
assert_eq!(cuts, 1 + 10 * CACHE_SIZE, "the steady one is cut once");
}
fn pts(p: &[(f32, f32)]) -> Vec<Pos2> {
p.iter().map(|&(x, y)| Pos2::new(x, y)).collect()
}
fn covered(p: &[Pos2], tris: &[u32]) -> f32 {
tris.chunks(3)
.map(|t| cross(p[t[0] as usize], p[t[1] as usize], p[t[2] as usize]).abs() / 2.0)
.sum()
}
fn filled(p: &[Pos2], tris: &[u32], q: Pos2) -> bool {
tris.chunks(3).any(|t| {
let (a, b, c) = (p[t[0] as usize], p[t[1] as usize], p[t[2] as usize]);
in_triangle(q, a, b, c, cross(a, b, c).signum())
})
}
#[test]
fn convex_polygons_are_left_to_egui() {
assert_eq!(
triangulate(&pts(&[(0., 0.), (10., 0.), (10., 10.), (0., 10.)])),
None
);
assert_eq!(triangulate(&pts(&[(0., 0.), (10., 0.), (5., 10.)])), None);
}
#[test]
fn an_l_shape_is_filled_but_not_its_notch() {
let l = pts(&[
(0., 0.),
(10., 0.),
(10., 4.),
(4., 4.),
(4., 10.),
(0., 10.),
]);
for outline in [l.clone(), l.into_iter().rev().collect()] {
let tris = triangulate(&outline).expect("concave");
assert_eq!(tris.len(), 3 * (outline.len() - 2));
assert_eq!(covered(&outline, &tris), 64.0);
assert!(filled(&outline, &tris, Pos2::new(2.0, 8.0)));
assert!(filled(&outline, &tris, Pos2::new(8.0, 2.0)));
assert!(!filled(&outline, &tris, Pos2::new(8.0, 8.0)), "the notch");
}
}
#[test]
fn an_arrow_is_filled_but_not_beside_its_shaft() {
let arrow = pts(&[
(0., 3.),
(6., 3.),
(6., 0.),
(10., 5.),
(6., 10.),
(6., 7.),
(0., 7.),
]);
let tris = triangulate(&arrow).expect("concave");
assert_eq!(covered(&arrow, &tris), 44.0);
assert!(!filled(&arrow, &tris, Pos2::new(3.0, 1.0)));
assert!(!filled(&arrow, &tris, Pos2::new(3.0, 9.0)));
assert!(filled(&arrow, &tris, Pos2::new(8.0, 5.0)));
}
#[test]
fn straight_runs_and_crossed_outlines_still_fill() {
let u = pts(&[
(0., 0.),
(3., 0.),
(3., 8.),
(6., 8.),
(6., 0.),
(9., 0.),
(9., 10.),
(4., 10.),
(0., 10.),
]);
let tris = triangulate(&u).expect("concave");
assert_eq!(covered(&u, &tris), 90.0 - 24.0);
let bow = pts(&[(0., 0.), (10., 10.), (10., 0.), (0., 10.), (-1., 5.)]);
assert!(triangulate(&bow).is_some_and(|t| t.len() % 3 == 0));
}
}