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}