Skip to main content

fast_mesh_fields/
sign.rs

1//! Sign data: what decides whether a point lies in front of or behind a mesh.
2//!
3//! Pseudonormals (Bærentzen and Aanæs, "Signed distance computation using the
4//! angle weighted pseudonormal", 2005): the sign of `dot(p − closest, n)`,
5//! where `n` is the normal of the feature the closest point lies on:
6//!
7//! - a face uses its own normal;
8//! - an edge uses the sum of the unit normals of the faces on it (only its own
9//!   face on a boundary edge);
10//! - a vertex uses the angle-weighted sum of the unit normals of the faces
11//!   around it.
12//!
13//! The sign is exact for closed, consistently oriented manifold meshes. On an
14//! open sheet it tells the side of the surface, so "behind the surface" reads
15//! as inside.
16//!
17//! **Zero-area triangles** (exactly collinear or coincident corners) have no
18//! normal. They are skipped when normals are summed, and an edge whose
19//! neighbor across is a zero-area sliver looks through it (up to
20//! [`SLIVER_HOPS`] slivers deep) for the face on the other side. When the
21//! closest point is reported on a zero-area triangle, the sign comes from the
22//! nearest feature of the non-degenerate triangles that share a vertex with
23//! it (ties go to the smallest triangle id).
24//!
25//! **Ties:** a point exactly on the plane of its feature's normal (the dot
26//! product is exactly zero, or the normal is zero) counts as outside, so its
27//! distance is positive. A point on the surface (distance zero) gets zero.
28
29use std::ops::Range;
30
31use fast_mesh_bvh::Bvh;
32use fast_mesh_query::{ClosestHit, Feature, QueryError};
33use fast_mesh_store::{
34    Adjacency, AdjacencyBuild, ChangeSet, EdgeKind, Incidence, IncidenceBuild, Mesh, TriangleId,
35    VertexId, par,
36};
37
38use crate::geom::{
39    add, closest_on_triangle, cross, dot, narrow, norm, scale, sub, triangle_normal, unit, widen,
40};
41use crate::winding::{Moments, MomentsBuild};
42use crate::{FieldsError, Result};
43
44/// How many zero-area slivers an edge normal looks through to find the face
45/// on the other side.
46const SLIVER_HOPS: u32 = 4;
47
48/// Elements (vertex slots, triangle slots, corners or BVH leaves) from which
49/// a pass of [`SignData::build`] runs in parallel chunks with the `parallel`
50/// feature. Measured on the i5-13600K with every pass parallel: a
51/// pseudonormal build of a 4k-vertex sheet breaks even (1 ms), of 1k vertices
52/// is slower in parallel, of 8k vertices twice as fast. Each pass decides on
53/// its own length, so a 4k-vertex sheet runs its triangle and corner passes
54/// in parallel and its vertex pass on the calling thread.
55const PAR_MIN_SIGN_ELEMS: usize = 8 * 1024;
56
57/// What a vertex pseudonormal counts for in a [`SignBuild::step`] budget,
58/// against one element of the cheaper passes. Measured on the i5-13600K at
59/// 1M triangles, single-threaded, in steps of 131,072 elements: about 70 ns
60/// per vertex normal, 10 to 15 ns per edge normal and per triangle of the
61/// adjacency build, and 5 ns or less per element of the incidence, face
62/// normal and topology passes.
63const VERTEX_NORMAL_WEIGHT: usize = 12;
64
65/// What an edge normal counts for in a [`SignBuild::step`] budget (see
66/// [`VERTEX_NORMAL_WEIGHT`]).
67const EDGE_NORMAL_WEIGHT: usize = 2;
68
69/// What a triangle or vertex slot of the adjacency build counts for in a
70/// [`SignBuild::step`] budget (see [`VERTEX_NORMAL_WEIGHT`]).
71const ADJACENCY_WEIGHT: usize = 3;
72
73/// Elements per parallel chunk of a [`SignData::build`] pass.
74const SIGN_CHUNK: usize = 2048;
75
76/// How the sign of a distance is decided.
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
78pub enum SignMode {
79    /// The side of the surface the point lies on, from the angle-weighted
80    /// pseudonormal at the closest feature. Exact for closed, consistently
81    /// oriented manifold meshes; on an open mesh "inside" means behind the
82    /// surface.
83    Pseudonormal,
84    /// The generalized winding number (see [`SignData::winding_number`]):
85    /// inside where it is at least one half. Robust to self-intersections and
86    /// inconsistent orientation, but it never reaches one half behind an open
87    /// sheet.
88    Winding,
89    /// [`SignMode::Pseudonormal`], except for a closed mesh whose orientation
90    /// is inconsistent, which needs [`SignMode::Winding`].
91    #[default]
92    Auto,
93}
94
95/// What [`SignData::build`] found out about a mesh, so that an application
96/// can warn when signs are doubtful.
97#[derive(Debug, Clone, Copy, PartialEq)]
98pub struct SignStatus {
99    /// The mode in use: never [`SignMode::Auto`].
100    pub mode: SignMode,
101    /// True when no edge is a boundary (used by one triangle) or non-manifold
102    /// (used by three or more). Zero-length edges of triangles that repeat a
103    /// vertex are not counted as edges.
104    pub closed: bool,
105    /// True when every edge shared by exactly two triangles is traversed in
106    /// opposite directions by them.
107    pub consistent_orientation: bool,
108    /// Alive vertices whose triangles do not form a single fan (see
109    /// [`Adjacency::is_non_manifold_vertex`]).
110    pub non_manifold_vertices: usize,
111    /// Alive triangles with exactly zero area.
112    pub degenerate_triangles: usize,
113    /// The signed volume enclosed by the alive triangles (O(n), in `f64`):
114    /// positive for a closed, outward mesh, negative for a consistently
115    /// inverted one (every sign wrong under `Auto`), near zero for an open
116    /// sheet. Apps can warn when a closed, consistently oriented mesh has
117    /// a negative volume.
118    pub signed_volume: f64,
119}
120
121/// Per-mesh data that decides the sign of a distance, built by
122/// [`SignData::build`] for one revision of a mesh and one state of its BVH.
123///
124/// In [`SignMode::Pseudonormal`] it holds a unit pseudonormal per vertex
125/// slot, an edge normal per triangle corner (for the edge opposite that
126/// corner) and, for every zero-area triangle, the non-degenerate triangles
127/// that share a vertex with it. Face normals are recomputed from the mesh when
128/// needed.
129///
130/// In [`SignMode::Winding`] it holds instead the order-1 dipole moments of
131/// every BVH node (area-weighted normal sum, area-weighted centroid and a
132/// bounding radius), for the fast winding number of Barill et al. 2018.
133#[derive(Debug, Clone)]
134pub struct SignData {
135    mesh_uid: u64,
136    epoch: u32,
137    revision: u64,
138    bvh_generation: u64,
139    status: SignStatus,
140    vertex_normals: Vec<[f32; 3]>,
141    /// Indexed by corner `3 * t + i`: the normal of the edge opposite corner `i`.
142    edge_normals: Vec<[f32; 3]>,
143    /// Alive zero-area triangles, in ascending id order.
144    degenerate: Vec<TriangleId>,
145    /// `candidates[offsets[i]..offsets[i + 1]]` are the non-degenerate
146    /// triangles sharing a vertex with `degenerate[i]`, in ascending id order.
147    offsets: Vec<u32>,
148    candidates: Vec<TriangleId>,
149    /// Winding moments per BVH node; empty unless the mode is `Winding`.
150    pub(crate) moments: Moments,
151}
152
153/// Pushes with a fallible reservation.
154fn try_push<T>(v: &mut Vec<T>, value: T) -> Result<()> {
155    v.try_reserve(1).map_err(|_| FieldsError::OutOfMemory)?;
156    v.push(value);
157    Ok(())
158}
159
160/// `f(i)` for every `i` in `span`, in order. With the `parallel` feature, a
161/// span of `PAR_MIN_SIGN_ELEMS` or more runs in chunks of [`SIGN_CHUNK`] on
162/// the pool; every element is computed alone, so the result is the same
163/// either way, and collecting a range in spans gives the same elements as
164/// all at once.
165pub(crate) fn collect_span<T: Send>(
166    span: Range<usize>,
167    f: impl Fn(usize) -> T + Sync,
168) -> Result<Vec<T>> {
169    let mut v = Vec::new();
170    let total = span.len();
171    extend_span(&mut v, span, total, f)?;
172    Ok(v)
173}
174
175/// Appends `f(i)` for every `i` in `span` to `v`, in order, in parallel
176/// under the rule of [`collect_span`]. `v` is reserved for `total` elements
177/// in all at once, so that filling it in spans never copies it.
178fn extend_span<T: Send>(
179    v: &mut Vec<T>,
180    span: Range<usize>,
181    total: usize,
182    f: impl Fn(usize) -> T + Sync,
183) -> Result<()> {
184    let oom = || FieldsError::OutOfMemory;
185    let (start, len) = (span.start, span.len());
186    let room = total.max(v.len() + len) - v.len();
187    v.try_reserve_exact(room).map_err(|_| oom())?;
188    if len < PAR_MIN_SIGN_ELEMS || !par::enabled() || par::force_sequential() {
189        v.extend(span.map(f));
190        return Ok(());
191    }
192    let parts = par::map_range(len, SIGN_CHUNK, PAR_MIN_SIGN_ELEMS, oom, |r| {
193        let mut part = Vec::new();
194        part.try_reserve_exact(r.len()).map_err(|_| oom())?;
195        part.extend((r.start + start..r.end + start).map(&f));
196        Ok(part)
197    })?;
198    parts.into_iter().for_each(|p| v.extend(p));
199    Ok(())
200}
201
202/// `f` over `span` cut into chunks of [`SIGN_CHUNK`], results in chunk
203/// order, in parallel under the same rule as [`collect_span`]. For
204/// reductions whose combine does not depend on the order (counts, flags).
205fn reduce_span<R: Send>(
206    span: Range<usize>,
207    f: impl Fn(Range<usize>) -> R + Sync,
208) -> Result<Vec<R>> {
209    let oom = || FieldsError::OutOfMemory;
210    let start = span.start;
211    par::map_range(span.len(), SIGN_CHUNK, PAR_MIN_SIGN_ELEMS, oom, |r| {
212        Ok(f(r.start + start..r.end + start))
213    })
214}
215
216fn corners(mesh: &Mesh, tri: [VertexId; 3]) -> [[f32; 3]; 3] {
217    tri.map(|v| mesh.positions()[v as usize])
218}
219
220/// Topology facts about the triangles in `span`: whether none has a boundary
221/// or non-manifold edge, whether every interior edge is traversed in
222/// opposite directions, and how many have zero area.
223fn topology_span(
224    mesh: &Mesh,
225    adj: &Adjacency,
226    face: &[[f64; 3]],
227    span: Range<usize>,
228) -> Result<(bool, bool, usize)> {
229    let tris = mesh.triangles();
230    let parts = reduce_span(span, |range| {
231        let (mut closed, mut consistent, mut degenerate) = (true, true, 0);
232        for t in range {
233            let tri = tris[t];
234            if !mesh.is_triangle_alive(t as TriangleId) {
235                continue;
236            }
237            if face[t] == [0.0; 3] {
238                degenerate += 1;
239            }
240            for i in 0..3 {
241                let (u, w) = (tri[(i + 1) % 3], tri[(i + 2) % 3]);
242                if u == w {
243                    continue;
244                }
245                let c = (3 * t + i) as u32;
246                match adj.edge_kind(c) {
247                    EdgeKind::Boundary | EdgeKind::NonManifold => closed = false,
248                    EdgeKind::Interior => {
249                        if let Some(o) = adj.opposite(c) {
250                            let (s, j) = ((o / 3) as usize, (o % 3) as usize);
251                            let other = tris[s];
252                            if (other[(j + 1) % 3], other[(j + 2) % 3]) != (w, u) {
253                                consistent = false;
254                            }
255                        }
256                    }
257                }
258            }
259        }
260        (closed, consistent, degenerate)
261    })?;
262    Ok(parts
263        .into_iter()
264        .fold((true, true, 0), |(a, b, n), (c, d, m)| {
265            (a && c, b && d, n + m)
266        }))
267}
268
269/// The signed volume contribution of triangle slot `t`:
270/// `dot(a, cross(b, c)) / 6` in `f64`, or zero for a dead slot.
271fn tri_volume(mesh: &Mesh, t: usize) -> f64 {
272    let Some(tri) = mesh.triangle(t as TriangleId) else {
273        return 0.0;
274    };
275    let p = corners(mesh, tri).map(widen);
276    dot(p[0], cross(p[1], p[2])) / 6.0
277}
278
279/// The alive non-manifold vertices among the vertex slots in `span`.
280fn non_manifold_span(mesh: &Mesh, adj: &Adjacency, span: Range<usize>) -> Result<usize> {
281    Ok(reduce_span(span, |range| {
282        range
283            .filter(|&v| {
284                let v = v as VertexId;
285                mesh.is_vertex_alive(v) && adj.is_non_manifold_vertex(v)
286            })
287            .count()
288    })?
289    .into_iter()
290    .sum())
291}
292
293/// The unit normal of triangle slot `t`; zero for a dead or zero-area one.
294fn face_normal(mesh: &Mesh, t: usize) -> [f64; 3] {
295    mesh.triangle(t as TriangleId)
296        .map_or([0.0; 3], |tri| unit(triangle_normal(corners(mesh, tri))))
297}
298
299/// The angle-weighted pseudonormal of vertex slot `v`, from the
300/// non-degenerate triangles around it.
301fn vertex_normal(mesh: &Mesh, inc: &Incidence, face: &[[f64; 3]], v: usize) -> [f32; 3] {
302    let mut sum = [0.0; 3];
303    for &t in inc.triangles(v as VertexId) {
304        let n = face[t as usize];
305        if n == [0.0; 3] {
306            continue;
307        }
308        let tri = mesh.triangles()[t as usize];
309        let Some(i) = tri.iter().position(|&x| x as usize == v) else {
310            continue;
311        };
312        let p = corners(mesh, tri).map(widen);
313        let (e1, e2) = (sub(p[(i + 1) % 3], p[i]), sub(p[(i + 2) % 3], p[i]));
314        let angle = norm(cross(e1, e2)).atan2(dot(e1, e2));
315        sum = add(sum, scale(n, angle));
316    }
317    narrow(unit(sum))
318}
319
320/// The corner of triangle `s` opposite its longest edge (the first on ties).
321fn longest_edge_corner(mesh: &Mesh, s: usize) -> u32 {
322    let p = corners(mesh, mesh.triangles()[s]).map(widen);
323    let len2 = |j: usize| {
324        let e = sub(p[(j + 2) % 3], p[(j + 1) % 3]);
325        dot(e, e)
326    };
327    let mut best = 0;
328    for j in 1..3 {
329        if len2(j) > len2(best) {
330            best = j;
331        }
332    }
333    best as u32
334}
335
336/// The unit normal of the face across the edge opposite corner `c`, looking
337/// through up to `hops` zero-area slivers, or zero when there is none.
338///
339/// Entering a sliver through one of its short edges, the surface continues
340/// across its longest edge. Entering through the longest edge, it continues
341/// across both short edges, and their normals are averaged.
342fn across(mesh: &Mesh, adj: &Adjacency, face: &[[f64; 3]], c: u32, hops: u32) -> [f64; 3] {
343    let Some(o) = adj.opposite(c) else {
344        return [0.0; 3];
345    };
346    let s = (o / 3) as usize;
347    if face[s] != [0.0; 3] {
348        return face[s];
349    }
350    if hops == 0 {
351        return [0.0; 3];
352    }
353    let long = longest_edge_corner(mesh, s);
354    let base = 3 * s as u32;
355    if o % 3 != long {
356        across(mesh, adj, face, base + long, hops - 1)
357    } else {
358        let sum = (0..3)
359            .filter(|&j| j != long)
360            .map(|j| across(mesh, adj, face, base + j, hops - 1))
361            .fold([0.0; 3], add);
362        unit(sum)
363    }
364}
365
366/// The edge normal of corner `c`: the sum of the unit normals of the faces
367/// on the edge opposite it.
368fn edge_normal(
369    mesh: &Mesh,
370    adj: &Adjacency,
371    inc: &Incidence,
372    face: &[[f64; 3]],
373    c: usize,
374) -> [f32; 3] {
375    let tris = mesh.triangles();
376    let (t, i) = (c / 3, c % 3);
377    if !mesh.is_triangle_alive(t as TriangleId) {
378        return [0.0f32; 3];
379    }
380    let tri = tris[t];
381    let mut sum = face[t];
382    match adj.edge_kind(c as u32) {
383        EdgeKind::Interior => sum = add(sum, across(mesh, adj, face, c as u32, SLIVER_HOPS)),
384        EdgeKind::Boundary => {}
385        EdgeKind::NonManifold => {
386            let (u, w) = (tri[(i + 1) % 3], tri[(i + 2) % 3]);
387            for &s in inc.triangles(u) {
388                if s as usize != t && tris[s as usize].contains(&w) {
389                    sum = add(sum, face[s as usize]);
390                }
391            }
392        }
393    }
394    narrow(sum)
395}
396
397/// As [`vertex_normal`], with face normals computed fresh from `mesh`:
398/// for [`SignData::try_refit`], which has no precomputed face array.
399/// Must give the same values (it runs the same arithmetic).
400fn refit_vertex_normal(mesh: &Mesh, inc: &Incidence, v: usize) -> [f32; 3] {
401    let mut sum = [0.0; 3];
402    for &t in inc.triangles(v as VertexId) {
403        let n = face_normal(mesh, t as usize);
404        if n == [0.0; 3] {
405            continue;
406        }
407        let tri = mesh.triangles()[t as usize];
408        let Some(i) = tri.iter().position(|&x| x as usize == v) else {
409            continue;
410        };
411        let p = corners(mesh, tri).map(widen);
412        let (e1, e2) = (sub(p[(i + 1) % 3], p[i]), sub(p[(i + 2) % 3], p[i]));
413        let angle = norm(cross(e1, e2)).atan2(dot(e1, e2));
414        sum = add(sum, scale(n, angle));
415    }
416    narrow(unit(sum))
417}
418
419/// As [`across`], with face normals computed fresh from `mesh`.
420fn refit_across(mesh: &Mesh, adj: &Adjacency, c: u32, hops: u32) -> [f64; 3] {
421    let Some(o) = adj.opposite(c) else {
422        return [0.0; 3];
423    };
424    let s = (o / 3) as usize;
425    let f = face_normal(mesh, s);
426    if f != [0.0; 3] {
427        return f;
428    }
429    if hops == 0 {
430        return [0.0; 3];
431    }
432    let long = longest_edge_corner(mesh, s);
433    let base = 3 * s as u32;
434    if o % 3 != long {
435        refit_across(mesh, adj, base + long, hops - 1)
436    } else {
437        let sum = (0..3)
438            .filter(|&j| j != long)
439            .map(|j| refit_across(mesh, adj, base + j, hops - 1))
440            .fold([0.0; 3], add);
441        unit(sum)
442    }
443}
444
445/// As [`edge_normal`], with face normals computed fresh from `mesh`.
446fn refit_edge_normal(mesh: &Mesh, adj: &Adjacency, inc: &Incidence, c: usize) -> [f32; 3] {
447    let tris = mesh.triangles();
448    let (t, i) = (c / 3, c % 3);
449    if !mesh.is_triangle_alive(t as TriangleId) {
450        return [0.0f32; 3];
451    }
452    let tri = tris[t];
453    let mut sum = face_normal(mesh, t);
454    match adj.edge_kind(c as u32) {
455        EdgeKind::Interior => sum = add(sum, refit_across(mesh, adj, c as u32, SLIVER_HOPS)),
456        EdgeKind::Boundary => {}
457        EdgeKind::NonManifold => {
458            let (u, w) = (tri[(i + 1) % 3], tri[(i + 2) % 3]);
459            for &s in inc.triangles(u) {
460                if s as usize != t && tris[s as usize].contains(&w) {
461                    sum = add(sum, face_normal(mesh, s as usize));
462                }
463            }
464        }
465    }
466    narrow(sum)
467}
468
469/// The pass a [`SignBuild`] is in, with its cursor.
470#[derive(Debug, Clone, Copy, PartialEq, Eq)]
471enum Stage {
472    /// Building the mesh's adjacency, unless cached.
473    Adjacency,
474    /// Building the mesh's incidence, unless cached.
475    Incidence,
476    /// The unit normals of the triangle slots, from this one on.
477    Faces(usize),
478    /// The topology facts of the triangle slots, from this one on.
479    Topology(usize),
480    /// The non-manifold count over the vertex slots, from this one on.
481    NonManifold(usize),
482    /// The pseudonormals of the vertex slots, from this one on.
483    VertexNormals(usize),
484    /// The edge normals of the corners, from this one on.
485    EdgeNormals(usize),
486    /// The zero-area triangles and their neighbours, from this slot on.
487    Degenerate(usize),
488    /// The winding moments, leaf by leaf.
489    Moments,
490    /// Finished: [`SignBuild::finish`] hands out the data.
491    Done,
492}
493
494/// [`SignData::build`] cut into steps, so that a caller can yield between
495/// them: the mesh's adjacency and incidence (when not cached), then each pass
496/// of the build, a bounded number of elements per step.
497///
498/// Every element of a pass is computed on its own, and the counts and flags
499/// combine in any order, so the finished data is the same, bit for bit, as
500/// from [`SignData::build`] (which runs one of these to the end), with or
501/// without the `parallel` feature: within a step, a long enough run of
502/// elements still runs in parallel chunks. A build is tied to the mesh state
503/// and BVH generation it started from ([`SignBuild::is_for`]); stepping it on
504/// another state fails.
505#[derive(Debug)]
506pub struct SignBuild {
507    data: SignData,
508    /// The mesh's vertex and triangle slots: an undo brings the revision
509    /// back but keeps slots it added.
510    slots: (usize, usize),
511    requested: SignMode,
512    stage: Stage,
513    adjacency: AdjacencyBuild,
514    incidence: IncidenceBuild,
515    face: Vec<[f64; 3]>,
516    moments: Option<MomentsBuild>,
517}
518
519impl SignBuild {
520    /// A build of `mesh`'s sign data in `mode`, against `bvh`, with nothing
521    /// done yet.
522    ///
523    /// # Errors
524    /// [`FieldsError::Query`] with `MeshMismatch` or `StaleBvh` when `bvh`
525    /// does not match the current `mesh`.
526    pub fn new(mesh: &Mesh, bvh: &Bvh, mode: SignMode) -> Result<SignBuild> {
527        if bvh.mesh_uid() != mesh.uid() {
528            return Err(QueryError::MeshMismatch.into());
529        }
530        if !bvh.is_current(mesh) {
531            return Err(QueryError::StaleBvh.into());
532        }
533        Ok(SignBuild {
534            data: SignData {
535                mesh_uid: mesh.uid(),
536                epoch: mesh.epoch(),
537                revision: mesh.revision(),
538                bvh_generation: bvh.generation(),
539                status: SignStatus {
540                    mode: SignMode::Pseudonormal,
541                    closed: true,
542                    consistent_orientation: true,
543                    non_manifold_vertices: 0,
544                    degenerate_triangles: 0,
545                    signed_volume: 0.0,
546                },
547                vertex_normals: Vec::new(),
548                edge_normals: Vec::new(),
549                degenerate: Vec::new(),
550                offsets: Vec::new(),
551                candidates: Vec::new(),
552                moments: Moments::default(),
553            },
554            slots: (mesh.vertex_slots(), mesh.triangle_slots()),
555            requested: mode,
556            stage: Stage::Adjacency,
557            adjacency: AdjacencyBuild::new(mesh),
558            incidence: IncidenceBuild::new(mesh),
559            face: Vec::new(),
560            moments: None,
561        })
562    }
563
564    /// Whether the build started from `mesh` and `bvh` as they are now (see
565    /// [`SignData::is_current`]), with the same slot counts.
566    pub fn is_for(&self, mesh: &Mesh, bvh: &Bvh) -> bool {
567        self.data.is_current(mesh, bvh)
568            && self.slots == (mesh.vertex_slots(), mesh.triangle_slots())
569    }
570
571    /// Whether every pass is done.
572    pub fn is_done(&self) -> bool {
573        self.stage == Stage::Done
574    }
575
576    /// Runs the build over up to `budget` more elements (at least one,
577    /// unless it is done): triangle, vertex or corner slots of a pass, or
578    /// leaves of the winding moments and their node slots while those
579    /// initialise, a vertex pseudonormal counting as 12, an edge normal as
580    /// 2 and a triangle of the adjacency build as 3, for what they cost. A
581    /// step that finishes the mesh's adjacency or incidence ends there.
582    /// Returns whether the build is done.
583    ///
584    /// # Errors
585    /// [`FieldsError::InvalidInput`] when `mesh` and `bvh` are not the state
586    /// the build started from ([`SignBuild::is_for`]);
587    /// [`FieldsError::OutOfMemory`] or [`FieldsError::Store`] when an array
588    /// cannot be allocated. The build is unusable after an error.
589    pub fn step(&mut self, mesh: &mut Mesh, bvh: &Bvh, budget: usize) -> Result<bool> {
590        if !self.is_for(mesh, bvh) {
591            return Err(FieldsError::InvalidInput(
592                "the sign data build started from another mesh or BVH state",
593            ));
594        }
595        let mut left = budget.max(1);
596        while left > 0 {
597            match self.stage {
598                Stage::Adjacency => {
599                    let budget = (left / ADJACENCY_WEIGHT).max(1);
600                    if mesh.step_adjacency(&mut self.adjacency, budget)? {
601                        self.stage = Stage::Incidence;
602                    }
603                    // A partial or finishing adjacency step used the budget.
604                    return Ok(false);
605                }
606                Stage::Incidence => {
607                    if mesh.step_incidence(&mut self.incidence, left)? {
608                        self.stage = Stage::Faces(0);
609                    }
610                    return Ok(false);
611                }
612                _ => {}
613            }
614            let mesh: &Mesh = mesh;
615            let missing = FieldsError::InvalidInput("mesh adjacency or incidence unavailable");
616            let adj = mesh.adjacency().ok_or(missing.clone())?;
617            let inc = mesh.incidence().ok_or(missing)?;
618            let span =
619                |from: usize, len: usize, left: usize| from..len.min(from.saturating_add(left));
620            match self.stage {
621                Stage::Adjacency | Stage::Incidence => unreachable!("handled above"),
622                Stage::Faces(from) => {
623                    let r = span(from, mesh.triangle_slots(), left);
624                    let (to, n) = (r.end, r.len());
625                    let total = mesh.triangle_slots();
626                    extend_span(&mut self.face, r, total, |t| face_normal(mesh, t))?;
627                    left -= n.min(left);
628                    self.stage = if to == mesh.triangle_slots() {
629                        Stage::Topology(0)
630                    } else {
631                        Stage::Faces(to)
632                    };
633                }
634                Stage::Topology(from) => {
635                    let r = span(from, mesh.triangles().len(), left);
636                    let (to, n) = (r.end, r.len());
637                    let (closed, consistent, degenerate) =
638                        topology_span(mesh, adj, &self.face, r.clone())?;
639                    let st = &mut self.data.status;
640                    st.closed &= closed;
641                    st.consistent_orientation &= consistent;
642                    st.degenerate_triangles += degenerate;
643                    // Sequentially in id order, so every budget sums the same bits.
644                    for t in r {
645                        st.signed_volume += tri_volume(mesh, t);
646                    }
647                    left -= n.min(left);
648                    self.stage = if to == mesh.triangles().len() {
649                        Stage::NonManifold(0)
650                    } else {
651                        Stage::Topology(to)
652                    };
653                }
654                Stage::NonManifold(from) => {
655                    let r = span(from, mesh.vertex_slots(), left);
656                    let (to, n) = (r.end, r.len());
657                    self.data.status.non_manifold_vertices += non_manifold_span(mesh, adj, r)?;
658                    left -= n.min(left);
659                    if to < mesh.vertex_slots() {
660                        self.stage = Stage::NonManifold(to);
661                        continue;
662                    }
663                    let st = &mut self.data.status;
664                    st.mode = match self.requested {
665                        SignMode::Pseudonormal => SignMode::Pseudonormal,
666                        SignMode::Auto if !(st.closed && !st.consistent_orientation) => {
667                            SignMode::Pseudonormal
668                        }
669                        SignMode::Auto | SignMode::Winding => SignMode::Winding,
670                    };
671                    if st.mode == SignMode::Winding {
672                        self.moments = Some(MomentsBuild::new(bvh)?);
673                        self.stage = Stage::Moments;
674                    } else {
675                        self.stage = Stage::VertexNormals(0);
676                    }
677                }
678                Stage::Moments => {
679                    let moments = self.moments.as_mut().ok_or(FieldsError::OutOfMemory)?;
680                    let before = moments.progress();
681                    let done = moments.step(mesh, bvh, left)?;
682                    left -= moments.progress().saturating_sub(before).min(left);
683                    if done {
684                        let moments = self.moments.take().ok_or(FieldsError::OutOfMemory)?;
685                        self.data.moments = moments.finish();
686                        self.face = Vec::new();
687                        self.stage = Stage::Done;
688                    }
689                }
690                Stage::VertexNormals(from) => {
691                    let w = VERTEX_NORMAL_WEIGHT;
692                    let r = span(from, mesh.vertex_slots(), (left / w).max(1));
693                    let (to, n) = (r.end, r.len());
694                    let face = &self.face;
695                    let total = mesh.vertex_slots();
696                    extend_span(&mut self.data.vertex_normals, r, total, |v| {
697                        vertex_normal(mesh, inc, face, v)
698                    })?;
699                    left -= n.saturating_mul(w).min(left);
700                    self.stage = if to == mesh.vertex_slots() {
701                        Stage::EdgeNormals(0)
702                    } else {
703                        Stage::VertexNormals(to)
704                    };
705                }
706                Stage::EdgeNormals(from) => {
707                    let w = EDGE_NORMAL_WEIGHT;
708                    let r = span(from, 3 * mesh.triangles().len(), (left / w).max(1));
709                    let (to, n) = (r.end, r.len());
710                    let face = &self.face;
711                    let total = 3 * mesh.triangles().len();
712                    extend_span(&mut self.data.edge_normals, r, total, |c| {
713                        edge_normal(mesh, adj, inc, face, c)
714                    })?;
715                    left -= n.saturating_mul(w).min(left);
716                    if to < 3 * mesh.triangles().len() {
717                        self.stage = Stage::EdgeNormals(to);
718                        continue;
719                    }
720                    try_push(&mut self.data.offsets, 0)?;
721                    self.stage = Stage::Degenerate(0);
722                }
723                Stage::Degenerate(from) => {
724                    let r = span(from, mesh.triangle_slots(), left);
725                    let (to, n) = (r.end, r.len());
726                    let SignData {
727                        degenerate,
728                        offsets,
729                        candidates,
730                        ..
731                    } = &mut self.data;
732                    let face = &self.face;
733                    for t in r {
734                        let Some(tri) = mesh.triangle(t as TriangleId) else {
735                            continue;
736                        };
737                        if face[t] != [0.0; 3] {
738                            continue;
739                        }
740                        let start = candidates.len();
741                        for v in tri {
742                            for &s in inc.triangles(v) {
743                                if face[s as usize] != [0.0; 3] {
744                                    try_push(candidates, s)?;
745                                }
746                            }
747                        }
748                        candidates[start..].sort_unstable();
749                        let mut kept = start;
750                        for i in start..candidates.len() {
751                            if kept == start || candidates[i] != candidates[kept - 1] {
752                                candidates[kept] = candidates[i];
753                                kept += 1;
754                            }
755                        }
756                        candidates.truncate(kept);
757                        try_push(degenerate, t as TriangleId)?;
758                        try_push(offsets, candidates.len() as u32)?;
759                    }
760                    left -= n.min(left);
761                    if to == mesh.triangle_slots() {
762                        self.face = Vec::new();
763                        self.stage = Stage::Done;
764                    } else {
765                        self.stage = Stage::Degenerate(to);
766                    }
767                }
768                Stage::Done => return Ok(true),
769            }
770            if self.stage == Stage::Done {
771                return Ok(true);
772            }
773        }
774        Ok(self.stage == Stage::Done)
775    }
776
777    /// The sign data, once [`SignBuild::step`] returned `true`; `None`
778    /// before.
779    pub fn finish(self) -> Option<SignData> {
780        (self.stage == Stage::Done).then_some(self.data)
781    }
782}
783
784impl SignData {
785    /// Builds the sign data of `mesh` in `mode`, building the mesh's adjacency
786    /// and incidence first if they are not cached. `Auto` is resolved here
787    /// (to `Winding` for a closed mesh with inconsistent orientation, to
788    /// `Pseudonormal` otherwise); [`SignData::status`] reports the outcome.
789    ///
790    /// # Errors
791    /// - [`FieldsError::Query`] with `MeshMismatch` or `StaleBvh` when `bvh`
792    ///   does not match the current `mesh`.
793    /// - [`FieldsError::OutOfMemory`] or [`FieldsError::Store`] when the
794    ///   adjacency, incidence, normals or winding moments cannot be built.
795    pub fn build(mesh: &mut Mesh, bvh: &Bvh, mode: SignMode) -> Result<SignData> {
796        let mut build = SignBuild::new(mesh, bvh, mode)?;
797        while !build.step(mesh, bvh, usize::MAX)? {}
798        build.finish().ok_or(FieldsError::OutOfMemory)
799    }
800
801    /// Refits pseudonormal data in place after a position-only edit,
802    /// recomputing only the vertex and edge normals around
803    /// `changes`' moved vertices (plus the signed volume, in O(n)).
804    /// Returns whether the refit ran: `false` (a full [`SignData::build`]
805    /// is needed) when the mode is not pseudonormal, the topology changed,
806    /// vertices or triangles were added or removed, the slot counts changed,
807    /// the mesh's adjacency or incidence is not cached, the BVH is not
808    /// current, or a dirty triangle changed its degenerate status. On `true`
809    /// the data is current for `mesh` and `bvh` and identical to a rebuild.
810    ///
811    /// # Errors
812    /// [`FieldsError::OutOfMemory`] when the dirty sets cannot be listed.
813    pub fn try_refit(&mut self, mesh: &Mesh, bvh: &Bvh, changes: &ChangeSet) -> Result<bool> {
814        if self.status.mode != SignMode::Pseudonormal {
815            return Ok(false);
816        }
817        if mesh.uid() != self.mesh_uid || mesh.epoch() != self.epoch {
818            return Ok(false);
819        }
820        if bvh.mesh_uid() != self.mesh_uid || !bvh.is_current(mesh) {
821            return Ok(false);
822        }
823        if mesh.vertex_slots() != self.vertex_normals.len()
824            || 3 * mesh.triangle_slots() != self.edge_normals.len()
825        {
826            return Ok(false);
827        }
828        if changes.topology_changed()
829            || !changes.vertices_added.is_empty()
830            || !changes.vertices_removed.is_empty()
831        {
832            return Ok(false);
833        }
834        let (Some(adj), Some(inc)) = (mesh.adjacency(), mesh.incidence()) else {
835            return Ok(false);
836        };
837        // The triangles incident to the moved vertices.
838        let mut dirty: Vec<TriangleId> = Vec::new();
839        for &v in &changes.vertices_moved {
840            let list = inc.triangles(v);
841            dirty
842                .try_reserve(list.len())
843                .map_err(|_| FieldsError::OutOfMemory)?;
844            dirty.extend_from_slice(list);
845        }
846        dirty.sort_unstable();
847        dirty.dedup();
848        // A dirty triangle that changed its degenerate status needs a rebuild
849        // (new neighbours for the fallback, new counts).
850        for &t in &dirty {
851            let zero = face_normal(mesh, t as usize) == [0.0; 3];
852            if self.degenerate.binary_search(&t).is_ok() != zero {
853                return Ok(false);
854            }
855        }
856        // The vertices of the dirty triangles: exactly those whose normals
857        // can have changed (a vertex normal sums its incident faces).
858        let tris = mesh.triangles();
859        let mut verts: Vec<VertexId> = Vec::new();
860        verts
861            .try_reserve(dirty.len().saturating_mul(3))
862            .map_err(|_| FieldsError::OutOfMemory)?;
863        for &t in &dirty {
864            verts.extend_from_slice(&tris[t as usize]);
865        }
866        verts.sort_unstable();
867        verts.dedup();
868        for &v in &verts {
869            self.vertex_normals[v as usize] = refit_vertex_normal(mesh, inc, v as usize);
870        }
871        // The corners of the dirty triangles, with the corners across their
872        // edges (whose sums include the dirty faces).
873        let mut corners: Vec<u32> = Vec::new();
874        corners
875            .try_reserve(dirty.len().saturating_mul(6))
876            .map_err(|_| FieldsError::OutOfMemory)?;
877        for &t in &dirty {
878            for i in 0..3u32 {
879                let c = 3 * t + i;
880                corners.push(c);
881                match adj.edge_kind(c) {
882                    EdgeKind::Interior => {
883                        if let Some(o) = adj.opposite(c) {
884                            corners.push(o);
885                        }
886                    }
887                    EdgeKind::Boundary => {}
888                    EdgeKind::NonManifold => {
889                        let tri = tris[t as usize];
890                        let (u, w) = (tri[((i + 1) % 3) as usize], tri[((i + 2) % 3) as usize]);
891                        for &s in inc.triangles(u) {
892                            if tris[s as usize].contains(&w) {
893                                let stri = tris[s as usize];
894                                for j in 0..3u32 {
895                                    let (a, b) = (
896                                        stri[((j + 1) % 3) as usize],
897                                        stri[((j + 2) % 3) as usize],
898                                    );
899                                    if (a == u && b == w) || (a == w && b == u) {
900                                        corners.push(3 * s + j);
901                                    }
902                                }
903                            }
904                        }
905                    }
906                }
907            }
908        }
909        corners.sort_unstable();
910        corners.dedup();
911        for &c in &corners {
912            self.edge_normals[c as usize] = refit_edge_normal(mesh, adj, inc, c as usize);
913        }
914        // The volume, over every triangle in id order (as the build sums it).
915        let mut volume = 0.0;
916        for t in 0..mesh.triangle_slots() {
917            volume += tri_volume(mesh, t);
918        }
919        self.status.signed_volume = volume;
920        self.revision = mesh.revision();
921        self.bvh_generation = bvh.generation();
922        Ok(true)
923    }
924
925    /// The generalized winding number of `mesh` at `p`: the sum of the solid
926    /// angles of its triangles seen from `p`, over 4π. It is 1 inside a
927    /// closed, outward-facing mesh, 0 outside it, and in between near holes.
928    ///
929    /// In [`SignMode::Winding`] this is the fast approximation of Barill et
930    /// al. 2018, order 1: a BVH node whose centroid is more than `beta = 8`
931    /// times its radius away from `p` contributes its dipole term, and nearer
932    /// leaves sum their triangles' exact solid angles (Van Oosterom–Strackee).
933    /// It is within 1e-3 of the exact value on the closed test meshes at
934    /// points farther than 1% of the bounding-box diagonal from the surface;
935    /// on a flat terrain the error is larger (about 1.2e-3, tested against
936    /// 2e-3), since the errors of a flat sheet add up. In
937    /// [`SignMode::Pseudonormal`] no moments are stored and every triangle is
938    /// summed exactly, which is slow on large meshes.
939    ///
940    /// Returns NaN when this data is not current for `mesh` and `bvh` (see
941    /// [`SignData::is_current`]). Nothing is allocated.
942    pub fn winding_number(&self, mesh: &Mesh, bvh: &Bvh, p: [f32; 3]) -> f64 {
943        if !self.is_current(mesh, bvh) {
944            return f64::NAN;
945        }
946        self.moments.winding(mesh, bvh, p).0
947    }
948
949    /// Whether the closest feature of `hit` touches a boundary edge of
950    /// `mesh`: a face interior never does, an edge when it is used by one
951    /// triangle only, a vertex when any edge at it is. A point past an open
952    /// rim is signed by the plane of its boundary face, so an antagonist
953    /// vertex just beyond a scan's margin can read as penetration; apps can
954    /// use this to detect that halo. Returns `None` when the mesh's
955    /// adjacency (or, for a vertex, its incidence) is not cached, or `hit`
956    /// is not on an alive triangle.
957    pub fn is_boundary_hit(mesh: &Mesh, hit: &ClosestHit) -> Option<bool> {
958        let adj = mesh.adjacency()?;
959        let t = hit.triangle;
960        if !mesh.is_triangle_alive(t) {
961            return None;
962        }
963        let tri = mesh.triangles()[t as usize];
964        match hit.feature {
965            Feature::Face => Some(false),
966            Feature::Edge(k) => {
967                let c = 3 * t + (u32::from(k) + 2) % 3;
968                Some(adj.edge_kind(c) == EdgeKind::Boundary)
969            }
970            Feature::Vertex(k) => {
971                let inc = mesh.incidence()?;
972                let v = tri[k as usize];
973                for &s in inc.triangles(v) {
974                    let stri = mesh.triangles()[s as usize];
975                    let Some(pos) = stri.iter().position(|&x| x == v) else {
976                        continue;
977                    };
978                    for j in [(pos + 1) % 3, (pos + 2) % 3] {
979                        if adj.edge_kind(3 * s + j as u32) == EdgeKind::Boundary {
980                            return Some(true);
981                        }
982                    }
983                }
984                Some(false)
985            }
986        }
987    }
988
989    /// Whether `p`, whose closest point on `mesh` is `hit`, counts as inside
990    /// (negative distance). `mesh` and `bvh` must be those this data is
991    /// current for. In winding mode the sign-only beta is used (fewer
992    /// triangles summed than [`SignData::winding_number`).
993    pub(crate) fn is_inside(&self, mesh: &Mesh, bvh: &Bvh, p: [f32; 3], hit: &ClosestHit) -> bool {
994        match self.status.mode {
995            SignMode::Winding => self.moments.sign_winding(mesh, bvh, p).0 >= 0.5,
996            _ => self.side(mesh, p, hit) < 0.0,
997        }
998    }
999
1000    /// What the build found out about the mesh, and the resolved mode.
1001    pub fn status(&self) -> SignStatus {
1002        self.status
1003    }
1004
1005    /// The resolved mode: never [`SignMode::Auto`].
1006    pub fn mode(&self) -> SignMode {
1007        self.status.mode
1008    }
1009
1010    /// True when this data was built for `mesh` at its current revision and
1011    /// epoch, and for `bvh` at its current generation.
1012    pub fn is_current(&self, mesh: &Mesh, bvh: &Bvh) -> bool {
1013        mesh.uid() == self.mesh_uid
1014            && bvh.mesh_uid() == self.mesh_uid
1015            && mesh.epoch() == self.epoch
1016            && mesh.revision() == self.revision
1017            && bvh.generation() == self.bvh_generation
1018    }
1019
1020    /// Which side of the surface `p` lies on, given its closest point `hit`:
1021    /// negative behind the surface, positive in front, zero when undecided.
1022    /// `mesh` must be the mesh this data is current for.
1023    pub(crate) fn side(&self, mesh: &Mesh, p: [f32; 3], hit: &ClosestHit) -> f64 {
1024        let q = widen(p);
1025        let t = hit.triangle;
1026        let tri = mesh.triangles()[t as usize];
1027        let n = triangle_normal(corners(mesh, tri));
1028        if n != [0.0; 3] {
1029            return self.side_at(q, widen(hit.point), t, tri, n, hit.feature);
1030        }
1031        // A zero-area triangle: use the nearest non-degenerate feature around it.
1032        let Ok(i) = self.degenerate.binary_search(&t) else {
1033            return 0.0;
1034        };
1035        let range = self.offsets[i] as usize..self.offsets[i + 1] as usize;
1036        let mut best: Option<(f64, TriangleId, [f64; 3], Feature)> = None;
1037        for &s in &self.candidates[range] {
1038            let p = corners(mesh, mesh.triangles()[s as usize]).map(widen);
1039            let (d2, x, f) = closest_on_triangle(q, p);
1040            // Candidates ascend, so a tie keeps the smaller id.
1041            if best.is_none_or(|b| d2 < b.0) {
1042                best = Some((d2, s, x, f));
1043            }
1044        }
1045        let Some((_, s, x, f)) = best else {
1046            return 0.0;
1047        };
1048        let stri = mesh.triangles()[s as usize];
1049        let n = triangle_normal(corners(mesh, stri));
1050        self.side_at(q, x, s, stri, n, f)
1051    }
1052
1053    /// `dot(q − x, n)` with `n` the pseudonormal of `feature` of triangle `t`
1054    /// (vertices `tri`, unnormalized face normal `face`).
1055    fn side_at(
1056        &self,
1057        q: [f64; 3],
1058        x: [f64; 3],
1059        t: TriangleId,
1060        tri: [VertexId; 3],
1061        face: [f64; 3],
1062        feature: Feature,
1063    ) -> f64 {
1064        let n = match feature {
1065            Feature::Face => face,
1066            Feature::Edge(k) => {
1067                // Edge k joins corners k and k + 1, so it is opposite corner k + 2.
1068                widen(self.edge_normals[3 * t as usize + (k as usize + 2) % 3])
1069            }
1070            Feature::Vertex(k) => widen(self.vertex_normals[tri[k as usize] as usize]),
1071        };
1072        dot(sub(q, x), n)
1073    }
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078    use fast_mesh_store::par;
1079
1080    use super::*;
1081    use crate::test_util::*;
1082
1083    fn status_of(mut mesh: Mesh, mode: SignMode) -> Result<SignStatus> {
1084        let bvh = build_bvh(&mesh);
1085        SignData::build(&mut mesh, &bvh, mode).map(|s| s.status())
1086    }
1087
1088    /// [`status_of`], with the volume zeroed: for tests of the topology
1089    /// flags (the volume is tested separately).
1090    fn flags_of(mesh: Mesh, mode: SignMode) -> Result<SignStatus> {
1091        status_of(mesh, mode).map(|mut s| {
1092            s.signed_volume = 0.0;
1093            s
1094        })
1095    }
1096
1097    #[test]
1098    fn refit_matches_full_rebuild() {
1099        // Position-only edits: refitting around the dirty vertices gives
1100        // the same normals, degenerate list and volume as a full rebuild.
1101        for (name, mut mesh, moved, positions) in [
1102            (
1103                "terrain",
1104                terrain(16),
1105                vec![10, 20, 30],
1106                vec![[0.5, 0.5, 1.0], [1.5, 2.5, 0.5], [3.0, 3.0, 2.0]],
1107            ),
1108            ("cube", cube(), vec![0], vec![[-1.2, -1.0, -0.8]]),
1109            (
1110                "torus",
1111                torus(),
1112                vec![5, 100, 200],
1113                vec![[1.1, 0.1, 0.2], [0.9, -0.1, 0.3], [1.0, 0.2, -0.1]],
1114            ),
1115        ] {
1116            let mut bvh = build_bvh(&mesh);
1117            let sign = SignData::build(&mut mesh, &bvh, SignMode::Pseudonormal).unwrap();
1118            let mut tx = mesh.begin_edit();
1119            tx.set_positions(&moved, &positions).unwrap();
1120            let patch = tx.commit().unwrap();
1121            bvh.update(&mut mesh, patch.changes()).unwrap();
1122            let mut refitted = sign.clone();
1123            assert!(
1124                refitted.try_refit(&mesh, &bvh, patch.changes()).unwrap(),
1125                "{name}"
1126            );
1127            let rebuilt = SignData::build(&mut mesh, &bvh, SignMode::Pseudonormal).unwrap();
1128            assert_eq!(fingerprint(&refitted), fingerprint(&rebuilt), "{name}");
1129        }
1130        // A topology edit cannot refit.
1131        let mut mesh = cube();
1132        let mut bvh = build_bvh(&mesh);
1133        let sign = SignData::build(&mut mesh, &bvh, SignMode::Pseudonormal).unwrap();
1134        let mut tx = mesh.begin_edit();
1135        tx.remove_triangles(&[0]).unwrap();
1136        let patch = tx.commit().unwrap();
1137        bvh.update(&mut mesh, patch.changes()).unwrap();
1138        let mut refitted = sign.clone();
1139        assert!(!refitted.try_refit(&mesh, &bvh, patch.changes()).unwrap());
1140    }
1141
1142    #[test]
1143    fn signed_volume_detects_inside_out() {
1144        // A closed, outward mesh has positive volume; flipping every
1145        // triangle (consistently inverted) negates it, so apps can warn.
1146        let closed = status_of(cube(), SignMode::Auto).unwrap();
1147        assert!(closed.closed && closed.consistent_orientation);
1148        assert!(closed.signed_volume > 0.0);
1149        let (p, t) = cube_buffers();
1150        let flipped: Vec<[u32; 3]> = t.iter().map(|tri| [tri[0], tri[2], tri[1]]).collect();
1151        let inside_out = status_of(mesh(&p, &flipped), SignMode::Auto).unwrap();
1152        assert!(inside_out.closed && inside_out.consistent_orientation);
1153        assert!(inside_out.signed_volume < 0.0);
1154        assert!(
1155            (closed.signed_volume + inside_out.signed_volume).abs()
1156                < 1e-6 * closed.signed_volume.abs()
1157        );
1158    }
1159
1160    #[test]
1161    fn status_reports_topology() {
1162        let closed = SignStatus {
1163            mode: SignMode::Pseudonormal,
1164            closed: true,
1165            consistent_orientation: true,
1166            non_manifold_vertices: 0,
1167            degenerate_triangles: 0,
1168            signed_volume: 0.0,
1169        };
1170        assert_eq!(flags_of(cube(), SignMode::Auto), Ok(closed));
1171        assert_eq!(flags_of(cube(), SignMode::Pseudonormal), Ok(closed));
1172        assert_eq!(flags_of(torus(), SignMode::Auto), Ok(closed));
1173
1174        // Open: a cube with its top face removed, and a terrain sheet.
1175        let (p, mut t) = cube_buffers();
1176        t.drain(2..4);
1177        let open = SignStatus {
1178            closed: false,
1179            ..closed
1180        };
1181        assert_eq!(flags_of(mesh(&p, &t), SignMode::Auto), Ok(open));
1182        assert_eq!(flags_of(terrain(4), SignMode::Auto), Ok(open));
1183
1184        // Two cubes meeting at one vertex: closed, one non-manifold vertex.
1185        let (mut p, mut t) = cube_buffers();
1186        let (p2, t2) = cube_buffers();
1187        // Corner 0 of the second cube lands on corner 7 of the first.
1188        p.extend(p2[1..].iter().map(|q| q.map(|c| c + 2.0)));
1189        t.extend(
1190            t2.iter()
1191                .map(|tri| tri.map(|v| if v == 0 { 7 } else { v + 7 })),
1192        );
1193        assert_eq!(
1194            flags_of(mesh(&p, &t), SignMode::Auto),
1195            Ok(SignStatus {
1196                non_manifold_vertices: 1,
1197                ..closed
1198            })
1199        );
1200
1201        // A zero-area triangle with a repeated vertex: it makes edge 0-1 non-manifold.
1202        let (p, mut t) = cube_buffers();
1203        t.push([0, 0, 1]);
1204        assert_eq!(
1205            flags_of(mesh(&p, &t), SignMode::Auto),
1206            Ok(SignStatus {
1207                closed: false,
1208                non_manifold_vertices: 2,
1209                degenerate_triangles: 1,
1210                ..closed
1211            })
1212        );
1213
1214        // A cube with one triangle flipped: open meshes keep pseudonormals,
1215        // closed ones fall back to winding numbers.
1216        let (p, mut t) = cube_buffers();
1217        t[0].swap(1, 2);
1218        let flipped = SignStatus {
1219            consistent_orientation: false,
1220            ..closed
1221        };
1222        assert_eq!(flags_of(mesh(&p, &t), SignMode::Pseudonormal), Ok(flipped));
1223        assert_eq!(
1224            flags_of(mesh(&p, &t), SignMode::Auto),
1225            Ok(SignStatus {
1226                mode: SignMode::Winding,
1227                ..flipped
1228            })
1229        );
1230        t.truncate(11);
1231        assert_eq!(
1232            flags_of(mesh(&p, &t), SignMode::Auto),
1233            Ok(SignStatus {
1234                closed: false,
1235                ..flipped
1236            })
1237        );
1238        assert_eq!(
1239            flags_of(cube(), SignMode::Winding),
1240            Ok(SignStatus {
1241                mode: SignMode::Winding,
1242                ..closed
1243            })
1244        );
1245    }
1246
1247    /// The status and every number the sign data holds, as bits.
1248    fn fingerprint(s: &SignData) -> (SignStatus, Vec<u64>) {
1249        let normals = s.vertex_normals.iter().chain(&s.edge_normals).flatten();
1250        let ids = s.degenerate.iter().chain(&s.offsets).chain(&s.candidates);
1251        let v = normals
1252            .map(|x| u64::from(x.to_bits()))
1253            .chain(ids.map(|&i| u64::from(i)))
1254            .chain(s.moments.bits())
1255            .chain(std::iter::once(s.status.signed_volume.to_bits()))
1256            .collect();
1257        (s.status, v)
1258    }
1259
1260    #[test]
1261    fn sign_build_bit_identical() {
1262        // The forced-sequential run is the code a feature-off build runs.
1263        let mut mesh = terrain(317);
1264        assert!(mesh.triangle_count() >= 200_000);
1265        let bvh = build_bvh(&mesh);
1266        for mode in [SignMode::Pseudonormal, SignMode::Winding] {
1267            let build = |mesh: &mut Mesh| fingerprint(&SignData::build(mesh, &bvh, mode).unwrap());
1268            par::set_force_sequential(true);
1269            let reference = build(&mut mesh);
1270            par::set_force_sequential(false);
1271            assert!(reference.1.len() > 200_000, "{mode:?}: nothing built");
1272            if !par::enabled() {
1273                // Every build below would be this one again.
1274                continue;
1275            }
1276            for k in [1, 2, 8] {
1277                let got = par::with_threads(k, || build(&mut mesh));
1278                assert!(got == reference, "{mode:?}: {k} threads");
1279            }
1280            let before = par::parallel_entries();
1281            assert!(build(&mut mesh) == reference, "{mode:?}: global pool");
1282            assert!(
1283                par::parallel_entries() > before,
1284                "{mode:?}: ran sequentially"
1285            );
1286        }
1287    }
1288
1289    #[test]
1290    fn small_inputs_stay_sequential() {
1291        let before = par::parallel_entries();
1292        for mode in [SignMode::Pseudonormal, SignMode::Winding] {
1293            for mesh in [cube(), icosphere(), torus(), terrain(16)] {
1294                status_of(mesh, mode).unwrap();
1295            }
1296        }
1297        assert_eq!(par::parallel_entries(), before);
1298    }
1299
1300    #[test]
1301    fn is_current_tracks_revision_and_bvh_generation() {
1302        let mut mesh = cube();
1303        let mut bvh = build_bvh(&mesh);
1304        let sign = SignData::build(&mut mesh, &bvh, SignMode::Auto).unwrap();
1305        assert!(sign.is_current(&mesh, &bvh));
1306        assert_eq!(sign.mode(), SignMode::Pseudonormal);
1307
1308        // Another mesh, or its BVH.
1309        let other = cube();
1310        let other_bvh = build_bvh(&other);
1311        assert!(!sign.is_current(&other, &other_bvh));
1312        assert!(!sign.is_current(&mesh, &other_bvh));
1313
1314        // An edit changes the revision; updating the BVH does not help.
1315        let mut tx = mesh.begin_edit();
1316        tx.set_positions(&[0], &[[-1.0, -1.0, -1.5]]).unwrap();
1317        let patch = tx.commit().unwrap();
1318        assert!(!sign.is_current(&mesh, &bvh));
1319        bvh.update(&mut mesh, patch.changes()).unwrap();
1320        assert!(!sign.is_current(&mesh, &bvh));
1321        let sign = SignData::build(&mut mesh, &bvh, SignMode::Auto).unwrap();
1322        assert!(sign.is_current(&mesh, &bvh));
1323
1324        // Restructuring the BVH changes its generation.
1325        bvh.rebuild(&mesh).unwrap();
1326        assert!(!sign.is_current(&mesh, &bvh));
1327        let sign = SignData::build(&mut mesh, &bvh, SignMode::Auto).unwrap();
1328        assert!(sign.is_current(&mesh, &bvh));
1329
1330        // Compaction changes the epoch.
1331        let mut tx = mesh.begin_edit();
1332        tx.remove_triangles(&[0]).unwrap();
1333        tx.commit().unwrap();
1334        mesh.compact().unwrap();
1335        let bvh = build_bvh(&mesh);
1336        assert!(!sign.is_current(&mesh, &bvh));
1337        let sign = SignData::build(&mut mesh, &bvh, SignMode::Auto).unwrap();
1338        assert!(sign.is_current(&mesh, &bvh));
1339    }
1340
1341    /// The meshes and modes of the stepped-build tests: both modes, slivers,
1342    /// a repeated vertex, an inconsistent closed mesh (`Auto` resolves to
1343    /// winding) and dead triangles.
1344    fn stepped_cases() -> Vec<(&'static str, Mesh, SignMode)> {
1345        let (p, mut t) = cube_buffers();
1346        t[0].swap(1, 2);
1347        let flipped = mesh(&p, &t);
1348        let flipped_pn = mesh(&p, &t);
1349        let (p, mut t) = cube_buffers();
1350        t.push([0, 0, 1]);
1351        let repeated = mesh(&p, &t);
1352        // A terrain with a collinear sliver and some triangles removed.
1353        let mut sliver = terrain(24);
1354        let mut tx = sliver.begin_edit();
1355        let v = tx
1356            .add_vertices(&[[0.5, 0.0, terrain_height(0.5, 0.0)]])
1357            .unwrap()[0];
1358        tx.add_triangles(&[[0, v, 1]]).unwrap();
1359        tx.remove_triangles(&[30, 31, 200]).unwrap();
1360        tx.commit().unwrap();
1361        vec![
1362            ("torus, auto", torus(), SignMode::Auto),
1363            ("flipped cube, auto", flipped, SignMode::Auto),
1364            (
1365                "flipped cube, pseudonormal",
1366                flipped_pn,
1367                SignMode::Pseudonormal,
1368            ),
1369            ("repeated vertex", repeated, SignMode::Pseudonormal),
1370            (
1371                "sliver terrain, pseudonormal",
1372                sliver,
1373                SignMode::Pseudonormal,
1374            ),
1375            ("terrain, winding", terrain(20), SignMode::Winding),
1376        ]
1377    }
1378
1379    /// A copy of `m` with nothing cached (no adjacency, no incidence) and
1380    /// the same slots, dead ones included.
1381    fn fresh_copy(m: &Mesh) -> Mesh {
1382        let mut c = mesh(m.positions(), m.triangles());
1383        let dead: Vec<TriangleId> = (0..m.triangle_slots() as TriangleId)
1384            .filter(|&t| !m.is_triangle_alive(t))
1385            .collect();
1386        if !dead.is_empty() {
1387            let mut tx = c.begin_edit();
1388            tx.remove_triangles(&dead).unwrap();
1389            tx.commit().unwrap();
1390        }
1391        c
1392    }
1393
1394    /// Steps `build` to the end with `budget`; returns the number of steps.
1395    fn step_all(build: &mut SignBuild, mesh: &mut Mesh, bvh: &Bvh, budget: usize) -> usize {
1396        let mut steps = 1;
1397        while !build.step(mesh, bvh, budget).unwrap() {
1398            steps += 1;
1399        }
1400        steps
1401    }
1402
1403    #[test]
1404    fn stepped_build_matches_one_call() {
1405        for (name, base, mode) in stepped_cases() {
1406            // The adjacency and incidence are built inside the stepped
1407            // builds: each starts from a fresh copy of the mesh.
1408            let mut m = fresh_copy(&base);
1409            let bvh = build_bvh(&m);
1410            let reference = fingerprint(&SignData::build(&mut m, &bvh, mode).unwrap());
1411            for budget in [1, 2, 7, 100, 5000, usize::MAX] {
1412                let mut m = fresh_copy(&base);
1413                let bvh = build_bvh(&m);
1414                let mut build = SignBuild::new(&m, &bvh, mode).unwrap();
1415                let steps = step_all(&mut build, &mut m, &bvh, budget);
1416                assert!(build.is_done(), "{name}");
1417                let got = fingerprint(&build.finish().unwrap());
1418                assert!(got == reference, "{name}: budget {budget}");
1419                if budget == 1 {
1420                    assert!(steps > m.triangle_slots(), "{name}: {steps} steps of one");
1421                }
1422            }
1423        }
1424    }
1425
1426    #[test]
1427    fn stepped_build_refuses_another_state() {
1428        let mut m = terrain(8);
1429        let bvh = build_bvh(&m);
1430        let mut build = SignBuild::new(&m, &bvh, SignMode::Pseudonormal).unwrap();
1431        assert!(!build.step(&mut m, &bvh, 3).unwrap());
1432        let mut tx = m.begin_edit();
1433        tx.set_positions(&[0], &[[0.0, 0.0, 1.0]]).unwrap();
1434        tx.commit().unwrap();
1435        assert!(!build.is_for(&m, &bvh));
1436        assert!(matches!(
1437            build.step(&mut m, &bvh, 3),
1438            Err(FieldsError::InvalidInput(_))
1439        ));
1440        assert!(build.finish().is_none());
1441        let other = terrain(8);
1442        assert!(matches!(
1443            SignBuild::new(&other, &bvh, SignMode::Auto),
1444            Err(FieldsError::Query(QueryError::MeshMismatch))
1445        ));
1446    }
1447
1448    #[test]
1449    fn stepped_build_bit_identical_in_parallel() {
1450        // Steps long enough to run their passes on the pool, and short ones.
1451        let base = terrain(317);
1452        for mode in [SignMode::Pseudonormal, SignMode::Winding] {
1453            par::set_force_sequential(true);
1454            let mut m = fresh_copy(&base);
1455            let bvh = build_bvh(&m);
1456            let reference = fingerprint(&SignData::build(&mut m, &bvh, mode).unwrap());
1457            par::set_force_sequential(false);
1458            for budget in [3_000, 40_000, 1_000_000] {
1459                for k in [1, 8] {
1460                    let got = par::with_threads(k, || {
1461                        let mut m = fresh_copy(&base);
1462                        let bvh = build_bvh(&m);
1463                        let mut build = SignBuild::new(&m, &bvh, mode).unwrap();
1464                        step_all(&mut build, &mut m, &bvh, budget);
1465                        fingerprint(&build.finish().unwrap())
1466                    });
1467                    assert!(got == reference, "{mode:?}: budget {budget}, {k} threads");
1468                }
1469            }
1470        }
1471    }
1472}