1use crate::error::Result;
8pub(crate) use crate::profile_generic::{rectangle_ring, triangulate_rings};
9pub use crate::profile_generic::{Triangulation, TriangulationOf};
10use crate::tessellation::TessellationQuality;
11use nalgebra::Point2;
12
13#[derive(Debug, Clone)]
15pub struct Profile2D {
16 pub outer: Vec<Point2<f64>>,
18 pub holes: Vec<Vec<Point2<f64>>>,
20}
21
22impl Profile2D {
23 pub fn new(outer: Vec<Point2<f64>>) -> Self {
25 Self {
26 outer,
27 holes: Vec::new(),
28 }
29 }
30
31 pub fn add_hole(&mut self, hole: Vec<Point2<f64>>) {
33 self.holes.push(hole);
34 }
35
36 pub fn center_on_bbox(&mut self) {
47 if self.outer.is_empty() {
48 return;
49 }
50 let mut min_x = f64::INFINITY;
51 let mut min_y = f64::INFINITY;
52 let mut max_x = f64::NEG_INFINITY;
53 let mut max_y = f64::NEG_INFINITY;
54 for p in &self.outer {
55 min_x = min_x.min(p.x);
56 min_y = min_y.min(p.y);
57 max_x = max_x.max(p.x);
58 max_y = max_y.max(p.y);
59 }
60 let cx = (min_x + max_x) / 2.0;
61 let cy = (min_y + max_y) / 2.0;
62 if cx == 0.0 && cy == 0.0 {
63 return;
64 }
65 for p in &mut self.outer {
66 p.x -= cx;
67 p.y -= cy;
68 }
69 for hole in &mut self.holes {
70 for p in hole {
71 p.x -= cx;
72 p.y -= cy;
73 }
74 }
75 }
76
77 pub fn make_outer_ccw(&mut self) {
85 if crate::bool2d::compute_signed_area(&self.outer) < 0.0 {
86 self.outer.reverse();
87 }
88 }
89
90 pub fn triangulate(&self) -> Result<Triangulation> {
93 triangulate_rings(&self.outer, &self.holes)
94 }
95}
96
97
98#[derive(Debug, Clone)]
104pub struct VoidInfo {
105 pub contour: Vec<Point2<f64>>,
107 pub depth_start: f64,
109 pub depth_end: f64,
111 pub is_through: bool,
113}
114
115impl VoidInfo {
116 pub fn new(
118 contour: Vec<Point2<f64>>,
119 depth_start: f64,
120 depth_end: f64,
121 is_through: bool,
122 ) -> Self {
123 Self {
124 contour,
125 depth_start,
126 depth_end,
127 is_through,
128 }
129 }
130
131 pub fn through(contour: Vec<Point2<f64>>, depth: f64) -> Self {
133 Self {
134 contour,
135 depth_start: 0.0,
136 depth_end: depth,
137 is_through: true,
138 }
139 }
140}
141
142#[derive(Debug, Clone)]
149pub struct Profile2DWithVoids {
150 pub profile: Profile2D,
152 pub voids: Vec<VoidInfo>,
154}
155
156impl Profile2DWithVoids {
157 pub fn new(profile: Profile2D, voids: Vec<VoidInfo>) -> Self {
159 Self { profile, voids }
160 }
161
162 pub fn from_profile(profile: Profile2D) -> Self {
164 Self {
165 profile,
166 voids: Vec::new(),
167 }
168 }
169
170 pub fn add_void(&mut self, void: VoidInfo) {
172 self.voids.push(void);
173 }
174
175 pub fn through_voids(&self) -> impl Iterator<Item = &VoidInfo> {
177 self.voids.iter().filter(|v| v.is_through)
178 }
179
180 pub fn partial_voids(&self) -> impl Iterator<Item = &VoidInfo> {
182 self.voids.iter().filter(|v| !v.is_through)
183 }
184
185 pub fn has_voids(&self) -> bool {
187 !self.voids.is_empty()
188 }
189
190 pub fn void_count(&self) -> usize {
192 self.voids.len()
193 }
194
195 pub fn profile_with_through_holes(&self) -> Profile2D {
200 let mut profile = self.profile.clone();
201
202 for void in self.through_voids() {
203 profile.add_hole(void.contour.clone());
204 }
205
206 profile
207 }
208}
209
210#[derive(Debug, Clone)]
212pub enum ProfileType {
213 Rectangle {
214 width: f64,
215 height: f64,
216 },
217 Circle {
218 radius: f64,
219 },
220 HollowCircle {
221 outer_radius: f64,
222 inner_radius: f64,
223 },
224 Polygon {
225 points: Vec<Point2<f64>>,
226 },
227}
228
229impl ProfileType {
230 pub fn to_profile(&self) -> Profile2D {
232 self.to_profile_with_quality(TessellationQuality::Medium)
233 }
234
235 pub fn to_profile_with_quality(&self, quality: TessellationQuality) -> Profile2D {
238 match self {
239 Self::Rectangle { width, height } => create_rectangle(*width, *height),
240 Self::Circle { radius } => create_circle(*radius, None, quality),
241 Self::HollowCircle {
242 outer_radius,
243 inner_radius,
244 } => create_circle(*outer_radius, Some(*inner_radius), quality),
245 Self::Polygon { points } => Profile2D::new(points.clone()),
246 }
247 }
248}
249
250#[inline]
252pub fn create_rectangle(width: f64, height: f64) -> Profile2D {
253 Profile2D::new(rectangle_ring(width, height))
254}
255
256pub fn create_circle(radius: f64, hole_radius: Option<f64>, quality: TessellationQuality) -> Profile2D {
262 let segments = calculate_circle_segments(radius, quality);
263
264 let mut outer = Vec::with_capacity(segments);
265
266 for i in 0..segments {
267 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (segments as f64);
268 outer.push(Point2::new(radius * angle.cos(), radius * angle.sin()));
269 }
270
271 let mut profile = Profile2D::new(outer);
272
273 if let Some(hole_r) = hole_radius {
275 let hole_segments = calculate_circle_segments(hole_r, quality);
276 let mut hole = Vec::with_capacity(hole_segments);
277
278 for i in 0..hole_segments {
279 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (hole_segments as f64);
280 hole.push(Point2::new(hole_r * angle.cos(), hole_r * angle.sin()));
282 }
283 hole.reverse(); profile.add_hole(hole);
286 }
287
288 profile
289}
290
291#[inline]
299pub fn calculate_circle_segments(radius: f64, quality: TessellationQuality) -> usize {
300 let base = ((radius.sqrt() * 8.0).ceil() as usize).clamp(8, 32);
303
304 quality.circle_profile_segments(base)
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_rectangle_profile() {
313 let profile = create_rectangle(10.0, 5.0);
314 assert_eq!(profile.outer.len(), 4);
315 assert_eq!(profile.holes.len(), 0);
316
317 assert_eq!(profile.outer[0], Point2::new(-5.0, -2.5));
319 assert_eq!(profile.outer[1], Point2::new(5.0, -2.5));
320 assert_eq!(profile.outer[2], Point2::new(5.0, 2.5));
321 assert_eq!(profile.outer[3], Point2::new(-5.0, 2.5));
322 }
323
324 #[test]
325 fn test_circle_profile() {
326 let profile = create_circle(5.0, None, TessellationQuality::Medium);
327 assert!(profile.outer.len() >= 8);
328 assert_eq!(profile.holes.len(), 0);
329
330 let first = profile.outer[0];
332 let dist = (first.x * first.x + first.y * first.y).sqrt();
333 assert!((dist - 5.0).abs() < 0.001);
334 }
335
336 #[test]
337 fn test_hollow_circle() {
338 let profile = create_circle(10.0, Some(5.0), TessellationQuality::Medium);
339 assert!(profile.outer.len() >= 8);
340 assert_eq!(profile.holes.len(), 1);
341
342 let hole = &profile.holes[0];
344 assert!(hole.len() >= 8);
345 }
346
347 #[test]
348 fn test_triangulate_rectangle() {
349 let profile = create_rectangle(10.0, 5.0);
350 let tri = profile.triangulate().unwrap();
351
352 assert_eq!(tri.points.len(), 4);
353 assert_eq!(tri.indices.len(), 6); }
355
356 #[test]
357 fn test_triangulate_circle() {
358 let profile = create_circle(5.0, None, TessellationQuality::Medium);
359 let tri = profile.triangulate().unwrap();
360
361 assert!(tri.points.len() >= 8);
362 assert_eq!(tri.indices.len(), (tri.points.len() - 2) * 3);
364 }
365
366 #[test]
367 fn test_triangulate_hollow_circle() {
368 let profile = create_circle(10.0, Some(5.0), TessellationQuality::Medium);
369 let tri = profile.triangulate().unwrap();
370
371 let outer_count = calculate_circle_segments(10.0, TessellationQuality::Medium);
373 let inner_count = calculate_circle_segments(5.0, TessellationQuality::Medium);
374 assert_eq!(tri.points.len(), outer_count + inner_count);
375 }
376
377 #[test]
378 fn test_circle_segments() {
379 use TessellationQuality::Medium;
380 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); }
385}