Skip to main content

axiolid/application/
drawing.rs

1//! Hidden-line drawings and visible faces on [`Application`] (ADR 0105).
2//!
3//! The exact scalar providers are stateless and their conformance suites
4//! deterministic, so each suite runs at most once per process, on the first
5//! call that reaches the provider ([`verified`]). `Application::portable()`
6//! therefore stays cheap (the face suite alone takes about half a second,
7//! and the C ABI builds an application per context). A provider that fails
8//! its suite refuses every call with
9//! [`ApplicationErrorSource::ProviderConformance`]; there is no output.
10//!
11//! The hidden-line provider also claims perspective views (ADR 0107). That
12//! claim has its own suite, run once and only by a call whose view is
13//! perspective, so an orthographic call never pays for it; a failure is
14//! folded into the same refusal as the case `perspective claim`, as the
15//! dispatch registry does.
16
17use std::sync::OnceLock;
18
19use axiolid_contracts::{
20    capability_ids, BackendId, CapabilityDescriptor, Exactness, ExecutionOptions, GeomError,
21    IntegrationDescriptor, Operation, Representation,
22};
23use axiolid_hidden_lines::{ScalarHiddenLines, ScalarVisibleFaces};
24use axiolid_mesh_hidden_lines_contract::conformance::{
25    faces::FaceConformanceSuite,
26    perspective::{PerspectiveConformanceReport, PerspectiveConformanceSuite},
27    ConformanceFailure, ConformanceReport, ConformanceSuite,
28};
29use axiolid_mesh_hidden_lines_contract::{
30    HiddenLineError, HiddenLineOutcome, HiddenLineRequest, RefusedMesh, ViewProjection,
31    VisibleFaceOutcome, VisibleFaceRequest,
32};
33
34use super::{
35    Application, ApplicationBuilder, ApplicationError, ApplicationErrorSource, CallContext,
36    ProviderConformanceReport, APPLICATION_ID, MESH_INPUT,
37};
38
39type Verdict = OnceLock<Result<(), ProviderConformanceReport>>;
40
41/// The hidden-line provider's verdict, tied to its suite.
42fn hidden_lines_verdict() -> Result<(), &'static ProviderConformanceReport> {
43    static CELL: Verdict = OnceLock::new();
44    verified(&CELL, || {
45        ProviderConformanceReport::MeshHiddenLines(Box::new(ConformanceSuite::run(
46            &ScalarHiddenLines::new(),
47        )))
48    })
49}
50
51/// The hidden-line provider's verdict on its perspective claim.
52fn perspective_hidden_lines_verdict() -> Result<(), &'static ProviderConformanceReport> {
53    static CELL: Verdict = OnceLock::new();
54    verified(&CELL, || {
55        perspective_report(PerspectiveConformanceSuite::run(&ScalarHiddenLines::new()))
56    })
57}
58
59/// A perspective suite report as a hidden-line report: a failure is the
60/// case `perspective claim`, as `register_conformant` of the dispatch
61/// registry reports it.
62fn perspective_report(report: PerspectiveConformanceReport) -> ProviderConformanceReport {
63    let mut folded = ConformanceReport::default();
64    if !report.is_success() {
65        folded.failures.push(ConformanceFailure::ReturnedError {
66            case: "perspective claim",
67            error: HiddenLineError::request(GeomError::InvalidInput(format!(
68                "perspective suite: {report}"
69            ))),
70        });
71    }
72    ProviderConformanceReport::MeshHiddenLines(Box::new(folded))
73}
74
75/// The visible-face provider's verdict, tied to its suite.
76fn visible_faces_verdict() -> Result<(), &'static ProviderConformanceReport> {
77    static CELL: Verdict = OnceLock::new();
78    verified(&CELL, || {
79        ProviderConformanceReport::MeshVisibleFaces(Box::new(FaceConformanceSuite::run(
80            &ScalarVisibleFaces::new(),
81        )))
82    })
83}
84
85/// Run `run` the first time `cell` is asked and keep its verdict.
86///
87/// A report that is not a success is kept as the refusal reason; every later
88/// call returns it without running the suite again.
89fn verified(
90    cell: &Verdict,
91    run: impl FnOnce() -> ProviderConformanceReport,
92) -> Result<(), &ProviderConformanceReport> {
93    cell.get_or_init(|| {
94        let report = run();
95        if report.is_success() {
96            Ok(())
97        } else {
98            Err(report)
99        }
100    })
101    .as_ref()
102    .map(|&()| ())
103}
104
105impl ApplicationBuilder {
106    /// Select the exact scalar hidden-line provider.
107    ///
108    /// Its conformance suite runs on first use, not here (see the module
109    /// docs).
110    #[must_use]
111    pub fn with_portable_hidden_lines(mut self) -> Self {
112        self.hidden_lines.register(0, ScalarHiddenLines::new());
113        self.hidden_lines_provider = Some(ScalarHiddenLines::ID);
114        self
115    }
116
117    /// Select the exact scalar visible-face provider.
118    ///
119    /// Its conformance suite runs on first use, not here (see the module
120    /// docs).
121    #[must_use]
122    pub fn with_portable_visible_faces(mut self) -> Self {
123        self.visible_faces.register(0, ScalarVisibleFaces::new());
124        self.visible_faces_provider = Some(ScalarVisibleFaces::ID);
125        self
126    }
127}
128
129/// Advertise the drawing providers the builder selected.
130pub(super) fn push_capabilities(
131    descriptor: &mut IntegrationDescriptor,
132    hidden_lines: Option<BackendId>,
133    visible_faces: Option<BackendId>,
134) {
135    for (id, operation, provider) in [
136        (
137            capability_ids::MESH_HIDDEN_LINES,
138            Operation::MeshHiddenLines,
139            hidden_lines,
140        ),
141        (
142            capability_ids::MESH_VISIBLE_FACES,
143            Operation::MeshVisibleFaces,
144            visible_faces,
145        ),
146    ] {
147        if let Some(provider) = provider {
148            descriptor.capabilities.push(CapabilityDescriptor {
149                id,
150                operation,
151                provider,
152                required_feature: "application",
153                inputs: MESH_INPUT,
154                output: Representation::Profile2d,
155                exactness: Exactness::ToleranceBounded,
156                deterministic: true,
157            });
158        }
159    }
160}
161
162impl Application {
163    /// Draw the edges of `request.meshes` with hidden-line removal.
164    ///
165    /// A refusal that names a mesh keeps it in
166    /// [`ApplicationErrorSource::HiddenLines`].
167    pub fn hidden_lines(
168        &self,
169        request: &HiddenLineRequest<'_>,
170        options: &ExecutionOptions,
171    ) -> Result<HiddenLineOutcome, ApplicationError> {
172        let provider = self.hidden_lines_provider.unwrap_or(APPLICATION_ID);
173        options.check_cancelled().map_err(|source| {
174            drawing_error(Operation::MeshHiddenLines, provider, options, source.into())
175        })?;
176        if self.hidden_lines_provider.is_some() {
177            hidden_lines_verdict().map_err(|report| {
178                conformance_error(Operation::MeshHiddenLines, provider, options, report)
179            })?;
180            if request.view.projection == ViewProjection::Perspective {
181                perspective_hidden_lines_verdict().map_err(|report| {
182                    conformance_error(Operation::MeshHiddenLines, provider, options, report)
183                })?;
184            }
185        }
186        self.hidden_lines
187            .hidden_lines(request, options)
188            .map_err(|source| drawing_error(Operation::MeshHiddenLines, provider, options, source))
189    }
190
191    /// Every input mesh [`hidden_lines`](Self::hidden_lines) would refuse,
192    /// in request order, without drawing.
193    pub fn audit_hidden_lines(
194        &self,
195        request: &HiddenLineRequest<'_>,
196        options: &ExecutionOptions,
197    ) -> Result<Vec<RefusedMesh>, ApplicationError> {
198        self.hidden_lines.audit(request, options).map_err(|source| {
199            drawing_error(
200                Operation::MeshHiddenLines,
201                self.hidden_lines_provider.unwrap_or(APPLICATION_ID),
202                options,
203                source,
204            )
205        })
206    }
207
208    /// Visible face regions of `request.meshes` in the view's image plane
209    /// (ADR 0094).
210    pub fn visible_faces(
211        &self,
212        request: &VisibleFaceRequest<'_>,
213        options: &ExecutionOptions,
214    ) -> Result<VisibleFaceOutcome, ApplicationError> {
215        let provider = self.visible_faces_provider.unwrap_or(APPLICATION_ID);
216        options.check_cancelled().map_err(|source| {
217            drawing_error(
218                Operation::MeshVisibleFaces,
219                provider,
220                options,
221                source.into(),
222            )
223        })?;
224        if self.visible_faces_provider.is_some() {
225            visible_faces_verdict().map_err(|report| {
226                conformance_error(Operation::MeshVisibleFaces, provider, options, report)
227            })?;
228        }
229        self.visible_faces
230            .visible_faces(request, options)
231            .map_err(|source| drawing_error(Operation::MeshVisibleFaces, provider, options, source))
232    }
233}
234
235fn drawing_error(
236    operation: Operation,
237    provider: BackendId,
238    options: &ExecutionOptions,
239    source: HiddenLineError,
240) -> ApplicationError {
241    ApplicationError {
242        context: CallContext {
243            operation,
244            provider,
245            tolerance: options.tolerance(),
246        },
247        source: Box::new(ApplicationErrorSource::HiddenLines(source)),
248    }
249}
250
251fn conformance_error(
252    operation: Operation,
253    provider: BackendId,
254    options: &ExecutionOptions,
255    report: &ProviderConformanceReport,
256) -> ApplicationError {
257    ApplicationError {
258        context: CallContext {
259            operation,
260            provider,
261            tolerance: options.tolerance(),
262        },
263        source: Box::new(ApplicationErrorSource::ProviderConformance(report.clone())),
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use std::cell::Cell;
270
271    use axiolid_mesh_hidden_lines_contract::conformance::{ConformanceFailure, ConformanceReport};
272
273    use super::*;
274
275    fn failing() -> ProviderConformanceReport {
276        let mut report = ConformanceReport::default();
277        report
278            .failures
279            .push(ConformanceFailure::NonDeterministic { case: "memo" });
280        ProviderConformanceReport::MeshHiddenLines(Box::new(report))
281    }
282
283    #[test]
284    fn a_failing_verdict_is_returned_and_the_suite_runs_once() {
285        static CELL: Verdict = OnceLock::new();
286        let runs = Cell::new(0);
287        for _ in 0..3 {
288            let verdict = verified(&CELL, || {
289                runs.set(runs.get() + 1);
290                failing()
291            });
292            let report = verdict.expect_err("the failing report is the refusal");
293            assert!(!report.is_success());
294        }
295        assert_eq!(runs.get(), 1);
296    }
297
298    #[test]
299    fn a_failed_perspective_suite_is_folded_into_a_hidden_line_report() {
300        use axiolid_mesh_hidden_lines_contract::conformance::perspective::PerspectiveConformanceFailure;
301
302        let mut failed = PerspectiveConformanceSuite::run(&ScalarHiddenLines::new());
303        assert!(failed.is_success(), "the shipped provider passes: {failed}");
304        failed
305            .failures
306            .push(PerspectiveConformanceFailure::ClaimUnverifiable {
307                operation: "hidden lines",
308            });
309        let folded = perspective_report(failed);
310        assert!(!folded.is_success());
311        let ProviderConformanceReport::MeshHiddenLines(report) = folded else {
312            panic!("a hidden-line report");
313        };
314        assert!(matches!(
315            report.failures[..],
316            [ConformanceFailure::ReturnedError {
317                case: "perspective claim",
318                ..
319            }]
320        ));
321        let passed =
322            perspective_report(PerspectiveConformanceSuite::run(&ScalarHiddenLines::new()));
323        assert!(passed.is_success());
324    }
325
326    #[test]
327    fn a_passing_verdict_is_kept() {
328        static CELL: Verdict = OnceLock::new();
329        let runs = Cell::new(0);
330        for _ in 0..2 {
331            verified(&CELL, || {
332                runs.set(runs.get() + 1);
333                ProviderConformanceReport::MeshHiddenLines(Box::default())
334            })
335            .expect("an empty report passes");
336        }
337        assert_eq!(runs.get(), 1);
338    }
339}