use crate::mesh::{Indices, Primitive, Topology};
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Profile2D {
pub outer: Vec<[f32; 2]>,
pub holes: Vec<Vec<[f32; 2]>>,
}
#[derive(Clone, Copy)]
struct RingVertex {
p: [f64; 2],
orig: u32,
}
fn cross2(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> f64 {
(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
}
fn signed_area2(pts: &[RingVertex]) -> f64 {
let n = pts.len();
let mut s = 0.0;
for i in 0..n {
let a = pts[i].p;
let b = pts[(i + 1) % n].p;
s += a[0] * b[1] - b[0] * a[1];
}
s
}
fn point_in_ccw_triangle(p: [f64; 2], a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
cross2(a, b, p) >= 0.0 && cross2(b, c, p) >= 0.0 && cross2(c, a, p) >= 0.0
}
fn clean_loop(pts: &[[f32; 2]], base: u32) -> Vec<RingVertex> {
let mut out: Vec<RingVertex> = Vec::with_capacity(pts.len());
for (i, p) in pts.iter().enumerate() {
let q = [f64::from(p[0]), f64::from(p[1])];
if let Some(last) = out.last() {
if last.p == q {
continue;
}
}
out.push(RingVertex {
p: q,
orig: base + i as u32,
});
}
while out.len() > 1 && out[0].p == out[out.len() - 1].p {
out.pop();
}
out
}
fn bridge_hole(ring: &[RingVertex], hole: &[RingVertex]) -> Option<Vec<RingVertex>> {
let n = ring.len();
let m_i = (0..hole.len())
.max_by(|&a, &b| {
let (pa, pb) = (hole[a].p, hole[b].p);
pa[0]
.partial_cmp(&pb[0])
.unwrap()
.then(pa[1].partial_cmp(&pb[1]).unwrap())
})
.expect("hole loops are non-empty by construction");
let m = hole[m_i].p;
let mut best: Option<(f64, usize)> = None; for i in 0..n {
let a = ring[i].p;
let b = ring[(i + 1) % n].p;
if (a[1] > m[1]) != (b[1] > m[1]) {
let t = a[0] + (m[1] - a[1]) * (b[0] - a[0]) / (b[1] - a[1]);
if t >= m[0] && t.is_finite() && best.map_or(true, |(bx, _)| t < bx) {
best = Some((t, i));
}
}
}
let (ix, ei) = best?;
let inter = [ix, m[1]];
let (a_i, b_i) = (ei, (ei + 1) % n);
let (a, b) = (ring[a_i].p, ring[b_i].p);
let cand = if a == inter {
a_i
} else if b == inter {
b_i
} else if a[0] > b[0] {
a_i
} else {
b_i
};
let pc = ring[cand].p;
let (t0, t1, t2) = if cross2(m, inter, pc) >= 0.0 {
(m, inter, pc)
} else {
(m, pc, inter)
};
let mut p_i = cand;
let mut best_key: Option<(f64, f64)> = None; for j in 0..n {
let prev = ring[(j + n - 1) % n].p;
let cur = ring[j].p;
let next = ring[(j + 1) % n].p;
if cross2(prev, cur, next) >= 0.0 {
continue; }
if cur == m || cur == inter || cur == pc {
continue;
}
if !point_in_ccw_triangle(cur, t0, t1, t2) {
continue;
}
let dx = cur[0] - m[0];
let dy = (cur[1] - m[1]).abs();
if dx <= 0.0 {
continue; }
let key = (dy / dx, dx * dx + dy * dy);
if best_key.map_or(true, |bk| key < bk) {
best_key = Some(key);
p_i = j;
}
}
let p_pos = ring[p_i].p;
p_i = (0..n).find(|&j| ring[j].p == p_pos).unwrap_or(p_i);
let mut out = Vec::with_capacity(n + hole.len() + 2);
out.extend_from_slice(&ring[..=p_i]);
out.extend_from_slice(&hole[m_i..]);
out.extend_from_slice(&hole[..=m_i]);
out.push(ring[p_i]);
out.extend_from_slice(&ring[p_i + 1..]);
Some(out)
}
fn ear_clip(ring: &[RingVertex], area_eps: f64) -> Option<Vec<[u32; 3]>> {
let n = ring.len();
if n < 3 {
return None;
}
let mut prev: Vec<usize> = (0..n).map(|i| (i + n - 1) % n).collect();
let mut next: Vec<usize> = (0..n).map(|i| (i + 1) % n).collect();
let mut alive = vec![true; n];
let cross_at = |i: usize, prev: &[usize], next: &[usize]| -> f64 {
cross2(ring[prev[i]].p, ring[i].p, ring[next[i]].p)
};
let mut reflex = vec![false; n];
let mut reflex_list: Vec<usize> = Vec::new();
for (i, r) in reflex.iter_mut().enumerate() {
if cross_at(i, &prev, &next) < 0.0 {
*r = true;
reflex_list.push(i);
}
}
let is_ear = |i: usize,
prev: &[usize],
next: &[usize],
alive: &[bool],
reflex: &[bool],
reflex_list: &[usize]|
-> bool {
let (a, b, c) = (ring[prev[i]].p, ring[i].p, ring[next[i]].p);
if cross2(a, b, c) <= 0.0 {
return false; }
for &j in reflex_list {
if !alive[j] || !reflex[j] || j == prev[i] || j == i || j == next[i] {
continue;
}
let p = ring[j].p;
if p == a || p == b || p == c {
continue; }
if point_in_ccw_triangle(p, a, b, c) {
return false;
}
}
true
};
let mut tris: Vec<[u32; 3]> = Vec::with_capacity(n.saturating_sub(2));
let mut remaining = n;
let mut cursor = 0usize;
macro_rules! clip {
($i:expr, $emit:expr) => {{
let i = $i;
if $emit {
let c2 = cross_at(i, &prev, &next);
if c2 > area_eps {
tris.push([ring[prev[i]].orig, ring[i].orig, ring[next[i]].orig]);
}
}
let (p, nx) = (prev[i], next[i]);
next[p] = nx;
prev[nx] = p;
alive[i] = false;
remaining -= 1;
for &k in &[p, nx] {
let was = reflex[k];
let now = cross_at(k, &prev, &next) < 0.0;
reflex[k] = now;
if now && !was {
reflex_list.push(k);
}
}
cursor = nx;
}};
}
while remaining > 3 {
let mut found = None;
let mut i = cursor;
for _ in 0..remaining {
if is_ear(i, &prev, &next, &alive, &reflex, &reflex_list) {
found = Some(i);
break;
}
i = next[i];
}
match found {
Some(i) => clip!(i, true),
None => {
let mut j = cursor;
let mut degenerate = None;
let mut convex = None;
for _ in 0..remaining {
let c2 = cross_at(j, &prev, &next);
if c2.abs() <= area_eps {
degenerate = Some(j);
break;
}
if c2 > 0.0 && convex.is_none() {
convex = Some(j);
}
j = next[j];
}
if let Some(d) = degenerate {
clip!(d, false);
} else {
let c = convex?;
clip!(c, true);
}
}
}
}
let i = (0..n).find(|&k| alive[k])?;
let c2 = cross_at(i, &prev, &next);
if c2 > area_eps {
tris.push([ring[prev[i]].orig, ring[i].orig, ring[next[i]].orig]);
}
if tris.is_empty() {
return None;
}
Some(tris)
}
impl Profile2D {
pub fn new(outer: Vec<[f32; 2]>) -> Self {
Self {
outer,
holes: Vec::new(),
}
}
pub fn with_hole(mut self, hole: Vec<[f32; 2]>) -> Self {
self.holes.push(hole);
self
}
pub fn vertex_count(&self) -> usize {
self.outer.len() + self.holes.iter().map(Vec::len).sum::<usize>()
}
pub fn area(&self) -> f64 {
let loop_area = |pts: &[[f32; 2]]| -> f64 {
let v = clean_loop(pts, 0);
if v.len() < 3 {
return 0.0;
}
signed_area2(&v).abs() / 2.0
};
let mut a = loop_area(&self.outer);
for h in &self.holes {
a -= loop_area(h);
}
a
}
fn coord_scale(&self) -> f64 {
let mut s = 0.0f64;
let mut visit = |pts: &[[f32; 2]]| {
for p in pts {
for c in p {
let a = f64::from(*c).abs();
if a.is_finite() && a > s {
s = a;
}
}
}
};
visit(&self.outer);
for h in &self.holes {
visit(h);
}
s
}
pub fn triangulate(&self) -> Option<Vec<[u32; 3]>> {
if self.vertex_count() > (u32::MAX / 2) as usize {
return None;
}
let scale = self.coord_scale();
if scale == 0.0 || !scale.is_finite() {
return None;
}
let area_eps = (scale * 1e-9) * (scale * 1e-9);
let mut ring = clean_loop(&self.outer, 0);
if ring.len() < 3 {
return None;
}
if ring
.iter()
.any(|v| !v.p[0].is_finite() || !v.p[1].is_finite())
{
return None;
}
if signed_area2(&ring).abs() <= area_eps {
return None;
}
if signed_area2(&ring) < 0.0 {
ring.reverse();
}
let mut holes: Vec<Vec<RingVertex>> = Vec::with_capacity(self.holes.len());
let mut base = self.outer.len() as u32;
for h in &self.holes {
let mut hv = clean_loop(h, base);
base += h.len() as u32;
if hv.len() < 3 || signed_area2(&hv).abs() <= area_eps {
return None;
}
if hv
.iter()
.any(|v| !v.p[0].is_finite() || !v.p[1].is_finite())
{
return None;
}
if signed_area2(&hv) > 0.0 {
hv.reverse();
}
holes.push(hv);
}
holes.sort_by(|a, b| {
let mx = |h: &[RingVertex]| h.iter().map(|v| v.p[0]).fold(f64::NEG_INFINITY, f64::max);
mx(b).partial_cmp(&mx(a)).unwrap()
});
for h in &holes {
ring = bridge_hole(&ring, h)?;
}
ear_clip(&ring, area_eps)
}
pub fn extrude(&self, direction: [f32; 3], depth: f32) -> Option<Primitive> {
if !depth.is_finite() || depth <= 0.0 {
return None;
}
let d = [
f64::from(direction[0]),
f64::from(direction[1]),
f64::from(direction[2]),
];
if d.iter().any(|c| !c.is_finite()) {
return None;
}
let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
if len == 0.0 || !len.is_finite() {
return None;
}
let offset = [
d[0] / len * f64::from(depth),
d[1] / len * f64::from(depth),
d[2] / len * f64::from(depth),
];
if offset[2] == 0.0 {
return None; }
let cap = self.triangulate()?;
let v = self.vertex_count() as u32;
let mut positions: Vec<[f32; 3]> = Vec::with_capacity(2 * v as usize);
let mut flat: Vec<[f32; 2]> = Vec::with_capacity(v as usize);
flat.extend_from_slice(&self.outer);
for h in &self.holes {
flat.extend_from_slice(h);
}
for p in &flat {
positions.push([p[0], p[1], 0.0]);
}
for p in &flat {
positions.push([
(f64::from(p[0]) + offset[0]) as f32,
(f64::from(p[1]) + offset[1]) as f32,
offset[2] as f32,
]);
}
let mut tris: Vec<[u32; 3]> = Vec::with_capacity(2 * cap.len() + 4 * v as usize);
for t in &cap {
tris.push([t[0], t[2], t[1]]);
tris.push([t[0] + v, t[1] + v, t[2] + v]);
}
let mut emit_walls = |pts: &[[f32; 2]], base: u32, want_ccw: bool| {
let mut lp = clean_loop(pts, base);
if lp.len() < 3 {
return;
}
let ccw = signed_area2(&lp) > 0.0;
if ccw != want_ccw {
lp.reverse();
}
for k in 0..lp.len() {
let u = lp[k].orig;
let w = lp[(k + 1) % lp.len()].orig;
tris.push([u, w, w + v]);
tris.push([u, w + v, u + v]);
}
};
emit_walls(&self.outer, 0, true);
let mut base = self.outer.len() as u32;
for h in &self.holes {
emit_walls(h, base, false);
base += h.len() as u32;
}
if offset[2] < 0.0 {
for t in &mut tris {
t.swap(1, 2);
}
}
let mut prim = Primitive::new(Topology::Triangles);
prim.positions = positions;
let flat_indices: Vec<u32> = tris.iter().flatten().copied().collect();
prim.indices = Some(if prim.positions.len() <= 65_536 {
Indices::U16(flat_indices.iter().map(|&i| i as u16).collect())
} else {
Indices::U32(flat_indices)
});
Some(prim)
}
}