1use std::collections::HashMap;
61
62use geop_core_geometry::nurb_surface::NurbSurface3D;
63use geop_core_math::{
64 geop_error::GeopResult,
65 plain::{bounds, distance, dot, sub},
66 polygon::ear_clip,
67 scalars::Scalar,
68 solvers::refine::{Refine, refine},
69 vector::{Vector2, Vector3},
70};
71use geop_core_topology::Face;
72
73use crate::clip::{Point, clip_to_rect, is_convex, subtract_convex};
74
75const MIN_GRID_N: usize = 2;
79const MAX_GRID_N: usize = 128;
83const MAX_DOUBLINGS: usize = 7;
86
87pub(crate) fn dist_point_to_segment(p: [f64; 3], a: [f64; 3], b: [f64; 3]) -> f64 {
91 let ab = sub(b, a);
92 let len2 = dot(ab, ab);
93 let t = if len2 > 1e-18 {
94 (dot(sub(p, a), ab) / len2).clamp(0.0, 1.0)
95 } else {
96 0.0
97 };
98 let closest = [a[0] + ab[0] * t, a[1] + ab[1] * t, a[2] + ab[2] * t];
99 distance(p, closest)
100}
101
102#[derive(Clone, Copy)]
110struct Domain {
111 u: (f64, f64),
112 v: (f64, f64),
113}
114
115impl Domain {
116 fn of(face: &Face) -> Self {
117 let (u0, u1) = face.surface.domain_u();
118 let (v0, v1) = face.surface.domain_v();
119 Self {
120 u: (u0.to_f64(), u1.to_f64()),
121 v: (v0.to_f64(), v1.to_f64()),
122 }
123 }
124 fn clamp(&self, p: Point) -> Point {
125 [
126 p[0].clamp(self.u.0, self.u.1),
127 p[1].clamp(self.v.0, self.v.1),
128 ]
129 }
130}
131
132pub(crate) struct Samples {
138 surface: NurbSurface3D,
139 domain: Domain,
140 points: HashMap<[u64; 2], Vector3>,
141 normals: HashMap<[u64; 2], Option<Vector3>>,
142}
143
144impl Samples {
145 pub(crate) fn new(face: &Face) -> Self {
146 Self {
147 surface: face.surface.clone(),
148 domain: Domain::of(face),
149 points: HashMap::new(),
150 normals: HashMap::new(),
151 }
152 }
153
154 fn at(&self, uv: Point) -> (Point, [u64; 2]) {
156 let uv = self.domain.clamp(uv);
157 (uv, [uv[0].to_bits(), uv[1].to_bits()])
158 }
159
160 pub(crate) fn uv(&self, uv: Point) -> Vector2 {
162 let (uv, _) = self.at(uv);
163 Vector2::from_array([Scalar::from_f64(uv[0]), Scalar::from_f64(uv[1])])
164 }
165
166 pub(crate) fn point(&mut self, uv: Point) -> GeopResult<Vector3> {
168 let (uv, key) = self.at(uv);
169 if let Some(p) = self.points.get(&key) {
170 return Ok(*p);
171 }
172 let p = self
173 .surface
174 .evaluate(Scalar::from_f64(uv[0]), Scalar::from_f64(uv[1]))?;
175 self.points.insert(key, p);
176 Ok(p)
177 }
178
179 fn point_f64(&mut self, uv: Point) -> GeopResult<[f64; 3]> {
180 let p = self.point(uv)?;
181 Ok(p.to_f64_array())
182 }
183
184 pub(crate) fn normal(&mut self, uv: Point) -> Option<Vector3> {
187 let (uv, key) = self.at(uv);
188 let surface = &self.surface;
189 *self.normals.entry(key).or_insert_with(|| {
190 surface
191 .normal(Scalar::from_f64(uv[0]), Scalar::from_f64(uv[1]))
192 .ok()
193 })
194 }
195}
196
197fn worst_grid_deviation(
203 samples: &mut Samples,
204 min: Point,
205 max: Point,
206 nu: usize,
207 nv: usize,
208) -> GeopResult<[f64; 2]> {
209 let at = |i: usize, nu: usize, j: usize, nv: usize| -> Point {
213 [
214 min[0] + (max[0] - min[0]) * i as f64 / nu as f64,
215 min[1] + (max[1] - min[1]) * j as f64 / nv as f64,
216 ]
217 };
218 let mut worst = [0.0f64; 2];
219 for j in 0..nv {
220 for i in 0..nu {
221 let p00 = samples.point_f64(at(i, nu, j, nv))?;
222 let p10 = samples.point_f64(at(i + 1, nu, j, nv))?;
223 let p01 = samples.point_f64(at(i, nu, j + 1, nv))?;
224 let mid_u = samples.point_f64(at(2 * i + 1, 2 * nu, j, nv))?;
225 let mid_v = samples.point_f64(at(i, nu, 2 * j + 1, 2 * nv))?;
226 worst[0] = worst[0].max(dist_point_to_segment(mid_u, p00, p10));
227 worst[1] = worst[1].max(dist_point_to_segment(mid_v, p00, p01));
228 }
229 }
230 Ok(worst)
231}
232
233pub(crate) fn triangulate_face(
239 outer: &[Point],
240 holes: &[Vec<Point>],
241 n: usize,
242 samples: &mut Samples,
243) -> GeopResult<Vec<[Point; 3]>> {
244 if outer.len() < 3 {
245 return Ok(Vec::new());
246 }
247 let (outer_f64, holes_f64) = (outer, holes);
248
249 let Some((min, max)) = bounds(outer_f64.iter().copied()) else {
250 return Ok(Vec::new());
251 };
252 if max[0] <= min[0] || max[1] <= min[1] {
253 return Ok(Vec::new());
254 }
255
256 let evaluated = outer_f64
264 .iter()
265 .map(|&p| samples.point_f64(p))
266 .collect::<GeopResult<Vec<_>>>()?;
267 let (bbox_min, bbox_max) = bounds(evaluated).expect("the outer loop has points");
268 let face_size = distance(bbox_min, bbox_max).max(1e-9);
269 let quality = n.max(2) as f64;
270 let max_sagitta = (face_size / (4.0 * quality * quality)).max(1e-9);
271
272 let cells = refine([MIN_GRID_N; 2], MAX_DOUBLINGS, |cells| -> GeopResult<_> {
276 let worst = worst_grid_deviation(samples, min, max, cells[0], cells[1])?;
277 let mut finer = cells;
278 for k in 0..2 {
279 if worst[k] > max_sagitta && cells[k] < MAX_GRID_N {
280 finer[k] = (cells[k] * 2).min(MAX_GRID_N);
281 }
282 }
283 Ok(if finer == cells {
284 Refine::Done(cells)
285 } else {
286 Refine::Finer(finer)
287 })
288 })?
289 .either();
290 let (nu, nv) = (cells[0], cells[1]);
291
292 Ok(triangulate_region(outer_f64, holes_f64, min, max, nu, nv))
293}
294
295pub fn triangulate_polygon(outer: &[Point], holes: &[Vec<Point>]) -> Vec<[Point; 3]> {
300 let Some(&first) = outer.first() else {
301 return Vec::new();
302 };
303 let (min, max) = outer.iter().fold((first, first), |(lo, hi), p| {
304 (
305 [lo[0].min(p[0]), lo[1].min(p[1])],
306 [hi[0].max(p[0]), hi[1].max(p[1])],
307 )
308 });
309 if outer.len() < 3 || max[0] <= min[0] || max[1] <= min[1] {
310 return Vec::new();
311 }
312 triangulate_region(outer, holes, min, max, 1, 1)
313}
314
315fn triangulate_region(
319 outer: &[Point],
320 holes: &[Vec<Point>],
321 min: Point,
322 max: Point,
323 nu: usize,
324 nv: usize,
325) -> Vec<[Point; 3]> {
326 let cell_u = |i: usize| min[0] + (max[0] - min[0]) * i as f64 / nu as f64;
327 let cell_v = |j: usize| min[1] + (max[1] - min[1]) * j as f64 / nv as f64;
328 let span = |lo: f64, hi: f64, start: f64, end: f64, n: usize| {
332 let at = |x: f64| ((x - start) / (end - start) * n as f64).floor() as isize;
333 let first = (at(lo) - 1).clamp(0, n as isize - 1) as usize;
334 let last = (at(hi) + 1).clamp(0, n as isize - 1) as usize;
335 first..=last
336 };
337
338 let into_cells = |polygon: &[Point], cells: &mut [Vec<Vec<Point>>]| {
349 let parts: Vec<Vec<Point>> = if is_convex(polygon) {
350 vec![polygon.to_vec()]
351 } else {
352 ear_clip(polygon)
353 .into_iter()
354 .map(|t| vec![polygon[t[0]], polygon[t[1]], polygon[t[2]]])
355 .collect()
356 };
357 for part in parts {
358 let (lo, hi) = part.iter().fold(
359 ([f64::INFINITY; 2], [f64::NEG_INFINITY; 2]),
360 |(lo, hi), p| {
361 (
362 [lo[0].min(p[0]), lo[1].min(p[1])],
363 [hi[0].max(p[0]), hi[1].max(p[1])],
364 )
365 },
366 );
367 for i in span(lo[0], hi[0], min[0], max[0], nu) {
368 for j in span(lo[1], hi[1], min[1], max[1], nv) {
369 let piece =
370 clip_to_rect(&part, cell_u(i), cell_u(i + 1), cell_v(j), cell_v(j + 1));
371 if piece.len() >= 3 {
372 cells[i * nv + j].push(piece);
373 }
374 }
375 }
376 }
377 };
378 let mut inside: Vec<Vec<Vec<Point>>> = vec![Vec::new(); nu * nv];
379 into_cells(outer, &mut inside);
380 let mut cut: Vec<Vec<Vec<Point>>> = vec![Vec::new(); nu * nv];
381 for hole in holes {
382 into_cells(hole, &mut cut);
383 }
384
385 let mut triangles = Vec::new();
389 for (pieces, holes) in inside.into_iter().zip(cut) {
390 let pieces = holes.iter().fold(pieces, |pieces, hole| {
391 pieces
392 .iter()
393 .flat_map(|piece| subtract_convex(piece, hole))
394 .collect()
395 });
396 for piece in &pieces {
397 for tri in ear_clip(piece) {
398 triangles.push([piece[tri[0]], piece[tri[1]], piece[tri[2]]]);
399 }
400 }
401 }
402 triangles
403}
404
405#[cfg(test)]
406mod tests {
407 fn covered(triangles: &[[Point; 3]]) -> f64 {
409 triangles
410 .iter()
411 .map(|[a, b, c]| {
412 ((b[0] - a[0]) * (c[1] - a[1]) - (c[0] - a[0]) * (b[1] - a[1])).abs() / 2.0
413 })
414 .sum()
415 }
416
417 #[test]
423 fn a_trim_entering_a_cell_twice_leaves_the_gap_uncovered() {
424 let u = [
425 [0.0, 0.0],
426 [3.0, 0.0],
427 [3.0, 3.0],
428 [2.0, 3.0],
429 [2.0, 1.0],
430 [1.0, 1.0],
431 [1.0, 3.0],
432 [0.0, 3.0],
433 ];
434 for (nu, nv) in [(1, 2), (1, 3), (2, 2), (3, 4)] {
435 let triangles = triangulate_region(&u, &[], [0.0, 0.0], [3.0, 3.0], nu, nv);
436 let area = covered(&triangles);
437 assert!(
438 (area - 7.0).abs() < 1e-9,
439 "{nu} x {nv} cells cover {area}, not 7"
440 );
441 }
442 }
443
444 #[test]
450 fn a_concave_hole_of_many_corners_stays_cheap() {
451 let square = [[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
452 let n = 60;
453 let arc = |r: f64, k: usize| {
454 let t = std::f64::consts::PI * (0.15 + 1.7 * k as f64 / n as f64);
455 [2.0 + r * t.cos(), 2.0 + r * t.sin()]
456 };
457 let mut crescent: Vec<Point> = (0..=n).rev().map(|k| arc(1.5, k)).collect();
459 crescent.extend((0..=n).map(|k| arc(1.0, k)));
460 let started = std::time::Instant::now();
461 let triangles =
462 triangulate_region(&square, &[crescent.clone()], [0.0, 0.0], [4.0, 4.0], 1, 1);
463 let hole = covered(
464 &ear_clip(&crescent)
465 .into_iter()
466 .map(|t| [crescent[t[0]], crescent[t[1]], crescent[t[2]]])
467 .collect::<Vec<_>>(),
468 );
469 assert!((covered(&triangles) - (16.0 - hole)).abs() < 1e-9);
470 assert!(
471 triangles.len() < 20 * crescent.len(),
472 "{} triangles",
473 triangles.len()
474 );
475 assert!(
476 started.elapsed().as_secs() < 2,
477 "took {:?}",
478 started.elapsed()
479 );
480 }
481
482 #[test]
485 fn a_round_hole_leaves_nothing_inside_it() {
486 let square = [[0.0, 0.0], [4.0, 0.0], [4.0, 4.0], [0.0, 4.0]];
487 let hole: Vec<Point> = (0..32)
489 .rev()
490 .map(|i| {
491 let a = std::f64::consts::TAU * i as f64 / 32.0;
492 [2.0 + a.cos(), 2.0 + a.sin()]
493 })
494 .collect();
495 let hole_area = 16.0 * (std::f64::consts::TAU / 32.0).sin();
496 for n in [1, 2, 3, 4, 8, 13] {
497 let triangles = triangulate_region(
498 &square,
499 std::slice::from_ref(&hole),
500 [0.0, 0.0],
501 [4.0, 4.0],
502 n,
503 n,
504 );
505 let inside: Vec<_> = triangles
506 .iter()
507 .filter(|t| {
508 let c = [0, 1].map(|k| (t[0][k] + t[1][k] + t[2][k]) / 3.0);
509 (c[0] - 2.0).hypot(c[1] - 2.0) < 0.95 && covered(&[**t]) > 1e-12
510 })
511 .collect();
512 assert!(
513 inside.is_empty(),
514 "{n} x {n} cells: {} triangles inside the hole, e.g. {:?}",
515 inside.len(),
516 inside.first()
517 );
518 let area = covered(&triangles);
519 assert!(
520 (area - (16.0 - hole_area)).abs() < 1e-9,
521 "{n} x {n} cells cover {area}, not 16 - {hole_area}"
522 );
523 }
524 }
525
526 #[test]
531 fn a_hole_touching_cell_lines_leaves_nothing_inside_it() {
532 let square = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
533 let samplings: [Vec<Point>; 2] = [
534 vec![
535 [0.7499999999999996, 0.49999999999999983],
536 [0.744503716533199, 0.44786620480482087],
537 [0.7268208044051963, 0.39486997246752176],
538 [0.6963909975788529, 0.3453048931931495],
539 [0.6546951068068501, 0.3036090024211465],
540 [0.6051300275324778, 0.2731791955948034],
541 [0.5521337951951788, 0.25549628346680053],
542 [0.49999999999999983, 0.24999999999999994],
543 [0.44786620480482087, 0.2554962834668005],
544 [0.39486997246752176, 0.2731791955948034],
545 [0.3453048931931495, 0.3036090024211465],
546 [0.3036090024211465, 0.34530489319314944],
547 [0.2731791955948034, 0.39486997246752187],
548 [0.25549628346680053, 0.44786620480482087],
549 [0.24999999999999994, 0.4999999999999999],
550 [0.2554962834668005, 0.5521337951951787],
551 [0.2731791955948034, 0.6051300275324778],
552 [0.3036090024211465, 0.6546951068068501],
553 [0.34530489319314944, 0.6963909975788529],
554 [0.39486997246752187, 0.7268208044051963],
555 [0.44786620480482087, 0.7445037165331991],
556 [0.4999999999999999, 0.7499999999999996],
557 [0.5521337951951787, 0.744503716533199],
558 [0.6051300275324778, 0.7268208044051963],
559 [0.6546951068068501, 0.6963909975788529],
560 [0.6963909975788529, 0.6546951068068501],
561 [0.7268208044051963, 0.6051300275324778],
562 [0.7445037165331991, 0.5521337951951788],
563 ],
564 vec![
565 [0.75, 0.5],
566 [0.744503716705367, 0.4478662048932165],
567 [0.7268208044115454, 0.39486997248604894],
568 [0.6963909976184368, 0.3453048930969089],
569 [0.6546951066702604, 0.30360900214873254],
570 [0.6051300275139511, 0.2731791955884546],
571 [0.5521337951067835, 0.25549628329463303],
572 [0.5, 0.25],
573 [0.4478662048932165, 0.2554962835274637],
574 [0.39486997248604894, 0.27317919535562396],
575 [0.3453048930969089, 0.3036090023815632],
576 [0.30360900214873254, 0.34530489332973957],
577 [0.2731791955884546, 0.39486997248604894],
578 [0.25549628329463303, 0.44786620466038585],
579 [0.25, 0.5],
580 [0.2554962835274637, 0.5521337951067835],
581 [0.27317919535562396, 0.6051300275139511],
582 [0.3036090023815632, 0.6546951066702604],
583 [0.34530489332973957, 0.6963909973856062],
584 [0.39486997248604894, 0.7268208044115454],
585 [0.44786620466038585, 0.7445037164725363],
586 [0.5, 0.75],
587 [0.5521337951067835, 0.744503716705367],
588 [0.6051300275139511, 0.7268208044115454],
589 [0.6546951066702604, 0.6963909976184368],
590 [0.6963909973856062, 0.6546951066702604],
591 [0.7268208044115454, 0.6051300275139511],
592 [0.7445037164725363, 0.5521337951067835],
593 ],
594 ];
595 for hole in samplings {
596 let hole_area: f64 = (0..hole.len())
597 .map(|i| {
598 let (a, b) = (hole[i], hole[(i + 1) % hole.len()]);
599 a[0] * b[1] - b[0] * a[1]
600 })
601 .sum::<f64>()
602 .abs()
603 / 2.0;
604 for n in [1, 2, 3, 4, 8] {
605 let triangles = triangulate_region(
606 &square,
607 std::slice::from_ref(&hole),
608 [0.0, 0.0],
609 [1.0, 1.0],
610 n,
611 n,
612 );
613 let area = covered(&triangles);
614 assert!(
615 (area - (1.0 - hole_area)).abs() < 1e-9,
616 "{n} x {n} cells cover {area}, not 1 - {hole_area}"
617 );
618 }
619 }
620 }
621
622 #[test]
625 fn a_hole_entering_a_cell_twice_takes_out_only_itself() {
626 let square = [[-1.0, -1.0], [4.0, -1.0], [4.0, 4.0], [-1.0, 4.0]];
627 let hole: Vec<Point> = [
629 [0.0, 0.0],
630 [3.0, 0.0],
631 [3.0, 3.0],
632 [2.0, 3.0],
633 [2.0, 1.0],
634 [1.0, 1.0],
635 [1.0, 3.0],
636 [0.0, 3.0],
637 ]
638 .into_iter()
639 .rev()
640 .collect();
641 for (nu, nv) in [(1, 1), (1, 2), (2, 3), (5, 5)] {
642 let triangles = triangulate_region(
643 &square,
644 std::slice::from_ref(&hole),
645 [-1.0, -1.0],
646 [4.0, 4.0],
647 nu,
648 nv,
649 );
650 let area = covered(&triangles);
651 assert!(
652 (area - 18.0).abs() < 1e-9,
653 "{nu} x {nv} cells cover {area}, not 25 - 7"
654 );
655 }
656 }
657 use super::*;
658 use geop_core_topology::test_fixtures::test_cube_model;
659
660 fn triangle_area(a: Vector2, b: Vector2, c: Vector2) -> f64 {
661 let (ax, ay) = (a[0].to_f64(), a[1].to_f64());
662 let (bx, by) = (b[0].to_f64(), b[1].to_f64());
663 let (cx, cy) = (c[0].to_f64(), c[1].to_f64());
664 ((bx - ax) * (cy - ay) - (cx - ax) * (by - ay)).abs() / 2.0
665 }
666
667 #[test]
668 fn flat_face_stays_coarse() {
669 let topology = &test_cube_model();
670 let face = topology.faces.values().next().unwrap();
671 let tris = crate::face_triangles_uv(topology, face, 32).unwrap().0;
672 assert_eq!(
677 tris.len(),
678 2 * MIN_GRID_N * MIN_GRID_N,
679 "flat face should stay at the coarsest grid, got {}",
680 tris.len()
681 );
682 }
683
684 #[test]
685 fn triangulation_covers_exact_area() {
686 let topology = &test_cube_model();
687 let face = topology.faces.values().next().unwrap();
688 let tris = crate::face_triangles_uv(topology, face, 8).unwrap().0;
689 let total: f64 = tris.iter().map(|(a, b, c)| triangle_area(*a, *b, *c)).sum();
690 assert!(
691 (total - 1.0).abs() < 1e-6,
692 "total uv area={total}, expected 1.0"
693 );
694 }
695}