axiolid_mesh_compile_contract/contract.rs
1//! Complete graph compilation capability.
2
3use crate::CompileOutcome;
4use axiolid_mesh::TriMesh;
5use axiolid_model::{GeometryGraph, NodeId};
6
7use axiolid_contracts::{Backend, ExecutionOptions, GeomResult, OutputBound, ScratchRequirement};
8
9/// Backend/orchestrator capable of lowering any advertised graph node to mesh.
10///
11/// Implementations must return [`axiolid_contracts::GeomError::Unsupported`] for a node they
12/// do not support. They must not silently omit it or approximate exact geometry
13/// unless the execution policy explicitly permits that precision.
14pub trait MeshCompiler: Backend {
15 /// Scratch this compiler needs beyond the graph and the produced meshes.
16 ///
17 /// Defaults to [`ScratchRequirement::Unbounded`]: an unaudited compiler is
18 /// treated as unbudgetable rather than silently assumed cheap.
19 fn scratch_requirement(&self) -> ScratchRequirement {
20 ScratchRequirement::Unbounded
21 }
22
23 /// Outputs produced per requested root.
24 ///
25 /// Compilation is one mesh per root, so the destination size is known
26 /// before the batch runs and workers can write disjoint slots.
27 fn output_bound(&self) -> OutputBound {
28 OutputBound::OneToOne
29 }
30
31 /// Compile one root.
32 fn compile_mesh(
33 &self,
34 graph: &GeometryGraph,
35 root: NodeId,
36 options: &ExecutionOptions,
37 ) -> GeomResult<TriMesh>;
38
39 /// Compile one root and report what happened to each attribute channel.
40 ///
41 /// The default wraps [`Self::compile_mesh`] and reports
42 /// [`CompileOutcome::attribute_fates`] as `None` (not tracked). A
43 /// compiler that carries channels overrides this so a caller can tell a
44 /// dropped channel from one that was never there.
45 fn compile_mesh_reported(
46 &self,
47 graph: &GeometryGraph,
48 root: NodeId,
49 options: &ExecutionOptions,
50 ) -> GeomResult<CompileOutcome> {
51 self.compile_mesh(graph, root, options)
52 .map(CompileOutcome::untracked)
53 }
54
55 /// Compile roots into a caller-provided buffer.
56 ///
57 /// This is the seam a batching implementation should override: the caller
58 /// owns the destination, so a provider can reserve once from
59 /// [`Self::output_bound`] and have workers write disjoint slots instead of
60 /// growing a vector under a lock. `destination` is appended to, never
61 /// cleared, so results can be accumulated across calls.
62 ///
63 /// The default is a serial loop over [`Self::compile_mesh`], which stays the
64 /// only required primitive.
65 fn compile_mesh_batch_into(
66 &self,
67 graph: &GeometryGraph,
68 roots: &[NodeId],
69 options: &ExecutionOptions,
70 destination: &mut Vec<TriMesh>,
71 ) -> GeomResult<()> {
72 destination.reserve(roots.len());
73 for &root in roots {
74 destination.push(self.compile_mesh(graph, root, options)?);
75 }
76 Ok(())
77 }
78
79 /// Compile roots as a batch.
80 ///
81 /// Convenience over [`Self::compile_mesh_batch_into`]; overriding that instead
82 /// gives both call shapes the batched behaviour.
83 fn compile_mesh_batch(
84 &self,
85 graph: &GeometryGraph,
86 roots: &[NodeId],
87 options: &ExecutionOptions,
88 ) -> GeomResult<Vec<TriMesh>> {
89 let mut destination = Vec::with_capacity(roots.len());
90 self.compile_mesh_batch_into(graph, roots, options, &mut destination)?;
91 Ok(destination)
92 }
93}