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}