1use std::sync::Arc;
4
5use axiolid_contracts::{
6 BackendId, DevicePreference, ExecutionOptions, ExecutionTarget, GeomError, GeomResult,
7 Operation, ScratchRequirement,
8};
9use axiolid_core::{Frame3, SpaceFrame, Tolerance};
10use axiolid_mesh::{audit_mesh_scratch_bytes, try_audit_mesh, TriMesh};
11use axiolid_mesh_contracts::SolidRequirements;
12use axiolid_mesh_section_contract::{
13 conformance::{ConformanceReport, ConformanceSuite},
14 MeshPlaneSection, SectionLimits, SectionOutcome,
15};
16
17#[derive(Debug, Clone)]
18struct RegisteredSection {
19 priority: i32,
20 provider: Arc<dyn MeshPlaneSection>,
21}
22
23#[derive(Debug, Clone, Default)]
28pub struct MeshPlaneSectionRegistry {
29 providers: Vec<RegisteredSection>,
30}
31
32impl MeshPlaneSectionRegistry {
33 pub const fn new() -> Self {
35 Self {
36 providers: Vec::new(),
37 }
38 }
39
40 pub fn register<P>(&mut self, priority: i32, provider: P)
42 where
43 P: MeshPlaneSection + 'static,
44 {
45 self.register_arc(priority, Arc::new(provider));
46 }
47
48 pub fn register_conformant<P>(
50 &mut self,
51 priority: i32,
52 provider: P,
53 ) -> Result<(), Box<ConformanceReport>>
54 where
55 P: MeshPlaneSection + 'static,
56 {
57 let report = ConformanceSuite::run(&provider);
58 if !report.is_success() {
59 return Err(Box::new(report));
60 }
61 self.register(priority, provider);
62 Ok(())
63 }
64
65 pub fn register_arc(&mut self, priority: i32, provider: Arc<dyn MeshPlaneSection>) {
67 self.providers
68 .push(RegisteredSection { priority, provider });
69 self.providers
70 .sort_by_key(|entry| std::cmp::Reverse(entry.priority));
71 }
72
73 pub fn providers(&self) -> impl Iterator<Item = &dyn MeshPlaneSection> {
75 self.providers.iter().map(|entry| entry.provider.as_ref())
76 }
77
78 pub fn section(
80 &self,
81 mesh: &TriMesh,
82 frame: Frame3,
83 limits: SectionLimits,
84 options: &ExecutionOptions,
85 ) -> GeomResult<SectionOutcome> {
86 options.check_cancelled()?;
87 validate_source_shape(mesh, frame, limits, options)?;
88
89 let mut has_matching_provider = false;
90 let mut has_budgeted_provider = false;
91 for entry in &self.providers {
92 let descriptor = entry.provider.descriptor();
93 if matches_device(options.device(), descriptor.id, descriptor.target) {
94 has_matching_provider = true;
95 has_budgeted_provider |=
96 section_scratch_fits(entry.provider.scratch_requirement(), mesh, options);
97 }
98 }
99 if !has_matching_provider {
100 return Err(GeomError::Unsupported {
101 backend: BackendId::new("mesh-section-registry"),
102 operation: Operation::MeshPlaneSection,
103 });
104 }
105 if !has_budgeted_provider {
106 return Err(GeomError::BudgetExceeded { resource: "memory" });
107 }
108
109 validate_source_topology(mesh, options)?;
112
113 let mut last_retryable = None;
114 for entry in &self.providers {
115 let descriptor = entry.provider.descriptor();
116 if !matches_device(options.device(), descriptor.id, descriptor.target)
117 || !section_scratch_fits(entry.provider.scratch_requirement(), mesh, options)
118 {
119 continue;
120 }
121 match entry.provider.section(mesh, frame, limits, options) {
122 Ok(outcome) => {
123 validate_outcome(descriptor.id, mesh, frame, limits, &outcome)?;
124 return Ok(outcome);
125 }
126 Err(error @ (GeomError::Unsupported { .. } | GeomError::Unavailable { .. })) => {
127 last_retryable = Some(error);
128 }
129 Err(error) => return Err(error),
130 }
131 }
132
133 Err(last_retryable.unwrap_or(GeomError::Unsupported {
134 backend: BackendId::new("mesh-section-registry"),
135 operation: Operation::MeshPlaneSection,
136 }))
137 }
138}
139
140fn validate_source_shape(
141 mesh: &TriMesh,
142 frame: Frame3,
143 limits: SectionLimits,
144 options: &ExecutionOptions,
145) -> GeomResult<()> {
146 if mesh.positions.len() > limits.max_source_vertices {
147 return Err(GeomError::BudgetExceeded {
148 resource: "section source vertices",
149 });
150 }
151 if mesh.triangle_count() > limits.max_source_triangles {
152 return Err(GeomError::BudgetExceeded {
153 resource: "section source triangles",
154 });
155 }
156 if mesh.indices.is_empty() {
157 return Err(GeomError::InvalidInput(
158 "section source mesh has no triangles".into(),
159 ));
160 }
161 validate_frame(frame, options.tolerance().angular())?;
162
163 Ok(())
164}
165
166fn validate_source_topology(mesh: &TriMesh, options: &ExecutionOptions) -> GeomResult<()> {
167 let health = try_audit_mesh(mesh, options.tolerance())
168 .map_err(|_| GeomError::BudgetExceeded { resource: "memory" })?;
169 if !health.is_closed_two_manifold() {
170 return Err(GeomError::NotManifold(format!(
171 "plane section requires a closed consistently wound two-manifold mesh; \
172 boundary={}, non_manifold={}, inconsistent_winding={}, degenerate={}",
173 health.boundary_edges,
174 health.non_manifold_edges,
175 health.inconsistent_winding_edges,
176 health.degenerate_triangles
177 )));
178 }
179 SolidRequirements::Oriented.validate(mesh, "section source")?;
180 Ok(())
181}
182
183fn section_scratch_fits(
184 provider: ScratchRequirement,
185 mesh: &TriMesh,
186 options: &ExecutionOptions,
187) -> bool {
188 let Some(budget) = options.memory_budget_bytes() else {
189 return true;
190 };
191 let Some(audit_bytes) = audit_mesh_scratch_bytes(mesh.triangle_count()) else {
192 return false;
193 };
194 let elements = mesh.positions.len().max(mesh.triangle_count());
195 let Some(provider_bytes) = provider.upper_bound_bytes(elements) else {
196 return false;
197 };
198 audit_bytes.max(provider_bytes) <= budget
201}
202
203fn validate_frame(frame: Frame3, angular_tolerance: f64) -> GeomResult<()> {
204 let limit = angular_tolerance.clamp(128.0 * f64::EPSILON, 1.0e-6);
209 let tolerance = Tolerance::new(limit, limit)
210 .map_err(|_| GeomError::InvalidInput("section frame tolerance is invalid".into()))?;
211 SpaceFrame::new(frame.origin, frame.x, frame.y, frame.z, tolerance)
212 .map(|_| ())
213 .map_err(|error| GeomError::InvalidInput(format!("section frame is invalid: {error}")))
214}
215
216fn validate_outcome(
217 backend: BackendId,
218 mesh: &TriMesh,
219 frame: Frame3,
220 limits: SectionLimits,
221 outcome: &SectionOutcome,
222) -> GeomResult<()> {
223 let violation = |detail: &str| GeomError::BackendContractViolation {
224 backend,
225 detail: detail.into(),
226 };
227 if outcome.frame != frame {
228 return Err(violation("provider changed the requested section frame"));
229 }
230 if outcome.contours.len() > limits.max_contours {
231 return Err(GeomError::BudgetExceeded {
232 resource: "section contours",
233 });
234 }
235 let mut vertices = 0usize;
236 for contour in &outcome.contours {
237 if contour.points.len() < 3 {
238 return Err(violation(
239 "section contour must be closed with at least three points",
240 ));
241 }
242 if !contour.points.iter().all(|point| point.is_finite()) {
243 return Err(violation("section contour contains a non-finite point"));
244 }
245 for index in 0..contour.points.len() {
246 if contour.points[index] == contour.points[(index + 1) % contour.points.len()] {
247 return Err(violation(
248 "section contour contains adjacent duplicate points",
249 ));
250 }
251 }
252 vertices = vertices
253 .checked_add(contour.points.len())
254 .ok_or(GeomError::BudgetExceeded {
255 resource: "section output vertices",
256 })?;
257 if vertices > limits.max_output_vertices {
258 return Err(GeomError::BudgetExceeded {
259 resource: "section output vertices",
260 });
261 }
262 }
263 if outcome.evidence.source_triangles != mesh.triangle_count()
264 || outcome.evidence.output_vertices != vertices
265 || outcome.evidence.output_contours != outcome.contours.len()
266 || !outcome.evidence.is_derived_from_input_mesh()
267 {
268 return Err(violation(
269 "section evidence does not match input/output counts",
270 ));
271 }
272 Ok(())
273}
274
275fn matches_device(preference: DevicePreference, id: BackendId, target: ExecutionTarget) -> bool {
276 match preference {
277 DevicePreference::Auto => true,
278 DevicePreference::Cpu => matches!(
279 target,
280 ExecutionTarget::PortableCpu | ExecutionTarget::OptimizedCpu
281 ),
282 DevicePreference::Gpu => matches!(target, ExecutionTarget::Gpu),
283 DevicePreference::Backend(required) => required == id,
284 }
285}