1use super::{Editable, NONE, RimId, Scope, index, prev};
96use core::f64::consts::FRAC_1_SQRT_2;
97use core::ops::Range;
98use ogeom_core::{Exact, FastMap, FastSet, OgeomResult, Predicates, Sign, ogeom_bail};
99use ogeom_math::{Point, SparseMatrix, Vector, least_squares_cgnr};
100
101pub const OPTIMAL_UP_TO: usize = 500;
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum Fairing {
108 Curved,
111 Flat,
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub struct Fill {
119 pub fairing: Fairing,
121 pub refine: bool,
125}
126
127#[derive(Debug, Clone, PartialEq)]
129pub struct FillReport {
130 pub faces: Range<u32>,
132 pub vertices: Range<u32>,
135 pub loops: usize,
139 pub flattened: usize,
142 pub largest_bend: f64,
147}
148
149impl Editable {
150 pub fn fill(&mut self, rim: RimId, fill: &Fill) -> OgeomResult<FillReport> {
182 let edges = self.rim_named(rim)?;
183 let (mut patch, loops) = self.gather(&edges)?;
184 let mut flattened = 0;
185 let mut largest_bend = 0.0_f64;
186 let mut drawn: FastSet<(u32, u32)> = FastSet::default();
187 for lp in &loops {
188 let ring = patch.ring(self, lp);
189 let free = |i: usize, j: usize| {
190 let (a, b) = (ring.ids[i], ring.ids[j]);
191 !patch.joined.contains(&(a.min(b), a.max(b)))
192 };
193 let triangles = if lp.len() <= OPTIMAL_UP_TO {
194 let Some(best) = optimal(&ring, &free) else {
195 ogeom_bail!(
196 Construction,
197 "rim {} has a loop of {} edges with no triangulation free of collinear \
198 corners and of edges the mesh already has",
199 rim.0,
200 lp.len()
201 );
202 };
203 best
204 } else {
205 flattened += 1;
206 ear_clip(&ring, &free)?
207 };
208 largest_bend = largest_bend.max(radians(measure(&ring, &triangles).0));
209 let n = ring.ids.len();
212 for t in &triangles {
213 for k in 0..3 {
214 let (i, j) = (t[k], t[(k + 1) % 3]);
215 if i > j || j - i == 1 || (i == 0 && j == n - 1) {
216 continue;
217 }
218 let (a, b) = (ring.ids[i], ring.ids[j]);
219 if !drawn.insert((a.min(b), a.max(b))) {
220 ogeom_bail!(
221 Construction,
222 "the loops of rim {} would draw one edge twice",
223 rim.0
224 );
225 }
226 }
227 patch.triangles.push(t.map(|k| ring.ids[k]));
228 }
229 }
230 if fill.refine {
231 patch.refine();
232 }
233 patch.fair(fill.fairing)?;
234 let faces = self.corners.len().saturating_add(patch.triangles.len());
235 let vertices = self.positions.len().saturating_add(patch.positions.len());
236 if faces.saturating_mul(3) >= NONE as usize || vertices >= NONE as usize {
237 ogeom_bail!(
238 Range,
239 "filling rim {} would make {faces} faces and {vertices} vertices, past u32 \
240 numbering",
241 rim.0
242 );
243 }
244 let (faces, vertices) = self.commit(&patch);
245 Ok(FillReport {
246 faces,
247 vertices,
248 loops: loops.len(),
249 flattened,
250 largest_bend,
251 })
252 }
253
254 fn rim_named(&self, rim: RimId) -> OgeomResult<Vec<u32>> {
256 let h = rim.0;
257 let k = h as usize;
258 if k >= self.twin.len() || !self.live[k / 3] || self.twin[k] != NONE {
259 ogeom_bail!(Dangling, "half-edge {h} is not on a rim");
260 }
261 let mut edges = Vec::new();
262 let mut g = h;
263 loop {
264 if g < h {
265 ogeom_bail!(
266 Dangling,
267 "half-edge {h} is on the rim of half-edge {g}, which names it"
268 );
269 }
270 edges.push(g);
271 g = self.rim_after(g);
272 if g == h {
273 break;
274 }
275 }
276 Ok(edges)
277 }
278
279 fn gather(&self, edges: &[u32]) -> OgeomResult<(Patch, Vec<Vec<u32>>)> {
282 let mut local: FastMap<u32, u32> = FastMap::default();
283 let mut vertex = Vec::new();
284 for &h in edges {
285 let v = self.tail(h);
286 local.entry(self.point[v as usize]).or_insert_with(|| {
287 vertex.push(v);
288 index(vertex.len() - 1)
289 });
290 }
291 let r = vertex.len();
292 let mut points: Vec<u32> = vertex.iter().map(|&v| self.point[v as usize]).collect();
293 points.sort_unstable();
294 let faces = self.about(&points);
295
296 let on_rim: FastSet<u32> = edges.iter().map(|&h| self.tail(h)).collect();
300 let mut uses: FastMap<u64, u32> = FastMap::default();
301 let mut joined = FastSet::default();
302 let mut next_to: Vec<Vec<(u32, f64)>> = vec![Vec::new(); r];
303 let mut mass = vec![0.0; r];
304 for &f in &faces {
305 let c = self.corners[f as usize];
306 let pull = spokes(c.map(|v| self.positions[v as usize]));
307 for k in 0..3 {
308 *uses.entry(self.edge_key(3 * f + index(k))).or_insert(0) += 1;
309 let ends = [c[k], c[(k + 1) % 3]].map(|v| local.get(&self.point[v as usize]));
310 if let [Some(&l), Some(&m)] = ends {
311 joined.insert((l.min(m), l.max(m)));
312 }
313 if !on_rim.contains(&c[k]) {
314 continue;
315 }
316 if let Some(&l) = local.get(&self.point[c[k] as usize]) {
317 let (a, b) = (c[(k + 1) % 3], c[(k + 2) % 3]);
318 let (wa, wb) = pull.map_or((0.0, 0.0), |(w, third)| {
319 mass[l as usize] += third;
320 (w[(k + 2) % 3], w[(k + 1) % 3])
321 });
322 next_to[l as usize].push((self.point[a as usize], wa));
323 next_to[l as usize].push((self.point[b as usize], wb));
324 }
325 }
326 }
327 for &h in edges {
328 let n = uses.get(&self.edge_key(h)).copied().unwrap_or(0);
329 if n != 1 {
330 ogeom_bail!(
331 Construction,
332 "rim edge {h} lies on an edge {n} faces use, which a patch cannot close"
333 );
334 }
335 }
336
337 let positions: Vec<Point> = vertex.iter().map(|&v| self.positions[v as usize]).collect();
338 let mut near = Vec::with_capacity(r);
339 let mut spacing = Vec::with_capacity(r);
340 for (l, list) in next_to.iter_mut().enumerate() {
341 merge(list);
342 let mut around = Near {
343 rim: Vec::new(),
344 ring: Vec::new(),
345 mass: mass[l],
346 };
347 let mut length = 0.0;
348 for &(q, w) in list.iter() {
349 let at = self.positions[q as usize];
350 length += at.distance(positions[l]);
351 match local.get(&q) {
352 Some(&m) => around.rim.push((m, w)),
353 None => around.ring.push((at, w)),
354 }
355 }
356 spacing.push(length / f64::from(index(list.len().max(1))));
357 near.push(around);
358 }
359
360 let mut at = vec![usize::MAX; r];
363 let mut stack: Vec<u32> = Vec::new();
364 let mut loops = Vec::new();
365 let slot = |h: u32| local[&self.point[self.tail(h) as usize]] as usize;
366 for &h in edges {
367 let l = slot(h);
368 if at[l] != usize::MAX {
369 let closed: Vec<u32> = stack.drain(at[l]..).collect();
370 for &e in &closed {
371 at[slot(e)] = usize::MAX;
372 }
373 loops.push(closed);
374 }
375 at[l] = stack.len();
376 stack.push(h);
377 }
378 loops.push(stack);
379 if let Some(short) = loops.iter().find(|l| l.len() < 3) {
380 ogeom_bail!(
381 Construction,
382 "rim {} has a loop of {} edges, which no face can fill",
383 edges[0],
384 short.len()
385 );
386 }
387
388 Ok((
389 Patch {
390 vertex,
391 local,
392 positions,
393 spacing,
394 triangles: Vec::new(),
395 joined,
396 near,
397 },
398 loops,
399 ))
400 }
401
402 fn commit(&mut self, patch: &Patch) -> (Range<u32>, Range<u32>) {
405 let first_face = index(self.corners.len());
406 let first_vertex = index(self.positions.len());
407 let r = patch.vertex.len();
408 let mut global = patch.vertex.clone();
409 for &p in &patch.positions[r..] {
410 global.push(self.add_vertex(p));
411 }
412 let mut points: Vec<u32> = patch
413 .vertex
414 .iter()
415 .map(|&v| self.point[v as usize])
416 .collect();
417 points.sort_unstable();
418 let mut faces = self.about(&points);
419 points.extend(first_vertex..index(self.positions.len()));
420 for t in &patch.triangles {
421 faces.push(self.add_face(t.map(|l| global[l as usize])));
422 }
423 self.relink(&Scope::Points(points), &faces);
424 (
425 first_face..index(self.corners.len()),
426 first_vertex..index(self.positions.len()),
427 )
428 }
429}
430
431struct Patch {
434 vertex: Vec<u32>,
436 local: FastMap<u32, u32>,
438 positions: Vec<Point>,
439 spacing: Vec<f64>,
443 triangles: Vec<[u32; 3]>,
445 joined: FastSet<(u32, u32)>,
447 near: Vec<Near>,
450}
451
452struct Near {
455 rim: Vec<(u32, f64)>,
458 ring: Vec<(Point, f64)>,
461 mass: f64,
463}
464
465fn spokes(c: [Point; 3]) -> Option<([f64; 3], f64)> {
469 let twice = (c[1] - c[0]).cross(c[2] - c[0]).magnitude();
470 if twice == 0.0 {
471 return None;
472 }
473 let half_cot = |k: usize| {
474 let (a, b) = (c[(k + 1) % 3] - c[k], c[(k + 2) % 3] - c[k]);
475 a.dot(b) / twice / 2.0
476 };
477 Some(([half_cot(0), half_cot(1), half_cot(2)], twice / 6.0))
478}
479
480fn merge(list: &mut Vec<(u32, f64)>) {
483 list.sort_by_key(|e| e.0);
484 let mut out: Vec<(u32, f64)> = Vec::with_capacity(list.len());
485 for &(k, w) in list.iter() {
486 match out.last_mut() {
487 Some(last) if last.0 == k => last.1 += w,
488 _ => out.push((k, w)),
489 }
490 }
491 *list = out;
492}
493
494struct Loop {
496 ids: Vec<u32>,
498 points: Vec<Point>,
499 beyond: Vec<Option<Point>>,
502}
503
504impl Patch {
505 fn ring(&self, mesh: &Editable, edges: &[u32]) -> Loop {
507 let at = |v: u32| mesh.positions[v as usize];
508 let ids: Vec<u32> = edges
509 .iter()
510 .map(|&h| self.local[&mesh.point[mesh.tail(h) as usize]])
511 .collect();
512 let points = ids.iter().map(|&l| self.positions[l as usize]).collect();
513 let beyond = edges
514 .iter()
515 .map(|&h| {
516 let (a, b, o) = (at(mesh.tail(h)), at(mesh.head(h)), at(mesh.tail(prev(h))));
517 ((b - a).cross(o - a) != Vector::ZERO).then_some(o)
518 })
519 .collect();
520 Loop {
521 ids,
522 points,
523 beyond,
524 }
525 }
526
527 fn refine(&mut self) {
530 let widest = self.spacing.iter().copied().fold(0.0, f64::max);
531 if widest <= 0.0 {
532 return;
533 }
534 let width = widest * FRAC_1_SQRT_2;
537 #[allow(clippy::cast_possible_truncation, reason = "saturating")]
538 let cell = |p: Point| [p.x, p.y, p.z].map(|x| (x / width).floor() as i64);
539 let mut grid: FastMap<[i64; 3], Vec<u32>> = FastMap::default();
540 for (v, &p) in self.positions.iter().enumerate() {
541 grid.entry(cell(p)).or_default().push(index(v));
542 }
543 loop {
544 let mut split = false;
545 for t in 0..self.triangles.len() {
546 let [a, b, c] = self.triangles[t];
547 let corners = [a, b, c].map(|v| self.positions[v as usize]);
548 let sum = corners
549 .iter()
550 .fold(Vector::ZERO, |s, &p| s + (p - Point::ORIGIN));
551 let g = Point::ORIGIN + sum / 3.0;
552 let s = [a, b, c]
553 .iter()
554 .map(|&v| self.spacing[v as usize])
555 .sum::<f64>()
556 / 3.0;
557 let far = |d2: f64, sigma: f64| 2.0 * d2 > sigma * sigma;
559 let corners_clear = [a, b, c].iter().zip(&corners).all(|(&v, &p)| {
560 let d2 = g.square_distance(p);
561 far(d2, self.spacing[v as usize])
562 });
563 if !corners_clear {
564 continue;
565 }
566 let [x, y, z] = cell(g);
567 let mut crowded = false;
568 'cells: for dx in -1..=1_i64 {
569 for dy in -1..=1_i64 {
570 for dz in -1..=1_i64 {
571 let key = [
572 x.saturating_add(dx),
573 y.saturating_add(dy),
574 z.saturating_add(dz),
575 ];
576 if let Some(list) = grid.get(&key)
577 && list.iter().any(|&v| {
578 !far(g.square_distance(self.positions[v as usize]), s)
579 })
580 {
581 crowded = true;
582 break 'cells;
583 }
584 }
585 }
586 }
587 if crowded {
588 continue;
589 }
590 let v = index(self.positions.len());
591 self.positions.push(g);
592 self.spacing.push(s);
593 grid.entry(cell(g)).or_default().push(v);
594 self.triangles[t] = [a, b, v];
595 self.triangles.push([b, c, v]);
596 self.triangles.push([c, a, v]);
597 split = true;
598 }
599 if !split {
600 break;
601 }
602 self.relax();
603 }
604 }
605
606 fn relax(&mut self) {
610 let r = index(self.vertex.len());
611 let mut at: FastMap<(u32, u32), u32> = FastMap::default();
612 for (t, c) in self.triangles.iter().enumerate() {
613 for k in 0..3 {
614 at.insert((c[k], c[(k + 1) % 3]), index(t));
615 }
616 }
617 let positions = &self.positions;
618 let p = |v: u32| positions[v as usize];
619 loop {
620 let mut flipped = false;
621 'triangles: for t in 0..self.triangles.len() {
622 for k in 0..3 {
623 let tri = self.triangles[t];
624 let (u, v, c) = (tri[k], tri[(k + 1) % 3], tri[(k + 2) % 3]);
625 if u > v {
626 continue;
627 }
628 let Some(&t2) = at.get(&(v, u)) else { continue };
629 let other = self.triangles[t2 as usize];
630 let Some(&d) = other.iter().find(|&&w| w != u && w != v) else {
631 continue;
632 };
633 if c == d
634 || at.contains_key(&(c, d))
635 || at.contains_key(&(d, c))
636 || (c < r && d < r && self.joined.contains(&(c.min(d), c.max(d))))
637 {
638 continue;
639 }
640 let n1 = (p(d) - p(u)).cross(p(c) - p(u));
641 let n2 = (p(v) - p(d)).cross(p(c) - p(d));
642 if n1 == Vector::ZERO || n2 == Vector::ZERO || n1.dot(n2) <= 0.0 {
643 continue;
644 }
645 let before =
646 smallest_angle(p(u), p(v), p(c)).min(smallest_angle(p(v), p(u), p(d)));
647 let after =
648 smallest_angle(p(u), p(d), p(c)).min(smallest_angle(p(d), p(v), p(c)));
649 if after <= before {
650 continue;
651 }
652 at.remove(&(u, v));
653 at.remove(&(v, u));
654 self.triangles[t] = [u, d, c];
655 self.triangles[t2 as usize] = [d, v, c];
656 for (e, f) in [((u, d), t), ((d, c), t), ((c, u), t)] {
657 at.insert(e, index(f));
658 }
659 for e in [(d, v), (v, c), (c, d)] {
660 at.insert(e, t2);
661 }
662 flipped = true;
663 continue 'triangles;
664 }
665 }
666 if !flipped {
667 break;
668 }
669 }
670 }
671
672 fn fair(&mut self, fairing: Fairing) -> OgeomResult<()> {
674 let r = self.vertex.len();
675 let total = self.positions.len();
676 let unknowns = total - r;
677 if unknowns == 0 {
678 return Ok(());
679 }
680 let mut spoke: Vec<Vec<(u32, f64)>> = vec![Vec::new(); total];
683 let mut mass = vec![0.0; total];
684 for t in &self.triangles {
685 let Some((w, third)) = spokes(t.map(|v| self.positions[v as usize])) else {
686 continue;
687 };
688 for k in 0..3 {
689 mass[t[k] as usize] += third;
690 let (a, b) = (t[(k + 1) % 3], t[(k + 2) % 3]);
691 spoke[a as usize].push((b, w[k]));
692 spoke[b as usize].push((a, w[k]));
693 }
694 }
695 for (l, near) in self.near.iter().enumerate() {
696 spoke[l].extend(&near.rim);
697 mass[l] += near.mass;
698 }
699 for list in &mut spoke {
700 merge(list);
701 }
702 let column = |l: u32| (l as usize).checked_sub(r);
703 let rows: Vec<usize> = (0..total)
704 .filter(|&w| {
705 w >= r
706 || (fairing == Fairing::Curved
707 && mass[w] > 0.0
708 && spoke[w].iter().any(|&(m, _)| m as usize >= r))
709 })
710 .collect();
711 let mut triplets = Vec::new();
716 let mut rhs = vec![0.0; 3 * rows.len()];
717 let none: Vec<(Point, f64)> = Vec::new();
718 for (row, &w) in rows.iter().enumerate() {
719 let ring = if w < r { &self.near[w].ring } else { &none };
720 let scale = match fairing {
723 Fairing::Curved if mass[w] > 0.0 => 1.0 / mass[w].sqrt(),
724 _ => 1.0,
725 };
726 let here = self.positions[w];
727 let mut lap = Vector::ZERO;
728 let mut sum = 0.0;
729 let mut put = |c: usize, value: f64| {
730 for k in 0..3 {
731 triplets.push((k * rows.len() + row, k * unknowns + c, value));
732 }
733 };
734 for &(m, weight) in &spoke[w] {
735 lap += (self.positions[m as usize] - here) * weight;
736 sum += weight;
737 if let Some(c) = column(m) {
738 put(c, scale * weight);
739 }
740 }
741 for &(q, weight) in ring {
742 lap += (q - here) * weight;
743 sum += weight;
744 }
745 if let Some(c) = column(index(w)) {
746 put(c, -scale * sum);
747 }
748 let lap = lap * scale;
749 rhs[row] = -lap.x;
750 rhs[rows.len() + row] = -lap.y;
751 rhs[2 * rows.len() + row] = -lap.z;
752 }
753 let a = SparseMatrix::from_triplets(3 * rows.len(), 3 * unknowns, &triplets);
754 let Some(d) = least_squares_cgnr(&a, &rhs, SETTLED, 3 * unknowns) else {
758 ogeom_bail!(
759 Numeric,
760 "the fairing of {unknowns} new vertices did not settle"
761 );
762 };
763 for c in 0..unknowns {
764 self.positions[r + c] += Vector::new(d[c], d[unknowns + c], d[2 * unknowns + c]);
765 }
766 Ok(())
767 }
768}
769
770const SETTLED: f64 = 1e-12;
776
777fn smallest_angle(a: Point, b: Point, c: Point) -> f64 {
779 let angle = |u: Vector, v: Vector| u.cross(v).magnitude().atan2(u.dot(v));
780 angle(b - a, c - a)
781 .min(angle(c - b, a - b))
782 .min(angle(a - c, b - c))
783}
784
785struct Axis {
790 from: Point,
791 e1: Vector,
792 e2: Vector,
793}
794
795impl Axis {
796 fn new(a: Point, b: Point) -> Self {
797 let u = b - a;
798 let length = u.magnitude();
799 let pick = if u.x.abs() <= u.y.abs() && u.x.abs() <= u.z.abs() {
801 Vector::X
802 } else if u.y.abs() <= u.z.abs() {
803 Vector::Y
804 } else {
805 Vector::Z
806 };
807 let e1 = u.cross(pick);
808 let e1 = e1 / e1.magnitude();
809 let e2 = u.cross(e1) / length;
810 Self { from: a, e1, e2 }
811 }
812
813 fn turn(&self, p: Point) -> [f64; 2] {
815 let w = p - self.from;
816 let (x, y) = (w.dot(self.e1), w.dot(self.e2));
817 let m = x.abs().max(y.abs());
820 let (x, y) = (x / m, y / m);
821 let r = (x * x + y * y).sqrt();
822 [x / r, y / r]
823 }
824}
825
826fn bend(a: [f64; 2], b: [f64; 2]) -> f64 {
833 let (x, y) = (a[0] + b[0], a[1] + b[1]);
834 x * x + y * y
835}
836
837fn radians(fold: f64) -> f64 {
839 2.0 * (fold.sqrt() / 2.0).min(1.0).asin()
840}
841
842fn axis_of(ring: &Loop, i: usize, j: usize) -> Axis {
844 Axis::new(ring.points[i.min(j)], ring.points[i.max(j)])
845}
846
847fn measure(ring: &Loop, triangles: &[[usize; 3]]) -> (f64, f64) {
850 let n = ring.points.len();
851 let p = &ring.points;
852 let mut apexes: FastMap<(usize, usize), Vec<usize>> = FastMap::default();
853 let mut area = 0.0;
854 for t in triangles {
855 area += (p[t[1]] - p[t[0]]).cross(p[t[2]] - p[t[0]]).magnitude() / 2.0;
856 for k in 0..3 {
857 let (i, j) = (t[k], t[(k + 1) % 3]);
858 apexes
859 .entry((i.min(j), i.max(j)))
860 .or_default()
861 .push(t[(k + 2) % 3]);
862 }
863 }
864 let mut keys: Vec<(usize, usize)> = apexes.keys().copied().collect();
865 keys.sort_unstable();
866 let mut largest = 0.0_f64;
867 for (i, j) in keys {
868 let axis = axis_of(ring, i, j);
869 let sides = &apexes[&(i, j)];
870 let b = if j - i == 1 || (i == 0 && j == n - 1) {
871 let edge = if j - i == 1 { i } else { n - 1 };
872 ring.beyond[edge].map_or(0.0, |o| bend(axis.turn(o), axis.turn(p[sides[0]])))
873 } else {
874 bend(axis.turn(p[sides[0]]), axis.turn(p[sides[1]]))
875 };
876 largest = largest.max(b);
877 }
878 (largest, area)
879}
880
881fn optimal(ring: &Loop, free: &dyn Fn(usize, usize) -> bool) -> Option<Vec<[usize; 3]>> {
887 const NO: f64 = f64::INFINITY;
888 let n = ring.points.len();
889 let p = &ring.points;
890 let mut open = vec![false; n * n];
891 for i in 0..n {
892 for j in i + 1..n {
893 open[i * n + j] = j - i == 1 || (i == 0 && j == n - 1) || free(i, j);
894 }
895 }
896 let mut start = vec![0_usize; n * n];
898 let mut total = 0;
899 for i in 0..n {
900 for k in i + 2..n {
901 start[i * n + k] = total;
902 total += k - i - 1;
903 }
904 }
905 let slot = |i: usize, m: usize, k: usize| start[i * n + k] + (m - i - 1);
906 let valid = |i: usize, m: usize, k: usize| {
907 open[i * n + m]
908 && open[m * n + k]
909 && open[i * n + k]
910 && (p[m] - p[i]).cross(p[k] - p[i]) != Vector::ZERO
911 };
912 let consumer = |a: usize, b: usize, c: usize| {
915 if c < a { slot(c, a, b) } else { slot(a, b, c) }
916 };
917 let outside = |a: usize, b: usize| (0..a).chain(b + 1..n);
918
919 let mut value = vec![0.0_f64; total];
922 let each = |value: &mut Vec<f64>, init: &dyn Fn(usize, usize, usize) -> f64| {
923 for i in 0..n {
924 for k in i + 2..n {
925 for m in i + 1..k {
926 value[slot(i, m, k)] = if valid(i, m, k) { init(i, m, k) } else { NO };
927 }
928 }
929 }
930 };
931 each(&mut value, &|_, _, _| 0.0);
932 for a in 0..n - 1 {
933 let axis = axis_of(ring, a, a + 1);
934 let far = ring.beyond[a].map(|o| axis.turn(o));
935 for c in outside(a, a + 1) {
936 let b = far.map_or(0.0, |t| bend(t, axis.turn(p[c])));
937 let s = consumer(a, a + 1, c);
938 value[s] = value[s].max(b);
939 }
940 }
941 let mut candidates: Vec<(f64, usize, [f64; 2])> = Vec::new();
942 let mut theta = NO;
943 for len in 2..n {
944 for a in 0..n - len {
945 let b = a + len;
946 let axis = axis_of(ring, a, b);
947 gather_candidates(&mut candidates, &value, &axis, p, a, b, &slot);
948 let least = |t: Option<[f64; 2]>| {
949 let mut best = NO;
950 for &(w, _, tx) in &candidates {
951 if w >= best {
952 break;
953 }
954 best = best.min(w.max(t.map_or(0.0, |t| bend(tx, t))));
955 }
956 best
957 };
958 if len == n - 1 {
959 theta = least(ring.beyond[n - 1].map(|o| axis.turn(o)));
960 } else {
961 for c in outside(a, b) {
962 let f = least(Some(axis.turn(p[c])));
963 let s = consumer(a, b, c);
964 value[s] = value[s].max(f);
965 }
966 }
967 }
968 }
969 if theta == NO {
970 return None;
971 }
972
973 each(&mut value, &|i, m, k| {
976 (p[m] - p[i]).cross(p[k] - p[i]).magnitude() / 2.0
977 });
978 let fits = |far: Option<[f64; 2]>, t: [f64; 2]| far.is_none_or(|o| bend(o, t) <= theta);
979 for a in 0..n - 1 {
980 let axis = axis_of(ring, a, a + 1);
981 let far = ring.beyond[a].map(|o| axis.turn(o));
982 for c in outside(a, a + 1) {
983 if !fits(far, axis.turn(p[c])) {
984 value[consumer(a, a + 1, c)] = NO;
985 }
986 }
987 }
988 let mut root = None;
989 for len in 2..n {
990 for a in 0..n - len {
991 let b = a + len;
992 let axis = axis_of(ring, a, b);
993 gather_candidates(&mut candidates, &value, &axis, p, a, b, &slot);
994 let first = |t: Option<[f64; 2]>| {
995 candidates
996 .iter()
997 .find(|&&(_, _, tx)| fits(t, tx))
998 .map(|&(g, x, _)| (g, x))
999 };
1000 if len == n - 1 {
1001 root = first(ring.beyond[n - 1].map(|o| axis.turn(o)));
1002 } else {
1003 for c in outside(a, b) {
1004 let s = consumer(a, b, c);
1005 value[s] = match first(Some(axis.turn(p[c]))) {
1006 Some((g, _)) => value[s] + g,
1007 None => NO,
1008 };
1009 }
1010 }
1011 }
1012 }
1013 let (_, m) = root?;
1014
1015 let choose = |a: usize, b: usize, c: usize| {
1018 let axis = axis_of(ring, a, b);
1019 let t = axis.turn(p[c]);
1020 let mut best: Option<(f64, usize)> = None;
1021 for x in a + 1..b {
1022 let g = value[slot(a, x, b)];
1023 if g < NO && bend(axis.turn(p[x]), t) <= theta && best.is_none_or(|(h, _)| g < h) {
1024 best = Some((g, x));
1025 }
1026 }
1027 best.map(|(_, x)| x)
1028 };
1029 let mut triangles = Vec::with_capacity(n - 2);
1030 let mut stack = vec![(0, m, n - 1)];
1031 while let Some((i, m, k)) = stack.pop() {
1032 triangles.push([k, m, i]);
1033 if m - i >= 2 {
1034 stack.push((i, choose(i, m, k)?, m));
1035 }
1036 if k - m >= 2 {
1037 stack.push((m, choose(m, k, i)?, k));
1038 }
1039 }
1040 Some(triangles)
1041}
1042
1043fn gather_candidates(
1046 candidates: &mut Vec<(f64, usize, [f64; 2])>,
1047 value: &[f64],
1048 axis: &Axis,
1049 p: &[Point],
1050 a: usize,
1051 b: usize,
1052 slot: &dyn Fn(usize, usize, usize) -> usize,
1053) {
1054 candidates.clear();
1055 for x in a + 1..b {
1056 let w = value[slot(a, x, b)];
1057 if w < f64::INFINITY {
1058 candidates.push((w, x, axis.turn(p[x])));
1059 }
1060 }
1061 candidates.sort_unstable_by(|u, v| u.0.total_cmp(&v.0).then(u.1.cmp(&v.1)));
1062}
1063
1064fn ear_clip(ring: &Loop, free: &dyn Fn(usize, usize) -> bool) -> OgeomResult<Vec<[usize; 3]>> {
1068 let n = ring.points.len();
1069 let p = &ring.points;
1070 let order: Vec<usize> = (0..n).map(|j| (n - j) % n).collect();
1072 let centre = p.iter().fold(Vector::ZERO, |s, &q| s + (q - Point::ORIGIN)) / f64::from(index(n));
1073 let at = |j: usize| p[order[j]] - Point::ORIGIN - centre;
1074 let mut normal = Vector::ZERO;
1075 for j in 0..n {
1076 normal += at(j).cross(at((j + 1) % n));
1077 }
1078 if normal == Vector::ZERO {
1079 ogeom_bail!(
1080 Construction,
1081 "a loop of {n} edges encloses no area across any plane"
1082 );
1083 }
1084 let axis = Axis::new(Point::ORIGIN, Point::ORIGIN + normal);
1085 let q: Vec<[f64; 2]> = (0..n)
1088 .map(|j| {
1089 let w = at(j);
1090 [w.dot(axis.e1), w.dot(axis.e2)]
1091 })
1092 .collect();
1093 let orient = |a: usize, b: usize, c: usize| Exact::orient2d(q[a], q[b], q[c]);
1094 if !simple(&q) {
1095 ogeom_bail!(
1096 Construction,
1097 "the shadow of a loop of {n} edges on its plane crosses itself"
1098 );
1099 }
1100
1101 let mut next: Vec<usize> = (0..n).map(|j| (j + 1) % n).collect();
1102 let mut before: Vec<usize> = (0..n).map(|j| (j + n - 1) % n).collect();
1103 let mut gone = vec![false; n];
1104 let convex = |before: &[usize], next: &[usize], j: usize| {
1105 orient(before[j], j, next[j]) == Sign::Positive
1106 };
1107 let mut triangles = Vec::with_capacity(n - 2);
1108 let mut left = n;
1109 let mut j = 0;
1110 let mut misses = 0;
1111 while left > 3 {
1112 let (a, c) = (before[j], next[j]);
1113 let ear = convex(&before, &next, j)
1114 && free(order[a].min(order[c]), order[a].max(order[c]))
1115 && (0..n).all(|v| {
1116 gone[v]
1117 || v == a
1118 || v == j
1119 || v == c
1120 || convex(&before, &next, v)
1121 || orient(a, j, v) == Sign::Negative
1122 || orient(j, c, v) == Sign::Negative
1123 || orient(c, a, v) == Sign::Negative
1124 });
1125 if ear {
1126 triangles.push([order[a], order[j], order[c]]);
1127 gone[j] = true;
1128 next[a] = c;
1129 before[c] = a;
1130 left -= 1;
1131 misses = 0;
1132 j = a;
1133 } else {
1134 j = next[j];
1135 misses += 1;
1136 if misses > left {
1137 ogeom_bail!(
1138 NotDone,
1139 "ear clipping a loop of {n} edges found no ear to cut with {left} points left"
1140 );
1141 }
1142 }
1143 }
1144 let (a, c) = (before[j], next[j]);
1145 if orient(a, j, c) != Sign::Positive {
1146 ogeom_bail!(
1147 NotDone,
1148 "ear clipping a loop of {n} edges left a last triangle with no area"
1149 );
1150 }
1151 triangles.push([order[a], order[j], order[c]]);
1152 Ok(triangles)
1153}
1154
1155fn simple(q: &[[f64; 2]]) -> bool {
1159 let n = q.len();
1160 let edge = |j: usize| (q[j], q[(j + 1) % n]);
1161 let mut order: Vec<usize> = (0..n).collect();
1162 let low = |j: usize| edge(j).0[0].min(edge(j).1[0]);
1163 let high = |j: usize| edge(j).0[0].max(edge(j).1[0]);
1164 order.sort_by(|&a, &b| low(a).total_cmp(&low(b)).then(a.cmp(&b)));
1165 let mut active: Vec<usize> = Vec::new();
1166 for &j in &order {
1167 active.retain(|&k| high(k) >= low(j));
1168 for &k in &active {
1169 let (a, b) = edge(j);
1170 let (c, d) = edge(k);
1171 let meets = if (j + 1) % n == k {
1172 folds_back(a, b, d)
1173 } else if (k + 1) % n == j {
1174 folds_back(c, d, b)
1175 } else {
1176 segments_meet(a, b, c, d)
1177 };
1178 if meets {
1179 return false;
1180 }
1181 }
1182 active.push(j);
1183 }
1184 true
1185}
1186
1187fn folds_back(a: [f64; 2], b: [f64; 2], c: [f64; 2]) -> bool {
1190 Exact::orient2d(a, b, c) == Sign::Zero
1191 && (a[0] - b[0]) * (c[0] - b[0]) + (a[1] - b[1]) * (c[1] - b[1]) > 0.0
1192}
1193
1194fn segments_meet(a: [f64; 2], b: [f64; 2], c: [f64; 2], d: [f64; 2]) -> bool {
1196 let within = |p: [f64; 2], q: [f64; 2], r: [f64; 2]| {
1197 r[0] >= p[0].min(q[0])
1198 && r[0] <= p[0].max(q[0])
1199 && r[1] >= p[1].min(q[1])
1200 && r[1] <= p[1].max(q[1])
1201 };
1202 let (o1, o2) = (Exact::orient2d(a, b, c), Exact::orient2d(a, b, d));
1203 let (o3, o4) = (Exact::orient2d(c, d, a), Exact::orient2d(c, d, b));
1204 if (o1 == Sign::Zero && within(a, b, c))
1205 || (o2 == Sign::Zero && within(a, b, d))
1206 || (o3 == Sign::Zero && within(c, d, a))
1207 || (o4 == Sign::Zero && within(c, d, b))
1208 {
1209 return true;
1210 }
1211 let opposite = |s: Sign, t: Sign| s != Sign::Zero && t != Sign::Zero && s != t;
1212 opposite(o1, o2) && opposite(o3, o4)
1213}
1214
1215#[cfg(test)]
1216#[allow(
1217 clippy::unwrap_used,
1218 clippy::cast_precision_loss,
1219 clippy::cast_possible_truncation,
1220 reason = "test code"
1221)]
1222mod tests {
1223 use super::super::tests::{check, grid, mesh, torus};
1224 use super::*;
1225 use core::f64::consts::{PI, TAU};
1226 use ogeom_topo::Triangulation;
1227 use proptest::prelude::*;
1228
1229 const CURVED: Fill = Fill {
1230 fairing: Fairing::Curved,
1231 refine: true,
1232 };
1233 const FLAT: Fill = Fill {
1234 fairing: Fairing::Flat,
1235 refine: true,
1236 };
1237 const BARE: Fill = Fill {
1238 fairing: Fairing::Flat,
1239 refine: false,
1240 };
1241
1242 fn sphere(radius: f64, bands: u32, sides: u32) -> Triangulation {
1245 let mut points = vec![Point::new(0.0, 0.0, radius)];
1246 for j in 1..bands {
1247 let polar = PI * f64::from(j) / f64::from(bands);
1248 let (s, c) = (radius * polar.sin(), radius * polar.cos());
1249 for i in 0..sides {
1250 let a = TAU * f64::from(i) / f64::from(sides);
1251 points.push(Point::new(s * a.cos(), s * a.sin(), c));
1252 }
1253 }
1254 let south = 1 + (bands - 1) * sides;
1255 points.push(Point::new(0.0, 0.0, -radius));
1256 let at = |j: u32, i: u32| 1 + (j - 1) * sides + i % sides;
1257 let mut triangles = Vec::new();
1258 for i in 0..sides {
1259 triangles.push([0, at(1, i), at(1, i + 1)]);
1260 triangles.push([south, at(bands - 1, i + 1), at(bands - 1, i)]);
1261 }
1262 for j in 1..bands - 1 {
1263 for i in 0..sides {
1264 let (a, b, c, d) = (at(j, i), at(j + 1, i), at(j + 1, i + 1), at(j, i + 1));
1265 triangles.extend([[a, b, c], [a, c, d]]);
1266 }
1267 }
1268 mesh(points, triangles)
1269 }
1270
1271 fn faces_where(e: &Editable, keep: impl Fn(Point) -> bool) -> Vec<u32> {
1273 (0..index(e.face_count()))
1274 .filter(|&f| {
1275 e.face(f).is_some_and(|c| {
1276 let sum = c.iter().fold(Vector::ZERO, |s, &v| {
1277 s + (e.position(v).unwrap() - Point::ORIGIN)
1278 });
1279 keep(Point::ORIGIN + sum / 3.0)
1280 })
1281 })
1282 .collect()
1283 }
1284
1285 fn fill_all(e: &mut Editable, fill: &Fill) -> Vec<FillReport> {
1287 let mut reports = Vec::new();
1288 while let Some(rim) = e.rims().first() {
1289 reports.push(e.fill(rim.id(), fill).unwrap());
1290 check(e);
1291 }
1292 reports
1293 }
1294
1295 fn area_of(e: &Editable, faces: Range<u32>) -> f64 {
1297 faces
1298 .map(|f| {
1299 let [a, b, c] = e.face(f).unwrap().map(|v| e.position(v).unwrap());
1300 (b - a).cross(c - a).magnitude() / 2.0
1301 })
1302 .sum()
1303 }
1304
1305 fn every(i: usize, k: usize) -> Vec<Vec<[usize; 3]>> {
1308 if k - i < 2 {
1309 return vec![Vec::new()];
1310 }
1311 let mut all = Vec::new();
1312 for m in i + 1..k {
1313 for left in every(i, m) {
1314 for right in every(m, k) {
1315 let mut t = left.clone();
1316 t.extend(&right);
1317 t.push([k, m, i]);
1318 all.push(t);
1319 }
1320 }
1321 }
1322 all
1323 }
1324
1325 struct Draw(u64);
1327
1328 impl Draw {
1329 fn next(&mut self) -> f64 {
1330 self.0 ^= self.0 << 13;
1331 self.0 ^= self.0 >> 7;
1332 self.0 ^= self.0 << 17;
1333 (self.0 >> 11) as f64 / (1_u64 << 53) as f64
1334 }
1335 }
1336
1337 proptest! {
1338 #![proptest_config(ProptestConfig::with_cases(256))]
1339
1340 #[test]
1346 fn the_programme_is_optimal_over_every_triangulation(
1347 n in 3_usize..=9,
1348 seed in 1_u64..u64::MAX,
1349 ) {
1350 let mut draw = Draw(seed);
1351 let mut points = Vec::new();
1352 let mut beyond = Vec::new();
1353 for i in 0..n {
1354 let a = TAU * i as f64 / n as f64;
1355 let r = 1.0 + 0.6 * (draw.next() - 0.5);
1356 points.push(Point::new(r * a.cos(), r * a.sin(), draw.next() - 0.5));
1357 }
1358 for i in 0..n {
1359 let (p, q) = (points[i], points[(i + 1) % n]);
1360 let out = Vector::new(p.x + q.x, p.y + q.y, 0.0) * 0.5;
1361 let o = p + (q - p) * 0.5 + out * draw.next()
1362 + Vector::new(0.0, 0.0, 2.0 * draw.next() - 1.0);
1363 beyond.push((draw.next() > 0.1).then_some(o));
1364 }
1365 let mut barred = vec![false; n * n];
1366 for b in &mut barred {
1367 *b = draw.next() < 0.15;
1368 }
1369 let ring = Loop { ids: (0..index(n)).collect(), points, beyond };
1370 let free = |i: usize, j: usize| !barred[i * n + j];
1371 let open = |i: usize, j: usize| {
1372 let (i, j) = (i.min(j), i.max(j));
1373 j - i == 1 || (i == 0 && j == n - 1) || free(i, j)
1374 };
1375 let p = &ring.points;
1376 let fits = |t: &[[usize; 3]]| {
1377 t.iter().all(|&[k, m, i]| {
1378 open(i, m) && open(m, k) && open(i, k)
1379 && (p[m] - p[i]).cross(p[k] - p[i]) != Vector::ZERO
1380 })
1381 };
1382 let best = every(0, n - 1)
1383 .into_iter()
1384 .filter(|t| fits(t))
1385 .map(|t| measure(&ring, &t))
1386 .min_by(|a, b| a.0.total_cmp(&b.0).then(a.1.total_cmp(&b.1)));
1387 let found = optimal(&ring, &free);
1388 match (best, found) {
1389 (None, None) => {}
1390 (Some((bend, area)), Some(t)) => {
1391 prop_assert_eq!(t.len(), n - 2);
1392 prop_assert!(fits(&t));
1393 let (b, a) = measure(&ring, &t);
1394 prop_assert_eq!(b, bend);
1395 prop_assert!((a - area).abs() <= 1e-12 * area, "{} against {}", a, area);
1396 }
1397 (best, found) => prop_assert!(false, "{:?} against {:?}", best, found),
1398 }
1399 }
1400 }
1401
1402 #[test]
1403 fn a_sphere_cap_filled_curved_comes_back_to_the_sphere() {
1404 let alpha = PI / 8.0;
1412 let top = alpha.cos() + alpha.sin().powi(2) / 2.0;
1413 let mut worst = Vec::new();
1414 let mut highest = Vec::new();
1415 for (bands, sides) in [(16, 24), (32, 48), (64, 96)] {
1416 let m = sphere(1.0, bands, sides);
1417 let mut e = Editable::from(&m, 0.0).unwrap();
1418 let rim_z = alpha.cos();
1419 e.remove_faces(&faces_where(&e, |c| c.z > rim_z));
1420 let rims = e.rims();
1421 assert_eq!(rims.len(), 1);
1422 assert_eq!(rims[0].edges.len(), sides as usize);
1423 let report = e.fill(rims[0].id(), &CURVED).unwrap();
1424 check(&e);
1425 assert!(e.is_closed());
1426 assert!(e.non_manifold().is_empty());
1427 assert_eq!(report.loops, 1);
1428 assert_eq!(report.flattened, 0);
1429 assert!(!report.vertices.is_empty());
1430 for (v, &p) in m.positions.iter().enumerate() {
1431 if let Some(q) = e.position(index(v)) {
1432 assert_eq!(q, p);
1433 }
1434 }
1435 let new = || report.vertices.clone().map(|v| e.position(v).unwrap());
1436 worst.push(
1437 new()
1438 .map(|p| ((p - Point::ORIGIN).magnitude() - 1.0).abs())
1439 .fold(0.0, f64::max),
1440 );
1441 highest.push(new().map(|p| p.z).fold(f64::MIN, f64::max));
1442 }
1443 assert!(worst.iter().all(|&w| w < 0.01), "{worst:?}");
1447 let gaps: Vec<f64> = highest.iter().map(|h| (h - top).abs()).collect();
1448 assert!(
1449 gaps.windows(2).all(|g| g[1] < g[0]),
1450 "{highest:?} against {top}"
1451 );
1452 }
1453
1454 #[test]
1455 fn a_flat_hole_filled_flat_stays_in_its_plane() {
1456 let n = 8;
1459 let u = Vector::new(0.6, 0.3, -0.2);
1460 let v = Vector::new(-0.1, 0.4, 0.7);
1461 let normal = u.cross(v) / u.cross(v).magnitude();
1462 let origin = Point::new(3.0, -2.0, 5.0);
1463 let mut m = grid(n);
1464 for p in &mut m.positions {
1465 *p = origin + u * p.x + v * p.y;
1466 }
1467 let cell_area = u.cross(v).magnitude();
1468 let mut cells = vec![(4, 1), (5, 1)];
1469 for i in 1..4 {
1470 for j in 1..4 {
1471 cells.push((i, j));
1472 }
1473 }
1474 let removed: Vec<u32> = cells
1475 .iter()
1476 .flat_map(|&(i, j)| [2 * (n * j + i), 2 * (n * j + i) + 1])
1477 .collect();
1478 for fill in [FLAT, BARE] {
1479 let mut e = Editable::from(&m, 0.0).unwrap();
1480 e.remove_faces(&removed);
1481 let rim = e.rims().into_iter().find(|r| r.edges.len() == 16).unwrap();
1482 let report = e.fill(rim.id(), &fill).unwrap();
1483 check(&e);
1484 assert_eq!(e.rims().len(), 1);
1485 assert!(e.non_manifold().is_empty());
1486 assert!(
1487 report.largest_bend < 1e-14,
1488 "{fill:?} {}",
1489 report.largest_bend
1490 );
1491 assert_eq!(report.vertices.is_empty(), !fill.refine);
1492 for v in report.vertices.clone() {
1493 let h = (e.position(v).unwrap() - origin).dot(normal);
1494 assert!(h.abs() < 1e-14, "{fill:?} {h}");
1495 }
1496 let area = area_of(&e, report.faces.clone());
1497 let hole = cells.len() as f64 * cell_area;
1498 assert!((area - hole).abs() < 1e-13 * hole, "{fill:?} {area} {hole}");
1499 }
1500 }
1501
1502 #[test]
1503 fn filling_every_hole_closes_a_torus() {
1504 let m = torus(16, 12);
1505 let whole = Editable::from(&m, 0.0).unwrap();
1506 let mut removed = vec![5, 40, 41];
1509 for (i, j) in [
1510 (6, 3),
1511 (6, 4),
1512 (7, 3),
1513 (7, 4),
1514 (8, 3),
1515 (8, 4),
1516 (12, 8),
1517 (13, 9),
1518 ] {
1519 removed.extend([2 * (i * 12 + j), 2 * (i * 12 + j) + 1]);
1520 }
1521 for fill in [CURVED, FLAT, BARE] {
1522 let mut e = whole.clone();
1523 e.remove_faces(&removed);
1524 assert_eq!(e.rims().len(), 4);
1525 let reports = fill_all(&mut e, &fill);
1526 assert_eq!(reports.len(), 4);
1527 assert!(e.is_closed(), "{fill:?}");
1528 assert!(e.non_manifold().is_empty());
1529 assert_eq!(e.components().len(), 1);
1530 let t = e.to_triangulation();
1532 let (v, f) = (t.positions.len(), t.triangles.len());
1533 assert_eq!(2 * v, f, "{fill:?}");
1534 if !fill.refine {
1536 assert_eq!(v, m.positions.len() - 2);
1537 }
1538 }
1539 }
1540
1541 #[test]
1542 fn holes_touching_at_a_point_are_closed_through_it() {
1543 let m = grid(4);
1546 let mut e = Editable::from(&m, 0.0).unwrap();
1547 let cell = |i: u32, j: u32| [2 * (4 * j + i), 2 * (4 * j + i) + 1];
1548 e.remove_faces(&[cell(1, 1), cell(2, 2)].concat());
1549 let pinch = Point::new(2.0, 2.0, 0.0);
1550 assert_eq!(e.non_manifold().len(), 2);
1551 let rims = e.rims();
1552 assert_eq!(rims.len(), 2);
1553 let inner = rims.iter().find(|r| r.edges.len() == 8).unwrap();
1554 let at_pinch = inner
1555 .edges
1556 .iter()
1557 .filter(|&&h| e.position(e.edge(h).unwrap()[0]) == Some(pinch))
1558 .count();
1559 assert_eq!(at_pinch, 2);
1560 for fill in [BARE, CURVED] {
1561 let mut f = e.clone();
1562 let report = f.fill(inner.id(), &fill).unwrap();
1563 check(&f);
1564 assert_eq!(report.loops, 2);
1565 assert!(f.non_manifold().is_empty(), "{fill:?}");
1566 assert_eq!(f.rims().len(), 1);
1567 let back = f.to_triangulation();
1568 assert_eq!(
1569 back.positions.iter().filter(|&&p| p == pinch).count(),
1570 1,
1571 "{fill:?}"
1572 );
1573 if !fill.refine {
1574 assert_eq!(back.positions, m.positions);
1575 assert_eq!(back.triangles.len(), m.triangles.len());
1576 }
1577 }
1578 }
1579
1580 #[test]
1581 fn an_edge_the_mesh_has_is_not_drawn_again() {
1582 let mut m = grid(3);
1586 let fin = index(m.positions.len());
1587 m.positions.push(Point::new(1.5, 1.5, 1.0));
1588 m.triangles.push([5, 10, fin]);
1589 let mut e = Editable::from(&m, 0.0).unwrap();
1590 e.remove_faces(&[8, 9]);
1591 let hole = e.rims().into_iter().find(|r| r.edges.len() == 4).unwrap();
1592 let report = e.fill(hole.id(), &BARE).unwrap();
1593 check(&e);
1594 let at = |v: u32| e.position(v).unwrap();
1595 for f in report.faces {
1596 let corners = e.face(f).unwrap().map(at);
1597 assert!(
1598 !(corners.contains(&Point::new(1.0, 1.0, 0.0))
1599 && corners.contains(&Point::new(2.0, 2.0, 0.0))),
1600 "{corners:?}"
1601 );
1602 }
1603 let rims = e.rims();
1604 assert_eq!(rims.len(), 2);
1605 assert!(rims.iter().any(|r| r.edges.len() == 3));
1606 }
1607
1608 #[test]
1609 fn a_long_loop_is_ear_clipped_on_its_plane() {
1610 let sides = 600;
1613 let mut positions = Vec::new();
1614 for r in [1.0, 2.0] {
1615 for i in 0..sides {
1616 let a = TAU * f64::from(i) / f64::from(sides);
1617 positions.push(Point::new(r * a.cos(), r * a.sin(), 0.0));
1618 }
1619 }
1620 let mut triangles = Vec::new();
1621 for i in 0..sides {
1622 let j = (i + 1) % sides;
1623 triangles.extend([[i, sides + i, sides + j], [i, sides + j, j]]);
1624 }
1625 let m = mesh(positions, triangles);
1626 let polygon = |r: f64| f64::from(sides) / 2.0 * r * r * (TAU / f64::from(sides)).sin();
1627 for fill in [BARE, FLAT] {
1628 let mut e = Editable::from(&m, 0.0).unwrap();
1629 let rims = e.rims();
1630 assert_eq!(rims.len(), 2);
1631 let inner = rims
1632 .iter()
1633 .find(|r| e.position(e.edge(r.edges[0]).unwrap()[0]).unwrap().x.abs() <= 1.0)
1634 .unwrap();
1635 let report = e.fill(inner.id(), &fill).unwrap();
1636 check(&e);
1637 assert_eq!(report.flattened, 1);
1638 let area = area_of(&e, report.faces.clone());
1639 assert!((area - polygon(1.0)).abs() < 1e-12, "{area}");
1640 for v in report.vertices.clone() {
1641 assert_eq!(e.position(v).unwrap().z, 0.0);
1642 }
1643 let report = e.fill(e.rims()[0].id(), &fill).unwrap();
1644 assert_eq!(report.flattened, 1);
1645 check(&e);
1646 assert!(e.is_closed());
1647 assert!(e.non_manifold().is_empty());
1648 }
1649 }
1650
1651 #[test]
1652 fn a_long_loop_whose_shadow_crosses_itself_is_refused() {
1653 let steps = 600;
1656 let mut positions = Vec::new();
1657 for lift in [0.0, 0.05] {
1658 for i in 0..steps {
1659 let t = TAU * f64::from(i) / f64::from(steps);
1660 let r = 3.0 + (3.0 * t).cos();
1661 positions.push(Point::new(
1662 r * (2.0 * t).cos(),
1663 r * (2.0 * t).sin(),
1664 (3.0 * t).sin() + lift,
1665 ));
1666 }
1667 }
1668 let mut triangles = Vec::new();
1669 for i in 0..steps {
1670 let j = (i + 1) % steps;
1671 triangles.extend([[i, j, steps + j], [i, steps + j, steps + i]]);
1672 }
1673 let mut e = Editable::from(&mesh(positions, triangles), 0.0).unwrap();
1674 let before = e.clone();
1675 let rim = e.rims()[0].id();
1676 let err = e.fill(rim, &BARE).unwrap_err();
1677 assert!(matches!(err, ogeom_core::OgeomError::Construction(_)));
1678 assert!(err.to_string().contains("crosses itself"), "{err}");
1679 assert_eq!(e, before);
1680 }
1681
1682 #[test]
1683 fn a_name_that_is_not_a_rim_is_refused() {
1684 let mut e = Editable::from(&grid(2), 0.0).unwrap();
1685 let rim = &e.rims()[0];
1686 let joined = (0..index(3 * e.face_count()))
1687 .find(|h| !rim.edges.contains(h))
1688 .unwrap();
1689 let later = rim.edges[1];
1690 let before = e.clone();
1691 for h in [joined, later, u32::MAX] {
1692 let err = e.fill(RimId(h), &BARE).unwrap_err();
1693 assert!(matches!(err, ogeom_core::OgeomError::Dangling(_)), "{err}");
1694 }
1695 assert_eq!(e, before);
1696 }
1697
1698 #[test]
1699 fn the_fill_is_deterministic() {
1700 let m = sphere(2.0, 18, 22);
1701 let mut a = Editable::from(&m, 0.0).unwrap();
1702 let removed = faces_where(&a, |c| c.x + 0.4 * c.y > 1.2 || c.z < -1.5);
1703 a.remove_faces(&removed);
1704 let mut b = a.clone();
1705 let ra = fill_all(&mut a, &CURVED);
1706 let rb = fill_all(&mut b, &CURVED);
1707 assert_eq!(ra, rb);
1708 assert_eq!(a, b);
1709 assert!(a.is_closed());
1710 }
1711}