1use crate::error::{Error, Result};
8use nalgebra::Point2;
9
10#[derive(Debug, Clone)]
12pub struct Profile2D {
13 pub outer: Vec<Point2<f64>>,
15 pub holes: Vec<Vec<Point2<f64>>>,
17}
18
19impl Profile2D {
20 pub fn new(outer: Vec<Point2<f64>>) -> Self {
22 Self {
23 outer,
24 holes: Vec::new(),
25 }
26 }
27
28 pub fn add_hole(&mut self, hole: Vec<Point2<f64>>) {
30 self.holes.push(hole);
31 }
32
33 pub fn triangulate(&self) -> Result<Triangulation> {
36 if self.outer.len() < 3 {
37 return Err(Error::InvalidProfile(
38 "Profile must have at least 3 vertices".to_string(),
39 ));
40 }
41
42 let mut vertices = Vec::with_capacity(
44 (self.outer.len() + self.holes.iter().map(|h| h.len()).sum::<usize>()) * 2,
45 );
46
47 for p in &self.outer {
49 vertices.push(p.x);
50 vertices.push(p.y);
51 }
52
53 let mut hole_indices = Vec::with_capacity(self.holes.len());
55 for hole in &self.holes {
56 hole_indices.push(vertices.len() / 2);
57 for p in hole {
58 vertices.push(p.x);
59 vertices.push(p.y);
60 }
61 }
62
63 let indices = if hole_indices.is_empty() {
65 earcutr::earcut(&vertices, &[], 2)
66 .map_err(|e| Error::TriangulationError(format!("{:?}", e)))?
67 } else {
68 earcutr::earcut(&vertices, &hole_indices, 2)
69 .map_err(|e| Error::TriangulationError(format!("{:?}", e)))?
70 };
71
72 let mut points = Vec::with_capacity(vertices.len() / 2);
74 for i in (0..vertices.len()).step_by(2) {
75 if i + 1 >= vertices.len() {
76 break;
77 }
78 points.push(Point2::new(vertices[i], vertices[i + 1]));
79 }
80
81 Ok(Triangulation { points, indices })
82 }
83}
84
85#[derive(Debug, Clone)]
87pub struct Triangulation {
88 pub points: Vec<Point2<f64>>,
90 pub indices: Vec<usize>,
92}
93
94#[derive(Debug, Clone)]
100pub struct VoidInfo {
101 pub contour: Vec<Point2<f64>>,
103 pub depth_start: f64,
105 pub depth_end: f64,
107 pub is_through: bool,
109}
110
111impl VoidInfo {
112 pub fn new(contour: Vec<Point2<f64>>, depth_start: f64, depth_end: f64, is_through: bool) -> Self {
114 Self {
115 contour,
116 depth_start,
117 depth_end,
118 is_through,
119 }
120 }
121
122 pub fn through(contour: Vec<Point2<f64>>, depth: f64) -> Self {
124 Self {
125 contour,
126 depth_start: 0.0,
127 depth_end: depth,
128 is_through: true,
129 }
130 }
131}
132
133#[derive(Debug, Clone)]
140pub struct Profile2DWithVoids {
141 pub profile: Profile2D,
143 pub voids: Vec<VoidInfo>,
145}
146
147impl Profile2DWithVoids {
148 pub fn new(profile: Profile2D, voids: Vec<VoidInfo>) -> Self {
150 Self { profile, voids }
151 }
152
153 pub fn from_profile(profile: Profile2D) -> Self {
155 Self {
156 profile,
157 voids: Vec::new(),
158 }
159 }
160
161 pub fn add_void(&mut self, void: VoidInfo) {
163 self.voids.push(void);
164 }
165
166 pub fn through_voids(&self) -> impl Iterator<Item = &VoidInfo> {
168 self.voids.iter().filter(|v| v.is_through)
169 }
170
171 pub fn partial_voids(&self) -> impl Iterator<Item = &VoidInfo> {
173 self.voids.iter().filter(|v| !v.is_through)
174 }
175
176 pub fn has_voids(&self) -> bool {
178 !self.voids.is_empty()
179 }
180
181 pub fn void_count(&self) -> usize {
183 self.voids.len()
184 }
185
186 pub fn profile_with_through_holes(&self) -> Profile2D {
191 let mut profile = self.profile.clone();
192
193 for void in self.through_voids() {
194 profile.add_hole(void.contour.clone());
195 }
196
197 profile
198 }
199}
200
201#[derive(Debug, Clone)]
203pub enum ProfileType {
204 Rectangle {
205 width: f64,
206 height: f64,
207 },
208 Circle {
209 radius: f64,
210 },
211 HollowCircle {
212 outer_radius: f64,
213 inner_radius: f64,
214 },
215 Polygon {
216 points: Vec<Point2<f64>>,
217 },
218}
219
220impl ProfileType {
221 pub fn to_profile(&self) -> Profile2D {
223 match self {
224 Self::Rectangle { width, height } => create_rectangle(*width, *height),
225 Self::Circle { radius } => create_circle(*radius, None),
226 Self::HollowCircle {
227 outer_radius,
228 inner_radius,
229 } => create_circle(*outer_radius, Some(*inner_radius)),
230 Self::Polygon { points } => Profile2D::new(points.clone()),
231 }
232 }
233}
234
235#[inline]
237pub fn create_rectangle(width: f64, height: f64) -> Profile2D {
238 let half_w = width / 2.0;
239 let half_h = height / 2.0;
240
241 Profile2D::new(vec![
242 Point2::new(-half_w, -half_h),
243 Point2::new(half_w, -half_h),
244 Point2::new(half_w, half_h),
245 Point2::new(-half_w, half_h),
246 ])
247}
248
249pub fn create_circle(radius: f64, hole_radius: Option<f64>) -> Profile2D {
252 let segments = calculate_circle_segments(radius);
253
254 let mut outer = Vec::with_capacity(segments);
255
256 for i in 0..segments {
257 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (segments as f64);
258 outer.push(Point2::new(radius * angle.cos(), radius * angle.sin()));
259 }
260
261 let mut profile = Profile2D::new(outer);
262
263 if let Some(hole_r) = hole_radius {
265 let hole_segments = calculate_circle_segments(hole_r);
266 let mut hole = Vec::with_capacity(hole_segments);
267
268 for i in 0..hole_segments {
269 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (hole_segments as f64);
270 hole.push(Point2::new(hole_r * angle.cos(), hole_r * angle.sin()));
272 }
273 hole.reverse(); profile.add_hole(hole);
276 }
277
278 profile
279}
280
281#[inline]
284pub fn calculate_circle_segments(radius: f64) -> usize {
285 let segments = (radius.sqrt() * 8.0).ceil() as usize;
288
289 segments.clamp(8, 32)
291}
292
293#[cfg(test)]
294mod tests {
295 use super::*;
296
297 #[test]
298 fn test_rectangle_profile() {
299 let profile = create_rectangle(10.0, 5.0);
300 assert_eq!(profile.outer.len(), 4);
301 assert_eq!(profile.holes.len(), 0);
302
303 assert_eq!(profile.outer[0], Point2::new(-5.0, -2.5));
305 assert_eq!(profile.outer[1], Point2::new(5.0, -2.5));
306 assert_eq!(profile.outer[2], Point2::new(5.0, 2.5));
307 assert_eq!(profile.outer[3], Point2::new(-5.0, 2.5));
308 }
309
310 #[test]
311 fn test_circle_profile() {
312 let profile = create_circle(5.0, None);
313 assert!(profile.outer.len() >= 8);
314 assert_eq!(profile.holes.len(), 0);
315
316 let first = profile.outer[0];
318 let dist = (first.x * first.x + first.y * first.y).sqrt();
319 assert!((dist - 5.0).abs() < 0.001);
320 }
321
322 #[test]
323 fn test_hollow_circle() {
324 let profile = create_circle(10.0, Some(5.0));
325 assert!(profile.outer.len() >= 8);
326 assert_eq!(profile.holes.len(), 1);
327
328 let hole = &profile.holes[0];
330 assert!(hole.len() >= 8);
331 }
332
333 #[test]
334 fn test_triangulate_rectangle() {
335 let profile = create_rectangle(10.0, 5.0);
336 let tri = profile.triangulate().unwrap();
337
338 assert_eq!(tri.points.len(), 4);
339 assert_eq!(tri.indices.len(), 6); }
341
342 #[test]
343 fn test_triangulate_circle() {
344 let profile = create_circle(5.0, None);
345 let tri = profile.triangulate().unwrap();
346
347 assert!(tri.points.len() >= 8);
348 assert_eq!(tri.indices.len(), (tri.points.len() - 2) * 3);
350 }
351
352 #[test]
353 fn test_triangulate_hollow_circle() {
354 let profile = create_circle(10.0, Some(5.0));
355 let tri = profile.triangulate().unwrap();
356
357 let outer_count = calculate_circle_segments(10.0);
359 let inner_count = calculate_circle_segments(5.0);
360 assert_eq!(tri.points.len(), outer_count + inner_count);
361 }
362
363 #[test]
364 fn test_circle_segments() {
365 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); }
370}