use crate::error::{Error, Result};
use crate::profile::TriangulationOf;
use crate::scalar::{magnitude_squared3, transform_point4, try_normalize3, GeomScalar, MeshSink};
use nalgebra::{Matrix4, Point2, Point3, Vector3};
#[inline]
pub(crate) fn extrude_rings_into<S: GeomScalar, M: MeshSink<S>>(
outer: &[Point2<S>],
holes: &[Vec<Point2<S>>],
depth: S,
transform: Option<Matrix4<S>>,
mesh: &mut M,
) -> Result<()> {
if depth.value() <= 0.0 {
return Err(Error::InvalidExtrusion(
"Depth must be positive".to_string(),
));
}
let should_skip_caps = profile_has_extreme_aspect_ratio(outer);
let triangulation = if should_skip_caps {
None
} else {
Some(crate::profile::triangulate_rings(outer, holes)?)
};
let cap_vertex_count = triangulation
.as_ref()
.map(|t| t.points.len() * 2)
.unwrap_or(0);
let side_vertex_count = outer.len() * 2;
let total_vertices = cap_vertex_count + side_vertex_count;
let cap_index_count = triangulation
.as_ref()
.map(|t| t.indices.len() * 2)
.unwrap_or(0);
mesh.reserve(total_vertices, cap_index_count + outer.len() * 6);
if let Some(ref tri) = triangulation {
create_cap_mesh(
tri,
S::from_f64(0.0),
Vector3::new(S::from_f64(0.0), S::from_f64(0.0), S::from_f64(-1.0)),
mesh,
);
create_cap_mesh(
tri,
depth,
Vector3::new(S::from_f64(0.0), S::from_f64(0.0), S::from_f64(1.0)),
mesh,
);
}
create_side_walls(outer, depth, mesh);
for hole in holes {
create_side_walls(hole, depth, mesh);
}
if let Some(mat) = transform {
apply_transform_generic(mesh, &mat);
}
Ok(())
}
#[inline]
pub(crate) fn profile_has_extreme_aspect_ratio<S: GeomScalar>(outer: &[Point2<S>]) -> bool {
if outer.len() < 3 {
return false;
}
let mut min_x = S::from_f64(f64::MAX);
let mut max_x = S::from_f64(f64::MIN);
let mut min_y = S::from_f64(f64::MAX);
let mut max_y = S::from_f64(f64::MIN);
for p in outer {
min_x = min_x.min(p.x);
max_x = max_x.max(p.x);
min_y = min_y.min(p.y);
max_y = max_y.max(p.y);
}
let width = max_x - min_x;
let height = max_y - min_y;
if width.value() < 0.001 || height.value() < 0.001 {
return false;
}
let aspect_ratio = (width / height).max(height / width);
aspect_ratio.value() > 10000.0
}
#[inline]
pub(crate) fn create_cap_mesh<S: GeomScalar, M: MeshSink<S>>(
triangulation: &TriangulationOf<S>,
z: S,
normal: Vector3<S>,
mesh: &mut M,
) {
let base_index = mesh.vertex_count() as u32;
for point in &triangulation.points {
mesh.add_vertex(Point3::new(point.x, point.y, z), normal);
}
for i in (0..triangulation.indices.len()).step_by(3) {
if i + 2 >= triangulation.indices.len() {
break;
}
let i0 = base_index + triangulation.indices[i] as u32;
let i1 = base_index + triangulation.indices[i + 1] as u32;
let i2 = base_index + triangulation.indices[i + 2] as u32;
if z.value() == 0.0 {
mesh.add_triangle(i0, i2, i1);
} else {
mesh.add_triangle(i0, i1, i2);
}
}
}
#[inline]
pub(crate) fn create_side_walls<S: GeomScalar, M: MeshSink<S>>(
boundary: &[nalgebra::Point2<S>],
depth: S,
mesh: &mut M,
) {
let n = boundary.len();
if n < 2 {
return;
}
let mut cx = S::from_f64(0.0);
let mut cy = S::from_f64(0.0);
for p in boundary.iter() {
cx = cx + p.x;
cy = cy + p.y;
}
cx = cx / S::from_f64(n as f64);
cy = cy / S::from_f64(n as f64);
let use_smooth_radial_normals = is_approximately_circular_profile(boundary, cx, cy);
let vertex_normals: Vec<Vector3<S>> = if use_smooth_radial_normals {
boundary
.iter()
.map(|p| {
try_normalize3(
&Vector3::new(p.x - cx, p.y - cy, S::from_f64(0.0)),
1e-10,
)
.unwrap_or(Vector3::new(
S::from_f64(0.0),
S::from_f64(0.0),
S::from_f64(1.0),
))
})
.collect()
} else {
Vec::new()
};
let signed_area2: S = (0..n)
.map(|i| {
let a = &boundary[i];
let b = &boundary[(i + 1) % n];
a.x * b.y - b.x * a.y
})
.fold(S::from_f64(0.0), |acc, t| acc + t);
let winding_sign = S::from_f64(if signed_area2.value() < 0.0 { -1.0 } else { 1.0 });
let base_index = mesh.vertex_count() as u32;
let mut quad_count = 0u32;
for i in 0..n {
let j = (i + 1) % n;
let p0 = &boundary[i];
let p1 = &boundary[j];
let edge = Vector3::new(p1.x - p0.x, p1.y - p0.y, S::from_f64(0.0));
if magnitude_squared3(&edge).value() < 1e-20 {
continue;
}
let flat_normal = try_normalize3(
&Vector3::new(edge.y, -edge.x, S::from_f64(0.0)),
1e-10,
)
.map(|v| Vector3::new(v.x * winding_sign, v.y * winding_sign, v.z * winding_sign))
.unwrap_or(Vector3::new(
S::from_f64(0.0),
S::from_f64(0.0),
S::from_f64(1.0),
));
let n0 = if use_smooth_radial_normals {
vertex_normals[i]
} else {
flat_normal
};
let n1 = if use_smooth_radial_normals {
vertex_normals[j]
} else {
flat_normal
};
let v0_bottom = Point3::new(p0.x, p0.y, S::from_f64(0.0));
let v1_bottom = Point3::new(p1.x, p1.y, S::from_f64(0.0));
let v0_top = Point3::new(p0.x, p0.y, depth);
let v1_top = Point3::new(p1.x, p1.y, depth);
let idx = base_index + (quad_count * 4);
mesh.add_vertex(v0_bottom, n0);
mesh.add_vertex(v1_bottom, n1);
mesh.add_vertex(v1_top, n1);
mesh.add_vertex(v0_top, n0);
if winding_sign.value() > 0.0 {
mesh.add_triangle(idx, idx + 1, idx + 2);
mesh.add_triangle(idx, idx + 2, idx + 3);
} else {
mesh.add_triangle(idx, idx + 2, idx + 1);
mesh.add_triangle(idx, idx + 3, idx + 2);
}
quad_count += 1;
}
}
#[inline]
pub(crate) fn is_approximately_circular_profile<S: GeomScalar>(
boundary: &[Point2<S>],
cx: S,
cy: S,
) -> bool {
if boundary.len() < 20 {
return false;
}
let mut radii: Vec<S> = Vec::with_capacity(boundary.len());
for p in boundary {
let dx = p.x - cx;
let dy = p.y - cy;
let r = (dx * dx + dy * dy).sqrt();
if !r.is_finite() || r.value() < 1e-9 {
return false;
}
radii.push(r);
}
let mean = radii.iter().fold(S::from_f64(0.0), |acc, r| acc + *r)
/ S::from_f64(radii.len() as f64);
if mean.value() < 1e-9 {
return false;
}
let variance = radii
.iter()
.map(|r| {
let d = *r - mean;
d * d
})
.fold(S::from_f64(0.0), |acc, t| acc + t)
/ S::from_f64(radii.len() as f64);
let std_dev = variance.sqrt();
let coeff_var = std_dev / mean;
coeff_var.value() < 0.15
}
#[inline]
pub(crate) fn apply_transform_generic<S: GeomScalar, M: MeshSink<S>>(
mesh: &mut M,
transform: &Matrix4<S>,
) {
for i in 0..mesh.vertex_count() {
let p = mesh.position(i);
mesh.set_position(i, transform_point4(transform, &p));
}
mesh.transform_normals(transform);
}