1use crate::error::{Error, Result};
8use crate::tessellation::TessellationQuality;
9use nalgebra::Point2;
10
11#[derive(Debug, Clone)]
13pub struct Profile2D {
14 pub outer: Vec<Point2<f64>>,
16 pub holes: Vec<Vec<Point2<f64>>>,
18}
19
20impl Profile2D {
21 pub fn new(outer: Vec<Point2<f64>>) -> Self {
23 Self {
24 outer,
25 holes: Vec::new(),
26 }
27 }
28
29 pub fn add_hole(&mut self, hole: Vec<Point2<f64>>) {
31 self.holes.push(hole);
32 }
33
34 pub fn center_on_bbox(&mut self) {
45 if self.outer.is_empty() {
46 return;
47 }
48 let mut min_x = f64::INFINITY;
49 let mut min_y = f64::INFINITY;
50 let mut max_x = f64::NEG_INFINITY;
51 let mut max_y = f64::NEG_INFINITY;
52 for p in &self.outer {
53 min_x = min_x.min(p.x);
54 min_y = min_y.min(p.y);
55 max_x = max_x.max(p.x);
56 max_y = max_y.max(p.y);
57 }
58 let cx = (min_x + max_x) / 2.0;
59 let cy = (min_y + max_y) / 2.0;
60 if cx == 0.0 && cy == 0.0 {
61 return;
62 }
63 for p in &mut self.outer {
64 p.x -= cx;
65 p.y -= cy;
66 }
67 for hole in &mut self.holes {
68 for p in hole {
69 p.x -= cx;
70 p.y -= cy;
71 }
72 }
73 }
74
75 pub fn triangulate(&self) -> Result<Triangulation> {
78 if self.outer.len() < 3 {
79 return Err(Error::InvalidProfile(
80 "Profile must have at least 3 vertices".to_string(),
81 ));
82 }
83
84 let mut vertices = Vec::with_capacity(
86 (self.outer.len() + self.holes.iter().map(|h| h.len()).sum::<usize>()) * 2,
87 );
88
89 for p in &self.outer {
91 vertices.push(p.x);
92 vertices.push(p.y);
93 }
94
95 let mut hole_indices = Vec::with_capacity(self.holes.len());
97 for hole in &self.holes {
98 hole_indices.push(vertices.len() / 2);
99 for p in hole {
100 vertices.push(p.x);
101 vertices.push(p.y);
102 }
103 }
104
105 let indices = if hole_indices.is_empty() {
107 crate::triangulation::safe_earcut(&vertices, &[], 2)
108 .map_err(Error::TriangulationError)?
109 } else {
110 crate::triangulation::safe_earcut(&vertices, &hole_indices, 2)
111 .map_err(Error::TriangulationError)?
112 };
113
114 let mut points = Vec::with_capacity(vertices.len() / 2);
116 for i in (0..vertices.len()).step_by(2) {
117 if i + 1 >= vertices.len() {
118 break;
119 }
120 points.push(Point2::new(vertices[i], vertices[i + 1]));
121 }
122
123 Ok(Triangulation { points, indices })
124 }
125}
126
127#[derive(Debug, Clone)]
129pub struct Triangulation {
130 pub points: Vec<Point2<f64>>,
132 pub indices: Vec<usize>,
134}
135
136#[derive(Debug, Clone)]
142pub struct VoidInfo {
143 pub contour: Vec<Point2<f64>>,
145 pub depth_start: f64,
147 pub depth_end: f64,
149 pub is_through: bool,
151}
152
153impl VoidInfo {
154 pub fn new(
156 contour: Vec<Point2<f64>>,
157 depth_start: f64,
158 depth_end: f64,
159 is_through: bool,
160 ) -> Self {
161 Self {
162 contour,
163 depth_start,
164 depth_end,
165 is_through,
166 }
167 }
168
169 pub fn through(contour: Vec<Point2<f64>>, depth: f64) -> Self {
171 Self {
172 contour,
173 depth_start: 0.0,
174 depth_end: depth,
175 is_through: true,
176 }
177 }
178}
179
180#[derive(Debug, Clone)]
187pub struct Profile2DWithVoids {
188 pub profile: Profile2D,
190 pub voids: Vec<VoidInfo>,
192}
193
194impl Profile2DWithVoids {
195 pub fn new(profile: Profile2D, voids: Vec<VoidInfo>) -> Self {
197 Self { profile, voids }
198 }
199
200 pub fn from_profile(profile: Profile2D) -> Self {
202 Self {
203 profile,
204 voids: Vec::new(),
205 }
206 }
207
208 pub fn add_void(&mut self, void: VoidInfo) {
210 self.voids.push(void);
211 }
212
213 pub fn through_voids(&self) -> impl Iterator<Item = &VoidInfo> {
215 self.voids.iter().filter(|v| v.is_through)
216 }
217
218 pub fn partial_voids(&self) -> impl Iterator<Item = &VoidInfo> {
220 self.voids.iter().filter(|v| !v.is_through)
221 }
222
223 pub fn has_voids(&self) -> bool {
225 !self.voids.is_empty()
226 }
227
228 pub fn void_count(&self) -> usize {
230 self.voids.len()
231 }
232
233 pub fn profile_with_through_holes(&self) -> Profile2D {
238 let mut profile = self.profile.clone();
239
240 for void in self.through_voids() {
241 profile.add_hole(void.contour.clone());
242 }
243
244 profile
245 }
246}
247
248#[derive(Debug, Clone)]
250pub enum ProfileType {
251 Rectangle {
252 width: f64,
253 height: f64,
254 },
255 Circle {
256 radius: f64,
257 },
258 HollowCircle {
259 outer_radius: f64,
260 inner_radius: f64,
261 },
262 Polygon {
263 points: Vec<Point2<f64>>,
264 },
265}
266
267impl ProfileType {
268 pub fn to_profile(&self) -> Profile2D {
270 self.to_profile_with_quality(TessellationQuality::Medium)
271 }
272
273 pub fn to_profile_with_quality(&self, quality: TessellationQuality) -> Profile2D {
276 match self {
277 Self::Rectangle { width, height } => create_rectangle(*width, *height),
278 Self::Circle { radius } => create_circle(*radius, None, quality),
279 Self::HollowCircle {
280 outer_radius,
281 inner_radius,
282 } => create_circle(*outer_radius, Some(*inner_radius), quality),
283 Self::Polygon { points } => Profile2D::new(points.clone()),
284 }
285 }
286}
287
288#[inline]
290pub fn create_rectangle(width: f64, height: f64) -> Profile2D {
291 let half_w = width / 2.0;
292 let half_h = height / 2.0;
293
294 Profile2D::new(vec![
295 Point2::new(-half_w, -half_h),
296 Point2::new(half_w, -half_h),
297 Point2::new(half_w, half_h),
298 Point2::new(-half_w, half_h),
299 ])
300}
301
302pub fn create_circle(radius: f64, hole_radius: Option<f64>, quality: TessellationQuality) -> Profile2D {
308 let segments = calculate_circle_segments(radius, quality);
309
310 let mut outer = Vec::with_capacity(segments);
311
312 for i in 0..segments {
313 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (segments as f64);
314 outer.push(Point2::new(radius * angle.cos(), radius * angle.sin()));
315 }
316
317 let mut profile = Profile2D::new(outer);
318
319 if let Some(hole_r) = hole_radius {
321 let hole_segments = calculate_circle_segments(hole_r, quality);
322 let mut hole = Vec::with_capacity(hole_segments);
323
324 for i in 0..hole_segments {
325 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (hole_segments as f64);
326 hole.push(Point2::new(hole_r * angle.cos(), hole_r * angle.sin()));
328 }
329 hole.reverse(); profile.add_hole(hole);
332 }
333
334 profile
335}
336
337#[inline]
345pub fn calculate_circle_segments(radius: f64, quality: TessellationQuality) -> usize {
346 let base = ((radius.sqrt() * 8.0).ceil() as usize).clamp(8, 32);
349
350 quality.circle_profile_segments(base)
351}
352
353#[cfg(test)]
354mod tests {
355 use super::*;
356
357 #[test]
358 fn test_rectangle_profile() {
359 let profile = create_rectangle(10.0, 5.0);
360 assert_eq!(profile.outer.len(), 4);
361 assert_eq!(profile.holes.len(), 0);
362
363 assert_eq!(profile.outer[0], Point2::new(-5.0, -2.5));
365 assert_eq!(profile.outer[1], Point2::new(5.0, -2.5));
366 assert_eq!(profile.outer[2], Point2::new(5.0, 2.5));
367 assert_eq!(profile.outer[3], Point2::new(-5.0, 2.5));
368 }
369
370 #[test]
371 fn test_circle_profile() {
372 let profile = create_circle(5.0, None, TessellationQuality::Medium);
373 assert!(profile.outer.len() >= 8);
374 assert_eq!(profile.holes.len(), 0);
375
376 let first = profile.outer[0];
378 let dist = (first.x * first.x + first.y * first.y).sqrt();
379 assert!((dist - 5.0).abs() < 0.001);
380 }
381
382 #[test]
383 fn test_hollow_circle() {
384 let profile = create_circle(10.0, Some(5.0), TessellationQuality::Medium);
385 assert!(profile.outer.len() >= 8);
386 assert_eq!(profile.holes.len(), 1);
387
388 let hole = &profile.holes[0];
390 assert!(hole.len() >= 8);
391 }
392
393 #[test]
394 fn test_triangulate_rectangle() {
395 let profile = create_rectangle(10.0, 5.0);
396 let tri = profile.triangulate().unwrap();
397
398 assert_eq!(tri.points.len(), 4);
399 assert_eq!(tri.indices.len(), 6); }
401
402 #[test]
403 fn test_triangulate_circle() {
404 let profile = create_circle(5.0, None, TessellationQuality::Medium);
405 let tri = profile.triangulate().unwrap();
406
407 assert!(tri.points.len() >= 8);
408 assert_eq!(tri.indices.len(), (tri.points.len() - 2) * 3);
410 }
411
412 #[test]
413 fn test_triangulate_hollow_circle() {
414 let profile = create_circle(10.0, Some(5.0), TessellationQuality::Medium);
415 let tri = profile.triangulate().unwrap();
416
417 let outer_count = calculate_circle_segments(10.0, TessellationQuality::Medium);
419 let inner_count = calculate_circle_segments(5.0, TessellationQuality::Medium);
420 assert_eq!(tri.points.len(), outer_count + inner_count);
421 }
422
423 #[test]
424 fn test_circle_segments() {
425 use TessellationQuality::Medium;
426 assert_eq!(calculate_circle_segments(1.0, Medium), 8); assert_eq!(calculate_circle_segments(4.0, Medium), 16); assert!(calculate_circle_segments(100.0, Medium) <= 32); assert!(calculate_circle_segments(0.1, Medium) >= 8); }
431}