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 triangulate(&self) -> Result<Triangulation> {
80 triangulate_rings(&self.outer, &self.holes)
81 }
82}
83
84
85#[derive(Debug, Clone)]
91pub struct VoidInfo {
92 pub contour: Vec<Point2<f64>>,
94 pub depth_start: f64,
96 pub depth_end: f64,
98 pub is_through: bool,
100}
101
102impl VoidInfo {
103 pub fn new(
105 contour: Vec<Point2<f64>>,
106 depth_start: f64,
107 depth_end: f64,
108 is_through: bool,
109 ) -> Self {
110 Self {
111 contour,
112 depth_start,
113 depth_end,
114 is_through,
115 }
116 }
117
118 pub fn through(contour: Vec<Point2<f64>>, depth: f64) -> Self {
120 Self {
121 contour,
122 depth_start: 0.0,
123 depth_end: depth,
124 is_through: true,
125 }
126 }
127}
128
129#[derive(Debug, Clone)]
136pub struct Profile2DWithVoids {
137 pub profile: Profile2D,
139 pub voids: Vec<VoidInfo>,
141}
142
143impl Profile2DWithVoids {
144 pub fn new(profile: Profile2D, voids: Vec<VoidInfo>) -> Self {
146 Self { profile, voids }
147 }
148
149 pub fn from_profile(profile: Profile2D) -> Self {
151 Self {
152 profile,
153 voids: Vec::new(),
154 }
155 }
156
157 pub fn add_void(&mut self, void: VoidInfo) {
159 self.voids.push(void);
160 }
161
162 pub fn through_voids(&self) -> impl Iterator<Item = &VoidInfo> {
164 self.voids.iter().filter(|v| v.is_through)
165 }
166
167 pub fn partial_voids(&self) -> impl Iterator<Item = &VoidInfo> {
169 self.voids.iter().filter(|v| !v.is_through)
170 }
171
172 pub fn has_voids(&self) -> bool {
174 !self.voids.is_empty()
175 }
176
177 pub fn void_count(&self) -> usize {
179 self.voids.len()
180 }
181
182 pub fn profile_with_through_holes(&self) -> Profile2D {
187 let mut profile = self.profile.clone();
188
189 for void in self.through_voids() {
190 profile.add_hole(void.contour.clone());
191 }
192
193 profile
194 }
195}
196
197#[derive(Debug, Clone)]
199pub enum ProfileType {
200 Rectangle {
201 width: f64,
202 height: f64,
203 },
204 Circle {
205 radius: f64,
206 },
207 HollowCircle {
208 outer_radius: f64,
209 inner_radius: f64,
210 },
211 Polygon {
212 points: Vec<Point2<f64>>,
213 },
214}
215
216impl ProfileType {
217 pub fn to_profile(&self) -> Profile2D {
219 self.to_profile_with_quality(TessellationQuality::Medium)
220 }
221
222 pub fn to_profile_with_quality(&self, quality: TessellationQuality) -> Profile2D {
225 match self {
226 Self::Rectangle { width, height } => create_rectangle(*width, *height),
227 Self::Circle { radius } => create_circle(*radius, None, quality),
228 Self::HollowCircle {
229 outer_radius,
230 inner_radius,
231 } => create_circle(*outer_radius, Some(*inner_radius), quality),
232 Self::Polygon { points } => Profile2D::new(points.clone()),
233 }
234 }
235}
236
237#[inline]
239pub fn create_rectangle(width: f64, height: f64) -> Profile2D {
240 Profile2D::new(rectangle_ring(width, height))
241}
242
243pub fn create_circle(radius: f64, hole_radius: Option<f64>, quality: TessellationQuality) -> Profile2D {
249 let segments = calculate_circle_segments(radius, quality);
250
251 let mut outer = Vec::with_capacity(segments);
252
253 for i in 0..segments {
254 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (segments as f64);
255 outer.push(Point2::new(radius * angle.cos(), radius * angle.sin()));
256 }
257
258 let mut profile = Profile2D::new(outer);
259
260 if let Some(hole_r) = hole_radius {
262 let hole_segments = calculate_circle_segments(hole_r, quality);
263 let mut hole = Vec::with_capacity(hole_segments);
264
265 for i in 0..hole_segments {
266 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (hole_segments as f64);
267 hole.push(Point2::new(hole_r * angle.cos(), hole_r * angle.sin()));
269 }
270 hole.reverse(); profile.add_hole(hole);
273 }
274
275 profile
276}
277
278#[inline]
286pub fn calculate_circle_segments(radius: f64, quality: TessellationQuality) -> usize {
287 let base = ((radius.sqrt() * 8.0).ceil() as usize).clamp(8, 32);
290
291 quality.circle_profile_segments(base)
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn test_rectangle_profile() {
300 let profile = create_rectangle(10.0, 5.0);
301 assert_eq!(profile.outer.len(), 4);
302 assert_eq!(profile.holes.len(), 0);
303
304 assert_eq!(profile.outer[0], Point2::new(-5.0, -2.5));
306 assert_eq!(profile.outer[1], Point2::new(5.0, -2.5));
307 assert_eq!(profile.outer[2], Point2::new(5.0, 2.5));
308 assert_eq!(profile.outer[3], Point2::new(-5.0, 2.5));
309 }
310
311 #[test]
312 fn test_circle_profile() {
313 let profile = create_circle(5.0, None, TessellationQuality::Medium);
314 assert!(profile.outer.len() >= 8);
315 assert_eq!(profile.holes.len(), 0);
316
317 let first = profile.outer[0];
319 let dist = (first.x * first.x + first.y * first.y).sqrt();
320 assert!((dist - 5.0).abs() < 0.001);
321 }
322
323 #[test]
324 fn test_hollow_circle() {
325 let profile = create_circle(10.0, Some(5.0), TessellationQuality::Medium);
326 assert!(profile.outer.len() >= 8);
327 assert_eq!(profile.holes.len(), 1);
328
329 let hole = &profile.holes[0];
331 assert!(hole.len() >= 8);
332 }
333
334 #[test]
335 fn test_triangulate_rectangle() {
336 let profile = create_rectangle(10.0, 5.0);
337 let tri = profile.triangulate().unwrap();
338
339 assert_eq!(tri.points.len(), 4);
340 assert_eq!(tri.indices.len(), 6); }
342
343 #[test]
344 fn test_triangulate_circle() {
345 let profile = create_circle(5.0, None, TessellationQuality::Medium);
346 let tri = profile.triangulate().unwrap();
347
348 assert!(tri.points.len() >= 8);
349 assert_eq!(tri.indices.len(), (tri.points.len() - 2) * 3);
351 }
352
353 #[test]
354 fn test_triangulate_hollow_circle() {
355 let profile = create_circle(10.0, Some(5.0), TessellationQuality::Medium);
356 let tri = profile.triangulate().unwrap();
357
358 let outer_count = calculate_circle_segments(10.0, TessellationQuality::Medium);
360 let inner_count = calculate_circle_segments(5.0, TessellationQuality::Medium);
361 assert_eq!(tri.points.len(), outer_count + inner_count);
362 }
363
364 #[test]
365 fn test_circle_segments() {
366 use TessellationQuality::Medium;
367 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); }
372}