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}