1use neco_cdt::CdtError;
4
5use crate::brep::{Curve3D, Edge, Face, Shell, Surface};
6use crate::vec3;
7
8#[derive(Debug, Clone)]
10pub struct TriMesh {
11 pub vertices: Vec<[f64; 3]>,
12 pub normals: Vec<[f64; 3]>, pub triangles: Vec<[usize; 3]>, }
15
16#[derive(Debug, Clone, Copy)]
17struct TrimSample {
18 uv: [f64; 2],
19 point: [f64; 3],
20}
21
22impl TriMesh {
23 pub fn new() -> Self {
25 Self {
26 vertices: Vec::new(),
27 normals: Vec::new(),
28 triangles: Vec::new(),
29 }
30 }
31
32 pub fn merge(&mut self, other: &TriMesh) {
34 let offset = self.vertices.len();
35 self.vertices.extend_from_slice(&other.vertices);
36 self.normals.extend_from_slice(&other.normals);
37 for tri in &other.triangles {
38 self.triangles
39 .push([tri[0] + offset, tri[1] + offset, tri[2] + offset]);
40 }
41 }
42}
43
44impl Default for TriMesh {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl TriMesh {
51 pub fn weld_vertices(&mut self, tolerance: f64) {
53 let n = self.vertices.len();
54 if n == 0 {
55 return;
56 }
57
58 let mut remap = vec![0usize; n];
60 let mut new_vertices: Vec<[f64; 3]> = Vec::new();
61 let mut new_normals: Vec<[f64; 3]> = Vec::new();
62 let tol_sq = tolerance * tolerance;
63
64 for (i, remap_slot) in remap.iter_mut().enumerate().take(n) {
65 let mut found = None;
66 for (j, nv) in new_vertices.iter().enumerate() {
67 let d = vec3::sub(self.vertices[i], *nv);
68 if d[0] * d[0] + d[1] * d[1] + d[2] * d[2] < tol_sq {
69 found = Some(j);
70 break;
71 }
72 }
73 match found {
74 Some(j) => {
75 *remap_slot = j;
76 }
77 None => {
78 *remap_slot = new_vertices.len();
79 new_vertices.push(self.vertices[i]);
80 new_normals.push(self.normals[i]);
81 }
82 }
83 }
84
85 for tri in &mut self.triangles {
87 tri[0] = remap[tri[0]];
88 tri[1] = remap[tri[1]];
89 tri[2] = remap[tri[2]];
90 }
91
92 self.vertices = new_vertices;
93 self.normals = new_normals;
94 }
95}
96
97impl Shell {
98 pub fn tessellate(&self, density: usize) -> Result<TriMesh, CdtError> {
103 let density = density.max(2);
104 let mut mesh = TriMesh::new();
105
106 let debug = std::env::var("DEBUG_FACE_TESSELLATE").is_ok();
107 for (face_index, face) in self.faces.iter().enumerate() {
108 if debug {
109 println!(
110 "tessellate face[{face_index}]: kind={} loop_edges={} reversed={}",
111 tessellation_surface_kind(&face.surface),
112 face.loop_edges.len(),
113 face.orientation_reversed
114 );
115 }
116 let face_mesh = tessellate_face(face, &self.vertices, &self.edges, density)?;
117 if debug {
118 println!(
119 " -> vertices={} triangles={}",
120 face_mesh.vertices.len(),
121 face_mesh.triangles.len()
122 );
123 }
124 mesh.merge(&face_mesh);
125 }
126
127 mesh.weld_vertices(1e-10);
129
130 Ok(mesh)
131 }
132}
133
134fn tessellation_surface_kind(surface: &Surface) -> &'static str {
135 match surface {
136 Surface::Plane { .. } => "Plane",
137 Surface::Cylinder { .. } => "Cylinder",
138 Surface::Cone { .. } => "Cone",
139 Surface::Sphere { .. } => "Sphere",
140 Surface::Ellipsoid { .. } => "Ellipsoid",
141 Surface::Torus { .. } => "Torus",
142 Surface::SurfaceOfRevolution { .. } => "SurfaceOfRevolution",
143 Surface::SurfaceOfSweep { .. } => "SurfaceOfSweep",
144 Surface::NurbsSurface { .. } => "NurbsSurface",
145 }
146}
147
148fn tessellate_face(
150 face: &Face,
151 vertices: &[[f64; 3]],
152 edges: &[Edge],
153 density: usize,
154) -> Result<TriMesh, CdtError> {
155 match &face.surface {
156 Surface::Plane { .. } => tessellate_plane(face, vertices, edges, density),
157 _ => tessellate_parametric(face, edges, density),
158 }
159}
160
161fn tessellate_plane(
165 face: &Face,
166 vertices: &[[f64; 3]],
167 edges: &[Edge],
168 density: usize,
169) -> Result<TriMesh, CdtError> {
170 let pts_3d = collect_loop_points(face, edges, density);
172 match tessellate_plane_polygon(face, &pts_3d) {
173 Ok(mesh) if !mesh.triangles.is_empty() => Ok(mesh),
174 Ok(_) | Err(_) => {
175 let fallback_pts = collect_loop_vertex_points(face, vertices, edges);
176 tessellate_plane_polygon(face, &fallback_pts)
177 }
178 }
179}
180
181fn tessellate_plane_polygon(face: &Face, pts_3d: &[[f64; 3]]) -> Result<TriMesh, CdtError> {
182 let mut normal = face.surface.normal_at(0.0, 0.0);
183 if face.orientation_reversed {
184 normal = vec3::scale(normal, -1.0);
185 }
186
187 let n = vec3::normalized(normal);
189 let up = if n[0].abs() < 0.9 {
190 [1.0, 0.0, 0.0]
191 } else {
192 [0.0, 1.0, 0.0]
193 };
194 let u_vec = vec3::normalized(vec3::cross(n, up));
195 let v_vec = vec3::cross(n, u_vec);
196
197 if pts_3d.len() < 3 {
198 return Ok(TriMesh::new());
199 }
200
201 let origin = pts_3d[0];
203
204 let pts_2d: Vec<[f64; 2]> = pts_3d
206 .iter()
207 .map(|p| {
208 let d = vec3::sub(*p, origin);
209 [vec3::dot(d, u_vec), vec3::dot(d, v_vec)]
210 })
211 .collect();
212
213 let mut min_x = f64::INFINITY;
215 let mut min_y = f64::INFINITY;
216 let mut max_x = f64::NEG_INFINITY;
217 let mut max_y = f64::NEG_INFINITY;
218 for p in &pts_2d {
219 min_x = min_x.min(p[0]);
220 min_y = min_y.min(p[1]);
221 max_x = max_x.max(p[0]);
222 max_y = max_y.max(p[1]);
223 }
224 let margin = ((max_x - min_x).max(max_y - min_y)) * 0.01;
225 let bounds = (
226 min_x - margin,
227 min_y - margin,
228 max_x + margin,
229 max_y + margin,
230 );
231
232 let mut cdt = neco_cdt::Cdt::new(bounds);
234 cdt.add_constraint_edges(&pts_2d, true)?;
235 let cdt_tris = cdt.triangles();
236
237 let mut mesh = TriMesh::new();
239
240 let user_verts = cdt.user_vertices();
242 for uv in user_verts {
243 let p3d = vec3::add(
244 vec3::add(origin, vec3::scale(u_vec, uv[0])),
245 vec3::scale(v_vec, uv[1]),
246 );
247 mesh.vertices.push(p3d);
248 mesh.normals.push(normal);
249 }
250
251 for tri in &cdt_tris {
253 let centroid_2d = [
255 (user_verts[tri[0]][0] + user_verts[tri[1]][0] + user_verts[tri[2]][0]) / 3.0,
256 (user_verts[tri[0]][1] + user_verts[tri[1]][1] + user_verts[tri[2]][1]) / 3.0,
257 ];
258 if point_in_polygon_2d(centroid_2d, &pts_2d) {
259 mesh.triangles.push(*tri);
260 }
261 }
262
263 Ok(mesh)
264}
265
266#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271enum TrimmedParametricFailure {
272 TrimLoopUnavailable,
273 CdtFailure,
274 EmptyMesh,
275 TriangleLeakage,
276}
277
278fn tessellate_parametric(face: &Face, edges: &[Edge], density: usize) -> Result<TriMesh, CdtError> {
279 if face.loop_edges.is_empty() {
280 return Ok(tessellate_parametric_full(face, density));
281 }
282
283 let trim_samples = match try_collect_trim_loop_samples(face, edges, density) {
284 Ok(trim_samples) => trim_samples,
285 Err(failure) => {
286 return Ok(parametric_failure_mesh(
287 face,
288 density,
289 failure,
290 TriMesh::new(),
291 ));
292 }
293 };
294
295 let trimmed = if matches!(
296 face.surface,
297 Surface::Sphere { .. } | Surface::Ellipsoid { .. }
298 ) && (face.loop_edges.len() == 3
299 || trim_loop_touches_parametric_singularity(&trim_samples))
300 {
301 Ok(tessellate_parametric_fan(
302 face,
303 density.max(2),
304 &trim_samples,
305 ))
306 } else {
307 tessellate_parametric_trimmed(face, density, &trim_samples)
308 };
309
310 match trimmed {
311 Ok(mesh) => {
312 if mesh.triangles.is_empty() {
313 return Ok(parametric_failure_mesh(
314 face,
315 density,
316 TrimmedParametricFailure::EmptyMesh,
317 mesh,
318 ));
319 }
320 if triangles_stay_inside_trim(face, &mesh, &trim_samples) {
321 Ok(mesh)
322 } else {
323 Ok(parametric_failure_mesh(
324 face,
325 density,
326 TrimmedParametricFailure::TriangleLeakage,
327 mesh,
328 ))
329 }
330 }
331 Err(err) => {
332 if allow_parametric_full_fallback(&face.surface, TrimmedParametricFailure::CdtFailure) {
333 Ok(tessellate_parametric_full(face, density))
334 } else {
335 Err(err)
336 }
337 }
338 }
339}
340
341fn allow_parametric_full_fallback(surface: &Surface, failure: TrimmedParametricFailure) -> bool {
342 matches!(
343 surface,
344 Surface::SurfaceOfRevolution { .. }
345 | Surface::SurfaceOfSweep { .. }
346 | Surface::NurbsSurface { .. }
347 ) && matches!(
348 failure,
349 TrimmedParametricFailure::TrimLoopUnavailable | TrimmedParametricFailure::CdtFailure
350 )
351}
352
353fn parametric_failure_mesh(
354 face: &Face,
355 density: usize,
356 failure: TrimmedParametricFailure,
357 default_mesh: TriMesh,
358) -> TriMesh {
359 if allow_parametric_full_fallback(&face.surface, failure) {
360 tessellate_parametric_full(face, density)
361 } else {
362 default_mesh
363 }
364}
365
366fn tessellate_parametric_full(face: &Face, density: usize) -> TriMesh {
368 let (u_min, u_max, v_min, v_max) = face.surface.param_range();
369 let nu = density;
370 let nv = density;
371
372 let mut mesh = TriMesh::new();
373
374 for iv in 0..=nv {
376 let v = v_min + (v_max - v_min) * iv as f64 / nv as f64;
377 for iu in 0..=nu {
378 let u = u_min + (u_max - u_min) * iu as f64 / nu as f64;
379 let p = face.surface.evaluate(u, v);
380 let mut n = face.surface.normal_at(u, v);
381 if face.orientation_reversed {
382 n = vec3::scale(n, -1.0);
383 }
384 mesh.vertices.push(p);
385 mesh.normals.push(n);
386 }
387 }
388
389 let cols = nu + 1;
391 for iv in 0..nv {
392 for iu in 0..nu {
393 let i00 = iv * cols + iu;
394 let i10 = iv * cols + (iu + 1);
395 let i01 = (iv + 1) * cols + iu;
396 let i11 = (iv + 1) * cols + (iu + 1);
397 mesh.triangles.push([i00, i10, i11]);
398 mesh.triangles.push([i00, i11, i01]);
399 }
400 }
401
402 mesh
403}
404
405fn tessellate_parametric_trimmed(
406 face: &Face,
407 density: usize,
408 trim_samples: &[TrimSample],
409) -> Result<TriMesh, CdtError> {
410 let trim_loop: Vec<[f64; 2]> = trim_samples.iter().map(|sample| sample.uv).collect();
411
412 if trim_loop.len() == 3 {
413 return Ok(tessellate_parametric_triangle(
414 face,
415 density.max(2),
416 [trim_loop[0], trim_loop[1], trim_loop[2]],
417 ));
418 }
419
420 let (min_u, max_u, min_v, max_v) = bounds_2d(&trim_loop);
421 let extent = (max_u - min_u).max(max_v - min_v).max(1e-9);
422 let margin = extent * 0.01;
423 let bounds = (
424 min_u - margin,
425 min_v - margin,
426 max_u + margin,
427 max_v + margin,
428 );
429
430 let mut cdt = neco_cdt::Cdt::new(bounds);
431 cdt.add_constraint_edges(&trim_loop, true)?;
432
433 let nu = density.max(2);
434 let nv = density.max(2);
435 let du = (max_u - min_u) / nu as f64;
436 let dv = (max_v - min_v) / nv as f64;
437 if du.is_finite() && dv.is_finite() && du > 0.0 && dv > 0.0 {
438 for iv in 0..nv {
439 let v = min_v + (iv as f64 + 0.5) * dv;
440 for iu in 0..nu {
441 let u = min_u + (iu as f64 + 0.5) * du;
442 let uv = [u, v];
443 if point_in_polygon_2d(uv, &trim_loop) {
444 cdt.insert(u, v);
445 }
446 }
447 }
448 }
449
450 let user_verts = cdt.user_vertices();
451 let cdt_tris = cdt.triangles();
452 let mut mesh = TriMesh::new();
453 for uv in user_verts {
454 push_parametric_vertex_with_boundary(&mut mesh, face, *uv, trim_samples);
455 }
456
457 for tri in &cdt_tris {
458 let centroid = [
459 (user_verts[tri[0]][0] + user_verts[tri[1]][0] + user_verts[tri[2]][0]) / 3.0,
460 (user_verts[tri[0]][1] + user_verts[tri[1]][1] + user_verts[tri[2]][1]) / 3.0,
461 ];
462 if point_in_polygon_2d(centroid, &trim_loop) {
463 mesh.triangles.push(*tri);
464 }
465 }
466
467 Ok(mesh)
468}
469
470fn tessellate_parametric_triangle(
471 face: &Face,
472 density: usize,
473 triangle_uv: [[f64; 2]; 3],
474) -> TriMesh {
475 let mut mesh = TriMesh::new();
476 let mut row_starts = Vec::with_capacity(density + 1);
477 let n = density as f64;
478
479 for i in 0..=density {
480 row_starts.push(mesh.vertices.len());
481 for j in 0..=(density - i) {
482 let w0 = (density - i - j) as f64 / n;
483 let w1 = i as f64 / n;
484 let w2 = j as f64 / n;
485 let uv = [
486 w0 * triangle_uv[0][0] + w1 * triangle_uv[1][0] + w2 * triangle_uv[2][0],
487 w0 * triangle_uv[0][1] + w1 * triangle_uv[1][1] + w2 * triangle_uv[2][1],
488 ];
489 push_parametric_vertex(&mut mesh, face, uv);
490 }
491 }
492
493 let ccw = polygon_signed_area_2d(&triangle_uv) >= 0.0;
494 for i in 0..density {
495 let row_len = density - i + 1;
496 for j in 0..(row_len - 1) {
497 let a = row_starts[i] + j;
498 let b = row_starts[i] + j + 1;
499 let c = row_starts[i + 1] + j;
500 push_parametric_triangle(&mut mesh, [a, c, b], ccw);
501
502 if j < row_len - 2 {
503 let d = row_starts[i + 1] + j + 1;
504 push_parametric_triangle(&mut mesh, [b, c, d], ccw);
505 }
506 }
507 }
508
509 mesh
510}
511
512fn tessellate_parametric_fan(face: &Face, density: usize, trim_samples: &[TrimSample]) -> TriMesh {
513 let trim_loop: Vec<[f64; 2]> = trim_samples.iter().map(|sample| sample.uv).collect();
514 let mut mesh = TriMesh::new();
515 let ccw = polygon_signed_area_2d(&trim_loop) >= 0.0;
516 let n = trim_loop.len();
517 let center = average_polygon_point_2d(&trim_loop);
518 push_parametric_vertex(&mut mesh, face, center);
519 let center_id = 0usize;
520 let mut previous_ring = vec![center_id; n];
521
522 for level in 1..=density {
523 let t = level as f64 / density as f64;
524 let mut ring = Vec::with_capacity(n);
525 for (idx, boundary_uv) in trim_loop.iter().enumerate() {
526 let uv = [
527 center[0] * (1.0 - t) + boundary_uv[0] * t,
528 center[1] * (1.0 - t) + boundary_uv[1] * t,
529 ];
530 ring.push(mesh.vertices.len());
531 if level == density {
532 push_parametric_boundary_vertex(&mut mesh, face, uv, trim_samples[idx].point);
533 } else {
534 push_parametric_vertex(&mut mesh, face, uv);
535 }
536 }
537
538 if level == 1 {
539 for i in 0..n {
540 let next = (i + 1) % n;
541 push_parametric_triangle(&mut mesh, [center_id, ring[i], ring[next]], ccw);
542 }
543 } else {
544 for i in 0..n {
545 let next = (i + 1) % n;
546 let inner_a = previous_ring[i];
547 let inner_b = previous_ring[next];
548 let outer_a = ring[i];
549 let outer_b = ring[next];
550 push_parametric_triangle(&mut mesh, [inner_a, outer_a, outer_b], ccw);
551 push_parametric_triangle(&mut mesh, [inner_a, outer_b, inner_b], ccw);
552 }
553 }
554
555 previous_ring = ring;
556 }
557
558 mesh
559}
560
561fn trim_loop_touches_parametric_singularity(trim_samples: &[TrimSample]) -> bool {
562 let singular_tol = 1e-6;
563 trim_samples.iter().any(|sample| {
564 sample.uv[1].abs() <= singular_tol
565 || (std::f64::consts::PI - sample.uv[1]).abs() <= singular_tol
566 })
567}
568
569fn push_parametric_vertex(mesh: &mut TriMesh, face: &Face, uv: [f64; 2]) {
570 let p = face.surface.evaluate(uv[0], uv[1]);
571 push_parametric_boundary_vertex(mesh, face, uv, p);
572}
573
574fn push_parametric_boundary_vertex(mesh: &mut TriMesh, face: &Face, uv: [f64; 2], point: [f64; 3]) {
575 let mut n = face.surface.normal_at(uv[0], uv[1]);
576 if face.orientation_reversed {
577 n = vec3::scale(n, -1.0);
578 }
579 mesh.vertices.push(point);
580 mesh.normals.push(n);
581}
582
583fn push_parametric_vertex_with_boundary(
584 mesh: &mut TriMesh,
585 face: &Face,
586 uv: [f64; 2],
587 trim_samples: &[TrimSample],
588) {
589 if let Some(sample) = find_trim_sample(uv, trim_samples) {
590 push_parametric_boundary_vertex(mesh, face, uv, sample.point);
591 } else {
592 push_parametric_vertex(mesh, face, uv);
593 }
594}
595
596fn push_parametric_triangle(mesh: &mut TriMesh, triangle: [usize; 3], ccw: bool) {
597 if ccw {
598 mesh.triangles.push(triangle);
599 } else {
600 mesh.triangles.push([triangle[0], triangle[2], triangle[1]]);
601 }
602}
603
604#[cfg(test)]
605fn collect_trim_loop_samples(face: &Face, edges: &[Edge], density: usize) -> Vec<TrimSample> {
606 try_collect_trim_loop_samples(face, edges, density)
607 .expect("trim loop point must be inverse-projectable onto its face surface")
608}
609
610fn try_collect_trim_loop_samples(
611 face: &Face,
612 edges: &[Edge],
613 density: usize,
614) -> Result<Vec<TrimSample>, TrimmedParametricFailure> {
615 let periods = surface_param_periods(&face.surface);
616 let mut trim_loop = Vec::new();
617 let mut previous_uv = None;
618
619 for edge_ref in &face.loop_edges {
620 let edge = &edges[edge_ref.edge_id];
621 let mut samples = sample_edge_parametric(edge, &face.surface, density)?;
622 if !edge_ref.forward {
623 samples.reverse();
624 }
625 if !trim_loop.is_empty() && !samples.is_empty() {
626 samples.remove(0);
627 }
628 for sample in samples {
629 let raw_uv = normalize_singular_uv(&face.surface, sample.uv.into(), previous_uv);
630 let uv = if let Some(prev) = previous_uv {
631 unwrap_uv_near_reference(raw_uv, prev, periods)
632 } else {
633 [raw_uv.0, raw_uv.1]
634 };
635 if previous_uv.is_none_or(|prev| !same_point_2d(prev, uv, 1e-10)) {
636 trim_loop.push(TrimSample {
637 uv,
638 point: sample.point,
639 });
640 previous_uv = Some(uv);
641 }
642 }
643 }
644
645 simplify_trim_samples(&mut trim_loop, 1e-8);
646
647 if trim_loop.len() >= 2 {
648 let first = trim_loop[0].uv;
649 let last = trim_loop[trim_loop.len() - 1].uv;
650 if same_point_2d(first, last, 1e-10) {
651 trim_loop.pop();
652 }
653 }
654
655 if trim_loop.len() < 3 {
656 return Err(TrimmedParametricFailure::TrimLoopUnavailable);
657 }
658
659 Ok(trim_loop)
660}
661
662fn triangles_stay_inside_trim(face: &Face, mesh: &TriMesh, trim_samples: &[TrimSample]) -> bool {
663 let trim_loop: Vec<[f64; 2]> = trim_samples.iter().map(|sample| sample.uv).collect();
664 if trim_loop.len() < 3 {
665 return false;
666 }
667 let reference = average_polygon_point_2d(&trim_loop);
668 let periods = surface_param_periods(&face.surface);
669
670 mesh.triangles.iter().all(|tri| {
671 let mut uv = [[0.0, 0.0]; 3];
672 for (slot, vertex_id) in uv.iter_mut().zip(tri) {
673 let Some(raw) = face.surface.inverse_project(&mesh.vertices[*vertex_id]) else {
674 return false;
675 };
676 *slot = unwrap_uv_near_reference(raw, reference, periods);
677 }
678 let centroid = [
679 (uv[0][0] + uv[1][0] + uv[2][0]) / 3.0,
680 (uv[0][1] + uv[1][1] + uv[2][1]) / 3.0,
681 ];
682 point_in_polygon_2d(centroid, &trim_loop)
683 })
684}
685
686fn surface_param_periods(surface: &Surface) -> (Option<f64>, Option<f64>) {
687 let tau = std::f64::consts::TAU;
688 match surface {
689 Surface::Cylinder { .. }
690 | Surface::Cone { .. }
691 | Surface::Sphere { .. }
692 | Surface::Ellipsoid { .. } => (Some(tau), None),
693 Surface::Torus { .. } => (Some(tau), Some(tau)),
694 Surface::SurfaceOfRevolution { theta_range, .. } if *theta_range >= tau - 1e-12 => {
695 (Some(tau), None)
696 }
697 _ => (None, None),
698 }
699}
700
701fn normalize_singular_uv(
702 surface: &Surface,
703 uv: (f64, f64),
704 previous_uv: Option<[f64; 2]>,
705) -> (f64, f64) {
706 let singular_tol = 1e-6;
707 match surface {
708 Surface::Sphere { .. } | Surface::Ellipsoid { .. }
709 if uv.1.abs() <= singular_tol
710 || (std::f64::consts::PI - uv.1).abs() <= singular_tol =>
711 {
712 (previous_uv.map_or(uv.0, |prev| prev[0]), uv.1)
713 }
714 _ => uv,
715 }
716}
717
718fn unwrap_uv_near_reference(
719 uv: (f64, f64),
720 reference: [f64; 2],
721 periods: (Option<f64>, Option<f64>),
722) -> [f64; 2] {
723 [
724 unwrap_periodic_component(uv.0, reference[0], periods.0),
725 unwrap_periodic_component(uv.1, reference[1], periods.1),
726 ]
727}
728
729fn unwrap_periodic_component(value: f64, reference: f64, period: Option<f64>) -> f64 {
730 match period {
731 Some(period) if period > 0.0 => {
732 let shift = ((reference - value) / period).round();
733 value + shift * period
734 }
735 _ => value,
736 }
737}
738
739fn bounds_2d(points: &[[f64; 2]]) -> (f64, f64, f64, f64) {
740 let mut min_x = f64::INFINITY;
741 let mut min_y = f64::INFINITY;
742 let mut max_x = f64::NEG_INFINITY;
743 let mut max_y = f64::NEG_INFINITY;
744
745 for point in points {
746 min_x = min_x.min(point[0]);
747 min_y = min_y.min(point[1]);
748 max_x = max_x.max(point[0]);
749 max_y = max_y.max(point[1]);
750 }
751
752 (min_x, max_x, min_y, max_y)
753}
754
755fn same_point_2d(a: [f64; 2], b: [f64; 2], tol: f64) -> bool {
756 let dx = a[0] - b[0];
757 let dy = a[1] - b[1];
758 dx * dx + dy * dy <= tol * tol
759}
760
761fn polygon_signed_area_2d(points: &[[f64; 2]]) -> f64 {
762 let mut area = 0.0;
763 for i in 0..points.len() {
764 let p = points[i];
765 let q = points[(i + 1) % points.len()];
766 area += p[0] * q[1] - q[0] * p[1];
767 }
768 area * 0.5
769}
770
771fn average_polygon_point_2d(points: &[[f64; 2]]) -> [f64; 2] {
772 let sum = points
773 .iter()
774 .copied()
775 .fold([0.0, 0.0], |acc, p| [acc[0] + p[0], acc[1] + p[1]]);
776 [sum[0] / points.len() as f64, sum[1] / points.len() as f64]
777}
778
779fn simplify_trim_samples(points: &mut Vec<TrimSample>, tol: f64) {
780 if points.len() < 3 {
781 return;
782 }
783
784 loop {
785 let n = points.len();
786 let mut simplified = Vec::with_capacity(n);
787 let mut changed = false;
788
789 for i in 0..n {
790 let prev = points[(i + n - 1) % n];
791 let curr = points[i];
792 let next = points[(i + 1) % n];
793 if point_is_redundant_2d(prev.uv, curr.uv, next.uv, tol) {
794 changed = true;
795 continue;
796 }
797 simplified.push(curr);
798 }
799
800 if !changed || simplified.len() < 3 {
801 if simplified.len() >= 3 {
802 *points = simplified;
803 }
804 return;
805 }
806
807 *points = simplified;
808 }
809}
810
811fn point_is_redundant_2d(prev: [f64; 2], curr: [f64; 2], next: [f64; 2], tol: f64) -> bool {
812 if same_point_2d(prev, curr, tol) || same_point_2d(curr, next, tol) {
813 return true;
814 }
815
816 let ab = [next[0] - prev[0], next[1] - prev[1]];
817 let ap = [curr[0] - prev[0], curr[1] - prev[1]];
818 let cross = ab[0] * ap[1] - ab[1] * ap[0];
819 let scale = (ab[0] * ab[0] + ab[1] * ab[1]).sqrt().max(1.0);
820 if cross.abs() > tol * scale {
821 return false;
822 }
823
824 let dot = ap[0] * ab[0] + ap[1] * ab[1];
825 let ab_len_sq = ab[0] * ab[0] + ab[1] * ab[1];
826 dot >= 0.0 && dot <= ab_len_sq
827}
828
829fn collect_loop_points(face: &Face, edges: &[Edge], density: usize) -> Vec<[f64; 3]> {
831 let mut pts = Vec::new();
832 for eref in &face.loop_edges {
833 let edge = &edges[eref.edge_id];
834 let mut points = sample_curve_parametric_points(&edge.curve, density);
835 if !eref.forward {
836 points.reverse();
837 }
838 if !pts.is_empty() && !points.is_empty() {
839 points.remove(0);
840 }
841 for point in points {
842 if pts
843 .last()
844 .is_none_or(|prev| vec3::distance(*prev, point) > 1e-10)
845 {
846 pts.push(point);
847 }
848 }
849 }
850 pts
851}
852
853fn collect_loop_vertex_points(face: &Face, vertices: &[[f64; 3]], edges: &[Edge]) -> Vec<[f64; 3]> {
854 let mut pts = Vec::with_capacity(face.loop_edges.len());
855 for edge_ref in &face.loop_edges {
856 let edge = &edges[edge_ref.edge_id];
857 let point = if edge_ref.forward {
858 vertices[edge.v_start]
859 } else {
860 vertices[edge.v_end]
861 };
862 if pts
863 .last()
864 .is_none_or(|prev| vec3::distance(*prev, point) > 1e-10)
865 {
866 pts.push(point);
867 }
868 }
869 pts
870}
871
872fn sample_curve_parametric_points(curve: &Curve3D, density: usize) -> Vec<[f64; 3]> {
873 let (t0, t1) = curve.param_range();
874 let tolerance = curve_sampling_tolerance(curve, density);
875 let mut points = vec![curve.evaluate(t0)];
876 sample_curve_parametric_segment(curve, t0, t1, tolerance, &mut points);
877 points
878}
879
880fn sample_curve_parametric_segment(
881 curve: &Curve3D,
882 t0: f64,
883 t1: f64,
884 tolerance: f64,
885 points: &mut Vec<[f64; 3]>,
886) {
887 if (t1 - t0).abs() < 1e-10 {
888 points.push(curve.evaluate(t1));
889 return;
890 }
891
892 let mid_t = (t0 + t1) * 0.5;
893 let p0 = curve.evaluate(t0);
894 let p1 = curve.evaluate(t1);
895 let mid_curve = curve.evaluate(mid_t);
896 let mid_chord = vec3::scale(vec3::add(p0, p1), 0.5);
897 let chord_height = vec3::length(vec3::sub(mid_curve, mid_chord));
898 if chord_height > tolerance {
899 sample_curve_parametric_segment(curve, t0, mid_t, tolerance, points);
900 sample_curve_parametric_segment(curve, mid_t, t1, tolerance, points);
901 } else {
902 points.push(curve.evaluate(t1));
903 }
904}
905
906fn curve_sampling_tolerance(curve: &Curve3D, density: usize) -> f64 {
907 let (t0, t1) = curve.param_range();
908 let p0 = curve.evaluate(t0);
909 let p1 = curve.evaluate(t1);
910 let pm = curve.evaluate((t0 + t1) * 0.5);
911 let scale = vec3::distance(p0, p1)
912 .max(vec3::distance(p0, pm))
913 .max(vec3::distance(pm, p1))
914 .max(1e-6);
915 scale / density.max(4) as f64 * 0.25
916}
917
918fn sample_edge_parametric(
919 edge: &Edge,
920 surface: &Surface,
921 density: usize,
922) -> Result<Vec<TrimSample>, TrimmedParametricFailure> {
923 let points = sample_curve_parametric_points(&edge.curve, density);
924 let mut samples = Vec::with_capacity(points.len());
925 for point in points {
926 let Some((u, v)) = surface.inverse_project(&point) else {
927 return Err(TrimmedParametricFailure::TrimLoopUnavailable);
928 };
929 samples.push(TrimSample { uv: [u, v], point });
930 }
931
932 Ok(samples)
933}
934
935fn find_trim_sample(uv: [f64; 2], trim_samples: &[TrimSample]) -> Option<TrimSample> {
936 trim_samples
937 .iter()
938 .copied()
939 .find(|sample| same_point_2d(sample.uv, uv, 1e-10))
940}
941
942fn point_in_polygon_2d(point: [f64; 2], polygon: &[[f64; 2]]) -> bool {
944 let n = polygon.len();
945 if n < 3 {
946 return false;
947 }
948 let mut inside = false;
949 let (px, py) = (point[0], point[1]);
950 let mut j = n - 1;
951 for i in 0..n {
952 let (xi, yi) = (polygon[i][0], polygon[i][1]);
953 let (xj, yj) = (polygon[j][0], polygon[j][1]);
954 if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
955 inside = !inside;
956 }
957 j = i;
958 }
959 inside
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965 use crate::brep::{Curve3D, EdgeRef, Surface};
966 use crate::primitives::{shell_from_cylinder, shell_from_sphere};
967
968 fn make_box_shell() -> Shell {
970 let mut shell = Shell::new();
971
972 let v = [
974 shell.add_vertex([0.0, 0.0, 0.0]), shell.add_vertex([1.0, 0.0, 0.0]), shell.add_vertex([1.0, 1.0, 0.0]), shell.add_vertex([0.0, 1.0, 0.0]), shell.add_vertex([0.0, 0.0, 1.0]), shell.add_vertex([1.0, 0.0, 1.0]), shell.add_vertex([1.0, 1.0, 1.0]), shell.add_vertex([0.0, 1.0, 1.0]), ];
983
984 let e_bot = [
987 shell.add_edge(
988 v[0],
989 v[1],
990 Curve3D::Line {
991 start: [0.0, 0.0, 0.0],
992 end: [1.0, 0.0, 0.0],
993 },
994 ),
995 shell.add_edge(
996 v[1],
997 v[2],
998 Curve3D::Line {
999 start: [1.0, 0.0, 0.0],
1000 end: [1.0, 1.0, 0.0],
1001 },
1002 ),
1003 shell.add_edge(
1004 v[2],
1005 v[3],
1006 Curve3D::Line {
1007 start: [1.0, 1.0, 0.0],
1008 end: [0.0, 1.0, 0.0],
1009 },
1010 ),
1011 shell.add_edge(
1012 v[3],
1013 v[0],
1014 Curve3D::Line {
1015 start: [0.0, 1.0, 0.0],
1016 end: [0.0, 0.0, 0.0],
1017 },
1018 ),
1019 ];
1020
1021 let e_top = [
1023 shell.add_edge(
1024 v[4],
1025 v[5],
1026 Curve3D::Line {
1027 start: [0.0, 0.0, 1.0],
1028 end: [1.0, 0.0, 1.0],
1029 },
1030 ),
1031 shell.add_edge(
1032 v[5],
1033 v[6],
1034 Curve3D::Line {
1035 start: [1.0, 0.0, 1.0],
1036 end: [1.0, 1.0, 1.0],
1037 },
1038 ),
1039 shell.add_edge(
1040 v[6],
1041 v[7],
1042 Curve3D::Line {
1043 start: [1.0, 1.0, 1.0],
1044 end: [0.0, 1.0, 1.0],
1045 },
1046 ),
1047 shell.add_edge(
1048 v[7],
1049 v[4],
1050 Curve3D::Line {
1051 start: [0.0, 1.0, 1.0],
1052 end: [0.0, 0.0, 1.0],
1053 },
1054 ),
1055 ];
1056
1057 let e_vert = [
1059 shell.add_edge(
1060 v[0],
1061 v[4],
1062 Curve3D::Line {
1063 start: [0.0, 0.0, 0.0],
1064 end: [0.0, 0.0, 1.0],
1065 },
1066 ),
1067 shell.add_edge(
1068 v[1],
1069 v[5],
1070 Curve3D::Line {
1071 start: [1.0, 0.0, 0.0],
1072 end: [1.0, 0.0, 1.0],
1073 },
1074 ),
1075 shell.add_edge(
1076 v[2],
1077 v[6],
1078 Curve3D::Line {
1079 start: [1.0, 1.0, 0.0],
1080 end: [1.0, 1.0, 1.0],
1081 },
1082 ),
1083 shell.add_edge(
1084 v[3],
1085 v[7],
1086 Curve3D::Line {
1087 start: [0.0, 1.0, 0.0],
1088 end: [0.0, 1.0, 1.0],
1089 },
1090 ),
1091 ];
1092
1093 shell.faces.push(Face {
1096 loop_edges: vec![
1097 EdgeRef {
1098 edge_id: e_bot[3],
1099 forward: false,
1100 }, EdgeRef {
1102 edge_id: e_bot[2],
1103 forward: false,
1104 }, EdgeRef {
1106 edge_id: e_bot[1],
1107 forward: false,
1108 }, EdgeRef {
1110 edge_id: e_bot[0],
1111 forward: false,
1112 }, ],
1114 surface: Surface::Plane {
1115 origin: [0.0, 0.0, 0.0],
1116 normal: [0.0, 0.0, -1.0],
1117 },
1118 orientation_reversed: false,
1119 });
1120
1121 shell.faces.push(Face {
1123 loop_edges: vec![
1124 EdgeRef {
1125 edge_id: e_top[0],
1126 forward: true,
1127 },
1128 EdgeRef {
1129 edge_id: e_top[1],
1130 forward: true,
1131 },
1132 EdgeRef {
1133 edge_id: e_top[2],
1134 forward: true,
1135 },
1136 EdgeRef {
1137 edge_id: e_top[3],
1138 forward: true,
1139 },
1140 ],
1141 surface: Surface::Plane {
1142 origin: [0.0, 0.0, 1.0],
1143 normal: [0.0, 0.0, 1.0],
1144 },
1145 orientation_reversed: false,
1146 });
1147
1148 shell.faces.push(Face {
1150 loop_edges: vec![
1151 EdgeRef {
1152 edge_id: e_bot[0],
1153 forward: true,
1154 }, EdgeRef {
1156 edge_id: e_vert[1],
1157 forward: true,
1158 }, EdgeRef {
1160 edge_id: e_top[0],
1161 forward: false,
1162 }, EdgeRef {
1164 edge_id: e_vert[0],
1165 forward: false,
1166 }, ],
1168 surface: Surface::Plane {
1169 origin: [0.0, 0.0, 0.0],
1170 normal: [0.0, -1.0, 0.0],
1171 },
1172 orientation_reversed: false,
1173 });
1174
1175 shell.faces.push(Face {
1177 loop_edges: vec![
1178 EdgeRef {
1179 edge_id: e_bot[1],
1180 forward: true,
1181 }, EdgeRef {
1183 edge_id: e_vert[2],
1184 forward: true,
1185 }, EdgeRef {
1187 edge_id: e_top[1],
1188 forward: false,
1189 }, EdgeRef {
1191 edge_id: e_vert[1],
1192 forward: false,
1193 }, ],
1195 surface: Surface::Plane {
1196 origin: [1.0, 0.0, 0.0],
1197 normal: [1.0, 0.0, 0.0],
1198 },
1199 orientation_reversed: false,
1200 });
1201
1202 shell.faces.push(Face {
1204 loop_edges: vec![
1205 EdgeRef {
1206 edge_id: e_bot[2],
1207 forward: true,
1208 }, EdgeRef {
1210 edge_id: e_vert[3],
1211 forward: true,
1212 }, EdgeRef {
1214 edge_id: e_top[2],
1215 forward: false,
1216 }, EdgeRef {
1218 edge_id: e_vert[2],
1219 forward: false,
1220 }, ],
1222 surface: Surface::Plane {
1223 origin: [0.0, 1.0, 0.0],
1224 normal: [0.0, 1.0, 0.0],
1225 },
1226 orientation_reversed: false,
1227 });
1228
1229 shell.faces.push(Face {
1231 loop_edges: vec![
1232 EdgeRef {
1233 edge_id: e_bot[3],
1234 forward: true,
1235 }, EdgeRef {
1237 edge_id: e_vert[0],
1238 forward: true,
1239 }, EdgeRef {
1241 edge_id: e_top[3],
1242 forward: false,
1243 }, EdgeRef {
1245 edge_id: e_vert[3],
1246 forward: false,
1247 }, ],
1249 surface: Surface::Plane {
1250 origin: [0.0, 0.0, 0.0],
1251 normal: [-1.0, 0.0, 0.0],
1252 },
1253 orientation_reversed: false,
1254 });
1255
1256 shell
1257 }
1258
1259 #[test]
1260 fn manual_box_tessellation() {
1261 let shell = make_box_shell();
1262 let mesh = shell.tessellate(4).unwrap();
1263
1264 assert!(!mesh.vertices.is_empty(), "vertices empty");
1266 assert!(
1267 mesh.triangles.len() >= 12,
1268 "too few triangles: {}",
1269 mesh.triangles.len()
1270 );
1271
1272 let v = mesh.validate();
1273 assert!(v.is_watertight, "not watertight");
1274 assert!(v.is_consistently_oriented, "inconsistent orientation");
1275 assert!(v.has_no_degenerate_faces, "degenerate faces exist");
1276 assert_eq!(v.euler_number, 2, "euler number != 2: {}", v.euler_number);
1277 assert!(
1278 v.signed_volume > 0.0,
1279 "signed volume not positive: {}",
1280 v.signed_volume
1281 );
1282 assert!(
1283 (v.signed_volume - 1.0).abs() < 1e-6,
1284 "volume != 1.0: {}",
1285 v.signed_volume
1286 );
1287 assert_eq!(
1288 v.n_connected_components, 1,
1289 "connected components != 1: {}",
1290 v.n_connected_components
1291 );
1292 }
1293
1294 #[test]
1295 fn parametric_sphere_tessellation() {
1296 let mut shell = Shell::new();
1297 shell.faces.push(Face {
1298 loop_edges: vec![],
1299 surface: Surface::Sphere {
1300 center: [0.0, 0.0, 0.0],
1301 radius: 1.0,
1302 },
1303 orientation_reversed: false,
1304 });
1305 let mesh = shell.tessellate(16).unwrap();
1306 assert!(!mesh.vertices.is_empty());
1307 assert!(!mesh.triangles.is_empty());
1308 assert_eq!(mesh.triangles.len(), 16 * 16 * 2);
1311 assert!(
1312 mesh.vertices.len() < 17 * 17,
1313 "poles and seam should be welded"
1314 );
1315 assert!(mesh.vertices.len() > 100, "too few vertices");
1316 }
1317
1318 #[test]
1319 fn point_in_polygon_basic() {
1320 let polygon = vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1321 assert!(point_in_polygon_2d([0.5, 0.5], &polygon));
1322 assert!(!point_in_polygon_2d([1.5, 0.5], &polygon));
1323 assert!(!point_in_polygon_2d([-0.5, 0.5], &polygon));
1324 }
1325
1326 #[test]
1327 fn sample_curve_parametric_points_line_and_arc() {
1328 let line = Curve3D::Line {
1329 start: [0.0, 0.0, 0.0],
1330 end: [1.0, 0.0, 0.0],
1331 };
1332 let line_points = sample_curve_parametric_points(&line, 8);
1333 assert_eq!(line_points.len(), 2);
1334 assert!(vec3::distance(line_points[0], [0.0, 0.0, 0.0]) < 1e-12);
1335 assert!(vec3::distance(line_points[1], [1.0, 0.0, 0.0]) < 1e-12);
1336
1337 let arc = Curve3D::Arc {
1338 center: [0.0, 0.0, 0.0],
1339 axis: [0.0, 0.0, 1.0],
1340 start: [1.0, 0.0, 0.0],
1341 end: [0.0, 1.0, 0.0],
1342 radius: 1.0,
1343 };
1344 let arc_points = sample_curve_parametric_points(&arc, 8);
1345 assert!(arc_points.len() > 2);
1346 assert!(vec3::distance(arc_points[0], [1.0, 0.0, 0.0]) < 1e-12);
1347 assert!(vec3::distance(*arc_points.last().unwrap(), [0.0, 1.0, 0.0]) < 1e-12);
1348 }
1349
1350 #[test]
1351 fn collect_loop_points_uses_parametric_sampling_for_full_ellipse() {
1352 let ellipse = Curve3D::Ellipse {
1353 center: [0.0, 0.0, 0.0],
1354 axis_u: [1.0, 0.0, 0.0],
1355 axis_v: [0.0, 0.5, 0.0],
1356 t_start: 0.0,
1357 t_end: std::f64::consts::TAU,
1358 };
1359 let face = Face {
1360 loop_edges: vec![EdgeRef {
1361 edge_id: 0,
1362 forward: true,
1363 }],
1364 surface: Surface::Plane {
1365 origin: [0.0, 0.0, 0.0],
1366 normal: [0.0, 0.0, 1.0],
1367 },
1368 orientation_reversed: false,
1369 };
1370 let edges = vec![Edge {
1371 v_start: 0,
1372 v_end: 0,
1373 curve: ellipse,
1374 }];
1375
1376 let points = collect_loop_points(&face, &edges, 12);
1377 assert!(
1378 points.len() > 8,
1379 "ellipse boundary should be adaptively sampled"
1380 );
1381 assert!(vec3::distance(points[0], [1.0, 0.0, 0.0]) < 1e-12);
1382 }
1383
1384 fn average_uv(points: &[[f64; 2]]) -> [f64; 2] {
1385 let sum = points
1386 .iter()
1387 .copied()
1388 .fold([0.0, 0.0], |acc, p| [acc[0] + p[0], acc[1] + p[1]]);
1389 [sum[0] / points.len() as f64, sum[1] / points.len() as f64]
1390 }
1391
1392 fn assert_triangles_stay_inside_trim(shell: &Shell, face_index: usize, density: usize) {
1393 let face = &shell.faces[face_index];
1394 let trim_samples = collect_trim_loop_samples(face, &shell.edges, density);
1395 let trim_loop: Vec<[f64; 2]> = trim_samples.iter().map(|sample| sample.uv).collect();
1396 let reference = average_uv(&trim_loop);
1397 let periods = surface_param_periods(&face.surface);
1398 let mesh = tessellate_face(face, &shell.vertices, &shell.edges, density).unwrap();
1399
1400 assert!(
1401 !mesh.triangles.is_empty(),
1402 "trimmed face produced no triangles"
1403 );
1404 for tri in &mesh.triangles {
1405 let mut uv = [[0.0, 0.0]; 3];
1406 for (slot, vertex_id) in uv.iter_mut().zip(tri) {
1407 let raw = face
1408 .surface
1409 .inverse_project(&mesh.vertices[*vertex_id])
1410 .unwrap();
1411 *slot = unwrap_uv_near_reference(raw, reference, periods);
1412 }
1413 let centroid = [
1414 (uv[0][0] + uv[1][0] + uv[2][0]) / 3.0,
1415 (uv[0][1] + uv[1][1] + uv[2][1]) / 3.0,
1416 ];
1417 assert!(
1418 point_in_polygon_2d(centroid, &trim_loop),
1419 "triangle centroid escaped trim loop: face_index={face_index} centroid={centroid:?} trim_loop={trim_loop:?}"
1420 );
1421 }
1422 }
1423
1424 #[test]
1425 fn sphere_trim_crossing_seam_stays_inside_trim() {
1426 let shell = shell_from_sphere(1.0);
1427 assert_triangles_stay_inside_trim(&shell, 3, 12);
1428 }
1429
1430 #[test]
1431 fn cylinder_trim_crossing_seam_stays_inside_trim() {
1432 let shell = shell_from_cylinder(1.0, None, 2.0);
1433 assert_triangles_stay_inside_trim(&shell, 3, 12);
1434 }
1435}