Skip to main content

fast_mesh_ops/
orient.rs

1//! Making triangle winding consistent.
2//!
3//! [`orient()`] reads its whole plan from `tx.mesh()`: the patches with each
4//! triangle's parity, each patch's majority, then the outward pass over the
5//! closed components. It writes every flip in one `set_triangles` call.
6
7use fast_mesh_store::{Mesh, Transaction, TriangleId, VertexId};
8
9use crate::topology::{
10    EdgeMap, components, cross, dot, point, proper_triangle, sub, triangle_area,
11};
12use crate::{Result, filled, push, reserve};
13
14const NONE: u32 = u32::MAX;
15
16/// What [`orient()`] did.
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
18pub struct OrientReport {
19    /// Triangles reversed.
20    pub flipped_triangles: usize,
21    /// Patches found: triangles connected through interior edges.
22    pub patches: usize,
23    /// Patches that admit no consistent winding, left unchanged.
24    pub non_orientable_patches: usize,
25    /// Closed components turned to face outward.
26    pub flipped_outward_components: usize,
27}
28
29/// Triangles connected through interior edges, as found by the search.
30struct Patch {
31    /// The patch's triangles are `order[start..end]`, its smallest id first.
32    start: usize,
33    end: usize,
34    /// `false` when some triangle would need to be both flipped and not.
35    orientable: bool,
36    /// The total area of the triangles with parity `false`, then `true`.
37    area: [f64; 2],
38}
39
40/// Makes triangle winding consistent, reversing triangles by swapping their
41/// last two vertices.
42///
43/// - Patches grow from the smallest unvisited triangle id across interior
44///   edges only (two triangles); boundary and non-manifold edges stop them.
45///   A neighbour that runs along the shared edge in the same direction gets
46///   the opposite parity.
47/// - A patch that reaches a triangle with both parities is non-orientable
48///   (a Möbius-like strip): it is counted and none of its triangles change.
49/// - Every orientable patch takes the winding of the larger area on its own,
50///   so patches meeting only at non-manifold edges are not aligned with each
51///   other. On a tie the winding of the patch's smallest triangle id wins.
52/// - Then each closed component (no boundary and no non-manifold edge) whose
53///   signed volume, with the planned flips, is negative is reversed whole,
54///   so that it faces outward.
55///
56/// Triangles with a repeated vertex id are skipped: never flipped, never
57/// part of a patch. A consistent mesh is left without a write.
58///
59/// # Errors
60/// [`OpsError::Store`] when memory runs out; nothing is written then.
61///
62/// [`OpsError::Store`]: crate::OpsError::Store
63pub fn orient(tx: &mut Transaction) -> Result<OrientReport> {
64    let mesh = tx.mesh();
65    let edges = EdgeMap::build(mesh)?;
66    let slots = mesh.triangle_slots();
67    let mut patch_of = filled(slots, NONE)?;
68    let mut parity = filled(slots, false)?;
69    // Every proper triangle in visiting order; each patch is a run of it.
70    let mut order: Vec<TriangleId> = Vec::new();
71    reserve(&mut order, mesh.triangle_count())?;
72    let mut patches: Vec<Patch> = Vec::new();
73    for seed in 0..slots as TriangleId {
74        if patch_of[seed as usize] != NONE || proper_triangle(mesh, seed).is_none() {
75            continue;
76        }
77        let index = patches.len() as u32;
78        let start = order.len();
79        let mut orientable = true;
80        patch_of[seed as usize] = index;
81        order.push(seed);
82        let mut head = start;
83        while head < order.len() {
84            let u = order[head];
85            head += 1;
86            let [x, y, z] = mesh.triangles()[u as usize];
87            for (a, b) in [(x, y), (y, z), (z, x)] {
88                let &[s, t] = edges.triangles(a, b) else {
89                    continue;
90                };
91                let w = if s == u { t } else { s };
92                // `u` runs from `a` to `b`; so does `w` exactly when the two
93                // disagree, and then `w` needs the opposite parity.
94                let want = parity[u as usize] ^ runs_forward(mesh, w, a, b);
95                if patch_of[w as usize] == NONE {
96                    patch_of[w as usize] = index;
97                    parity[w as usize] = want;
98                    order.push(w);
99                } else if parity[w as usize] != want {
100                    orientable = false;
101                }
102            }
103        }
104        let mut area = [0.0; 2];
105        for &t in &order[start..] {
106            let tri = mesh.triangles()[t as usize];
107            area[usize::from(parity[t as usize])] += triangle_area(mesh, tri);
108        }
109        push(
110            &mut patches,
111            Patch {
112                start,
113                end: order.len(),
114                orientable,
115                area,
116            },
117        )?;
118    }
119
120    let mut flip = filled(slots, false)?;
121    for p in patches.iter().filter(|p| p.orientable) {
122        // The seed has parity `false`, so `false` keeps the tie.
123        let odd_wins = p.area[1] > p.area[0];
124        for &t in &order[p.start..p.end] {
125            flip[t as usize] = parity[t as usize] != odd_wins;
126        }
127    }
128
129    let mut report = OrientReport {
130        patches: patches.len(),
131        non_orientable_patches: patches.iter().filter(|p| !p.orientable).count(),
132        ..OrientReport::default()
133    };
134    for c in components(mesh, &edges)? {
135        // A closed component is a single patch, but a non-orientable one
136        // (a Klein bottle) has no outside.
137        let orientable = c
138            .triangles
139            .iter()
140            .all(|&t| patches[patch_of[t as usize] as usize].orientable);
141        if c.closed && orientable && signed_volume(mesh, &c.triangles, &flip) < 0.0 {
142            for &t in &c.triangles {
143                flip[t as usize] = !flip[t as usize];
144            }
145            report.flipped_outward_components += 1;
146        }
147    }
148
149    let count = flip.iter().filter(|&&f| f).count();
150    let (mut ids, mut flipped) = (Vec::new(), Vec::new());
151    reserve(&mut ids, count)?;
152    reserve(&mut flipped, count)?;
153    for t in 0..slots as TriangleId {
154        if flip[t as usize] {
155            let [a, b, c] = mesh.triangles()[t as usize];
156            ids.push(t);
157            flipped.push([a, c, b]);
158        }
159    }
160    report.flipped_triangles = count;
161    tx.set_triangles(&ids, &flipped)?;
162    Ok(report)
163}
164
165/// Whether triangle `t` runs along its edge `a`-`b` from `a` to `b`.
166fn runs_forward(mesh: &Mesh, t: TriangleId, a: VertexId, b: VertexId) -> bool {
167    let [x, y, z] = mesh.triangles()[t as usize];
168    (x, y) == (a, b) || (y, z) == (a, b) || (z, x) == (a, b)
169}
170
171/// Six times the signed volume enclosed by `triangles` once those marked in
172/// `flip` are reversed, taken about the first triangle's first vertex so
173/// that large coordinates do not swamp it.
174fn signed_volume(mesh: &Mesh, triangles: &[TriangleId], flip: &[bool]) -> f64 {
175    let Some(&first) = triangles.first() else {
176        return 0.0;
177    };
178    let o = point(mesh, mesh.triangles()[first as usize][0]);
179    let mut sum = 0.0;
180    for &t in triangles {
181        let [a, mut b, mut c] = mesh.triangles()[t as usize];
182        if flip[t as usize] {
183            std::mem::swap(&mut b, &mut c);
184        }
185        let [pa, pb, pc] = [a, b, c].map(|v| sub(point(mesh, v), o));
186        sum += dot(pa, cross(pb, pc));
187    }
188    sum
189}
190
191#[cfg(test)]
192mod tests {
193    use fast_mesh_store::{Mesh, TriangleId};
194
195    use super::*;
196    use crate::analyze::{AnalyzeOptions, analyze};
197    use crate::boundary_loops;
198    use crate::test_util::*;
199
200    fn conflicts(mesh: &Mesh) -> usize {
201        analyze(mesh, &AnalyzeOptions::default())
202            .unwrap()
203            .orientation_conflicts
204    }
205
206    /// The corners of triangle `t`'s vertices in `f64`.
207    fn corners(mesh: &Mesh, t: TriangleId) -> [[f64; 3]; 3] {
208        mesh.triangle(t)
209            .unwrap()
210            .map(|v| mesh.positions()[v as usize].map(f64::from))
211    }
212
213    /// The dot product of triangle `t`'s (unnormalised) normal and its
214    /// centroid: positive when it faces away from the origin.
215    fn facing(mesh: &Mesh, t: TriangleId) -> f64 {
216        let [a, b, c] = corners(mesh, t);
217        let (u, v) = (
218            std::array::from_fn::<f64, 3, _>(|k| b[k] - a[k]),
219            std::array::from_fn::<f64, 3, _>(|k| c[k] - a[k]),
220        );
221        let n = [
222            u[1] * v[2] - u[2] * v[1],
223            u[2] * v[0] - u[0] * v[2],
224            u[0] * v[1] - u[1] * v[0],
225        ];
226        (0..3).map(|k| n[k] * (a[k] + b[k] + c[k])).sum()
227    }
228
229    /// The mesh's signed volume about its first vertex.
230    fn volume(mesh: &Mesh) -> f64 {
231        let o = mesh.positions()[0].map(f64::from);
232        let mut sum = 0.0;
233        for t in 0..mesh.triangle_slots() as TriangleId {
234            if mesh.triangle(t).is_none() {
235                continue;
236            }
237            let [a, b, c] =
238                corners(mesh, t).map(|p| std::array::from_fn::<f64, 3, _>(|k| p[k] - o[k]));
239            sum += a[0] * (b[1] * c[2] - b[2] * c[1]) - a[1] * (b[0] * c[2] - b[2] * c[0])
240                + a[2] * (b[0] * c[1] - b[1] * c[0]);
241        }
242        sum / 6.0
243    }
244
245    /// `count` distinct ids below `n` from a fixed linear congruential sequence, ascending.
246    fn scattered(n: u32, count: usize) -> Vec<TriangleId> {
247        let mut out = Vec::new();
248        let mut x: u64 = 12345;
249        while out.len() < count {
250            x = x
251                .wrapping_mul(6364136223846793005)
252                .wrapping_add(1442695040888963407);
253            let t = ((x >> 33) % u64::from(n)) as TriangleId;
254            if !out.contains(&t) {
255                out.push(t);
256            }
257        }
258        out.sort_unstable();
259        out
260    }
261
262    #[test]
263    fn hemisphere_random_flips_fixed() {
264        let clean = hemisphere(8, 24, 1.0);
265        assert_eq!(clean.triangle_count(), 360);
266        let random = scattered(360, 4);
267        let patch: Vec<TriangleId> = (0..30).collect();
268        for flips in [random, patch] {
269            let mut mesh = flip(&clean, &flips);
270            assert!(conflicts(&mesh) > 0);
271            let (r, _) = edit(&mut mesh, orient);
272            assert_eq!(
273                r,
274                OrientReport {
275                    flipped_triangles: flips.len(),
276                    patches: 1,
277                    non_orientable_patches: 0,
278                    flipped_outward_components: 0,
279                }
280            );
281            assert_eq!(conflicts(&mesh), 0);
282            for t in 0..360 {
283                assert!(facing(&mesh, t) > 0.0, "triangle {t} faces inward");
284            }
285            assert_eq!(parts(&mesh), parts(&clean));
286            let (_, empty) = edit(&mut mesh, orient);
287            assert!(empty, "a second run writes nothing");
288        }
289    }
290
291    #[test]
292    fn inward_sphere_turned_outward() {
293        let clean = uv_sphere(8, 12, 1.0);
294        let n = clean.triangle_count();
295        let all: Vec<TriangleId> = (0..n as TriangleId).collect();
296        let mut mesh = flip(&clean, &all);
297        assert!(volume(&mesh) < 0.0);
298        let (r, _) = edit(&mut mesh, orient);
299        assert_eq!(
300            r,
301            OrientReport {
302                flipped_triangles: n,
303                patches: 1,
304                non_orientable_patches: 0,
305                flipped_outward_components: 1,
306            }
307        );
308        assert!(volume(&mesh) > 0.0);
309        assert_eq!(parts(&mesh), parts(&clean));
310
311        // Inward with a few outward strays: the majority turns the strays
312        // inward, then the whole sphere outward, so the strays stay as they are.
313        let strays = scattered(n as u32, 5);
314        let inward: Vec<TriangleId> = all
315            .iter()
316            .copied()
317            .filter(|t| !strays.contains(t))
318            .collect();
319        let mut mesh = flip(&clean, &inward);
320        let (r, _) = edit(&mut mesh, orient);
321        assert_eq!(
322            (r.flipped_triangles, r.flipped_outward_components),
323            (n - 5, 1)
324        );
325        assert_eq!(parts(&mesh), parts(&clean));
326
327        // Far from the origin, where the volume is taken about a local point.
328        let far = box_mesh([1e5; 3], [1e5 + 1.0; 3]);
329        let mut mesh = flip(&far, &(0..12).collect::<Vec<_>>());
330        let (r, _) = edit(&mut mesh, orient);
331        assert_eq!((r.flipped_triangles, r.flipped_outward_components), (12, 1));
332        assert_eq!(parts(&mesh), parts(&far));
333    }
334
335    #[test]
336    fn mobius_reported_unchanged() {
337        let mut mesh = mobius(12);
338        let before = parts(&mesh);
339        let (r, empty) = edit(&mut mesh, orient);
340        assert_eq!(
341            r,
342            OrientReport {
343                flipped_triangles: 0,
344                patches: 1,
345                non_orientable_patches: 1,
346                flipped_outward_components: 0,
347            }
348        );
349        assert!(empty);
350        assert_eq!(parts(&mesh), before);
351
352        // Beside it, a damaged hemisphere is still repaired.
353        let clean = combine(&[&mobius(12), &hemisphere(8, 24, 1.0)]);
354        let mut mesh = flip(&clean, &[30, 100, 101, 250]);
355        let (r, _) = edit(&mut mesh, orient);
356        assert_eq!(
357            (r.flipped_triangles, r.patches, r.non_orientable_patches),
358            (4, 2, 1)
359        );
360        assert_eq!(parts(&mesh), parts(&clean));
361    }
362
363    #[test]
364    fn klein_bottle_skips_outward_pass_unchanged() {
365        // Closed (no boundary, nothing non-manifold) but non-orientable: a
366        // single patch with no outside, left exactly as it was.
367        let mut mesh = klein(16, 12);
368        assert!(boundary_loops(&mesh).unwrap().is_empty());
369        let a = analyze(&mesh, &AnalyzeOptions::default()).unwrap();
370        assert_eq!(a.non_manifold_edges, 0);
371        assert_eq!(a.components.len(), 1);
372        assert!(a.components[0].closed);
373        let before = parts(&mesh);
374        let (r, empty) = edit(&mut mesh, orient);
375        assert_eq!(
376            r,
377            OrientReport {
378                flipped_triangles: 0,
379                patches: 1,
380                non_orientable_patches: 1,
381                flipped_outward_components: 0,
382            }
383        );
384        assert!(empty);
385        assert_eq!(parts(&mesh), before);
386    }
387
388    #[test]
389    fn fin_patches_independent() {
390        // Quad 0: triangle 1 reversed, triangle 0 (the smallest id) wins the
391        // tie. Quad 1: triangle 2 reversed and wins the tie. Quad 2: both
392        // reversed, consistent on its own, so left alone.
393        let clean = fin();
394        let mut mesh = flip(&clean, &[1, 2, 4, 5]);
395        let (r, _) = edit(&mut mesh, orient);
396        assert_eq!(
397            r,
398            OrientReport {
399                flipped_triangles: 2,
400                patches: 3,
401                non_orientable_patches: 0,
402                flipped_outward_components: 0,
403            }
404        );
405        assert_eq!(conflicts(&mesh), 0);
406        let expected = flip(&clean, &[2, 3, 4, 5]);
407        assert_eq!(parts(&mesh), parts(&expected));
408    }
409
410    #[test]
411    fn already_consistent_is_empty_patch() {
412        for clean in [uv_sphere(8, 12, 1.0), hemisphere(4, 8, 1.0), grid(3), fin()] {
413            let mut mesh = clean;
414            let (r, empty) = edit(&mut mesh, orient);
415            assert_eq!(r.flipped_triangles, 0);
416            assert!(empty);
417        }
418    }
419
420    #[test]
421    fn damaged_sphere_flips_fixed_and_degenerates_skipped() {
422        let mut mesh = damaged_sphere();
423        let before = parts(&mesh);
424        let (r, _) = edit(&mut mesh, orient);
425        // Holes, slits and the copied triangle leave the sphere open: no
426        // outward pass, only the five reversed triangles are turned back.
427        assert_eq!(r.flipped_triangles, 5);
428        assert_eq!(
429            (r.non_orientable_patches, r.flipped_outward_components),
430            (0, 0)
431        );
432        assert_eq!(conflicts(&mesh), 0);
433        assert_eq!(mesh.triangle(166), before.1[166].into());
434        assert_eq!(mesh.triangle(167), before.1[167].into());
435    }
436
437    #[test]
438    fn deterministic() {
439        let damaged = || flip(&hemisphere(8, 24, 1.0), &scattered(360, 40));
440        let (mut a, mut b) = (damaged(), damaged());
441        let (ra, _) = edit(&mut a, orient);
442        let (rb, _) = edit(&mut b, orient);
443        assert_eq!(ra, rb);
444        assert_eq!(parts(&a), parts(&b));
445    }
446
447    #[test]
448    fn out_of_memory_is_reported_and_writes_nothing() {
449        assert!(sweep(damaged_sphere(), |tx| orient(tx).map(drop)) > 5);
450        let clean = uv_sphere(8, 12, 1.0);
451        let inward = flip(
452            &clean,
453            &(0..clean.triangle_count() as TriangleId).collect::<Vec<_>>(),
454        );
455        assert!(sweep(inward, |tx| orient(tx).map(drop)) > 5);
456    }
457}