Skip to main content

axiolid_mesh_hidden_lines_contract/
faces.rs

1//! Portable visible-face-region provider contract (ADR 0094).
2//!
3//! The request is a hidden-line request without edge selection: the same
4//! meshes, view, crop and error type, plus per-triangle [`FaceGroup`] ids.
5//! The outcome lists, per mesh and source, the regions of the image where a
6//! face of that source is frontmost. Every decision is exact and no tolerance
7//! enters; the contract fixes what the decision means:
8//!
9//! - a point of the image belongs to source `(mesh, source)` when some kept
10//!   surface of that source covers it and no kept surface lies strictly in
11//!   front of that surface there;
12//! - a kept surface is a triangle of nonzero image area inside
13//!   `far <= W <= near` and the crop box, or the cap of a [`MeshRole::Solid`](crate::MeshRole::Solid)
14//!   cut by `near` ([`FaceSource::Cap`], a surface at depth `near`);
15//! - equal depth never occludes, so sources tied over an area each get it
16//!   and the evidence counts the tied sides;
17//! - a triangle seen edge-on covers no area: it makes no region and never
18//!   occludes a region, and is counted in the evidence.
19//!
20//! A region is a maximal edge-connected open set of one source, given by its
21//! closure. Its outer ring runs counter-clockwise and each hole clockwise;
22//! every ring starts at its lexicographically smallest point and does not
23//! repeat it at the end.
24//!
25//! The regions are the exact regions snap-rounded to the binary64 grid (ADR
26//! 0100). Where no exact edge passes through the rounding cell of another
27//! ring vertex, every vertex is the exact point rounded once to the nearest
28//! binary64, as for [`HiddenLineSegment`](crate::HiddenLineSegment)
29//! endpoints. Otherwise the edge is routed through that vertex, and a part
30//! of a region whose area vanishes under rounding is dropped and counted in
31//! the evidence, never refused. Rings are weakly simple: they may touch at a
32//! vertex, never cross and never run along each other, and no ring vertex
33//! lies inside the edge of any ring. A ring vertex is kept unless exactly two
34//! boundary pieces of the snapped boundary meet there in a straight line, so
35//! neighbouring regions share bit-identical vertices.
36
37use axiolid_contracts::{
38    Backend, CancellationGranularity, DisplacementBound, ExecutionOptions, ScratchRequirement,
39};
40use axiolid_core::{Aabb2, Point2};
41
42use crate::{HiddenLineError, HiddenLineMesh, HiddenLineSource, MeshId, View, ViewProjection};
43
44/// Caller-chosen id of a set of triangles that share one surface pattern.
45///
46/// A material id and a planar face are both encodings the caller picks; the
47/// kernel infers planes only on request, as [`FaceGroups::Planar`].
48#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct FaceGroup(pub u32);
50
51/// The face groups of one mesh.
52#[non_exhaustive]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub enum FaceGroups<'a> {
55    /// Every triangle is in `FaceGroup(0)`.
56    Single,
57    /// One id per triangle; the length equals the triangle count.
58    PerTriangle(&'a [FaceGroup]),
59    /// The planar patches of the mesh (ADR 0106). Two triangles are in one
60    /// group when a chain of triangles joins them in which every two
61    /// consecutive triangles share an edge that has exactly these two
62    /// triangles and whose model-space normals are parallel and point the
63    /// same way. Parallel and same-facing is decided exactly as
64    /// `n1 × n2 = 0` in all three components and `n1 · n2 > 0`; no tolerance
65    /// enters. A group's id is `FaceGroup(i)` for the smallest triangle
66    /// index `i` in it, so it does not depend on the view or on the winding
67    /// of the whole mesh. Providers resolve the groups before use.
68    Planar,
69}
70
71impl FaceGroups<'_> {
72    /// The group of triangle `triangle`.
73    ///
74    /// # Panics
75    ///
76    /// Panics for `PerTriangle` when `triangle` is out of range; providers
77    /// check the length first. Panics for `Planar`, whose groups depend on
78    /// the geometry; providers resolve it first.
79    pub fn of(&self, triangle: usize) -> FaceGroup {
80        match self {
81            Self::Single => FaceGroup(0),
82            Self::PerTriangle(groups) => groups[triangle],
83            Self::Planar => panic!("planar face groups are resolved from the mesh"),
84        }
85    }
86}
87
88/// What a visible region is a region of.
89///
90/// The derived order puts every `Group` before `Cap`.
91#[non_exhaustive]
92#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
93pub enum FaceSource {
94    /// The triangles of one group.
95    Group(FaceGroup),
96    /// The cap of a [`MeshRole::Solid`](crate::MeshRole::Solid) cut by the near plane: a surface at
97    /// depth `near` covering the image points whose point in that plane lies
98    /// inside the solid. A drawing fills it with the cut pattern.
99    Cap,
100}
101
102/// Hard bounds for one visible-face request.
103#[non_exhaustive]
104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub struct VisibleFaceLimits {
106    /// Maximum input meshes.
107    pub max_meshes: usize,
108    /// Maximum source positions, summed over all meshes.
109    pub max_source_vertices: usize,
110    /// Maximum source triangles, summed over all meshes.
111    pub max_source_triangles: usize,
112    /// Maximum candidate pairs examined, over all pair work.
113    pub max_candidate_pairs: usize,
114    /// Maximum output regions.
115    pub max_regions: usize,
116    /// Maximum ring vertices, summed over all output regions.
117    pub max_region_vertices: usize,
118}
119
120impl VisibleFaceLimits {
121    /// Construct explicit input and output work limits.
122    pub const fn new(
123        max_meshes: usize,
124        max_source_vertices: usize,
125        max_source_triangles: usize,
126        max_candidate_pairs: usize,
127        max_regions: usize,
128        max_region_vertices: usize,
129    ) -> Self {
130        Self {
131            max_meshes,
132            max_source_vertices,
133            max_source_triangles,
134            max_candidate_pairs,
135            max_regions,
136            max_region_vertices,
137        }
138    }
139}
140
141/// One visible-face request.
142///
143/// All meshes occlude each other and themselves. A `region` crops the image
144/// (closed box in image coordinates); geometry outside it neither makes a
145/// region nor, being outside the image, occludes anything inside.
146#[non_exhaustive]
147#[derive(Debug, Clone)]
148pub struct VisibleFaceRequest<'a> {
149    /// Input meshes; their order orders the output.
150    pub meshes: &'a [HiddenLineMesh<'a>],
151    /// The view.
152    pub view: View,
153    /// Optional image crop.
154    pub region: Option<Aabb2>,
155    /// Work limits.
156    pub limits: VisibleFaceLimits,
157}
158
159impl<'a> VisibleFaceRequest<'a> {
160    /// Find the visible faces of `meshes` in `view` without a crop.
161    pub const fn new(
162        meshes: &'a [HiddenLineMesh<'a>],
163        view: View,
164        limits: VisibleFaceLimits,
165    ) -> Self {
166        Self {
167            meshes,
168            view,
169            region: None,
170            limits,
171        }
172    }
173
174    /// Crop the image to a closed box.
175    pub const fn with_region(mut self, region: Aabb2) -> Self {
176        self.region = Some(region);
177        self
178    }
179}
180
181/// A maximal region of the image where one source is frontmost.
182///
183/// Rings are counter-clockwise (outer) and clockwise (holes), start at their
184/// lexicographically smallest point and do not repeat it.
185#[non_exhaustive]
186#[derive(Debug, Clone, PartialEq)]
187pub struct VisibleRegion {
188    /// The mesh the region belongs to.
189    pub mesh: MeshId,
190    /// The group or cap the region belongs to.
191    pub source: FaceSource,
192    /// Outer boundary.
193    pub outer: Vec<Point2>,
194    /// Holes, sorted lexicographically by their points.
195    pub holes: Vec<Vec<Point2>>,
196    /// Indices of the mesh's triangles that are frontmost somewhere in an
197    /// exact region this region was snapped from, ascending. A region that
198    /// snapping split at a pinch lists the whole set in each part. Empty for
199    /// [`FaceSource::Cap`].
200    pub triangles: Vec<u32>,
201}
202
203impl VisibleRegion {
204    /// Construct a region of `source` of `mesh`.
205    pub fn new(
206        mesh: MeshId,
207        source: FaceSource,
208        outer: Vec<Point2>,
209        holes: Vec<Vec<Point2>>,
210        triangles: Vec<u32>,
211    ) -> Self {
212        Self {
213            mesh,
214            source,
215            outer,
216            holes,
217            triangles,
218        }
219    }
220}
221
222/// Auditable counts for one visible-face operation.
223#[non_exhaustive]
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225pub struct VisibleFaceEvidence {
226    /// Approximation source.
227    pub source: HiddenLineSource,
228    /// Input meshes.
229    pub source_meshes: usize,
230    /// Input triangles, summed over all meshes.
231    pub source_triangles: usize,
232    /// Triangles seen edge-on: they cover no area and make no region.
233    pub edge_on_triangles: usize,
234    /// Triangles removed entirely by depth clipping or the crop.
235    pub removed_triangles: usize,
236    /// Boundary pieces of the arrangement that come from triangle edges and
237    /// cap outlines.
238    pub boundary_segments: usize,
239    /// Input segments of the arrangement where a triangle of one surface cuts
240    /// another, before the arrangement cuts them into pieces.
241    pub intersection_segments: usize,
242    /// Crossings of segments in the image.
243    pub crossings: usize,
244    /// Pieces of the arrangement.
245    pub pieces: usize,
246    /// Sides of arrangement pieces that two or more sources share by equal
247    /// depth, interior pieces included. The count depends on how the
248    /// arrangement cuts the plane, so only whether it is zero is a property
249    /// of the scene.
250    pub tied_sides: usize,
251    /// Candidate pairs examined, over all pair work.
252    pub candidate_pairs: usize,
253    /// Decisions the fast filter could not settle and the exact tier did.
254    /// This count may differ between two otherwise identical requests; it is
255    /// not part of the result.
256    pub exact_escalations: usize,
257    /// Output regions.
258    pub regions: usize,
259    /// Output regions whose source is [`FaceSource::Cap`].
260    pub cap_regions: usize,
261    /// Bound, in the maximum norm of the image, on how far snap rounding
262    /// moved region boundaries (ADR 0100): an image point farther than this
263    /// from the exact boundary of a source lies in that source's regions
264    /// exactly when it lies in its exact regions. Zero when every ring
265    /// vertex is a binary64 point and no edge was rerouted.
266    pub snap_displacement: DisplacementBound,
267    /// Exact ring edges routed through the rounded point of another ring
268    /// vertex whose rounding cell they pass through.
269    pub rerouted_edges: usize,
270    /// Exact regions whose area collapsed under snap rounding; no region is
271    /// returned for them.
272    pub collapsed_regions: usize,
273    /// Holes of returned regions that collapsed under snap rounding.
274    pub collapsed_holes: usize,
275}
276
277impl VisibleFaceEvidence {
278    /// Evidence for a result derived from the input meshes; the remaining
279    /// counts start at zero for the provider to fill in.
280    pub const fn input_mesh(source_meshes: usize, source_triangles: usize) -> Self {
281        Self {
282            source: HiddenLineSource::InputMesh,
283            source_meshes,
284            source_triangles,
285            edge_on_triangles: 0,
286            removed_triangles: 0,
287            boundary_segments: 0,
288            intersection_segments: 0,
289            crossings: 0,
290            pieces: 0,
291            tied_sides: 0,
292            candidate_pairs: 0,
293            exact_escalations: 0,
294            regions: 0,
295            cap_regions: 0,
296            snap_displacement: DisplacementBound::ZERO,
297            rerouted_edges: 0,
298            collapsed_regions: 0,
299            collapsed_holes: 0,
300        }
301    }
302
303    /// Whether this result came from the discrete input meshes.
304    pub const fn is_derived_from_input_mesh(self) -> bool {
305        matches!(self.source, HiddenLineSource::InputMesh)
306    }
307}
308
309/// Visible regions plus evidence.
310///
311/// Regions are ordered by the mesh's position in the request, then by
312/// `source`, then by the outer ring's points, lexicographically.
313#[non_exhaustive]
314#[derive(Debug, Clone, PartialEq)]
315pub struct VisibleFaceOutcome {
316    /// The view the regions are expressed in, unchanged.
317    pub view: View,
318    /// The regions.
319    pub regions: Vec<VisibleRegion>,
320    /// Counts.
321    pub evidence: VisibleFaceEvidence,
322}
323
324impl VisibleFaceOutcome {
325    /// Construct a provider result. Registries validate it before returning it.
326    pub fn new(view: View, regions: Vec<VisibleRegion>, evidence: VisibleFaceEvidence) -> Self {
327        Self {
328            view,
329            regions,
330            evidence,
331        }
332    }
333}
334
335/// Provider for deterministic visible-face regions of triangle meshes.
336///
337/// Provider obligations beyond the types:
338///
339/// - a [`FaceSource::Cap`] region exists only for a [`MeshRole::Solid`](crate::MeshRole::Solid) that
340///   `view.near` cuts, and has no `triangles`;
341/// - regions of one source never overlap; regions of different sources
342///   overlap only where they tie in depth;
343/// - flipping every triangle gives identical regions and identical evidence
344///   except `exact_escalations`;
345/// - the regions are the exact regions snap-rounded to the binary64 grid (ADR
346///   0100), with the rings of [`VisibleRegion`] and the evidence of
347///   [`VisibleFaceEvidence`]; a region is never refused because rounding
348///   collapses it, and where no edge was rerouted the ring vertices are the
349///   exact points rounded once to nearest;
350/// - the tolerance of `options` applies to mesh audit and frame validation
351///   only, never to a visibility decision;
352/// - the same input refusals as [`MeshHiddenLines`](crate::MeshHiddenLines),
353///   and a `PerTriangle` group slice whose length differs from the triangle
354///   count is `InvalidInput` naming the mesh; no substitute result is
355///   returned;
356/// - [`FaceGroups::Planar`] is resolved by the provider as documented there,
357///   exactly, with polled cancellation and a memory-budget refusal, and the
358///   result equals the `PerTriangle` request with the resolved groups, except
359///   `exact_escalations`;
360/// - output storage is outside the scratch budget.
361pub trait MeshVisibleFaces: Backend {
362    /// Whether the provider computes regions for views of this projection.
363    /// The default is `true` for [`ViewProjection::Orthographic`] only; see
364    /// [`MeshHiddenLines::supports_projection`](crate::MeshHiddenLines::supports_projection)
365    /// (ADR 0107).
366    fn supports_projection(&self, projection: ViewProjection) -> bool {
367        projection == ViewProjection::Orthographic
368    }
369
370    /// Scratch needed beyond inputs and result.
371    fn scratch_requirement(&self) -> ScratchRequirement {
372        ScratchRequirement::Unbounded
373    }
374
375    /// How finely the provider polls cancellation.
376    fn cancellation_granularity(&self) -> CancellationGranularity {
377        CancellationGranularity::None
378    }
379
380    /// Find the visible face regions of `request.meshes` in `request.view`.
381    /// Implementations must enforce `request.limits` before growing output.
382    fn visible_faces(
383        &self,
384        request: &VisibleFaceRequest<'_>,
385        options: &ExecutionOptions,
386    ) -> Result<VisibleFaceOutcome, HiddenLineError>;
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use crate::MeshRole;
393    use axiolid_mesh::TriMesh;
394
395    #[test]
396    fn single_group_is_zero_for_every_triangle() {
397        assert_eq!(FaceGroups::Single.of(0), FaceGroup(0));
398        assert_eq!(FaceGroups::Single.of(41), FaceGroup(0));
399    }
400
401    #[test]
402    fn per_triangle_groups_are_indexed() {
403        let ids = [FaceGroup(5), FaceGroup(2)];
404        let groups = FaceGroups::PerTriangle(&ids);
405        assert_eq!(groups.of(0), FaceGroup(5));
406        assert_eq!(groups.of(1), FaceGroup(2));
407    }
408
409    #[test]
410    fn mesh_defaults_to_single_and_with_groups_replaces_it() {
411        let mesh = TriMesh::default();
412        let m = HiddenLineMesh::new(MeshId(1), MeshRole::Sheet, &mesh);
413        assert_eq!(m.groups, FaceGroups::Single);
414        let ids = [FaceGroup(3)];
415        let m = m.with_groups(&ids);
416        assert_eq!(m.groups, FaceGroups::PerTriangle(&ids));
417    }
418
419    #[test]
420    fn with_planar_groups_replaces_the_groups() {
421        let mesh = TriMesh::default();
422        let m = HiddenLineMesh::new(MeshId(1), MeshRole::Sheet, &mesh).with_planar_groups();
423        assert_eq!(m.groups, FaceGroups::Planar);
424    }
425
426    #[test]
427    #[should_panic(expected = "planar face groups")]
428    fn planar_groups_are_not_indexable() {
429        let _ = FaceGroups::Planar.of(0);
430    }
431
432    #[test]
433    fn groups_order_before_the_cap() {
434        let mut sources = [
435            FaceSource::Cap,
436            FaceSource::Group(FaceGroup(9)),
437            FaceSource::Group(FaceGroup(1)),
438        ];
439        sources.sort();
440        assert_eq!(
441            sources,
442            [
443                FaceSource::Group(FaceGroup(1)),
444                FaceSource::Group(FaceGroup(9)),
445                FaceSource::Cap
446            ]
447        );
448    }
449
450    #[test]
451    fn input_mesh_evidence_starts_at_zero() {
452        let e = VisibleFaceEvidence::input_mesh(2, 12);
453        assert!(e.is_derived_from_input_mesh());
454        assert_eq!((e.source_meshes, e.source_triangles), (2, 12));
455        let counts = [
456            e.edge_on_triangles,
457            e.removed_triangles,
458            e.boundary_segments,
459            e.intersection_segments,
460            e.crossings,
461            e.pieces,
462            e.tied_sides,
463            e.candidate_pairs,
464            e.exact_escalations,
465            e.regions,
466            e.cap_regions,
467            e.rerouted_edges,
468            e.collapsed_regions,
469            e.collapsed_holes,
470        ];
471        assert!(counts.iter().all(|&c| c == 0));
472        assert_eq!(e.snap_displacement, DisplacementBound::ZERO);
473    }
474}