Skip to main content

axiolid_mesh_hidden_lines_contract/
contract.rs

1//! Portable hidden-line provider contract: the views, the request and the
2//! drawing a provider returns.
3
4use axiolid_contracts::{
5    Backend, CancellationGranularity, ExecutionOptions, GeomError, ScratchRequirement,
6};
7use axiolid_core::{Aabb2, Frame3, Point2};
8use axiolid_mesh::TriMesh;
9
10use crate::{FaceGroup, FaceGroups, HiddenLineError};
11
12/// Caller-chosen identity of one input mesh, echoed in every segment and
13/// refusal that concerns it. Ids must be unique within a request.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
15pub struct MeshId(pub u64);
16
17/// How an input mesh occludes and whether it is capped by a near plane.
18#[non_exhaustive]
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum MeshRole {
21    /// Closed, consistently wound two-manifold. A near clipping plane that
22    /// cuts it (some vertex beyond the plane, `W > near`, and some not)
23    /// leaves a cap that occludes: a point with `W < near`, farther from the
24    /// viewer than the plane, is hidden by the cap when the point of the plane
25    /// above it is inside the solid. A face of the solid in the near plane is
26    /// no special case: it hides what lies below it itself.
27    Solid,
28    /// Edge-manifold with consistent winding, open or closed. Never capped.
29    Sheet,
30}
31
32/// One input mesh with its identity and role.
33#[non_exhaustive]
34#[derive(Debug, Clone, Copy)]
35pub struct HiddenLineMesh<'a> {
36    /// Identity echoed in the outcome.
37    pub id: MeshId,
38    /// Occlusion role.
39    pub role: MeshRole,
40    /// Geometry. Winding does not change the outcome.
41    pub mesh: &'a TriMesh,
42    /// Face groups, read by `MeshVisibleFaces` and, when
43    /// [`EdgeSelection::group_boundary`] is set, by `MeshHiddenLines`.
44    pub groups: FaceGroups<'a>,
45}
46
47impl<'a> HiddenLineMesh<'a> {
48    /// Bind a mesh to an id and a role; every triangle is in
49    /// [`FaceGroup`]`(0)`.
50    pub const fn new(id: MeshId, role: MeshRole, mesh: &'a TriMesh) -> Self {
51        Self {
52            id,
53            role,
54            mesh,
55            groups: FaceGroups::Single,
56        }
57    }
58
59    /// Give every triangle its own group id; `groups.len()` must equal the
60    /// mesh's triangle count, which a provider checks.
61    pub const fn with_groups(mut self, groups: &'a [FaceGroup]) -> Self {
62        self.groups = FaceGroups::PerTriangle(groups);
63        self
64    }
65
66    /// Group the triangles into planar patches ([`FaceGroups::Planar`]).
67    pub const fn with_planar_groups(mut self) -> Self {
68        self.groups = FaceGroups::Planar;
69        self
70    }
71}
72
73/// How a view maps model points to the image.
74///
75/// [`Orthographic`](Self::Orthographic) is the default. The in-tree hidden-line
76/// providers draw both projections; the visible-face providers draw only the
77/// orthographic one. A provider says which it draws with
78/// `supports_projection` on [`MeshHiddenLines`] and [`crate::MeshVisibleFaces`]
79/// (ADR 0107), and a registry never hands it a view it does not support.
80#[non_exhaustive]
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum ViewProjection {
83    /// Parallel projection along the frame's `-z` axis.
84    #[default]
85    Orthographic,
86    /// Central projection from the eye at the frame's origin onto the plane
87    /// `W = -1`: the image of a point with view coordinates `(U, V, W)` and
88    /// `W < 0` is `(U / -W, V / -W)`. The view requires `near < 0`, so every
89    /// kept point is strictly in front of the eye.
90    Perspective,
91}
92
93/// A view: orthographic by default, or perspective (see [`ViewProjection`]).
94///
95/// For a model point `p` the view coordinates are `((p - o)·x, (p - o)·y)`
96/// and the depth `W = (p - o)·z`, where `o`, `x`, `y`, `z` are the frame's
97/// origin and axes. The viewer sits on the `+z` side and looks along `-z`, so
98/// a larger `W` is nearer. An orthographic view's image is the view
99/// coordinates; a perspective view's is described at
100/// [`ViewProjection::Perspective`]. Geometry is kept where `far <= W <= near`
101/// (both closed); an absent bound is no bound, except that a perspective view
102/// has a `near` below zero.
103#[non_exhaustive]
104#[derive(Debug, Clone, Copy, PartialEq)]
105pub struct View {
106    /// Right-handed orthonormal view frame.
107    pub frame: Frame3,
108    /// Nearest kept depth.
109    pub near: Option<f64>,
110    /// Farthest kept depth.
111    pub far: Option<f64>,
112    /// How points are projected to the image.
113    pub projection: ViewProjection,
114}
115
116/// The former name of [`View`], from when every view was orthographic (ADR
117/// 0107).
118#[deprecated(note = "use View")]
119pub type OrthographicView = View;
120
121impl View {
122    /// An orthographic view without depth clipping.
123    pub const fn new(frame: Frame3) -> Self {
124        Self {
125            frame,
126            near: None,
127            far: None,
128            projection: ViewProjection::Orthographic,
129        }
130    }
131
132    /// A perspective view from the eye at `frame.origin`, keeping depths
133    /// `W <= near`. `near` must be finite and below zero; anything else is
134    /// refused, since the central projection is undefined at and behind the
135    /// eye.
136    pub fn perspective(frame: Frame3, near: f64) -> Result<Self, HiddenLineError> {
137        if !near.is_finite() || near >= 0.0 {
138            return Err(HiddenLineError::request(GeomError::InvalidInput(
139                "perspective near depth must be finite and below zero".into(),
140            )));
141        }
142        Ok(Self {
143            frame,
144            near: Some(near),
145            far: None,
146            projection: ViewProjection::Perspective,
147        })
148    }
149
150    /// Keep only depths `W <= near`.
151    pub const fn with_near(mut self, near: f64) -> Self {
152        self.near = Some(near);
153        self
154    }
155
156    /// Keep only depths `W >= far`.
157    pub const fn with_far(mut self, far: f64) -> Self {
158        self.far = Some(far);
159        self
160    }
161}
162
163/// Which edges to draw.
164///
165/// An edge with one incident triangle is a boundary edge. An edge with two
166/// is a silhouette when the two triangles' image orientations differ (a
167/// triangle seen edge-on is its own orientation, so the test is symmetric
168/// and independent of winding), and otherwise a crease when the cosine of
169/// the angle between their normals is strictly below `crease_cosine`, and
170/// otherwise, when `group_boundary` is set, a group boundary when the two
171/// triangles are in different [`FaceGroup`]s (ADR 0106). The kinds are
172/// exclusive in that order: a silhouette is never a crease or a group
173/// boundary, and an edge that is a crease at `crease_cosine` is never a group
174/// boundary. With `group_boundary` unset the face groups are ignored.
175#[non_exhaustive]
176#[derive(Debug, Clone, Copy, PartialEq)]
177pub struct EdgeSelection {
178    /// Crease threshold in `[-1, 1]`; `None` draws no creases.
179    pub crease_cosine: Option<f64>,
180    /// Draw boundary edges.
181    pub boundary: bool,
182    /// Draw silhouette edges.
183    pub silhouette: bool,
184    /// Draw edges between triangles of different face groups that are
185    /// neither silhouettes nor selected creases. The groups are resolved as
186    /// for `MeshVisibleFaces` ([`FaceGroups`]), and a `PerTriangle` slice of
187    /// the wrong length is refused.
188    pub group_boundary: bool,
189    /// Also report the hidden pieces.
190    pub hidden: bool,
191}
192
193impl EdgeSelection {
194    /// Every edge kind, hidden pieces included.
195    pub const fn new(crease_cosine: Option<f64>) -> Self {
196        Self {
197            crease_cosine,
198            boundary: true,
199            silhouette: true,
200            group_boundary: false,
201            hidden: true,
202        }
203    }
204
205    /// Select or deselect boundary edges.
206    pub const fn with_boundary(mut self, boundary: bool) -> Self {
207        self.boundary = boundary;
208        self
209    }
210
211    /// Select or deselect silhouette edges.
212    pub const fn with_silhouette(mut self, silhouette: bool) -> Self {
213        self.silhouette = silhouette;
214        self
215    }
216
217    /// Select or deselect group-boundary edges.
218    pub const fn with_group_boundary(mut self, group_boundary: bool) -> Self {
219        self.group_boundary = group_boundary;
220        self
221    }
222
223    /// Report or omit hidden pieces.
224    pub const fn with_hidden(mut self, hidden: bool) -> Self {
225        self.hidden = hidden;
226        self
227    }
228}
229
230/// Hard bounds for one hidden-line request.
231#[non_exhaustive]
232#[derive(Debug, Clone, Copy, PartialEq, Eq)]
233pub struct HiddenLineLimits {
234    /// Maximum input meshes.
235    pub max_meshes: usize,
236    /// Maximum source positions, summed over all meshes.
237    pub max_source_vertices: usize,
238    /// Maximum source triangles, summed over all meshes.
239    pub max_source_triangles: usize,
240    /// Maximum (edge, triangle) pairs examined for occlusion.
241    pub max_candidate_pairs: usize,
242    /// Maximum output segments.
243    pub max_output_segments: usize,
244}
245
246impl HiddenLineLimits {
247    /// Construct explicit input and output work limits.
248    pub const fn new(
249        max_meshes: usize,
250        max_source_vertices: usize,
251        max_source_triangles: usize,
252        max_candidate_pairs: usize,
253        max_output_segments: usize,
254    ) -> Self {
255        Self {
256            max_meshes,
257            max_source_vertices,
258            max_source_triangles,
259            max_candidate_pairs,
260            max_output_segments,
261        }
262    }
263}
264
265/// One hidden-line request.
266///
267/// All meshes occlude each other and themselves. A `region` crops the image
268/// (closed box in image coordinates); geometry outside it neither is drawn
269/// nor, being outside the image, occludes anything inside.
270#[non_exhaustive]
271#[derive(Debug, Clone)]
272pub struct HiddenLineRequest<'a> {
273    /// Input meshes; their order orders the output.
274    pub meshes: &'a [HiddenLineMesh<'a>],
275    /// The view.
276    pub view: View,
277    /// Edge kinds to draw.
278    pub edges: EdgeSelection,
279    /// Optional image crop.
280    pub region: Option<Aabb2>,
281    /// Work limits.
282    pub limits: HiddenLineLimits,
283}
284
285impl<'a> HiddenLineRequest<'a> {
286    /// Draw `meshes` in `view` without a crop.
287    pub const fn new(
288        meshes: &'a [HiddenLineMesh<'a>],
289        view: View,
290        edges: EdgeSelection,
291        limits: HiddenLineLimits,
292    ) -> Self {
293        Self {
294            meshes,
295            view,
296            edges,
297            region: None,
298            limits,
299        }
300    }
301
302    /// Crop the image to a closed box.
303    pub const fn with_region(mut self, region: Aabb2) -> Self {
304        self.region = Some(region);
305        self
306    }
307}
308
309/// Why an edge is drawn.
310#[non_exhaustive]
311#[derive(Debug, Clone, Copy, PartialEq, Eq)]
312pub enum EdgeKind {
313    /// One incident triangle.
314    Boundary,
315    /// Two incident triangles seen with different image orientations.
316    Silhouette,
317    /// Two incident triangles meeting at a sharp angle.
318    Crease,
319    /// Two incident triangles of different face groups that are neither a
320    /// silhouette nor a selected crease (ADR 0106).
321    GroupBoundary,
322}
323
324/// Whether a piece of an edge can be seen.
325#[non_exhaustive]
326#[derive(Debug, Clone, Copy, PartialEq, Eq)]
327pub enum Visibility {
328    /// Nothing kept lies strictly in front of it.
329    Visible,
330    /// A kept triangle point lies strictly in front of it.
331    Hidden,
332}
333
334/// A source mesh edge.
335#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
336pub struct SourceEdge {
337    /// The mesh the edge belongs to.
338    pub mesh: MeshId,
339    /// Position indices, ascending.
340    pub vertices: [u32; 2],
341}
342
343/// A maximal positive-length piece of one source edge with one visibility.
344///
345/// Each endpoint is the exact point of the edge rounded once: every image
346/// coordinate of the (rational) point is evaluated exactly and rounded to
347/// the nearest binary64. An endpoint is therefore a function of the exact
348/// point alone, whichever decision produced it, and equal points from
349/// different decisions are bit-identical. All pieces of one source edge
350/// have the same `kind`.
351#[non_exhaustive]
352#[derive(Debug, Clone, Copy, PartialEq)]
353pub struct HiddenLineSegment {
354    /// The edge this piece belongs to.
355    pub source: SourceEdge,
356    /// Why the edge is drawn.
357    pub kind: EdgeKind,
358    /// Visibility of the whole piece.
359    pub visibility: Visibility,
360    /// Image point nearer `source.vertices[0]`.
361    pub start: Point2,
362    /// Image point nearer `source.vertices[1]`.
363    pub end: Point2,
364}
365
366impl HiddenLineSegment {
367    /// Construct a piece of `source`.
368    pub const fn new(
369        source: SourceEdge,
370        kind: EdgeKind,
371        visibility: Visibility,
372        start: Point2,
373        end: Point2,
374    ) -> Self {
375        Self {
376            source,
377            kind,
378            visibility,
379            start,
380            end,
381        }
382    }
383}
384
385/// Provenance of a hidden-line drawing.
386#[non_exhaustive]
387#[derive(Debug, Clone, Copy, PartialEq, Eq)]
388pub enum HiddenLineSource {
389    /// Segments were derived from the supplied triangle meshes.
390    InputMesh,
391}
392
393/// Auditable counts for one hidden-line operation.
394#[non_exhaustive]
395#[derive(Debug, Clone, Copy, PartialEq, Eq)]
396pub struct HiddenLineEvidence {
397    /// Approximation source.
398    pub source: HiddenLineSource,
399    /// Input meshes.
400    pub source_meshes: usize,
401    /// Input triangles, summed over all meshes.
402    pub source_triangles: usize,
403    /// Edges of the selected kinds, before clipping.
404    pub selected_edges: usize,
405    /// Selected edges whose image is a single point. Such an edge is counted
406    /// here only, before clipping: it is neither drawn nor counted in
407    /// `clipped_edges`, whatever the depth range or the region.
408    pub point_edges: usize,
409    /// Selected edges removed entirely by depth clipping or the region.
410    pub clipped_edges: usize,
411    /// (edge, triangle) pairs examined for occlusion.
412    pub candidate_pairs: usize,
413    /// Decisions the fast filter could not settle and the exact tier did.
414    /// This count may differ between two otherwise identical requests (for
415    /// example with the triangles reversed); it is not part of the drawing.
416    pub exact_escalations: usize,
417    /// Pieces dropped because their rounded endpoints coincide.
418    pub collapsed_segments: usize,
419    /// Visible output segments.
420    pub visible_segments: usize,
421    /// Hidden output segments.
422    pub hidden_segments: usize,
423}
424
425impl HiddenLineEvidence {
426    /// Evidence for a drawing derived from the input meshes; the remaining
427    /// counts start at zero for the provider to fill in.
428    pub const fn input_mesh(source_meshes: usize, source_triangles: usize) -> Self {
429        Self {
430            source: HiddenLineSource::InputMesh,
431            source_meshes,
432            source_triangles,
433            selected_edges: 0,
434            point_edges: 0,
435            clipped_edges: 0,
436            candidate_pairs: 0,
437            exact_escalations: 0,
438            collapsed_segments: 0,
439            visible_segments: 0,
440            hidden_segments: 0,
441        }
442    }
443
444    /// Whether this result came from the discrete input meshes.
445    pub const fn is_derived_from_input_mesh(self) -> bool {
446        matches!(self.source, HiddenLineSource::InputMesh)
447    }
448}
449
450/// Image-space segments plus evidence.
451///
452/// Segments are ordered by the mesh's position in the request, then by
453/// `source.vertices`, then by parameter along the edge.
454#[derive(Debug, Clone, PartialEq)]
455pub struct HiddenLineOutcome {
456    /// The view the segments are expressed in, unchanged.
457    pub view: View,
458    /// Visible and hidden pieces.
459    pub segments: Vec<HiddenLineSegment>,
460    /// Counts.
461    pub evidence: HiddenLineEvidence,
462}
463
464impl HiddenLineOutcome {
465    /// Construct a provider result. Registries validate it before returning it.
466    pub fn new(view: View, segments: Vec<HiddenLineSegment>, evidence: HiddenLineEvidence) -> Self {
467        Self {
468            view,
469            segments,
470            evidence,
471        }
472    }
473}
474
475/// Provider for deterministic hidden-line drawings of triangle meshes.
476///
477/// Provider obligations beyond the types:
478///
479/// - each segment is a maximal positive-length piece of one edge with one
480///   visibility, finite, with `start != end`;
481/// - a selected edge whose image is a point is counted in `point_edges`
482///   and not drawn;
483/// - with `edges.hidden == false` no hidden segment is returned;
484/// - flipping every triangle gives identical segments and identical
485///   evidence except `exact_escalations`;
486/// - with `edges.group_boundary` the groups are resolved exactly as for
487///   `MeshVisibleFaces`, and a `PerTriangle` group slice whose length differs
488///   from the triangle count is `InvalidInput` naming the mesh; without it
489///   the groups are not read;
490/// - endpoints are the exact points rounded once to nearest (see
491///   [`HiddenLineSegment`]);
492/// - the tolerance of `options` applies to mesh audit and frame validation
493///   only, never to a visibility decision;
494/// - input that cannot be decided is refused with a typed error naming the
495///   mesh ([`HiddenLineError::mesh`]) where one mesh is at fault; no
496///   substitute drawing is returned;
497/// - output storage is outside the scratch budget.
498pub trait MeshHiddenLines: Backend {
499    /// Whether the provider draws views of this projection. The default is
500    /// `true` for [`ViewProjection::Orthographic`] only, so a provider written
501    /// before [`ViewProjection`] existed, which ignores the field, is never given
502    /// a perspective view by a registry (ADR 0107). A provider that returns
503    /// `true` for a projection must draw it exactly and pass that
504    /// projection's conformance suite.
505    fn supports_projection(&self, projection: ViewProjection) -> bool {
506        projection == ViewProjection::Orthographic
507    }
508
509    /// Scratch needed beyond inputs and result.
510    fn scratch_requirement(&self) -> ScratchRequirement {
511        ScratchRequirement::Unbounded
512    }
513
514    /// How finely the provider polls cancellation.
515    fn cancellation_granularity(&self) -> CancellationGranularity {
516        CancellationGranularity::None
517    }
518
519    /// Draw the selected edges of `request.meshes` in `request.view`.
520    /// Implementations must enforce `request.limits` before growing output.
521    fn hidden_lines(
522        &self,
523        request: &HiddenLineRequest<'_>,
524        options: &ExecutionOptions,
525    ) -> Result<HiddenLineOutcome, HiddenLineError>;
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531    use crate::fixtures::plan_frame;
532
533    #[test]
534    fn a_view_is_orthographic_unless_built_as_perspective() {
535        let view = View::new(plan_frame());
536        assert_eq!(view.projection, ViewProjection::Orthographic);
537        assert_eq!(ViewProjection::default(), ViewProjection::Orthographic);
538        assert_eq!(
539            view.with_near(-1.0).projection,
540            ViewProjection::Orthographic
541        );
542    }
543
544    #[test]
545    #[allow(deprecated)]
546    fn the_old_name_is_the_same_type() {
547        let old: crate::OrthographicView = crate::OrthographicView::new(plan_frame());
548        let new: View = old;
549        assert_eq!(new, View::new(plan_frame()));
550    }
551
552    #[test]
553    fn a_perspective_view_needs_a_finite_negative_near() {
554        let view = View::perspective(plan_frame(), -1.0).expect("valid");
555        assert_eq!(view.projection, ViewProjection::Perspective);
556        assert_eq!(view.near, Some(-1.0));
557        assert_eq!(view.far, None);
558        for near in [0.0, -0.0, 1.0, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
559            let error = View::perspective(plan_frame(), near).expect_err("refused");
560            assert_eq!(error.mesh, None);
561            assert!(matches!(error.error, GeomError::InvalidInput(_)), "{near}");
562        }
563    }
564}