use core::fmt;
use axiolid_core::{BooleanOperator, Tolerance};
use crate::{BooleanOutcome, MeshBoolean};
use axiolid_contracts::{Determinism, ExecutionOptions, GeomError};
use axiolid_mesh::TriMesh;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Outcome {
Passed,
Skipped {
reason: String,
},
Failed {
detail: String,
},
}
#[derive(Debug, Clone)]
pub struct Check {
pub name: &'static str,
pub outcome: Outcome,
}
#[derive(Debug, Clone, Default)]
pub struct ConformanceReport {
pub checks: Vec<Check>,
}
impl ConformanceReport {
#[must_use]
pub fn is_conformant(&self) -> bool {
!self
.checks
.iter()
.any(|check| matches!(check.outcome, Outcome::Failed { .. }))
}
#[must_use]
pub fn exercised(&self) -> usize {
self.checks
.iter()
.filter(|check| check.outcome == Outcome::Passed)
.count()
}
#[must_use]
pub fn skipped(&self) -> usize {
self.checks
.iter()
.filter(|check| matches!(check.outcome, Outcome::Skipped { .. }))
.count()
}
fn record(&mut self, name: &'static str, outcome: Outcome) {
self.checks.push(Check { name, outcome });
}
}
impl fmt::Display for ConformanceReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"conformance: {} passed, {} skipped, {} failed",
self.exercised(),
self.skipped(),
self.checks.len() - self.exercised() - self.skipped()
)?;
for check in &self.checks {
match &check.outcome {
Outcome::Passed => writeln!(f, " pass {}", check.name)?,
Outcome::Skipped { reason } => {
writeln!(f, " skip {} ({reason})", check.name)?;
}
Outcome::Failed { detail } => {
writeln!(f, " FAIL {} -- {detail}", check.name)?;
}
}
}
Ok(())
}
}
#[must_use]
pub fn box_at(min: [f64; 3], max: [f64; 3]) -> TriMesh {
let [x0, y0, z0] = min;
let [x1, y1, z1] = max;
let positions = vec![
[x0, y0, z0].into(),
[x1, y0, z0].into(),
[x1, y1, z0].into(),
[x0, y1, z0].into(),
[x0, y0, z1].into(),
[x1, y0, z1].into(),
[x1, y1, z1].into(),
[x0, y1, z1].into(),
];
let indices = vec![
0, 2, 1, 0, 3, 2, 4, 5, 6, 4, 6, 7, 0, 1, 5, 0, 5, 4, 1, 2, 6, 1, 6, 5, 2, 3, 7, 2, 7, 6,
3, 0, 4, 3, 4, 7,
];
TriMesh::new(positions, indices)
}
#[must_use]
pub fn volume(mesh: &TriMesh) -> f64 {
mesh.indices
.chunks_exact(3)
.map(|t| {
let a = mesh.positions[t[0] as usize];
let b = mesh.positions[t[1] as usize];
let c = mesh.positions[t[2] as usize];
a.dot(b.cross(c))
})
.sum::<f64>()
/ 6.0
}
fn options() -> ExecutionOptions {
ExecutionOptions::new(Tolerance::METRE)
}
fn attempt(
provider: &impl MeshBoolean,
subject: &TriMesh,
tool: &TriMesh,
operation: BooleanOperator,
) -> Result<BooleanOutcome, Outcome> {
match provider.boolean(subject, tool, operation, &options()) {
Ok(outcome) => Ok(outcome),
Err(GeomError::Unsupported { .. }) => Err(Outcome::Skipped {
reason: format!("provider does not support {operation:?}"),
}),
Err(error) => Err(Outcome::Failed {
detail: format!("{operation:?} on admissible operands failed: {error}"),
}),
}
}
#[must_use]
pub fn run(provider: &impl MeshBoolean) -> ConformanceReport {
let mut report = ConformanceReport::default();
check_disjoint_algebra(provider, &mut report);
check_identical_operands(provider, &mut report);
check_difference_is_ordered(provider, &mut report);
check_empty_result_is_a_value(provider, &mut report);
check_inadmissible_operands_rejected(provider, &mut report);
check_evidence_is_populated(provider, &mut report);
check_determinism(provider, &mut report);
check_determinism_is_not_overstated(provider, &mut report);
check_declared_granularity_is_honoured(provider, &mut report);
report
}
fn check_disjoint_algebra(provider: &impl MeshBoolean, report: &mut ConformanceReport) {
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_at([9.0, 9.0, 9.0], [10.0, 10.0, 10.0]);
let outcome = match attempt(provider, &a, &b, BooleanOperator::Union) {
Ok(outcome) => outcome,
Err(skip_or_fail) => return report.record("disjoint_union_sums_volume", skip_or_fail),
};
let measured = volume(&outcome.mesh);
report.record(
"disjoint_union_sums_volume",
if (measured - 2.0).abs() < 1e-9 {
Outcome::Passed
} else {
Outcome::Failed {
detail: format!("expected volume 2.0 for two disjoint unit cubes, got {measured}"),
}
},
);
match attempt(provider, &a, &b, BooleanOperator::Difference) {
Ok(outcome) => {
let measured = volume(&outcome.mesh);
report.record(
"disjoint_difference_preserves_subject",
if (measured - 1.0).abs() < 1e-9 {
Outcome::Passed
} else {
Outcome::Failed {
detail: format!("A \\ B must equal A when disjoint, got volume {measured}"),
}
},
);
}
Err(skip_or_fail) => {
report.record("disjoint_difference_preserves_subject", skip_or_fail);
}
}
}
fn check_identical_operands(provider: &impl MeshBoolean, report: &mut ConformanceReport) {
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
match attempt(provider, &a, &a, BooleanOperator::Union) {
Ok(outcome) => {
let measured = volume(&outcome.mesh);
report.record(
"self_union_is_idempotent",
if (measured - 1.0).abs() < 1e-9 {
Outcome::Passed
} else {
Outcome::Failed {
detail: format!("A union A must equal A, got volume {measured}"),
}
},
);
}
Err(skip_or_fail) => report.record("self_union_is_idempotent", skip_or_fail),
}
match attempt(provider, &a, &a, BooleanOperator::Difference) {
Ok(outcome) => {
let measured = volume(&outcome.mesh).abs();
report.record(
"self_difference_annihilates",
if measured < 1e-9 {
Outcome::Passed
} else {
Outcome::Failed {
detail: format!("A minus A must be empty, got volume {measured}"),
}
},
);
}
Err(skip_or_fail) => report.record("self_difference_annihilates", skip_or_fail),
}
}
fn check_difference_is_ordered(provider: &impl MeshBoolean, report: &mut ConformanceReport) {
let outer = box_at([0.0, 0.0, 0.0], [4.0, 4.0, 4.0]);
let inner = box_at([1.0, 1.0, 1.0], [2.0, 2.0, 2.0]);
let forward = attempt(provider, &outer, &inner, BooleanOperator::Difference);
let reverse = attempt(provider, &inner, &outer, BooleanOperator::Difference);
match (forward, reverse) {
(Ok(forward), Ok(reverse)) => {
let (big, small) = (volume(&forward.mesh), volume(&reverse.mesh).abs());
report.record(
"difference_respects_operand_order",
if (big - 63.0).abs() < 1e-9 && small < 1e-9 {
Outcome::Passed
} else {
Outcome::Failed {
detail: format!(
"outer minus inner should be 63.0 and inner minus outer empty, \
got {big} and {small}"
),
}
},
);
}
(Err(skip_or_fail), _) | (_, Err(skip_or_fail)) => {
report.record("difference_respects_operand_order", skip_or_fail);
}
}
}
fn check_empty_result_is_a_value(provider: &impl MeshBoolean, report: &mut ConformanceReport) {
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_at([9.0, 9.0, 9.0], [10.0, 10.0, 10.0]);
match attempt(provider, &a, &b, BooleanOperator::Intersection) {
Ok(outcome) => report.record(
"empty_intersection_is_a_value_not_an_error",
if outcome.mesh.triangle_count() == 0 {
Outcome::Passed
} else {
Outcome::Failed {
detail: format!(
"disjoint intersection must be empty, got {} triangles",
outcome.mesh.triangle_count()
),
}
},
),
Err(skip_or_fail) => {
report.record("empty_intersection_is_a_value_not_an_error", skip_or_fail);
}
}
}
fn check_inadmissible_operands_rejected(
provider: &impl MeshBoolean,
report: &mut ConformanceReport,
) {
let good = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let empty = TriMesh::new(Vec::new(), Vec::new());
let result = provider.boolean(&good, &empty, BooleanOperator::Union, &options());
report.record(
"inadmissible_operand_is_refused",
match result {
Err(GeomError::InvalidInput(_) | GeomError::Degenerate(_)) => Outcome::Passed,
Err(GeomError::Unsupported { .. }) => Outcome::Skipped {
reason: "provider declined before validating".into(),
},
Err(other) => Outcome::Failed {
detail: format!("expected InvalidInput or Degenerate, got {other:?}"),
},
Ok(_) => Outcome::Failed {
detail: "an empty mesh is not a solid and must be refused".into(),
},
},
);
}
fn check_evidence_is_populated(provider: &impl MeshBoolean, report: &mut ConformanceReport) {
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_at([9.0, 9.0, 9.0], [10.0, 10.0, 10.0]);
match attempt(provider, &a, &b, BooleanOperator::Union) {
Ok(outcome) => {
let evidence = &outcome.evidence;
let mut problems = Vec::new();
if evidence.subject_triangles != a.triangle_count() {
problems.push(format!(
"subject_triangles {} != {}",
evidence.subject_triangles,
a.triangle_count()
));
}
if evidence.output_triangles != outcome.mesh.triangle_count() {
problems.push(format!(
"output_triangles {} != actual {}",
evidence.output_triangles,
outcome.mesh.triangle_count()
));
}
if evidence.sub_operations == 0 {
problems.push("sub_operations must be at least 1".into());
}
report.record(
"evidence_describes_the_work",
if problems.is_empty() {
Outcome::Passed
} else {
Outcome::Failed {
detail: problems.join("; "),
}
},
);
}
Err(skip_or_fail) => report.record("evidence_describes_the_work", skip_or_fail),
}
}
fn check_determinism(provider: &impl MeshBoolean, report: &mut ConformanceReport) {
let a = box_at([0.0, 0.0, 0.0], [4.0, 4.0, 4.0]);
let b = box_at([1.0, 1.0, 1.0], [2.0, 2.0, 2.0]);
let first = attempt(provider, &a, &b, BooleanOperator::Difference);
let second = attempt(provider, &a, &b, BooleanOperator::Difference);
match (first, second) {
(Ok(first), Ok(second)) => report.record(
"repeated_calls_are_deterministic",
if first.mesh.positions == second.mesh.positions
&& first.mesh.indices == second.mesh.indices
{
Outcome::Passed
} else {
Outcome::Failed {
detail: "identical inputs produced different geometry".into(),
}
},
),
(Err(skip_or_fail), _) | (_, Err(skip_or_fail)) => {
report.record("repeated_calls_are_deterministic", skip_or_fail);
}
}
}
fn check_determinism_is_not_overstated(
provider: &impl MeshBoolean,
report: &mut ConformanceReport,
) {
let declared = provider.determinism();
report.record(
"determinism_declaration_is_verifiable",
if declared == Determinism::Bitwise {
Outcome::Failed {
detail: "provider declares Determinism::Bitwise, which this suite cannot \
verify: cross-process byte equality is not observable from a \
single-process check. Declare the strongest level the provider \
can prove by construction instead"
.into(),
}
} else {
Outcome::Passed
},
);
}
fn check_declared_granularity_is_honoured(
provider: &impl MeshBoolean,
report: &mut ConformanceReport,
) {
use axiolid_contracts::{CancellationGranularity, CancellationToken};
let granularity = provider.cancellation_granularity();
if granularity == CancellationGranularity::None {
report.record(
"declared_cancellation_is_honoured",
Outcome::Skipped {
reason: "provider declares it does not poll".into(),
},
);
return;
}
let a = box_at([0.0, 0.0, 0.0], [1.0, 1.0, 1.0]);
let b = box_at([9.0, 9.0, 9.0], [10.0, 10.0, 10.0]);
let token = CancellationToken::new();
token.cancel();
let cancelled = options().with_cancellation(token);
let result = provider.boolean(&a, &b, BooleanOperator::Union, &cancelled);
report.record(
"declared_cancellation_is_honoured",
match result {
Err(GeomError::Cancelled) => Outcome::Passed,
Err(GeomError::Unsupported { .. }) => Outcome::Skipped {
reason: "provider declined the operation".into(),
},
Ok(_) => Outcome::Failed {
detail: format!(
"provider declares {granularity:?} polling but ran to completion \
with a pre-cancelled token"
),
},
Err(other) => Outcome::Failed {
detail: format!("expected Cancelled, got {other:?}"),
},
},
);
}