#![cfg(feature = "mesh-boolean")]
use axiolid_contracts::{
Backend, BackendDescriptor, BackendId, CancellationGranularity, CancellationToken,
ExecutionOptions, ExecutionTarget, GeomError, GeomResult, ScratchRequirement,
};
use axiolid_core::{BooleanOperator, Tolerance};
use axiolid_dispatch::MeshBooleanRegistry;
use axiolid_mesh::TriMesh;
use axiolid_mesh_boolean_contract::{BooleanEvidence, BooleanOutcome, MeshBoolean};
use axiolid_mesh_contracts::SolidRequirements;
fn cube() -> TriMesh {
let positions = vec![
[0.0, 0.0, 0.0].into(),
[1.0, 0.0, 0.0].into(),
[1.0, 1.0, 0.0].into(),
[0.0, 1.0, 0.0].into(),
[0.0, 0.0, 1.0].into(),
[1.0, 0.0, 1.0].into(),
[1.0, 1.0, 1.0].into(),
[0.0, 1.0, 1.0].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)
}
fn options() -> ExecutionOptions {
ExecutionOptions::new(Tolerance::METRE)
}
#[derive(Debug, Default)]
struct Recorder {
granularity: Option<CancellationGranularity>,
}
impl Backend for Recorder {
fn descriptor(&self) -> BackendDescriptor {
BackendDescriptor::new(BackendId::new("recorder"), ExecutionTarget::PortableCpu)
}
}
impl MeshBoolean for Recorder {
fn scratch_requirement(&self) -> ScratchRequirement {
ScratchRequirement::None
}
fn cancellation_granularity(&self) -> CancellationGranularity {
self.granularity.unwrap_or(CancellationGranularity::None)
}
fn boolean(
&self,
subject: &TriMesh,
_tool: &TriMesh,
_operation: BooleanOperator,
options: &ExecutionOptions,
) -> GeomResult<BooleanOutcome> {
options.check_cancelled()?;
let evidence = BooleanEvidence::record(subject.triangle_count(), 0, 0, 0);
Ok(BooleanOutcome::new(subject.clone(), evidence))
}
}
fn registry_with(provider: Recorder) -> MeshBooleanRegistry {
let mut registry = MeshBooleanRegistry::new();
registry.register(0, provider);
registry
}
#[test]
fn the_operand_set_matches_the_planar_contract() {
assert_eq!(BooleanOperator::ALL.len(), 4);
assert!(BooleanOperator::ALL.contains(&BooleanOperator::SymmetricDifference));
}
#[test]
fn difference_is_the_only_ordered_operand() {
assert!(!BooleanOperator::Difference.is_commutative());
for operation in BooleanOperator::ALL {
if operation != BooleanOperator::Difference {
assert!(
operation.is_commutative(),
"{operation:?} should be commutative"
);
}
}
}
#[test]
fn the_contract_rejects_inadmissible_operands_before_dispatch() {
let registry = registry_with(Recorder::default());
let good = cube();
let mut flipped = cube();
for triangle in flipped.indices.chunks_exact_mut(3) {
triangle.swap(0, 1);
}
let error = registry
.boolean(&good, &flipped, BooleanOperator::Difference, &options())
.unwrap_err();
assert!(
matches!(error, GeomError::InvalidInput(ref detail) if detail.contains("tool[0]")),
"rejection must name the offending operand, got {error:?}"
);
let empty = TriMesh::new(Vec::new(), Vec::new());
assert!(matches!(
registry.boolean(&good, &empty, BooleanOperator::Union, &options()),
Err(GeomError::InvalidInput(_))
));
}
#[test]
fn admissibility_levels_are_cumulative() {
let good = cube();
let sheet = TriMesh::new(
vec![
[0.0, 0.0, 0.0].into(),
[1.0, 0.0, 0.0].into(),
[1.0, 1.0, 0.0].into(),
],
vec![0, 1, 2],
);
assert!(SolidRequirements::Structural.validate(&sheet, "s").is_ok());
assert!(matches!(
SolidRequirements::Enclosing.validate(&sheet, "s"),
Err(GeomError::Degenerate(_))
));
assert!(SolidRequirements::Oriented.validate(&good, "s").is_ok());
}
#[test]
fn solid_admission_rejects_positive_infinite_signed_volume() {
let magnitude = 1.0e308;
let epsilon = 1.0e-308;
let overflow = TriMesh::new(
vec![
[epsilon, epsilon, 1.0].into(),
[magnitude, 1.0, epsilon].into(),
[epsilon, magnitude, 1.0].into(),
],
vec![0, 1, 2],
);
let six_volume = overflow.positions[0].dot(overflow.positions[1].cross(overflow.positions[2]));
assert!(six_volume == f64::INFINITY, "fixture produced {six_volume}");
let error = SolidRequirements::Oriented
.validate(&overflow, "subject")
.unwrap_err();
assert!(
matches!(error, GeomError::InvalidInput(ref detail) if detail.contains("non-finite signed volume")),
"positive-infinite signed volume must fail closed, got {error:?}"
);
}
#[test]
fn every_boolean_reports_what_it_did() {
let registry = registry_with(Recorder::default());
let cube = cube();
let outcome = registry
.boolean(&cube, &cube, BooleanOperator::Union, &options())
.expect("admissible operands");
assert_eq!(outcome.evidence.subject_triangles, cube.triangle_count());
assert_eq!(outcome.evidence.sub_operations, 1);
assert_eq!(outcome.mesh.triangle_count(), cube.triangle_count());
assert!(!outcome.is_empty());
}
#[test]
fn evidence_absorbs_sub_operations_without_losing_input_counts() {
let mut total = BooleanEvidence::record(12, 6, 12, 1);
total.absorb(BooleanEvidence::record(0, 0, 8, 2).with_sub_operations(3));
assert_eq!(total.subject_triangles, 12);
assert_eq!(total.tool_triangles, 6);
assert_eq!(total.output_triangles, 8);
assert_eq!(total.output_components, 2);
assert_eq!(total.sub_operations, 4);
}
#[test]
fn a_cancelled_token_stops_the_operation() {
let registry = registry_with(Recorder::default());
let cube = cube();
let token = CancellationToken::new();
token.cancel();
let error = registry
.boolean(
&cube,
&cube,
BooleanOperator::Union,
&options().with_cancellation(token),
)
.unwrap_err();
assert_eq!(error, GeomError::Cancelled);
}
#[test]
fn cancellation_is_opt_in_and_absent_by_default() {
assert!(options().cancellation().is_none());
assert!(options().check_cancelled().is_ok());
let token = CancellationToken::new();
let with = options().with_cancellation(token.clone());
assert!(with.check_cancelled().is_ok(), "not cancelled yet");
token.cancel();
assert!(with.check_cancelled().is_err(), "clones share one flag");
}
#[test]
fn token_equality_is_identity_not_current_state() {
let first = CancellationToken::new();
let second = CancellationToken::new();
assert!(!first.is_cancelled() && !second.is_cancelled());
assert_ne!(first, second, "distinct sources must not compare equal");
let shared = first.clone();
assert_eq!(first, shared);
first.cancel();
assert_eq!(first, shared, "cancelling must not change identity");
assert!(shared.is_cancelled(), "the flag is genuinely shared");
}
#[test]
fn a_provider_declares_its_real_polling_granularity() {
assert_eq!(
Recorder::default().cancellation_granularity(),
CancellationGranularity::None
);
let batching = Recorder {
granularity: Some(CancellationGranularity::BetweenOperations),
};
assert_eq!(
batching.cancellation_granularity(),
CancellationGranularity::BetweenOperations
);
}