use crate::error::{Error, Result};
use nalgebra::Point2;
#[derive(Debug, Clone)]
pub struct Profile2D {
pub outer: Vec<Point2<f64>>,
pub holes: Vec<Vec<Point2<f64>>>,
}
impl Profile2D {
pub fn new(outer: Vec<Point2<f64>>) -> Self {
Self {
outer,
holes: Vec::new(),
}
}
pub fn add_hole(&mut self, hole: Vec<Point2<f64>>) {
self.holes.push(hole);
}
pub fn triangulate(&self) -> Result<Triangulation> {
if self.outer.len() < 3 {
return Err(Error::InvalidProfile(
"Profile must have at least 3 vertices".to_string(),
));
}
let mut vertices = Vec::with_capacity(
(self.outer.len() + self.holes.iter().map(|h| h.len()).sum::<usize>()) * 2,
);
for p in &self.outer {
vertices.push(p.x);
vertices.push(p.y);
}
let mut hole_indices = Vec::with_capacity(self.holes.len());
for hole in &self.holes {
hole_indices.push(vertices.len() / 2);
for p in hole {
vertices.push(p.x);
vertices.push(p.y);
}
}
let indices = if hole_indices.is_empty() {
earcutr::earcut(&vertices, &[], 2)
.map_err(|e| Error::TriangulationError(format!("{:?}", e)))?
} else {
earcutr::earcut(&vertices, &hole_indices, 2)
.map_err(|e| Error::TriangulationError(format!("{:?}", e)))?
};
let mut points = Vec::with_capacity(vertices.len() / 2);
for i in (0..vertices.len()).step_by(2) {
if i + 1 >= vertices.len() {
break;
}
points.push(Point2::new(vertices[i], vertices[i + 1]));
}
Ok(Triangulation { points, indices })
}
}
#[derive(Debug, Clone)]
pub struct Triangulation {
pub points: Vec<Point2<f64>>,
pub indices: Vec<usize>,
}
#[derive(Debug, Clone)]
pub struct VoidInfo {
pub contour: Vec<Point2<f64>>,
pub depth_start: f64,
pub depth_end: f64,
pub is_through: bool,
}
impl VoidInfo {
pub fn new(
contour: Vec<Point2<f64>>,
depth_start: f64,
depth_end: f64,
is_through: bool,
) -> Self {
Self {
contour,
depth_start,
depth_end,
is_through,
}
}
pub fn through(contour: Vec<Point2<f64>>, depth: f64) -> Self {
Self {
contour,
depth_start: 0.0,
depth_end: depth,
is_through: true,
}
}
}
#[derive(Debug, Clone)]
pub struct Profile2DWithVoids {
pub profile: Profile2D,
pub voids: Vec<VoidInfo>,
}
impl Profile2DWithVoids {
pub fn new(profile: Profile2D, voids: Vec<VoidInfo>) -> Self {
Self { profile, voids }
}
pub fn from_profile(profile: Profile2D) -> Self {
Self {
profile,
voids: Vec::new(),
}
}
pub fn add_void(&mut self, void: VoidInfo) {
self.voids.push(void);
}
pub fn through_voids(&self) -> impl Iterator<Item = &VoidInfo> {
self.voids.iter().filter(|v| v.is_through)
}
pub fn partial_voids(&self) -> impl Iterator<Item = &VoidInfo> {
self.voids.iter().filter(|v| !v.is_through)
}
pub fn has_voids(&self) -> bool {
!self.voids.is_empty()
}
pub fn void_count(&self) -> usize {
self.voids.len()
}
pub fn profile_with_through_holes(&self) -> Profile2D {
let mut profile = self.profile.clone();
for void in self.through_voids() {
profile.add_hole(void.contour.clone());
}
profile
}
}
#[derive(Debug, Clone)]
pub enum ProfileType {
Rectangle {
width: f64,
height: f64,
},
Circle {
radius: f64,
},
HollowCircle {
outer_radius: f64,
inner_radius: f64,
},
Polygon {
points: Vec<Point2<f64>>,
},
}
impl ProfileType {
pub fn to_profile(&self) -> Profile2D {
match self {
Self::Rectangle { width, height } => create_rectangle(*width, *height),
Self::Circle { radius } => create_circle(*radius, None),
Self::HollowCircle {
outer_radius,
inner_radius,
} => create_circle(*outer_radius, Some(*inner_radius)),
Self::Polygon { points } => Profile2D::new(points.clone()),
}
}
}
#[inline]
pub fn create_rectangle(width: f64, height: f64) -> Profile2D {
let half_w = width / 2.0;
let half_h = height / 2.0;
Profile2D::new(vec![
Point2::new(-half_w, -half_h),
Point2::new(half_w, -half_h),
Point2::new(half_w, half_h),
Point2::new(-half_w, half_h),
])
}
pub fn create_circle(radius: f64, hole_radius: Option<f64>) -> Profile2D {
let segments = calculate_circle_segments(radius);
let mut outer = Vec::with_capacity(segments);
for i in 0..segments {
let angle = 2.0 * std::f64::consts::PI * (i as f64) / (segments as f64);
outer.push(Point2::new(radius * angle.cos(), radius * angle.sin()));
}
let mut profile = Profile2D::new(outer);
if let Some(hole_r) = hole_radius {
let hole_segments = calculate_circle_segments(hole_r);
let mut hole = Vec::with_capacity(hole_segments);
for i in 0..hole_segments {
let angle = 2.0 * std::f64::consts::PI * (i as f64) / (hole_segments as f64);
hole.push(Point2::new(hole_r * angle.cos(), hole_r * angle.sin()));
}
hole.reverse();
profile.add_hole(hole);
}
profile
}
#[inline]
pub fn calculate_circle_segments(radius: f64) -> usize {
let segments = (radius.sqrt() * 8.0).ceil() as usize;
segments.clamp(8, 32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rectangle_profile() {
let profile = create_rectangle(10.0, 5.0);
assert_eq!(profile.outer.len(), 4);
assert_eq!(profile.holes.len(), 0);
assert_eq!(profile.outer[0], Point2::new(-5.0, -2.5));
assert_eq!(profile.outer[1], Point2::new(5.0, -2.5));
assert_eq!(profile.outer[2], Point2::new(5.0, 2.5));
assert_eq!(profile.outer[3], Point2::new(-5.0, 2.5));
}
#[test]
fn test_circle_profile() {
let profile = create_circle(5.0, None);
assert!(profile.outer.len() >= 8);
assert_eq!(profile.holes.len(), 0);
let first = profile.outer[0];
let dist = (first.x * first.x + first.y * first.y).sqrt();
assert!((dist - 5.0).abs() < 0.001);
}
#[test]
fn test_hollow_circle() {
let profile = create_circle(10.0, Some(5.0));
assert!(profile.outer.len() >= 8);
assert_eq!(profile.holes.len(), 1);
let hole = &profile.holes[0];
assert!(hole.len() >= 8);
}
#[test]
fn test_triangulate_rectangle() {
let profile = create_rectangle(10.0, 5.0);
let tri = profile.triangulate().unwrap();
assert_eq!(tri.points.len(), 4);
assert_eq!(tri.indices.len(), 6); }
#[test]
fn test_triangulate_circle() {
let profile = create_circle(5.0, None);
let tri = profile.triangulate().unwrap();
assert!(tri.points.len() >= 8);
assert_eq!(tri.indices.len(), (tri.points.len() - 2) * 3);
}
#[test]
fn test_triangulate_hollow_circle() {
let profile = create_circle(10.0, Some(5.0));
let tri = profile.triangulate().unwrap();
let outer_count = calculate_circle_segments(10.0);
let inner_count = calculate_circle_segments(5.0);
assert_eq!(tri.points.len(), outer_count + inner_count);
}
#[test]
fn test_circle_segments() {
assert_eq!(calculate_circle_segments(1.0), 8); assert_eq!(calculate_circle_segments(4.0), 16); assert!(calculate_circle_segments(100.0) <= 32); assert!(calculate_circle_segments(0.1) >= 8); }
}