use std::sync::Arc;
use axiolid_contracts::{
BackendId, DevicePreference, ExecutionOptions, ExecutionTarget, GeomError, GeomResult,
Operation, ScratchRequirement,
};
use axiolid_core::Frame3;
use axiolid_mesh::{audit_mesh_scratch_bytes, try_audit_mesh, TriMesh};
use axiolid_mesh_contracts::SolidRequirements;
use axiolid_mesh_section_contract::{MeshPlaneSection, SectionLimits, SectionOutcome};
#[derive(Debug, Clone)]
struct RegisteredSection {
priority: i32,
provider: Arc<dyn MeshPlaneSection>,
}
#[derive(Debug, Clone, Default)]
pub struct MeshPlaneSectionRegistry {
providers: Vec<RegisteredSection>,
}
impl MeshPlaneSectionRegistry {
pub const fn new() -> Self {
Self {
providers: Vec::new(),
}
}
pub fn register<P>(&mut self, priority: i32, provider: P)
where
P: MeshPlaneSection + 'static,
{
self.register_arc(priority, Arc::new(provider));
}
pub fn register_arc(&mut self, priority: i32, provider: Arc<dyn MeshPlaneSection>) {
self.providers
.push(RegisteredSection { priority, provider });
self.providers
.sort_by_key(|entry| std::cmp::Reverse(entry.priority));
}
pub fn providers(&self) -> impl Iterator<Item = &dyn MeshPlaneSection> {
self.providers.iter().map(|entry| entry.provider.as_ref())
}
pub fn section(
&self,
mesh: &TriMesh,
frame: Frame3,
limits: SectionLimits,
options: &ExecutionOptions,
) -> GeomResult<SectionOutcome> {
options.check_cancelled()?;
validate_source_shape(mesh, frame, limits, options)?;
let mut has_matching_provider = false;
let mut has_budgeted_provider = false;
for entry in &self.providers {
let descriptor = entry.provider.descriptor();
if matches_device(options.device(), descriptor.id, descriptor.target) {
has_matching_provider = true;
has_budgeted_provider |=
section_scratch_fits(entry.provider.scratch_requirement(), mesh, options);
}
}
if !has_matching_provider {
return Err(GeomError::Unsupported {
backend: BackendId::new("mesh-section-registry"),
operation: Operation::MeshPlaneSection,
});
}
if !has_budgeted_provider {
return Err(GeomError::BudgetExceeded { resource: "memory" });
}
validate_source_topology(mesh, options)?;
let mut last_retryable = None;
for entry in &self.providers {
let descriptor = entry.provider.descriptor();
if !matches_device(options.device(), descriptor.id, descriptor.target)
|| !section_scratch_fits(entry.provider.scratch_requirement(), mesh, options)
{
continue;
}
match entry.provider.section(mesh, frame, limits, options) {
Ok(outcome) => {
validate_outcome(descriptor.id, mesh, frame, limits, &outcome)?;
return Ok(outcome);
}
Err(error @ (GeomError::Unsupported { .. } | GeomError::Unavailable { .. })) => {
last_retryable = Some(error);
}
Err(error) => return Err(error),
}
}
Err(last_retryable.unwrap_or(GeomError::Unsupported {
backend: BackendId::new("mesh-section-registry"),
operation: Operation::MeshPlaneSection,
}))
}
}
fn validate_source_shape(
mesh: &TriMesh,
frame: Frame3,
limits: SectionLimits,
options: &ExecutionOptions,
) -> GeomResult<()> {
if mesh.positions.len() > limits.max_source_vertices {
return Err(GeomError::BudgetExceeded {
resource: "section source vertices",
});
}
if mesh.triangle_count() > limits.max_source_triangles {
return Err(GeomError::BudgetExceeded {
resource: "section source triangles",
});
}
if mesh.indices.is_empty() {
return Err(GeomError::InvalidInput(
"section source mesh has no triangles".into(),
));
}
validate_frame(frame, options.tolerance().angular())?;
Ok(())
}
fn validate_source_topology(mesh: &TriMesh, options: &ExecutionOptions) -> GeomResult<()> {
let health = try_audit_mesh(mesh, options.tolerance())
.map_err(|_| GeomError::BudgetExceeded { resource: "memory" })?;
if !health.is_closed_two_manifold() {
return Err(GeomError::NotManifold(format!(
"plane section requires a closed consistently wound two-manifold mesh; \
boundary={}, non_manifold={}, inconsistent_winding={}, degenerate={}",
health.boundary_edges,
health.non_manifold_edges,
health.inconsistent_winding_edges,
health.degenerate_triangles
)));
}
SolidRequirements::Oriented.validate(mesh, "section source")?;
Ok(())
}
fn section_scratch_fits(
provider: ScratchRequirement,
mesh: &TriMesh,
options: &ExecutionOptions,
) -> bool {
let Some(budget) = options.memory_budget_bytes() else {
return true;
};
let Some(audit_bytes) = audit_mesh_scratch_bytes(mesh.triangle_count()) else {
return false;
};
let elements = mesh.positions.len().max(mesh.triangle_count());
let Some(provider_bytes) = provider.upper_bound_bytes(elements) else {
return false;
};
audit_bytes.max(provider_bytes) <= budget
}
fn validate_frame(frame: Frame3, angular_tolerance: f64) -> GeomResult<()> {
let values = [frame.origin, frame.x, frame.y, frame.z];
if !values.iter().all(|value| value.is_finite()) {
return Err(GeomError::InvalidInput(
"section frame components must be finite".into(),
));
}
let limit = angular_tolerance.clamp(128.0 * f64::EPSILON, 1.0e-6);
for (name, axis) in [("x", frame.x), ("y", frame.y), ("z", frame.z)] {
if (axis.length() - 1.0).abs() > limit {
return Err(GeomError::InvalidInput(format!(
"section frame {name} axis must be unit length"
)));
}
}
if frame.x.dot(frame.y).abs() > limit
|| frame.x.dot(frame.z).abs() > limit
|| frame.y.dot(frame.z).abs() > limit
|| (frame.x.cross(frame.y).dot(frame.z) - 1.0).abs() > limit
{
return Err(GeomError::InvalidInput(
"section frame must be right-handed and orthonormal".into(),
));
}
Ok(())
}
fn validate_outcome(
backend: BackendId,
mesh: &TriMesh,
frame: Frame3,
limits: SectionLimits,
outcome: &SectionOutcome,
) -> GeomResult<()> {
let violation = |detail: &str| GeomError::BackendContractViolation {
backend,
detail: detail.into(),
};
if outcome.frame != frame {
return Err(violation("provider changed the requested section frame"));
}
if outcome.contours.len() > limits.max_contours {
return Err(GeomError::BudgetExceeded {
resource: "section contours",
});
}
let mut vertices = 0usize;
for contour in &outcome.contours {
if contour.points.len() < 3 {
return Err(violation(
"section contour must be closed with at least three points",
));
}
if !contour.points.iter().all(|point| point.is_finite()) {
return Err(violation("section contour contains a non-finite point"));
}
for index in 0..contour.points.len() {
if contour.points[index] == contour.points[(index + 1) % contour.points.len()] {
return Err(violation(
"section contour contains adjacent duplicate points",
));
}
}
vertices = vertices
.checked_add(contour.points.len())
.ok_or(GeomError::BudgetExceeded {
resource: "section output vertices",
})?;
if vertices > limits.max_output_vertices {
return Err(GeomError::BudgetExceeded {
resource: "section output vertices",
});
}
}
if outcome.evidence.source_triangles != mesh.triangle_count()
|| outcome.evidence.output_vertices != vertices
|| outcome.evidence.output_contours != outcome.contours.len()
|| !outcome.evidence.is_derived_from_input_mesh()
{
return Err(violation(
"section evidence does not match input/output counts",
));
}
Ok(())
}
fn matches_device(preference: DevicePreference, id: BackendId, target: ExecutionTarget) -> bool {
match preference {
DevicePreference::Auto => true,
DevicePreference::Cpu => matches!(
target,
ExecutionTarget::PortableCpu | ExecutionTarget::OptimizedCpu
),
DevicePreference::Gpu => matches!(target, ExecutionTarget::Gpu),
DevicePreference::Backend(required) => required == id,
}
}