use crate::processors::*;
use crate::{BoolFailure, GeometryProcessor, GeometryRouter, Mesh, Result, TessellationQuality};
use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcSchema, IfcType};
use std::cell::Cell;
use std::rc::Rc;
const UNSUPPORTED_BOOLEAN: &str = "#1=IFCBOOLEANRESULT(.DIFFERENCE.,#2,#3);\n\
#2=IFCSECTIONEDSPINE($,(),());\n#3=IFCEXTRUDEDAREASOLID($,$,$,1.);";
fn mesh_unsupported_boolean(router: &GeometryRouter) {
let mut decoder = EntityDecoder::new(UNSUPPORTED_BOOLEAN);
let entity = decoder.decode_by_id(1).unwrap();
let mesh = router.process_representation_item(&entity, &mut decoder).unwrap();
assert!(mesh.is_empty(), "unsupported base operands must not invent geometry");
}
fn failure_count(router: &GeometryRouter) -> usize {
router.take_csg_failures().values().map(Vec::len).sum()
}
struct RegisteredBoolean {
processor: BooleanClippingProcessor,
types: Vec<IfcType>,
drops: Rc<Cell<usize>>,
}
impl Drop for RegisteredBoolean {
fn drop(&mut self) { self.drops.set(self.drops.get() + 1); }
}
impl GeometryProcessor for RegisteredBoolean {
fn process(&self, entity: &DecodedEntity, decoder: &mut EntityDecoder,
schema: &IfcSchema, quality: TessellationQuality) -> Result<Mesh> {
self.processor.process(entity, decoder, schema, quality)
}
fn supported_types(&self) -> Vec<IfcType> { self.types.clone() }
fn take_bool_failures(&self) -> Vec<BoolFailure> { self.processor.take_bool_failures() }
}
fn register_boolean(router: &mut GeometryRouter, types: Vec<IfcType>, drops: &Rc<Cell<usize>>) {
router.register(Box::new(RegisteredBoolean {
processor: BooleanClippingProcessor::new(), types, drops: drops.clone(),
}));
}
#[test]
fn issue_3987_default_dispatch_preserves_every_advertised_subtype() {
let schema = IfcSchema::new();
let references: Vec<Box<dyn GeometryProcessor>> = vec![
Box::new(ExtrudedAreaSolidProcessor::new(schema.clone())),
Box::new(ExtrudedAreaSolidTaperedProcessor::new(schema.clone())),
Box::new(TriangulatedFaceSetProcessor::new()), Box::new(PolygonalFaceSetProcessor::new()),
Box::new(FacetedBrepProcessor::new()), Box::new(BooleanClippingProcessor::new()),
Box::new(SweptDiskSolidProcessor::new(schema.clone())),
Box::new(RevolvedAreaSolidProcessor::new(schema.clone())),
Box::new(SurfaceCurveSweptAreaSolidProcessor::new(schema.clone())),
Box::new(SectionedSolidHorizontalProcessor::new(schema.clone())),
Box::new(AdvancedBrepProcessor::new()), Box::new(BSplineSurfaceProcessor::new()),
Box::new(ShellBasedSurfaceModelProcessor::new()), Box::new(FaceBasedSurfaceModelProcessor::new()),
Box::new(BlockProcessor::new()), Box::new(SphereProcessor::new()),
Box::new(CsgSolidProcessor::new()), Box::new(IfcAlignmentProcessor::new()),
];
let router = GeometryRouter::new();
for processor in references {
for ifc_type in processor.supported_types() {
let source = format!("#1={}();", ifc_type.to_string().to_uppercase());
let mut direct_decoder = EntityDecoder::new(&source);
let mut routed_decoder = EntityDecoder::new(&source);
let direct_entity = direct_decoder.decode_by_id(1).unwrap();
let routed_entity = routed_decoder.decode_by_id(1).unwrap();
let expected = processor.process(&direct_entity, &mut direct_decoder, &schema, TessellationQuality::Medium);
let actual = router.process_representation_item(&routed_entity, &mut routed_decoder);
match (expected, actual) {
(Err(expected), Err(actual)) => assert_eq!(actual.to_string(), expected.to_string(), "{ifc_type:?}"),
(Ok(expected), Ok(actual)) => {
assert_eq!(actual.positions, expected.positions, "{ifc_type:?}");
assert_eq!(actual.indices, expected.indices, "{ifc_type:?}");
assert_eq!(actual.normals, expected.normals, "{ifc_type:?}");
}
(expected, actual) => panic!("{ifc_type:?}: direct={expected:?}, routed={actual:?}"),
}
}
}
}
#[test]
fn issue_3987_partial_override_preserves_the_remaining_alias_diagnostics() {
let mut router = GeometryRouter::new();
mesh_unsupported_boolean(&router);
let drops = Rc::new(Cell::new(0));
register_boolean(&mut router, vec![IfcType::IfcBooleanResult], &drops);
assert_eq!(failure_count(&router), 1, "the clipping alias still owns the original log");
mesh_unsupported_boolean(&router);
assert_eq!(failure_count(&router), 1, "the replacement reports its own meshing failure");
assert_eq!(failure_count(&router), 0, "aliases drain a shared log only once");
}
#[test]
fn issue_3987_full_override_releases_the_replaced_processor_and_its_log() {
let mut router = GeometryRouter::new();
mesh_unsupported_boolean(&router);
router.register(Box::new(BooleanClippingProcessor::new()));
assert_eq!(failure_count(&router), 0, "last-alias override must discard the old built-in log");
let drops = Rc::new(Cell::new(0));
register_boolean(&mut router, vec![IfcType::IfcBooleanResult, IfcType::IfcBooleanClippingResult], &drops);
mesh_unsupported_boolean(&router);
let replacements = Rc::new(Cell::new(0));
register_boolean(&mut router, vec![IfcType::IfcBooleanResult], &replacements);
assert_eq!(drops.get(), 0, "one remaining alias still owns the processor");
register_boolean(&mut router, vec![IfcType::IfcBooleanClippingResult], &replacements);
assert_eq!(drops.get(), 1, "the final alias releases the processor immediately");
assert_eq!(failure_count(&router), 0, "orphaned processors cannot report stale failures");
mesh_unsupported_boolean(&router);
assert_eq!(failure_count(&router), 1);
}
#[test]
fn issue_3987_two_routers_cannot_share_failure_state() {
let first = GeometryRouter::new();
let second = GeometryRouter::new();
mesh_unsupported_boolean(&first);
assert_eq!(failure_count(&second), 0);
mesh_unsupported_boolean(&second);
assert_eq!(failure_count(&first), 1);
assert_eq!(failure_count(&second), 1);
}