use super::*;
use ifc_lite_core::EntityDecoder;
fn wrap(data: &str) -> String {
format!(
"ISO-10303-21;\nHEADER;\nFILE_DESCRIPTION((''),'2;1');\n\
FILE_NAME('t.ifc','2024-01-01T00:00:00',(''),(''),'','','');\n\
FILE_SCHEMA(('IFC4'));\nENDSEC;\nDATA;\n{data}ENDSEC;\nEND-ISO-10303-21;\n"
)
}
fn profile_points(data: &str, id: u32) -> Vec<Point2<f64>> {
let content = wrap(data);
let mut decoder = EntityDecoder::new(&content);
SurfaceOfLinearExtrusionProcessor::get_profile_curve_points(id, &mut decoder)
.expect("profile points")
}
const WELLFORMED: &str = "#10=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#20);\n\
#20=IFCCOMPOSITECURVE((#21),.F.);\n\
#21=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#22);\n\
#22=IFCPOLYLINE((#23,#24,#25));\n\
#23=IFCCARTESIANPOINT((0.,0.));\n\
#24=IFCCARTESIANPOINT((1.,0.));\n\
#25=IFCCARTESIANPOINT((1.,1.));\n";
#[test]
fn a_composite_curve_profile_yields_its_segment_points() {
let pts = profile_points(WELLFORMED, 10);
assert_eq!(
pts.len(),
3,
"a composite profile with one 3-point polyline segment must yield 3 points, got {}",
pts.len()
);
assert_eq!(pts[0], Point2::new(0.0, 0.0));
assert_eq!(pts[2], Point2::new(1.0, 1.0));
}
#[test]
fn two_segments_join_without_duplicating_the_seam() {
let data = "#10=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#20);\n\
#20=IFCCOMPOSITECURVE((#21,#31),.F.);\n\
#21=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#22);\n\
#22=IFCPOLYLINE((#23,#24,#25));\n\
#23=IFCCARTESIANPOINT((0.,0.));\n\
#24=IFCCARTESIANPOINT((1.,0.));\n\
#25=IFCCARTESIANPOINT((1.,1.));\n\
#31=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#32);\n\
#32=IFCPOLYLINE((#33,#34,#35));\n\
#33=IFCCARTESIANPOINT((1.,1.));\n\
#34=IFCCARTESIANPOINT((2.,1.));\n\
#35=IFCCARTESIANPOINT((2.,2.));\n";
assert_eq!(profile_points(data, 10).len(), 5);
}
#[test]
fn self_referential_composite_curve_terminates() {
let data = "#10=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#20);\n\
#20=IFCCOMPOSITECURVE((#21),.F.);\n\
#21=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#20);\n";
assert!(profile_points(data, 10).is_empty());
}
#[test]
fn two_node_composite_curve_cycle_terminates() {
let data = "#10=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#20);\n\
#20=IFCCOMPOSITECURVE((#21),.F.);\n\
#21=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#30);\n\
#30=IFCCOMPOSITECURVE((#31),.F.);\n\
#31=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#20);\n";
assert!(profile_points(data, 10).is_empty());
}
#[test]
fn cyclic_composite_curve_with_fan_out_terminates() {
let data = "#10=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#20);\n\
#20=IFCCOMPOSITECURVE((#21,#22,#23,#24),.F.);\n\
#21=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#20);\n\
#22=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#20);\n\
#23=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#20);\n\
#24=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#20);\n";
assert!(profile_points(data, 10).is_empty());
}
#[test]
fn parent_curve_pointing_at_a_profile_terminates() {
let data = "#10=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#20);\n\
#20=IFCCOMPOSITECURVE((#21),.F.);\n\
#21=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#10);\n";
assert!(profile_points(data, 10).is_empty());
}
#[test]
fn a_long_acyclic_curve_chain_terminates() {
let n: u32 = 3_000;
let mut data = String::from("#1=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#100);\n");
for i in 0..n {
let cc = 100 + i * 2;
let seg = 101 + i * 2;
let next = if i + 1 == n { 90000 } else { 100 + (i + 1) * 2 };
data.push_str(&format!("#{cc}=IFCCOMPOSITECURVE((#{seg}),.F.);\n"));
data.push_str(&format!(
"#{seg}=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#{next});\n"
));
}
data.push_str("#90000=IFCPOLYLINE((#90001,#90002));\n");
data.push_str("#90001=IFCCARTESIANPOINT((0.,0.));\n");
data.push_str("#90002=IFCCARTESIANPOINT((1.,0.));\n");
assert!(profile_points(&data, 1).is_empty());
}
#[test]
fn a_reused_parent_curve_contributes_to_every_segment() {
let data = "#10=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#20);\n\
#20=IFCCOMPOSITECURVE((#21,#31),.F.);\n\
#21=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#22);\n\
#31=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#22);\n\
#22=IFCPOLYLINE((#23,#24,#25));\n\
#23=IFCCARTESIANPOINT((0.,0.));\n\
#24=IFCCARTESIANPOINT((1.,0.));\n\
#25=IFCCARTESIANPOINT((1.,1.));\n";
assert_eq!(profile_points(data, 10).len(), 6);
}
#[test]
fn same_sense_false_reverses_the_segment() {
let data = "#10=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#20);\n\
#20=IFCCOMPOSITECURVE((#21),.F.);\n\
#21=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.F.,#22);\n\
#22=IFCPOLYLINE((#23,#24,#25));\n\
#23=IFCCARTESIANPOINT((0.,0.));\n\
#24=IFCCARTESIANPOINT((1.,0.));\n\
#25=IFCCARTESIANPOINT((1.,1.));\n";
let pts = profile_points(data, 10);
assert_eq!(pts.len(), 3);
assert_eq!(pts[0], Point2::new(1.0, 1.0), "reversed: last authored point first");
assert_eq!(pts[2], Point2::new(0.0, 0.0));
}
#[test]
fn a_discontinuous_joint_keeps_both_endpoints() {
let data = "#10=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#20);\n\
#20=IFCCOMPOSITECURVE((#21,#31),.F.);\n\
#21=IFCCOMPOSITECURVESEGMENT(.DISCONTINUOUS.,.T.,#22);\n\
#22=IFCPOLYLINE((#23,#24));\n\
#23=IFCCARTESIANPOINT((0.,0.));\n\
#24=IFCCARTESIANPOINT((1.,0.));\n\
#31=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#32);\n\
#32=IFCPOLYLINE((#33,#34));\n\
#33=IFCCARTESIANPOINT((5.,5.));\n\
#34=IFCCARTESIANPOINT((6.,5.));\n";
let pts = profile_points(data, 10);
assert_eq!(pts.len(), 4, "the joint does not coincide, so no point is dropped");
assert_eq!(pts[2], Point2::new(5.0, 5.0));
}
fn dag_data(levels: u32) -> String {
let mut d = String::from("#1=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#10);\n");
for i in 0..levels {
let cc = 10 + i;
let next = if i + 1 == levels { 9000 } else { 10 + i + 1 };
let (s1, s2) = (1000 + i * 2, 1001 + i * 2);
d.push_str(&format!("#{cc}=IFCCOMPOSITECURVE((#{s1},#{s2}),.F.);\n"));
d.push_str(&format!("#{s1}=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#{next});\n"));
d.push_str(&format!("#{s2}=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#{next});\n"));
}
d.push_str("#9000=IFCPOLYLINE((#9001,#9002));\n");
d.push_str("#9001=IFCCARTESIANPOINT((0.,0.));\n");
d.push_str("#9002=IFCCARTESIANPOINT((1.,0.));\n");
d
}
#[test]
fn a_modest_acyclic_dag_still_resolves_completely() {
let content = wrap(&dag_data(8));
let mut decoder = EntityDecoder::new(&content);
let item = decoder.decode_by_id(1).expect("decode profile");
let pts = SurfaceOfLinearExtrusionProcessor::get_profile_curve_points(item.id, &mut decoder)
.expect("8 levels must resolve");
assert!(pts.len() > 100, "expected the full expansion, got {}", pts.len());
}
#[test]
fn a_wide_acyclic_dag_is_bounded_by_the_node_budget() {
let content = wrap(&dag_data(30));
let mut decoder = EntityDecoder::new(&content);
let item = decoder.decode_by_id(1).expect("decode profile");
let err = SurfaceOfLinearExtrusionProcessor::get_profile_curve_points(item.id, &mut decoder)
.expect_err("a 2^30 traversal must be refused, not attempted");
assert!(
err.to_string().contains("exceeded"),
"the node budget must be what stops it, got: {err}"
);
}
#[test]
fn exactly_max_curve_nodes_visits_is_not_exhaustion() {
let mut walk = CurveWalk::new();
for i in 0..MAX_CURVE_NODES {
walk.spend()
.unwrap_or_else(|e| panic!("visit {i} of {MAX_CURVE_NODES} must be permitted: {e}"));
}
assert_eq!(walk.budget, 0, "the budget is spent to the last unit");
assert!(
!walk.exhausted,
"spending the budget exactly is not exhaustion"
);
assert!(walk.spend().is_err(), "the next visit must be refused");
assert!(walk.exhausted, "and it must record that it refused one");
}
#[test]
fn a_traversal_of_exactly_the_budget_still_returns_its_profile() {
let segs = MAX_CURVE_NODES - 1;
let mut d = String::with_capacity(segs as usize * 60);
let refs: Vec<String> = (0..segs).map(|i| format!("#{}", 500_000 + i)).collect();
d.push_str("#1=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#10);\n");
d.push_str(&format!("#10=IFCCOMPOSITECURVE(({}),.F.);\n", refs.join(",")));
for i in 0..segs {
d.push_str(&format!(
"#{}=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#9000);\n",
500_000 + i
));
}
d.push_str("#9000=IFCPOLYLINE((#9001,#9002));\n");
d.push_str("#9001=IFCCARTESIANPOINT((0.,0.));\n");
d.push_str("#9002=IFCCARTESIANPOINT((1.,0.));\n");
let content = wrap(&d);
let mut decoder = EntityDecoder::new(&content);
let item = decoder.decode_by_id(1).expect("decode profile");
let pts = SurfaceOfLinearExtrusionProcessor::get_profile_curve_points(item.id, &mut decoder)
.expect("a traversal that spends the budget exactly must not be refused");
assert!(!pts.is_empty(), "and it must return its points");
}