1use std::sync::Arc;
4
5use axiolid_contracts::{
6 BackendId, ExecutionOptions, GeomError, GeomResult, Operation, ScratchRequirement,
7};
8use axiolid_core::Frame3;
9use axiolid_mesh::{audit_mesh_scratch_bytes, try_audit_mesh, TriMesh};
10use axiolid_mesh_contracts::SolidRequirements;
11use axiolid_mesh_section_contract::{
12 conformance::{
13 ConformanceReport, ConformanceSuite, RegionConformanceFailure, RegionConformanceReport,
14 RegionConformanceSuite,
15 },
16 MeshPlaneSection, PlaneSection, SectionLimits, SectionOutcome, SectionRequest,
17};
18
19use crate::device::matches_device;
20use crate::frame::validate_frame;
21use crate::section_request::{
22 validate_cut_request, validate_plane_section, validate_request_topology,
23};
24
25#[derive(Debug, Clone)]
26struct RegisteredSection {
27 priority: i32,
28 provider: Arc<dyn MeshPlaneSection>,
29}
30
31#[derive(Debug, Clone, Default)]
36pub struct MeshPlaneSectionRegistry {
37 providers: Vec<RegisteredSection>,
38}
39
40impl MeshPlaneSectionRegistry {
41 pub const fn new() -> Self {
43 Self {
44 providers: Vec::new(),
45 }
46 }
47
48 pub fn register<P>(&mut self, priority: i32, provider: P)
50 where
51 P: MeshPlaneSection + 'static,
52 {
53 self.register_arc(priority, Arc::new(provider));
54 }
55
56 pub fn register_conformant<P>(
58 &mut self,
59 priority: i32,
60 provider: P,
61 ) -> Result<(), Box<ConformanceReport>>
62 where
63 P: MeshPlaneSection + 'static,
64 {
65 let report = ConformanceSuite::run(&provider);
66 if !report.is_success() {
67 return Err(Box::new(report));
68 }
69 self.register(priority, provider);
70 Ok(())
71 }
72
73 pub fn register_request_conformant<P>(
78 &mut self,
79 priority: i32,
80 provider: P,
81 ) -> Result<(), Box<RegionConformanceReport>>
82 where
83 P: MeshPlaneSection + 'static,
84 {
85 let mut report = RegionConformanceSuite::run(&provider);
86 if !report.claimed {
89 report.failures.extend(
90 ConformanceSuite::run(&provider)
91 .failures
92 .into_iter()
93 .map(RegionConformanceFailure::Base),
94 );
95 }
96 if !report.is_success() {
97 return Err(Box::new(report));
98 }
99 self.register(priority, provider);
100 Ok(())
101 }
102
103 pub fn register_arc(&mut self, priority: i32, provider: Arc<dyn MeshPlaneSection>) {
105 self.providers
106 .push(RegisteredSection { priority, provider });
107 self.providers
108 .sort_by_key(|entry| std::cmp::Reverse(entry.priority));
109 }
110
111 pub fn providers(&self) -> impl Iterator<Item = &dyn MeshPlaneSection> {
113 self.providers.iter().map(|entry| entry.provider.as_ref())
114 }
115
116 pub fn section(
118 &self,
119 mesh: &TriMesh,
120 frame: Frame3,
121 limits: SectionLimits,
122 options: &ExecutionOptions,
123 ) -> GeomResult<SectionOutcome> {
124 options.check_cancelled()?;
125 validate_source_shape(mesh, frame, limits, options)?;
126
127 let mut has_matching_provider = false;
128 let mut has_budgeted_provider = false;
129 for entry in &self.providers {
130 let descriptor = entry.provider.descriptor();
131 if matches_device(options.device(), descriptor.id, descriptor.target) {
132 has_matching_provider = true;
133 has_budgeted_provider |=
134 section_scratch_fits(entry.provider.scratch_requirement(), mesh, options);
135 }
136 }
137 if !has_matching_provider {
138 return Err(GeomError::Unsupported {
139 backend: BackendId::new("mesh-section-registry"),
140 operation: Operation::MeshPlaneSection,
141 });
142 }
143 if !has_budgeted_provider {
144 return Err(GeomError::BudgetExceeded { resource: "memory" });
145 }
146
147 validate_source_topology(mesh, options)?;
150
151 let mut last_retryable = None;
152 for entry in &self.providers {
153 let descriptor = entry.provider.descriptor();
154 if !matches_device(options.device(), descriptor.id, descriptor.target)
155 || !section_scratch_fits(entry.provider.scratch_requirement(), mesh, options)
156 {
157 continue;
158 }
159 match entry.provider.section(mesh, frame, limits, options) {
160 Ok(outcome) => {
161 validate_outcome(descriptor.id, mesh, frame, limits, &outcome)?;
162 return Ok(outcome);
163 }
164 Err(error @ (GeomError::Unsupported { .. } | GeomError::Unavailable { .. })) => {
165 last_retryable = Some(error);
166 }
167 Err(error) => return Err(error),
168 }
169 }
170
171 Err(last_retryable.unwrap_or(GeomError::Unsupported {
172 backend: BackendId::new("mesh-section-registry"),
173 operation: Operation::MeshPlaneSection,
174 }))
175 }
176
177 pub fn section_request(
188 &self,
189 mesh: &TriMesh,
190 request: &SectionRequest,
191 options: &ExecutionOptions,
192 ) -> GeomResult<PlaneSection> {
193 options.check_cancelled()?;
194 validate_source_shape(mesh, request.frame, request.limits, options)?;
195 validate_cut_request(request)?;
196
197 let mut has_matching_provider = false;
198 let mut has_budgeted_provider = false;
199 for entry in &self.providers {
200 let descriptor = entry.provider.descriptor();
201 if matches_device(options.device(), descriptor.id, descriptor.target) {
202 has_matching_provider = true;
203 has_budgeted_provider |= section_scratch_fits(
204 entry.provider.request_scratch_requirement(),
205 mesh,
206 options,
207 );
208 }
209 }
210 if !has_matching_provider {
211 return Err(GeomError::Unsupported {
212 backend: BackendId::new("mesh-section-registry"),
213 operation: Operation::MeshPlaneSection,
214 });
215 }
216 if !has_budgeted_provider {
217 return Err(GeomError::BudgetExceeded { resource: "memory" });
218 }
219
220 let boundary_edges = validate_request_topology(mesh, request.role, options)?;
221
222 let mut last_retryable = None;
223 for entry in &self.providers {
224 let descriptor = entry.provider.descriptor();
225 if !matches_device(options.device(), descriptor.id, descriptor.target)
226 || !section_scratch_fits(
227 entry.provider.request_scratch_requirement(),
228 mesh,
229 options,
230 )
231 {
232 continue;
233 }
234 match entry.provider.section_request(mesh, request, options) {
235 Ok(section) if request.cut_regions.is_some() && section.cut_regions.is_none() => {
238 last_retryable = Some(GeomError::Unsupported {
239 backend: descriptor.id,
240 operation: Operation::MeshPlaneSection,
241 });
242 }
243 Ok(section) => {
244 validate_plane_section(descriptor.id, mesh, request, boundary_edges, §ion)?;
245 return Ok(section);
246 }
247 Err(error @ (GeomError::Unsupported { .. } | GeomError::Unavailable { .. })) => {
248 last_retryable = Some(error);
249 }
250 Err(error) => return Err(error),
251 }
252 }
253
254 Err(last_retryable.unwrap_or(GeomError::Unsupported {
255 backend: BackendId::new("mesh-section-registry"),
256 operation: Operation::MeshPlaneSection,
257 }))
258 }
259}
260
261fn validate_source_shape(
262 mesh: &TriMesh,
263 frame: Frame3,
264 limits: SectionLimits,
265 options: &ExecutionOptions,
266) -> GeomResult<()> {
267 if mesh.positions.len() > limits.max_source_vertices {
268 return Err(GeomError::BudgetExceeded {
269 resource: "section source vertices",
270 });
271 }
272 if mesh.triangle_count() > limits.max_source_triangles {
273 return Err(GeomError::BudgetExceeded {
274 resource: "section source triangles",
275 });
276 }
277 if mesh.indices.is_empty() {
278 return Err(GeomError::InvalidInput(
279 "section source mesh has no triangles".into(),
280 ));
281 }
282 if !mesh.indices.len().is_multiple_of(3) {
283 return Err(GeomError::InvalidInput(
284 "section source mesh index count is not a multiple of three".into(),
285 ));
286 }
287 validate_frame(frame, options.tolerance().angular(), "section frame")?;
288
289 Ok(())
290}
291
292pub(crate) fn validate_source_topology(
293 mesh: &TriMesh,
294 options: &ExecutionOptions,
295) -> GeomResult<()> {
296 let health = try_audit_mesh(mesh, options.tolerance())
297 .map_err(|_| GeomError::BudgetExceeded { resource: "memory" })?;
298 if !health.is_closed_two_manifold() {
299 return Err(GeomError::NotManifold(format!(
300 "plane section requires a closed consistently wound two-manifold mesh; \
301 boundary={}, non_manifold={}, inconsistent_winding={}, degenerate={}",
302 health.boundary_edges,
303 health.non_manifold_edges,
304 health.inconsistent_winding_edges,
305 health.degenerate_triangles
306 )));
307 }
308 SolidRequirements::Oriented.validate(mesh, "section source")?;
309 Ok(())
310}
311
312fn section_scratch_fits(
313 provider: ScratchRequirement,
314 mesh: &TriMesh,
315 options: &ExecutionOptions,
316) -> bool {
317 let Some(budget) = options.memory_budget_bytes() else {
318 return true;
319 };
320 let Some(audit_bytes) = audit_mesh_scratch_bytes(mesh.triangle_count()) else {
321 return false;
322 };
323 let elements = mesh.positions.len().max(mesh.triangle_count());
324 let Some(provider_bytes) = provider.upper_bound_bytes(elements) else {
325 return false;
326 };
327 audit_bytes.max(provider_bytes) <= budget
330}
331
332fn validate_outcome(
333 backend: BackendId,
334 mesh: &TriMesh,
335 frame: Frame3,
336 limits: SectionLimits,
337 outcome: &SectionOutcome,
338) -> GeomResult<()> {
339 let violation = |detail: &str| GeomError::BackendContractViolation {
340 backend,
341 detail: detail.into(),
342 };
343 if outcome.frame != frame {
344 return Err(violation("provider changed the requested section frame"));
345 }
346 if outcome.contours.len() > limits.max_contours {
347 return Err(GeomError::BudgetExceeded {
348 resource: "section contours",
349 });
350 }
351 let mut vertices = 0usize;
352 for contour in &outcome.contours {
353 if contour.points.len() < 3 {
354 return Err(violation(
355 "section contour must be closed with at least three points",
356 ));
357 }
358 if !contour.points.iter().all(|point| point.is_finite()) {
359 return Err(violation("section contour contains a non-finite point"));
360 }
361 for index in 0..contour.points.len() {
362 if contour.points[index] == contour.points[(index + 1) % contour.points.len()] {
363 return Err(violation(
364 "section contour contains adjacent duplicate points",
365 ));
366 }
367 }
368 vertices = vertices
369 .checked_add(contour.points.len())
370 .ok_or(GeomError::BudgetExceeded {
371 resource: "section output vertices",
372 })?;
373 if vertices > limits.max_output_vertices {
374 return Err(GeomError::BudgetExceeded {
375 resource: "section output vertices",
376 });
377 }
378 }
379 if outcome.evidence.source_triangles != mesh.triangle_count()
380 || outcome.evidence.output_vertices != vertices
381 || outcome.evidence.output_contours != outcome.contours.len()
382 || !outcome.evidence.is_derived_from_input_mesh()
383 {
384 return Err(violation(
385 "section evidence does not match input/output counts",
386 ));
387 }
388 Ok(())
389}