Skip to main content

geop_core_rasterize/
grid.rs

1//! Face rasterization: an axis-aligned `(u, v)` grid, sized to the face's
2//! own curvature, with every cell clipped exactly against the trim
3//! boundary.
4//!
5//! Deliberately simple, after a constrained-Delaunay approach (now removed) turned out both too slow (the same triangle set could be
6//! reached by directly rasterizing a grid, without ever needing an
7//! incremental triangulation to converge) and occasionally wrong (a
8//! *reconstructed* Delaunay edge near the trim can end up connecting two
9//! vertices without actually following the boundary between them, cutting
10//! across the interior instead of tracing it). A grid cell's edges are
11//! either untouched (fully inside the trim) or literally computed by
12//! [`super::clip`] as the exact intersection with the boundary — there is
13//! no reconstruction step that could get an edge wrong.
14//!
15//! Pipeline:
16//! 1. The outer boundary and every hole come as polygons: the samples of
17//!    the face's edges, shared with the faces across them (see
18//!    [`super::mesh`]).
19//! 2. Pick a uniform grid resolution: start from `n`, and keep doubling it
20//!    until every cell's flat approximation is within a curvature-derived
21//!    tolerance of the true surface (so a flat face stays coarse and a
22//!    curved one gets whatever resolution its curvature actually needs —
23//!    still simple, just not spatially varying the way a quadtree would).
24//! 3. Split the outer boundary and every hole into convex parts — itself,
25//!    if it is convex, else its triangles — and clip each part to the grid
26//!    cells it overlaps ([`super::clip::clip_to_rect`], exact for a convex
27//!    part: one convex piece, or none). Then in each cell subtract the
28//!    holes' pieces from the outer boundary's
29//!    ([`super::clip::subtract_convex`]), and ear-clip whatever's left
30//!    ([`geop_core_math::polygon::ear_clip`]).
31//!
32//!    Three earlier versions of this step turned out wrong. The first
33//!    globally merged outer+holes into one ring (via a zero-width "slit"
34//!    bridge, since removed) and
35//!    clipped *that* per cell: a slit is one specific pair of edges,
36//!    traversed in each direction once, and if a grid cell boundary
37//!    happened to cross it the two traversals could clip to slightly
38//!    different results, leaving a sliver of the slit exposed as a real —
39//!    visibly wrong — mesh edge. The second instead re-derived that same
40//!    slit-bridge technique *per cell*, on each cell's own already-clipped
41//!    outer/hole fragments — better (no more cross-cell inconsistency),
42//!    but the bridge-visibility heuristic could still pick a technically-
43//!    valid-looking bridge across the wrong part of a small fragment,
44//!    producing the same kind of escaping sliver. [`super::clip::subtract_convex`]
45//!    has no bridge/visibility step to get wrong: it computes the exact
46//!    complement of a convex hole directly, via the same half-plane
47//!    primitive `clip_to_rect` itself is built from.
48//!
49//!    A third clipped the (possibly concave) outer boundary to each cell
50//!    directly, subtracting holes the same way: Sutherland–Hodgman returns a
51//!    boundary that enters a cell twice as one polygon, its two pieces
52//!    joined by a zero-width bridge along the cell's edge, and ear-clipping
53//!    that covered the gap between them — a face whose trim went around a
54//!    hole was drawn across it.
55//!
56//! Plain `f64` throughout — see [`super::clip`]'s doc comment for why
57//! that's the right call for a rendering-only algorithm even in an
58//! otherwise interval-exact kernel.
59
60use 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
75/// Where the grid starts, before curvature refines it: a flat face needs
76/// nothing finer, whatever `n` the caller asked for, since its cells
77/// approximate it exactly and the trim is clipped against them exactly.
78const MIN_GRID_N: usize = 2;
79/// Grid resolution never grows past this (in either direction), how ever
80/// far short of tolerance a face's curvature would otherwise ask for: a
81/// point budget.
82const MAX_GRID_N: usize = 128;
83/// How many times resolution may double while searching for one that
84/// meets tolerance.
85const MAX_DOUBLINGS: usize = 7;
86
87/// Perpendicular distance from `p` to segment `a..b` — see `grid`'s
88/// module doc comment on why curvature is measured this way (immune to a
89/// merely non-uniformly parametrized but still flat surface).
90pub(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/// A face's `(u, v)` domain, in plain `f64`, and a clamp into it — see
103/// [`Samples`]: plain-`f64` grid/midpoint arithmetic on a
104/// value already at a domain edge can legitimately land a ULP or two
105/// outside it, which a real `Scalar` interval never does (its width
106/// already encloses the true value) but a degenerate zero-width interval
107/// reconstructed via `Scalar::from_f64` has no such margin, so the kernel's
108/// (correctly strict) domain check can reject it.
109#[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
132/// A face's surface at the `(u, v)` its mesh samples, each evaluated once:
133/// a grid corner is shared by up to six triangles, and by every finer grid
134/// the refinement measures, whose cell edges' midpoints are the next grid's
135/// corners. It owns the surface, so that a face meshed again alike reads
136/// the samples it took before (see [`crate::Rasterizer`]).
137pub(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    /// `uv` in the domain, and the key it is kept under.
155    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    /// `uv` in the domain, as the surface's parameters.
161    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    /// The surface's point at `uv`.
167    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    /// The surface's normal at `uv`: none where it has none (a pole, where
185    /// the two derivatives are parallel).
186    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
197/// The worst deviation, over a `nu x nv` grid spanning `[min, max]`,
198/// between the surface's actual point at a cell edge's midpoint and the
199/// straight chord between its corners — reported per direction, since a
200/// face can curve in one and be straight in the other (a cylinder wall,
201/// an extruded profile), and then only that direction needs refining.
202fn worst_grid_deviation(
203    samples: &mut Samples,
204    min: Point,
205    max: Point,
206    nu: usize,
207    nv: usize,
208) -> GeopResult<[f64; 2]> {
209    // The point `i / nu` and `j / nv` of the way across, as
210    // `triangulate_region` places its cells: the midpoints of a cell's edges
211    // are then exactly the corners of the grid twice as fine.
212    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
233/// Triangulate the region of a face's `(u, v)` domain inside `outer` and
234/// outside every one of `holes` — its trim loops, as polygons: a uniform
235/// grid (its resolution chosen from the face's own curvature — see the
236/// module doc comment), each cell clipped exactly against the trim. The
237/// triangles' corners are in `(u, v)`, as `samples` takes them.
238pub(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    // Curvature tolerance derived from the face's own evaluated size, so
257    // `n` means a quality rather than one absolute size that would over- or
258    // under-tessellate depending on scale. It shrinks with `n²` because the
259    // deviation of a grid cell from the surface does too (halving a cell
260    // quarters its sagitta): that makes `n` mean "about this many cells
261    // across a face that curves through a quarter turn", and it is what
262    // decides how round a cylinder's silhouette looks.
263    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    // `u` and `v` are refined independently: a cylinder wall curves in one
273    // direction only, and a square grid would spend as many triangles along
274    // its straight direction as along its round one.
275    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
295/// The region inside the simple polygon `outer` and outside every one of
296/// `holes`, as triangles: the grid's own clipping on one cell, the
297/// polygon's bounding box — no bridge joins a hole to the outline (see the
298/// module doc). None for a polygon that encloses nothing.
299pub 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
315/// The region inside `outer` and outside every one of `holes`, cut along a
316/// `nu` x `nv` grid over `[min, max]` into triangles that each lie in one
317/// cell.
318fn 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    // The cells `[lo, hi]` spans along one axis, one either side to spare:
329    // a point on a cell line may round into either cell, and clipping
330    // exactly discards a cell it only touches.
331    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    // Every boundary is split once into convex parts, and each part is
339    // clipped to the cells it overlaps. Sutherland–Hodgman clips a convex
340    // polygon to a cell exactly, into one convex piece. A concave one it
341    // returns as one polygon however often the boundary enters the cell,
342    // joining its separate pieces by zero-width bridges along the cell's
343    // edge, and ear-clipping that — no longer a simple polygon — may cover
344    // the gap between them. So a concave boundary is split into triangles,
345    // once, as the simple polygon it is, and a convex one stays whole: its
346    // pieces then have no seams inside them, where rounding would leave
347    // hairline slivers.
348    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    // In each cell, every hole's convex pieces are subtracted from the outer
386    // boundary's: what is left of a convex piece is convex pieces again,
387    // which ear-clip exactly.
388    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    /// The area the triangles of `triangulate_region` cover.
408    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    /// A trim that enters a cell twice leaves the cell two separate
418    /// pieces, with what lies between them outside the trim: a U whose two
419    /// arms cross the upper cell. Covering the gap between the arms is
420    /// what drew a face across a hole it went around (see
421    /// `cylinder_joined_over_a_hole` in `geop-cad-base`).
422    #[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    /// A concave hole of many corners in a single cell — a crescent, as a
445    /// solid's face is left with where a torus was joined over it — is
446    /// split into many triangles, each taken out of what is left of the
447    /// cell in turn. That must stay cheap: a piece a triangle does not
448    /// overlap is left whole, not cut along the triangle's sides.
449    #[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        // Clockwise, as a hole: along the outer arc, back along the inner.
458        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    /// A round hole takes out itself, and leaves nothing inside it, on a
483    /// grid of any fineness.
484    #[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        // A 32-gon of radius 1 around (2, 2), clockwise.
488        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    /// A round hole whose quarter points lie on the cell lines — as an
527    /// extruded cap's is sampled, where the grid halves the face right
528    /// through them: a rounding error off them in plain `f64`, and exactly
529    /// on them in fixed point.
530    #[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    /// The same for a hole: one that enters a cell twice takes out only
623    /// itself, not what lies between its arms.
624    #[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        // The U, clockwise, as a hole's boundary runs.
628        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        // Zero curvature never triggers a resolution doubling, so the grid
673        // stays at the minimum whatever quality was asked for: a flat face
674        // is approximated exactly by any grid, and its trim is clipped
675        // against the cells exactly either way.
676        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}