use egui::{Color32, Painter, Pos2, Rect};
use super::scissor::clip_poly_to_rect;
pub type ShadedFace = (f32, [Pos2; 3], Color32);
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ShadedTri {
pub pts: [Pos2; 3],
pub vz: [f32; 3],
pub col: Color32,
}
impl ShadedTri {
#[must_use]
pub fn depth(&self) -> f32 {
(self.vz[0] + self.vz[1] + self.vz[2]) / 3.0
}
#[must_use]
pub fn face(&self) -> ShadedFace {
(self.depth(), self.pts, self.col)
}
}
pub const MIN_FACE_AREA2_PX: f32 = 1.0e-3;
const ESCAPE_EPS: f32 = 1e-2;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FaceStats {
pub visible_triangles: usize,
pub degenerate_dropped: usize,
pub offscreen_dropped: usize,
pub lit_fraction: f32,
pub ink_outside_rect: usize,
pub raw_overshoot_px: f32,
pub depth_monotone: bool,
pub max_extent_px: f32,
}
impl FaceStats {
#[must_use]
pub fn detail(&self) -> String {
format!(
"visible_triangles={} ink_outside_rect={} lit_fraction={:.2} max_extent_px={:.1} \
raw_overshoot_px={:.1} depth_monotone={} degenerate_dropped={} offscreen_dropped={}",
self.visible_triangles,
self.ink_outside_rect,
self.lit_fraction,
self.max_extent_px,
self.raw_overshoot_px,
self.depth_monotone,
self.degenerate_dropped,
self.offscreen_dropped,
)
}
}
#[inline]
fn area2(p: &[Pos2; 3]) -> f32 {
(p[1].x - p[0].x) * (p[2].y - p[0].y) - (p[1].y - p[0].y) * (p[2].x - p[0].x)
}
#[must_use]
pub fn tessellate_faces(faces: &mut [ShadedFace], rect: Rect) -> (egui::Mesh, FaceStats) {
faces.sort_by(|l, r| l.0.partial_cmp(&r.0).unwrap_or(std::cmp::Ordering::Equal));
let mut mesh = egui::Mesh::default();
mesh.reserve_vertices(faces.len() * 3);
mesh.reserve_triangles(faces.len());
let mut stats = FaceStats { depth_monotone: true, ..FaceStats::default() };
let mut lit = 0usize;
let mut last_depth = f32::NEG_INFINITY;
for (depth, pts, col) in faces.iter() {
if *depth < last_depth - 1e-3 {
stats.depth_monotone = false;
}
last_depth = *depth;
for p in pts {
let over = (rect.left() - p.x).max(p.x - rect.right()).max(rect.top() - p.y).max(p.y - rect.bottom());
if over > stats.raw_overshoot_px {
stats.raw_overshoot_px = over;
}
}
if !pts.iter().all(|p| p.x.is_finite() && p.y.is_finite()) {
stats.degenerate_dropped += 1;
continue;
}
if area2(pts).abs() < MIN_FACE_AREA2_PX {
stats.degenerate_dropped += 1;
continue;
}
let poly = clip_poly_to_rect(pts, rect);
if poly.len() < 3 {
stats.offscreen_dropped += 1;
continue;
}
let base = mesh.vertices.len() as u32;
let (mut lo, mut hi) = (poly[0], poly[0]);
for p in &poly {
mesh.colored_vertex(*p, *col);
lo = Pos2::new(lo.x.min(p.x), lo.y.min(p.y));
hi = Pos2::new(hi.x.max(p.x), hi.y.max(p.y));
}
for i in 1..poly.len() as u32 - 1 {
mesh.add_triangle(base, base + i, base + i + 1);
}
stats.visible_triangles += 1;
let extent = (hi.x - lo.x).max(hi.y - lo.y);
if extent > stats.max_extent_px {
stats.max_extent_px = extent;
}
if u32::from(col.r()) + u32::from(col.g()) + u32::from(col.b()) > 90 {
lit += 1;
}
}
stats.lit_fraction =
if stats.visible_triangles == 0 { 0.0 } else { lit as f32 / stats.visible_triangles as f32 };
for v in &mesh.vertices {
let over = (rect.left() - v.pos.x)
.max(v.pos.x - rect.right())
.max(rect.top() - v.pos.y)
.max(v.pos.y - rect.bottom());
if over > ESCAPE_EPS {
stats.ink_outside_rect += 1;
}
}
(mesh, stats)
}
pub fn paint_faces(painter: &Painter, rect: Rect, faces: &mut [ShadedFace]) -> FaceStats {
let (mesh, stats) = tessellate_faces(faces, rect);
if !mesh.is_empty() {
painter.add(egui::Shape::mesh(mesh));
}
stats
}
#[must_use]
pub fn raster_zbuffer(
tris: &[ShadedTri],
width: usize,
height: usize,
origin: Pos2,
ppp: f32,
) -> Vec<Color32> {
let mut color = vec![Color32::TRANSPARENT; width * height];
if width == 0 || height == 0 {
return color;
}
let mut zbuf = vec![f32::NEG_INFINITY; width * height];
let (fw, fh) = (width as f32, height as f32);
for t in tris {
let px = [
(t.pts[0].x - origin.x) * ppp,
(t.pts[1].x - origin.x) * ppp,
(t.pts[2].x - origin.x) * ppp,
];
let py = [
(t.pts[0].y - origin.y) * ppp,
(t.pts[1].y - origin.y) * ppp,
(t.pts[2].y - origin.y) * ppp,
];
let area = (px[1] - px[0]) * (py[2] - py[0]) - (px[2] - px[0]) * (py[1] - py[0]);
if !area.is_finite() || area.abs() < 1e-6 {
continue; }
let inv_area = 1.0 / area;
let minx = px.iter().copied().fold(f32::MAX, f32::min).floor().clamp(0.0, fw - 1.0) as usize;
let maxx = px.iter().copied().fold(f32::MIN, f32::max).ceil().clamp(0.0, fw - 1.0) as usize;
let miny = py.iter().copied().fold(f32::MAX, f32::min).floor().clamp(0.0, fh - 1.0) as usize;
let maxy = py.iter().copied().fold(f32::MIN, f32::max).ceil().clamp(0.0, fh - 1.0) as usize;
for y in miny..=maxy {
for x in minx..=maxx {
let (sx, sy) = (x as f32 + 0.5, y as f32 + 0.5);
let w0 = ((px[1] - sx) * (py[2] - sy) - (px[2] - sx) * (py[1] - sy)) * inv_area;
let w1 = ((px[2] - sx) * (py[0] - sy) - (px[0] - sx) * (py[2] - sy)) * inv_area;
let w2 = 1.0 - w0 - w1;
if w0 < -1e-4 || w1 < -1e-4 || w2 < -1e-4 {
continue; }
let d = w0 * t.vz[0] + w1 * t.vz[1] + w2 * t.vz[2];
let idx = y * width + x;
if d > zbuf[idx] {
zbuf[idx] = d;
color[idx] = t.col;
}
}
}
}
color
}
#[must_use]
pub fn tri_stats(tris: &mut [ShadedTri], rect: Rect) -> FaceStats {
let mut faces: Vec<ShadedFace> = tris.iter().map(ShadedTri::face).collect();
let stats = tessellate_faces(&mut faces, rect).1;
tris.sort_by(|l, r| l.depth().partial_cmp(&r.depth()).unwrap_or(std::cmp::Ordering::Equal));
stats
}
pub fn paint_tris_zbuffered(
ui: &mut egui::Ui,
rect: Rect,
key: &str,
tris: &mut Vec<ShadedTri>,
) -> FaceStats {
let stats = tri_stats(tris, rect);
if tris.is_empty() {
return stats;
}
let ppp = ui.ctx().pixels_per_point().max(0.1);
let w = ((rect.width() * ppp).round() as usize).clamp(1, 8192);
let h = ((rect.height() * ppp).round() as usize).clamp(1, 8192);
let pixels = raster_zbuffer(tris, w, h, rect.min, ppp);
let image = egui::ColorImage::new([w, h], pixels);
let tex = ui.ctx().load_texture(key, image, egui::TextureOptions::LINEAR);
ui.painter_at(rect).image(
tex.id(),
rect,
Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0)),
Color32::WHITE,
);
stats
}
#[must_use]
pub fn fit_scale(rect: Rect, radius: f32, margin: f32) -> f32 {
let half = rect.size().min_elem() * 0.5;
let r = radius.max(1e-6);
(half * margin.clamp(0.0, 1.0) / r).max(0.0)
}
#[must_use]
pub fn bounding_radius<'a>(points: impl IntoIterator<Item = &'a [f32; 3]>) -> f32 {
points
.into_iter()
.map(|p| (p[0] * p[0] + p[1] * p[1] + p[2] * p[2]).sqrt())
.fold(0.0_f32, f32::max)
}
#[must_use]
pub fn boundary_edge_count(positions: &[[f32; 3]], indices: &[u32]) -> usize {
use std::collections::{HashMap, HashSet};
let key = |p: [f32; 3]| {
[(p[0] * 1e5).round() as i64, (p[1] * 1e5).round() as i64, (p[2] * 1e5).round() as i64]
};
let mut weld: HashMap<[i64; 3], u32> = HashMap::new();
let mut id: Vec<u32> = Vec::with_capacity(positions.len());
for p in positions {
let n = weld.len() as u32;
id.push(*weld.entry(key(*p)).or_insert(n));
}
let mut dir: HashSet<(u32, u32)> = HashSet::with_capacity(indices.len());
for t in indices.chunks_exact(3) {
let (a, b, c) = (id[t[0] as usize], id[t[1] as usize], id[t[2] as usize]);
for e in [(a, b), (b, c), (c, a)] {
dir.insert(e);
}
}
dir.iter().filter(|(a, b)| !dir.contains(&(*b, *a))).count()
}
#[must_use]
pub fn is_watertight(positions: &[[f32; 3]], indices: &[u32]) -> bool {
boundary_edge_count(positions, indices) == 0
}
#[must_use]
pub fn inside_out_triangles(positions: &[[f32; 3]], normals: &[[f32; 3]], indices: &[u32]) -> usize {
let mut bad = 0usize;
for t in indices.chunks_exact(3) {
let (i0, i1, i2) = (t[0] as usize, t[1] as usize, t[2] as usize);
let (Some(p), Some(q), Some(r)) = (positions.get(i0), positions.get(i1), positions.get(i2))
else {
continue;
};
let u = [q[0] - p[0], q[1] - p[1], q[2] - p[2]];
let v = [r[0] - p[0], r[1] - p[1], r[2] - p[2]];
let fnm = [u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]];
let mut vn = [0.0f32; 3];
for i in [i0, i1, i2] {
if let Some(n) = normals.get(i) {
for c in 0..3 {
vn[c] += n[c];
}
}
}
if fnm[0] * vn[0] + fnm[1] * vn[1] + fnm[2] * vn[2] < 0.0 {
bad += 1;
}
}
bad
}
#[derive(Clone, Debug, PartialEq)]
pub struct Pose {
pub yaw: f32,
pub tilt: f32,
pub viewport: Rect,
pub label: String,
}
#[must_use]
pub fn pose_grid(yaw_steps: usize, tilts: &[f32], viewports: &[Rect]) -> Vec<Pose> {
let mut out = Vec::with_capacity(yaw_steps * tilts.len() * viewports.len());
for i in 0..yaw_steps.max(1) {
let yaw = i as f32 * std::f32::consts::TAU / yaw_steps.max(1) as f32;
for &tilt in tilts {
for &viewport in viewports {
out.push(Pose {
yaw,
tilt,
viewport,
label: format!(
"yaw {:3.0}° · tilt {tilt:+.2} · {:.0}x{:.0}",
yaw.to_degrees(),
viewport.width(),
viewport.height()
),
});
}
}
}
out
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct SweepReport {
pub poses: usize,
pub worst_ink: (usize, String),
pub min_visible: (usize, String),
pub worst_extent: (f32, String),
pub worst_raw_overshoot: (f32, String),
pub non_monotone_poses: usize,
pub min_lit_fraction: (f32, String),
}
impl SweepReport {
#[must_use]
pub fn detail(&self) -> String {
format!(
"poses={} worst_ink_outside_rect={} (at {}) min_visible_triangles={} (at {}) \
worst_max_extent_px={:.1} (at {}) worst_raw_overshoot_px={:.1} min_lit_fraction={:.2} \
non_monotone_poses={}",
self.poses,
self.worst_ink.0,
self.worst_ink.1,
self.min_visible.0,
self.min_visible.1,
self.worst_extent.0,
self.worst_extent.1,
self.worst_raw_overshoot.0,
self.min_lit_fraction.0,
self.non_monotone_poses,
)
}
}
pub fn sweep(poses: &[Pose], mut stats_of: impl FnMut(&Pose) -> FaceStats) -> SweepReport {
let mut r = SweepReport {
poses: poses.len(),
min_visible: (usize::MAX, String::new()),
min_lit_fraction: (f32::MAX, String::new()),
..SweepReport::default()
};
for p in poses {
let s = stats_of(p);
if s.ink_outside_rect > r.worst_ink.0 {
r.worst_ink = (s.ink_outside_rect, p.label.clone());
}
if s.visible_triangles < r.min_visible.0 {
r.min_visible = (s.visible_triangles, p.label.clone());
}
if s.max_extent_px > r.worst_extent.0 {
r.worst_extent = (s.max_extent_px, p.label.clone());
}
if s.raw_overshoot_px > r.worst_raw_overshoot.0 {
r.worst_raw_overshoot = (s.raw_overshoot_px, p.label.clone());
}
if s.lit_fraction < r.min_lit_fraction.0 {
r.min_lit_fraction = (s.lit_fraction, p.label.clone());
}
if !s.depth_monotone {
r.non_monotone_poses += 1;
}
}
if r.min_visible.0 == usize::MAX {
r.min_visible = (0, "no poses".into());
}
if r.min_lit_fraction.0 == f32::MAX {
r.min_lit_fraction = (0.0, "no poses".into());
}
r
}
#[cfg(test)]
mod tests {
use super::*;
use egui::pos2;
fn rect() -> Rect {
Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0))
}
#[test]
fn sprawling_face_is_confined_and_leaks_no_ink() {
let mut faces: Vec<ShadedFace> =
vec![(0.0, [pos2(-900.0, 40.0), pos2(40.0, -900.0), pos2(900.0, 900.0)], Color32::WHITE)];
let (mesh, st) = tessellate_faces(&mut faces, rect());
assert_eq!(st.ink_outside_rect, 0, "no emitted vertex escapes the rect");
assert_eq!(st.visible_triangles, 1, "the crossing face still draws");
assert!(!mesh.is_empty(), "it produced real geometry");
assert!(st.max_extent_px <= 100.0 + 1e-3, "extent is bounded by the rect: {}", st.max_extent_px);
assert!(st.raw_overshoot_px > 100.0, "the RAW face really did poke out: {}", st.raw_overshoot_px);
}
#[test]
fn degenerate_slivers_are_rejected() {
let mut faces: Vec<ShadedFace> =
vec![(0.0, [pos2(10.0, 50.0), pos2(50.0, 50.0), pos2(90.0, 50.0)], Color32::WHITE)];
let (mesh, st) = tessellate_faces(&mut faces, rect());
assert_eq!(st.degenerate_dropped, 1, "the collinear sliver is dropped");
assert_eq!(st.visible_triangles, 0);
assert!(mesh.is_empty(), "nothing is emitted for a zero-area face");
}
#[test]
fn offscreen_dropped_and_inside_kept() {
let mut faces: Vec<ShadedFace> = vec![
(0.0, [pos2(300.0, 300.0), pos2(340.0, 300.0), pos2(320.0, 340.0)], Color32::WHITE),
(1.0, [pos2(10.0, 10.0), pos2(90.0, 10.0), pos2(50.0, 90.0)], Color32::WHITE),
];
let (_m, st) = tessellate_faces(&mut faces, rect());
assert_eq!(st.offscreen_dropped, 1);
assert_eq!(st.visible_triangles, 1);
assert_eq!(st.ink_outside_rect, 0);
assert!(st.depth_monotone, "the pass sorts far→near itself");
assert!((st.lit_fraction - 1.0).abs() < 1e-6, "a white face is lit");
}
#[test]
fn fit_scale_keeps_a_unit_sphere_inside_the_rect() {
let r = Rect::from_min_max(pos2(0.0, 0.0), pos2(400.0, 200.0));
let s = fit_scale(r, 1.0, 0.94);
assert!(s * 1.0 <= r.size().min_elem() * 0.5 + 1e-3, "scale {s} keeps radius 1 inside");
assert!(s > 0.0);
assert!((fit_scale(r, 2.0, 0.94) - s * 0.5).abs() < 1e-3);
}
#[test]
fn zbuffer_resolves_interpenetrating_faces_either_way_round() {
let red = Color32::from_rgb(255, 0, 0);
let blue = Color32::from_rgb(0, 0, 255);
let a = ShadedTri {
pts: [pos2(0.0, 0.0), pos2(10.0, 0.0), pos2(0.0, 10.0)],
vz: [1.0, -1.0, 0.0],
col: red,
};
let b = ShadedTri {
pts: [pos2(0.0, 0.0), pos2(10.0, 0.0), pos2(0.0, 10.0)],
vz: [-1.0, 1.0, 0.0],
col: blue,
};
assert!((a.depth() - b.depth()).abs() < 1e-6, "equal mean depth: a sort cannot help");
for (order, tris) in [("a,b", vec![a, b]), ("b,a", vec![b, a])] {
let px = raster_zbuffer(&tris, 10, 10, pos2(0.0, 0.0), 1.0);
assert_eq!(px[10 * 1 + 0], red, "order {order}: left column is the nearer (red) face");
assert_eq!(px[10 * 0 + 8], blue, "order {order}: right column is the nearer (blue) face");
}
}
#[test]
fn zbuffer_raster_confines_a_sprawling_triangle() {
let t = ShadedTri {
pts: [pos2(-9000.0, 40.0), pos2(40.0, -9000.0), pos2(9000.0, 9000.0)],
vz: [0.0, 0.0, 0.0],
col: Color32::WHITE,
};
let px = raster_zbuffer(&[t], 32, 32, pos2(0.0, 0.0), 1.0);
assert_eq!(px.len(), 32 * 32, "the buffer is exactly the rect");
assert!(px.iter().any(|c| *c != Color32::TRANSPARENT), "and it did draw inside it");
}
#[test]
fn watertight_and_inside_out_oracles_are_sensitive() {
let pos = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
let closed: Vec<u32> = vec![0, 2, 1, 0, 1, 3, 0, 3, 2, 1, 2, 3];
assert_eq!(boundary_edge_count(&pos, &closed), 0, "a closed tetrahedron has no boundary");
assert!(is_watertight(&pos, &closed));
let c = [0.25f32, 0.25, 0.25];
let nrm: Vec<[f32; 3]> = pos.iter().map(|p| [p[0] - c[0], p[1] - c[1], p[2] - c[2]]).collect();
assert_eq!(inside_out_triangles(&pos, &nrm, &closed), 0, "outward-wound: nothing inside-out");
let open: Vec<u32> = closed[..9].to_vec();
assert_eq!(boundary_edge_count(&pos, &open), 3, "the missing face leaves a 3-edge rim");
assert!(!is_watertight(&pos, &open), "an open shell is NOT watertight");
let mut flipped = closed.clone();
flipped.swap(0, 1);
assert_eq!(inside_out_triangles(&pos, &nrm, &flipped), 1, "the flipped face is named");
}
#[test]
fn bounding_radius_is_the_farthest_point() {
let pts = [[0.0, 0.0, 0.0], [3.0, 4.0, 0.0], [1.0, 1.0, 1.0]];
assert!((bounding_radius(pts.iter()) - 5.0).abs() < 1e-5);
assert_eq!(bounding_radius(std::iter::empty()), 0.0);
}
#[test]
fn sweep_reports_the_worst_pose_by_name() {
let vps = [Rect::from_min_max(pos2(0.0, 0.0), pos2(100.0, 100.0))];
let poses = pose_grid(8, &[0.0, 0.5], &vps);
assert_eq!(poses.len(), 16, "8 yaws × 2 tilts × 1 viewport");
assert!(poses[0].label.contains("yaw 0°"), "{}", poses[0].label);
let bad = poses[5].label.clone();
let r = sweep(&poses, |p| FaceStats {
visible_triangles: if p.label == bad { 1 } else { 40 },
ink_outside_rect: usize::from(p.label == bad) * 7,
max_extent_px: if p.label == bad { 900.0 } else { 30.0 },
depth_monotone: p.label != bad,
lit_fraction: 1.0,
..FaceStats::default()
});
assert_eq!(r.poses, 16);
assert_eq!(r.worst_ink, (7, bad.clone()), "the leaking pose is named");
assert_eq!(r.min_visible, (1, bad.clone()), "the geometry-losing pose is named");
assert_eq!(r.worst_extent.0, 900.0);
assert_eq!(r.non_monotone_poses, 1);
}
}