Skip to main content

axiolid_mesh_section_contract/
request.rs

1//! Opt-in section request: faces lying in the cutting plane become regions.
2//!
3//! The legacy [`MeshPlaneSection::section`](crate::MeshPlaneSection::section)
4//! refuses a mesh with a face in the cutting plane. This module adds the
5//! request, the result and the refusal names for
6//! [`MeshPlaneSection::section_request`](crate::MeshPlaneSection::section_request),
7//! which reports such faces as [`InPlaneRegion`]s tagged with the side their
8//! material lies on, kept apart from the cut curves (ADR 0091, #359).
9//!
10//! # Cut curves
11//!
12//! A [`SectionChain`] contains transversal crossings only. A mesh edge lying
13//! in the plane yields a cut segment only when both of its triangles are off
14//! the plane and their third vertices lie on opposite sides. An edge next to
15//! an in-plane triangle is a region boundary, and an edge whose neighbours
16//! all lie on one side only touches the plane; it is counted, not emitted.
17//! A chain can therefore be open and end on a region boundary. Chain end
18//! points and region vertices are projected by the same function, so they are
19//! bit-identical.
20//!
21//! # Sheets
22//!
23//! [`SectionRequest::role`] says what the mesh is. A [`SectionRole::Solid`]
24//! is a closed, consistently wound, outward oriented two-manifold, as for the
25//! legacy method. A [`SectionRole::Sheet`] is an open surface (terrain, a
26//! surface body): boundary edges are allowed, degenerate, non-manifold and
27//! inconsistently wound input is refused. A cut chain of a sheet can end on
28//! the mesh boundary ([`ChainEnd::MeshBoundary`]). The side of a region of a
29//! sheet is the side its face's back lies on. The evidence reports the role
30//! and the number of boundary edges.
31//!
32//! # In-plane regions
33//!
34//! Faces lying in the plane may overlap, and faces of different shells may
35//! meet in it. Let `Up` be the union of the in-plane faces whose material lies
36//! below the plane ([`InPlaneSide::Below`]) and `Down` the union of the others.
37//! A point of the plane is `Below` when it is in `Up` and not in `Down`,
38//! `Above` when it is in `Down` and not in `Up`, and `Both` when it is in
39//! both: a region of the plane with material on both sides. A region is a
40//! maximal edge-connected part of one class, so faces of one side that overlap
41//! or only share a boundary segment form one region, and the regions depend
42//! on the covered area, not on how it is triangulated. A provider that does
43//! not merge may refuse overlapping faces by name
44//! ([`REFUSED_OVERLAPPING_IN_PLANE_FACES`],
45//! [`REFUSED_IN_PLANE_FACES_ON_BOTH_SIDES`]) instead (ADR 0093, #366).
46//!
47//! # Output invariants
48//!
49//! All points are finite and in the plane frame. A closed chain has at least
50//! three points, starts at its lexicographically
51//! smallest point (`x`, then `y`, by `total_cmp`) and has no adjacent
52//! duplicates, wrap-around included. A closed chain of a solid keeps the
53//! material on its left: counter-clockwise around a cut, clockwise around a
54//! hole. An open chain has at least two points,
55//! no adjacent duplicates, and `points[0] < points[last]`. Chains are sorted
56//! by their point sequences, a closed chain before an open one at a tie.
57//! A region's outer ring is counter-clockwise and its holes are clockwise;
58//! every ring has at least three points and starts at its lexicographically
59//! smallest point. Holes are sorted by point sequence, regions by
60//! `(side, outer)`. Rings may touch at vertices but never cross, and regions
61//! have pairwise disjoint interiors. Where a hole or a footprint touches the
62//! outer boundary at a vertex, the region has one weakly simple outer ring that
63//! visits that vertex twice, and no hole. A self-intersecting mesh is not
64//! certified (ADR 0033). The registry does not check ring orientation: a
65//! floating-point area sign is unreliable far from the origin (#274); the
66//! conformance suite checks it on fixtures.
67
68use axiolid_core::{Frame3, Point2};
69
70use crate::contract::{SectionLimits, SectionSource};
71use crate::cut::{CutRegion, CutRegionLimits};
72
73/// Refusal detail: in-plane faces of one side overlap each other.
74pub const REFUSED_OVERLAPPING_IN_PLANE_FACES: &str = "overlapping in-plane faces of one side";
75/// Refusal detail: in-plane faces of opposite sides overlap each other.
76pub const REFUSED_IN_PLANE_FACES_ON_BOTH_SIDES: &str = "in-plane faces of opposite sides overlap";
77/// Refusal detail: more than two cut segments meet at one mesh vertex.
78pub const REFUSED_SADDLE_VERTEX: &str = "section through a saddle vertex";
79
80/// Hard bounds on the in-plane region work of one request.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[non_exhaustive]
83pub struct RegionLimits {
84    /// Maximum output regions.
85    pub max_regions: usize,
86    /// Maximum vertices across all output region rings.
87    pub max_region_vertices: usize,
88    /// Maximum candidate pairs examined for the in-plane faces: overlap tests
89    /// or merging, and the containment of holes.
90    pub max_in_plane_pairs: usize,
91}
92
93impl RegionLimits {
94    /// Construct explicit region limits.
95    pub const fn new(
96        max_regions: usize,
97        max_region_vertices: usize,
98        max_in_plane_pairs: usize,
99    ) -> Self {
100        Self {
101            max_regions,
102            max_region_vertices,
103            max_in_plane_pairs,
104        }
105    }
106}
107
108/// What the sectioned mesh is.
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
110#[non_exhaustive]
111pub enum SectionRole {
112    /// A closed, consistently wound, outward oriented two-manifold. A closed
113    /// chain keeps the solid on its left.
114    Solid,
115    /// An open or closed surface without volume: boundary edges are allowed.
116    /// A chain can end on the mesh boundary.
117    Sheet,
118}
119
120/// One section request that reports in-plane faces as regions.
121///
122/// `limits.max_contours` bounds the chains and `limits.max_output_vertices`
123/// bounds the chain points.
124#[derive(Debug, Clone, Copy, PartialEq)]
125#[non_exhaustive]
126pub struct SectionRequest {
127    /// Right-handed orthonormal mapping from local `(x,y,0)` to model space.
128    pub frame: Frame3,
129    /// Source, chain and chain-point bounds.
130    pub limits: SectionLimits,
131    /// Region bounds.
132    pub region_limits: RegionLimits,
133    /// What the mesh is; [`SectionRole::Solid`] unless set.
134    pub role: SectionRole,
135    /// Cut-region bounds; `None` unless set, which requests no cut regions.
136    pub cut_regions: Option<CutRegionLimits>,
137}
138
139impl SectionRequest {
140    /// Construct a request for a [`SectionRole::Solid`].
141    pub const fn new(frame: Frame3, limits: SectionLimits, region_limits: RegionLimits) -> Self {
142        Self {
143            frame,
144            limits,
145            region_limits,
146            role: SectionRole::Solid,
147            cut_regions: None,
148        }
149    }
150
151    /// Set what the mesh is.
152    #[must_use]
153    pub const fn with_role(mut self, role: SectionRole) -> Self {
154        self.role = role;
155        self
156    }
157
158    /// Also report the cut as closed regions (ADR 0092).
159    #[must_use]
160    pub const fn with_cut_regions(mut self, limits: CutRegionLimits) -> Self {
161        self.cut_regions = Some(limits);
162        self
163    }
164}
165
166/// What an open chain ends on.
167#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
168#[non_exhaustive]
169pub enum ChainEnd {
170    /// The end lies on the boundary of an in-plane region.
171    InPlaneRegion,
172    /// The end lies on the boundary of the mesh, which is a sheet.
173    MeshBoundary,
174}
175
176/// One plane-local cut polyline: closed, or open and ending on a region.
177///
178/// A closed chain's terminal point is implicit and must not duplicate the
179/// first point.
180#[derive(Debug, Clone, PartialEq)]
181#[non_exhaustive]
182pub struct SectionChain {
183    /// Plane-local points in traversal order.
184    pub points: Vec<Point2>,
185    /// What the first and last point end on; `None` for a closed chain.
186    pub ends: Option<[ChainEnd; 2]>,
187}
188
189impl SectionChain {
190    /// Construct a closed chain.
191    pub fn closed(points: Vec<Point2>) -> Self {
192        Self { points, ends: None }
193    }
194
195    /// Construct an open chain from what its first and last point end on.
196    pub fn open(points: Vec<Point2>, start: ChainEnd, end: ChainEnd) -> Self {
197        Self {
198            points,
199            ends: Some([start, end]),
200        }
201    }
202
203    /// Whether the last point connects back to the first.
204    pub const fn is_closed(&self) -> bool {
205        self.ends.is_none()
206    }
207}
208
209/// The side of the cutting plane a face's material lies on.
210///
211/// Declaration order is the sort order.
212#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
213#[non_exhaustive]
214pub enum InPlaneSide {
215    /// The face normal agrees with the frame's `+z`, so the material (for a
216    /// sheet, the back of the face) lies on the frame's `-z` side.
217    Below,
218    /// The face normal opposes the frame's `+z`; the material lies on `+z`.
219    Above,
220    /// Faces of both facings coincide: the plane has material on both sides
221    /// here. With cut regions requested, every `Both` region is a cut region
222    /// (see [`CutRegion`]).
223    Both,
224}
225
226/// A planar region of the section lying in the cutting plane.
227#[derive(Debug, Clone, PartialEq)]
228#[non_exhaustive]
229pub struct InPlaneRegion {
230    /// The side of the plane the material lies on.
231    pub side: InPlaneSide,
232    /// Counter-clockwise outer ring.
233    pub outer: Vec<Point2>,
234    /// Clockwise hole rings.
235    pub holes: Vec<Vec<Point2>>,
236}
237
238impl InPlaneRegion {
239    /// Construct a region.
240    pub fn new(side: InPlaneSide, outer: Vec<Point2>, holes: Vec<Vec<Point2>>) -> Self {
241        Self { side, outer, holes }
242    }
243}
244
245/// Auditable counts for one request-based section.
246#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247#[non_exhaustive]
248pub struct PlaneSectionEvidence {
249    /// Approximation source.
250    pub source: SectionSource,
251    /// Source triangles inspected.
252    pub source_triangles: usize,
253    /// What the mesh was sectioned as.
254    pub role: SectionRole,
255    /// Mesh edges used by one triangle only; zero for a solid.
256    pub boundary_edges: usize,
257    /// Source triangles lying in the plane.
258    pub in_plane_triangles: usize,
259    /// Plane edges that only touch the plane: nothing is emitted for them.
260    pub touching_edges: usize,
261    /// Candidate pairs examined for the in-plane faces: overlap tests, or the
262    /// segment, piece-triangle and hole containment pairs of a merge.
263    pub in_plane_pairs: usize,
264    /// Distinct points, other than source vertices, where in-plane source
265    /// edges cross.
266    pub in_plane_crossings: usize,
267    /// Output closed chains.
268    pub closed_chains: usize,
269    /// Output open chains.
270    pub open_chains: usize,
271    /// Points across all output chains.
272    pub chain_vertices: usize,
273    /// Output regions.
274    pub regions: usize,
275    /// Points across all output region rings.
276    pub region_vertices: usize,
277    /// Output cut regions.
278    pub cut_regions: usize,
279    /// Points across all output cut region rings.
280    pub cut_region_vertices: usize,
281    /// Candidate pairs compared while nesting the cut rings.
282    pub cut_pairs: usize,
283}
284
285impl PlaneSectionEvidence {
286    /// Record evidence for a section derived from the input mesh; every
287    /// other count starts at zero.
288    pub const fn input_mesh(source_triangles: usize) -> Self {
289        Self {
290            source: SectionSource::InputMesh,
291            source_triangles,
292            role: SectionRole::Solid,
293            boundary_edges: 0,
294            in_plane_triangles: 0,
295            touching_edges: 0,
296            in_plane_pairs: 0,
297            in_plane_crossings: 0,
298            closed_chains: 0,
299            open_chains: 0,
300            chain_vertices: 0,
301            regions: 0,
302            region_vertices: 0,
303            cut_regions: 0,
304            cut_region_vertices: 0,
305            cut_pairs: 0,
306        }
307    }
308
309    /// Record the counts taken from the source mesh.
310    pub const fn with_source_counts(
311        mut self,
312        in_plane_triangles: usize,
313        touching_edges: usize,
314        in_plane_pairs: usize,
315    ) -> Self {
316        self.in_plane_triangles = in_plane_triangles;
317        self.touching_edges = touching_edges;
318        self.in_plane_pairs = in_plane_pairs;
319        self
320    }
321
322    /// Record the distinct points, other than source vertices, where
323    /// in-plane source edges cross.
324    pub const fn with_in_plane_crossings(mut self, in_plane_crossings: usize) -> Self {
325        self.in_plane_crossings = in_plane_crossings;
326        self
327    }
328
329    /// Record the role and the number of boundary edges of the source.
330    pub const fn with_role(mut self, role: SectionRole, boundary_edges: usize) -> Self {
331        self.role = role;
332        self.boundary_edges = boundary_edges;
333        self
334    }
335
336    /// Record the counts of the output.
337    pub fn with_output(mut self, chains: &[SectionChain], regions: &[InPlaneRegion]) -> Self {
338        self.closed_chains = chains.iter().filter(|chain| chain.is_closed()).count();
339        self.open_chains = chains.len() - self.closed_chains;
340        self.chain_vertices = chains.iter().map(|chain| chain.points.len()).sum();
341        self.regions = regions.len();
342        self.region_vertices = regions
343            .iter()
344            .map(|region| region.outer.len() + region.holes.iter().map(Vec::len).sum::<usize>())
345            .sum();
346        self
347    }
348
349    /// Record the candidate pairs compared while nesting the cut rings.
350    pub const fn with_cut_pairs(mut self, cut_pairs: usize) -> Self {
351        self.cut_pairs = cut_pairs;
352        self
353    }
354
355    /// Record the counts of the output cut regions.
356    pub fn with_cut_output(mut self, cut_regions: &[CutRegion]) -> Self {
357        self.cut_regions = cut_regions.len();
358        self.cut_region_vertices = cut_regions
359            .iter()
360            .map(|region| region.outer.len() + region.holes.iter().map(Vec::len).sum::<usize>())
361            .sum();
362        self
363    }
364
365    /// Whether this result came from the discrete input mesh.
366    pub const fn is_derived_from_input_mesh(self) -> bool {
367        matches!(self.source, SectionSource::InputMesh)
368    }
369}
370
371/// Cut chains and in-plane regions plus evidence.
372#[derive(Debug, Clone, PartialEq)]
373#[non_exhaustive]
374pub struct PlaneSection {
375    /// Right-handed orthonormal mapping from local `(x,y,0)` to model space.
376    pub frame: Frame3,
377    /// Transversal cut chains. Empty means no crossing.
378    pub chains: Vec<SectionChain>,
379    /// Regions of faces lying in the plane.
380    pub regions: Vec<InPlaneRegion>,
381    /// Closed regions of the cut; `None` when none were requested.
382    pub cut_regions: Option<Vec<CutRegion>>,
383    /// Source and output counts.
384    pub evidence: PlaneSectionEvidence,
385}
386
387impl PlaneSection {
388    /// Construct a provider result. Registries validate it before returning it.
389    pub fn new(
390        frame: Frame3,
391        chains: Vec<SectionChain>,
392        regions: Vec<InPlaneRegion>,
393        evidence: PlaneSectionEvidence,
394    ) -> Self {
395        Self {
396            frame,
397            chains,
398            regions,
399            cut_regions: None,
400            evidence,
401        }
402    }
403
404    /// Attach the cut regions of a request that asked for them.
405    #[must_use]
406    pub fn with_cut_regions(mut self, cut_regions: Vec<CutRegion>) -> Self {
407        self.cut_regions = Some(cut_regions);
408        self
409    }
410}