use super::*;
#[test]
fn test_rectangle_profile() {
let content = r#"
#1=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,100.0,200.0);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(1).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 4);
assert!(!profile.outer.is_empty());
}
#[test]
fn test_circle_profile() {
let content = r#"
#1=IFCCIRCLEPROFILEDEF(.AREA.,$,$,50.0);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(1).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 36); assert!(!profile.outer.is_empty());
}
#[test]
fn test_i_shape_profile() {
let content = r#"
#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,200.0,300.0,10.0,15.0,$,$,$,$);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(1).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 12); assert!(!profile.outer.is_empty());
}
fn outer_area(profile: &Profile2D) -> f64 {
let p = &profile.outer;
let n = p.len();
let mut a = 0.0;
for i in 0..n {
let b = p[(i + 1) % n];
a += p[i].x * b.y - b.x * p[i].y;
}
a.abs() * 0.5
}
#[test]
fn test_i_shape_honours_fillet_radius() {
let sharp = process_content(
"#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,180.,171.,6.,9.5,$,$,$);\n",
1,
);
let filleted = process_content(
"#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,180.,171.,6.,9.5,15.,$,$);\n",
1,
);
assert_eq!(sharp.outer.len(), 12, "sharp I should stay 12 points");
assert!(
filleted.outer.len() > 12,
"fillets not generated: {} points",
filleted.outer.len()
);
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let expected = 4332.0 + 4.0 * 15.0 * 15.0 * k;
let area = outer_area(&filleted);
assert!(
(area - expected).abs() < 15.0 && area > outer_area(&sharp) + 100.0,
"I fillet area {area:.2} vs expected {expected:.2} (sharp {:.2})",
outer_area(&sharp)
);
let (mnx, mny, mxx, mxy) = outer_bbox(&filleted);
assert!((mxx - mnx - 180.0).abs() < 1e-6 && (mxy - mny - 171.0).abs() < 1e-6);
}
#[test]
fn test_u_shape_honours_radii() {
let sharp = process_content(
"#1=IFCUSHAPEPROFILEDEF(.AREA.,$,$,200.,80.,10.,12.,$,$,$,$);\n",
1,
);
let filleted = process_content(
"#1=IFCUSHAPEPROFILEDEF(.AREA.,$,$,200.,80.,10.,12.,12.,6.,$,$);\n",
1,
);
assert_eq!(sharp.outer.len(), 8);
assert!(filleted.outer.len() > 8, "U fillets not generated");
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let expected = 3680.0 + 2.0 * 144.0 * k - 2.0 * 36.0 * k;
let area = outer_area(&filleted);
assert!(
(area - expected).abs() < 12.0,
"U area {area:.2} vs expected {expected:.2}"
);
let (mnx, mny, mxx, mxy) = outer_bbox(&filleted);
assert!((mxx - mnx - 80.0).abs() < 1e-6 && (mxy - mny - 200.0).abs() < 1e-6);
}
#[test]
fn test_t_shape_honours_radii() {
let sharp = process_content(
"#1=IFCTSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,12.,$,$,$,$,$);\n",
1,
);
let filleted = process_content(
"#1=IFCTSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,12.,8.,4.,3.,$,$);\n",
1,
);
assert_eq!(sharp.outer.len(), 8);
assert!(filleted.outer.len() > 8, "T fillets not generated");
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let expected = 1840.0 + 2.0 * 64.0 * k - 2.0 * 16.0 * k - 2.0 * 9.0 * k;
let area = outer_area(&filleted);
assert!(
(area - expected).abs() < 10.0,
"T area {area:.2} vs expected {expected:.2}"
);
let (mnx, mny, mxx, mxy) = outer_bbox(&filleted);
assert!((mxx - mnx - 80.0).abs() < 1e-6 && (mxy - mny - 100.0).abs() < 1e-6);
}
fn outer_bbox(profile: &Profile2D) -> (f64, f64, f64, f64) {
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for p in &profile.outer {
min_x = min_x.min(p.x);
min_y = min_y.min(p.y);
max_x = max_x.max(p.x);
max_y = max_y.max(p.y);
}
(min_x, min_y, max_x, max_y)
}
fn process_content(content: &str, id: u32) -> Profile2D {
process_content_at(content, id, TessellationQuality::Medium)
}
fn process_content_at(content: &str, id: u32, quality: TessellationQuality) -> Profile2D {
let mut decoder = EntityDecoder::new(content);
let processor = ProfileProcessor::new(IfcSchema::new());
let entity = decoder.decode_by_id(id).unwrap();
processor.process(&entity, &mut decoder, quality).unwrap()
}
#[test]
fn test_u_shape_is_centered() {
let profile =
process_content("#1=IFCUSHAPEPROFILEDEF(.AREA.,$,$,160.,64.,5.,8.4,$,$,$,$);\n", 1);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((min_x + max_x).abs() < 1e-9, "X not centred: {min_x}..{max_x}");
assert!((min_y + max_y).abs() < 1e-9, "Y not centred: {min_y}..{max_y}");
assert!((max_x - min_x - 64.0).abs() < 1e-9, "width should be FlangeWidth");
assert!((max_y - min_y - 160.0).abs() < 1e-9, "height should be Depth");
}
#[test]
fn test_l_shape_is_centered() {
let profile =
process_content("#1=IFCLSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,$,$,$,$,$);\n", 1);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((min_x + max_x).abs() < 1e-9, "X not centred: {min_x}..{max_x}");
assert!((min_y + max_y).abs() < 1e-9, "Y not centred: {min_y}..{max_y}");
assert!((max_x - min_x - 80.0).abs() < 1e-9, "width should be Width");
assert!((max_y - min_y - 100.0).abs() < 1e-9, "height should be Depth");
}
#[test]
fn test_l_shape_honours_fillet_and_edge_radii() {
let profile = process_content(
"#1=IFCLSHAPEPROFILEDEF(.AREA.,$,$,100.,100.,10.,12.,6.,$,$,$);\n",
1,
);
assert!(
profile.outer.len() > 6,
"fillets not generated: {} points",
profile.outer.len()
);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((max_x - min_x - 100.0).abs() < 1e-6, "width {}", max_x - min_x);
assert!((max_y - min_y - 100.0).abs() < 1e-6, "height {}", max_y - min_y);
let k = 1.0 - std::f64::consts::FRAC_PI_4;
let expected = 1900.0 + (144.0 - 72.0) * k;
let n = profile.outer.len();
let mut area = 0.0;
for i in 0..n {
let a = profile.outer[i];
let b = profile.outer[(i + 1) % n];
area += a.x * b.y - b.x * a.y;
}
area = area.abs() * 0.5;
assert!(
(area - expected).abs() < 5.0,
"L fillet area {area:.2} vs expected {expected:.2} — wrong fillet sign/placement"
);
}
#[test]
fn test_t_shape_is_centered() {
let profile = process_content(
"#1=IFCTSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,12.,$,$,$,$,$);\n",
1,
);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((min_x + max_x).abs() < 1e-9, "X not centred: {min_x}..{max_x}");
assert!((min_y + max_y).abs() < 1e-9, "Y not centred: {min_y}..{max_y}");
assert!((max_x - min_x - 80.0).abs() < 1e-9, "width should be FlangeWidth");
assert!((max_y - min_y - 100.0).abs() < 1e-9, "height should be Depth");
}
#[test]
fn test_c_shape_spans_width_and_depth() {
let profile = process_content(
"#1=IFCCSHAPEPROFILEDEF(.AREA.,$,$,200.,80.,6.,20.,$);\n",
1,
);
let (min_x, min_y, max_x, max_y) = outer_bbox(&profile);
assert!((min_x + max_x).abs() < 1e-9, "X not centred: {min_x}..{max_x}");
assert!((min_y + max_y).abs() < 1e-9, "Y not centred: {min_y}..{max_y}");
assert!(
(max_x - min_x - 80.0).abs() < 1e-9,
"width should be Width (80), got {}",
max_x - min_x
);
assert!(
(max_y - min_y - 200.0).abs() < 1e-9,
"height should be Depth (200), got {}",
max_y - min_y
);
}
#[test]
fn test_arbitrary_profile() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0));
#2=IFCCARTESIANPOINT((100.0,0.0));
#3=IFCCARTESIANPOINT((100.0,100.0));
#4=IFCCARTESIANPOINT((0.0,100.0));
#5=IFCPOLYLINE((#1,#2,#3,#4,#1));
#6=IFCARBITRARYCLOSEDPROFILEDEF(.AREA.,$,#5);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(6).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 5); assert!(!profile.outer.is_empty());
}
#[test]
fn test_derived_profile_applies_translation_rotation_and_scale() {
let content = r#"
#1=IFCDIRECTION((0.0,1.0));
#2=IFCCARTESIANPOINT((10.0,20.0));
#3=IFCCARTESIANTRANSFORMATIONOPERATOR2D(#1,$,#2,2.0);
#4=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,2.0,4.0);
#5=IFCDERIVEDPROFILEDEF(.AREA.,$,#4,#3,$);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(5).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 4);
assert!(profile.outer.contains(&Point2::new(14.0, 18.0)));
assert!(profile.outer.contains(&Point2::new(14.0, 22.0)));
assert!(profile.outer.contains(&Point2::new(6.0, 22.0)));
assert!(profile.outer.contains(&Point2::new(6.0, 18.0)));
}
#[test]
fn test_mirrored_profile_uses_derived_operator() {
let content = r#"
#1=IFCDIRECTION((-1.0,0.0));
#2=IFCDIRECTION((0.0,1.0));
#3=IFCCARTESIANPOINT((0.0,0.0));
#4=IFCCARTESIANTRANSFORMATIONOPERATOR2D(#1,#2,#3,1.0);
#5=IFCRECTANGLEPROFILEDEF(.AREA.,$,$,2.0,4.0);
#6=IFCMIRROREDPROFILEDEF(.AREA.,$,#5,#4,$);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let profile_entity = decoder.decode_by_id(6).unwrap();
let profile = processor
.process(&profile_entity, &mut decoder, TessellationQuality::Medium)
.unwrap();
assert_eq!(profile.outer.len(), 4);
assert!(profile.outer.contains(&Point2::new(1.0, -2.0)));
assert!(profile.outer.contains(&Point2::new(-1.0, -2.0)));
assert!(profile.outer.contains(&Point2::new(-1.0, 2.0)));
assert!(profile.outer.contains(&Point2::new(1.0, 2.0)));
}
fn approx_eq_p3(a: Point3<f64>, b: Point3<f64>, tol: f64) -> bool {
(a.x - b.x).abs() < tol && (a.y - b.y).abs() < tol && (a.z - b.z).abs() < tol
}
#[test]
fn test_trim_polyline_full_range() {
let pts = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(2.0, 0.0, 0.0),
];
let out = trim_polyline(&pts, 0.0, 1.0);
assert_eq!(out.len(), 3);
assert!(approx_eq_p3(out[0], pts[0], 1e-9));
assert!(approx_eq_p3(out[1], pts[1], 1e-9));
assert!(approx_eq_p3(out[2], pts[2], 1e-9));
}
#[test]
fn test_trim_polyline_halves() {
let pts = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(2.0, 0.0, 0.0),
];
let first_half = trim_polyline(&pts, 0.0, 0.5);
assert_eq!(first_half.len(), 2);
assert!(approx_eq_p3(first_half[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(first_half[1], Point3::new(1.0, 0.0, 0.0), 1e-9));
let second_half = trim_polyline(&pts, 0.5, 1.0);
assert_eq!(second_half.len(), 2);
assert!(approx_eq_p3(second_half[0], Point3::new(1.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(second_half[1], Point3::new(2.0, 0.0, 0.0), 1e-9));
}
#[test]
fn test_trim_polyline_strict_interior() {
let pts: Vec<Point3<f64>> = (0..5)
.map(|i| Point3::new(i as f64, 0.0, 0.0))
.collect();
let out = trim_polyline(&pts, 0.25, 0.75);
assert_eq!(out.len(), 3);
assert!(approx_eq_p3(out[0], Point3::new(1.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(out[1], Point3::new(2.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(out[2], Point3::new(3.0, 0.0, 0.0), 1e-9));
}
#[test]
fn test_trim_polyline_invalid_range() {
let pts = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)];
assert!(trim_polyline(&pts, 0.5, 0.5).is_empty());
assert!(trim_polyline(&pts, 0.6, 0.4).is_empty());
assert!(trim_polyline(&pts[..1], 0.0, 1.0).is_empty());
}
#[test]
fn test_trim_polyline_two_points_partial() {
let pts = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(10.0, 0.0, 0.0)];
let out = trim_polyline(&pts, 0.3, 0.7);
assert_eq!(out.len(), 2);
assert!(approx_eq_p3(out[0], Point3::new(3.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(out[1], Point3::new(7.0, 0.0, 0.0), 1e-9));
}
#[test]
fn test_composite_curve_trim_first_segment_only() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCCARTESIANPOINT((0.0,4.0,0.0));
#4=IFCCARTESIANPOINT((0.0,6.0,0.0));
#5=IFCPOLYLINE((#1,#2));
#6=IFCPOLYLINE((#2,#3));
#7=IFCPOLYLINE((#3,#4));
#8=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#5);
#9=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#6);
#10=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#7);
#11=IFCCOMPOSITECURVE((#8,#9,#10),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(11).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(1.0))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 2.0, 0.0), 1e-9));
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(1.0), Some(2.0))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 2.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 4.0, 0.0), 1e-9));
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(3.0))
.unwrap();
assert_eq!(pts.len(), 4);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[3], Point3::new(0.0, 6.0, 0.0), 1e-9));
}
#[test]
fn test_composite_curve_trim_clamps_out_of_range() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCPOLYLINE((#1,#2));
#4=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#3);
#5=IFCCOMPOSITECURVE((#4),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(5).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(-5.0), Some(1.0))
.unwrap();
assert_eq!(pts.len(), 2);
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(99.0))
.unwrap();
assert_eq!(pts.len(), 2);
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.5), Some(0.5))
.unwrap();
assert!(pts.is_empty());
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.8), Some(0.2))
.unwrap();
assert!(pts.is_empty());
}
#[test]
fn test_composite_curve_trim_fractional_multi_segment() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCCARTESIANPOINT((0.0,4.0,0.0));
#4=IFCCARTESIANPOINT((0.0,6.0,0.0));
#5=IFCPOLYLINE((#1,#2));
#6=IFCPOLYLINE((#2,#3));
#7=IFCPOLYLINE((#3,#4));
#8=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#5);
#9=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#6);
#10=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#7);
#11=IFCCOMPOSITECURVE((#8,#9,#10),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(11).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.5), Some(2.5))
.unwrap();
let ys: Vec<f64> = pts.iter().map(|p| p.y).collect();
assert_eq!(ys.len(), 4, "got points: {:?}", pts);
assert!((ys[0] - 1.0).abs() < 1e-9);
assert!((ys[1] - 2.0).abs() < 1e-9);
assert!((ys[2] - 4.0).abs() < 1e-9);
assert!((ys[3] - 5.0).abs() < 1e-9);
}
#[test]
fn test_polyline_trim_first_segment() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCCARTESIANPOINT((0.0,4.0,0.0));
#4=IFCCARTESIANPOINT((0.0,6.0,0.0));
#5=IFCPOLYLINE((#1,#2,#3,#4));
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(5).unwrap();
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(0.0), Some(1.0))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 2.0, 0.0), 1e-9));
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(1.0), Some(2.0))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 2.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 4.0, 0.0), 1e-9));
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(0.5), Some(2.5))
.unwrap();
let ys: Vec<f64> = pts.iter().map(|p| p.y).collect();
assert_eq!(ys.len(), 4, "got points: {:?}", pts);
assert!((ys[0] - 1.0).abs() < 1e-9);
assert!((ys[1] - 2.0).abs() < 1e-9);
assert!((ys[2] - 4.0).abs() < 1e-9);
assert!((ys[3] - 5.0).abs() < 1e-9);
}
#[test]
fn test_polyline_trim_clamps_and_inverts() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCPOLYLINE((#1,#2));
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(3).unwrap();
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, None, None)
.unwrap();
assert_eq!(pts.len(), 2);
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(0.8), Some(0.2))
.unwrap();
assert!(pts.is_empty());
let pts = processor
.get_polyline_points_trimmed(&curve, &mut decoder, Some(-5.0), Some(99.0))
.unwrap();
assert_eq!(pts.len(), 2);
}
#[test]
fn test_composite_curve_trim_keeps_non_coincident_junction() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,2.0,0.0));
#3=IFCCARTESIANPOINT((0.0,2.5,0.0));
#4=IFCCARTESIANPOINT((0.0,4.5,0.0));
#5=IFCPOLYLINE((#1,#2));
#6=IFCPOLYLINE((#3,#4));
#7=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#5);
#8=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.T.,#6);
#9=IFCCOMPOSITECURVE((#7,#8),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(9).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(2.0))
.unwrap();
assert_eq!(pts.len(), 4, "got points: {:?}", pts);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 0.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 2.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[2], Point3::new(0.0, 2.5, 0.0), 1e-9));
assert!(approx_eq_p3(pts[3], Point3::new(0.0, 4.5, 0.0), 1e-9));
}
#[test]
fn test_composite_curve_trim_same_sense_false() {
let content = r#"
#1=IFCCARTESIANPOINT((0.0,0.0,0.0));
#2=IFCCARTESIANPOINT((0.0,10.0,0.0));
#3=IFCPOLYLINE((#1,#2));
#4=IFCCOMPOSITECURVESEGMENT(.CONTINUOUS.,.F.,#3);
#5=IFCCOMPOSITECURVE((#4),.F.);
"#;
let mut decoder = EntityDecoder::new(content);
let schema = IfcSchema::new();
let processor = ProfileProcessor::new(schema);
let curve = decoder.decode_by_id(5).unwrap();
let pts = processor
.get_composite_curve_points_trimmed(&curve, &mut decoder, Some(0.0), Some(0.3))
.unwrap();
assert_eq!(pts.len(), 2);
assert!(approx_eq_p3(pts[0], Point3::new(0.0, 10.0, 0.0), 1e-9));
assert!(approx_eq_p3(pts[1], Point3::new(0.0, 7.0, 0.0), 1e-9));
}
#[test]
fn negative_profile_thickness_errors_not_panics() {
let bad = [
"#1=IFCLSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,-10.,$,$,$,$,$);\n",
"#1=IFCUSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,-10.,12.,$,$,$);\n",
"#1=IFCTSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,-10.,12.,$,$,$,$,$);\n",
"#1=IFCCSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,-10.,20.,$);\n",
"#1=IFCZSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,-10.,12.,$,$);\n",
];
for content in bad {
let mut decoder = EntityDecoder::new(content);
let processor = ProfileProcessor::new(IfcSchema::new());
let entity = decoder.decode_by_id(1).unwrap();
let result = processor.process(&entity, &mut decoder, TessellationQuality::Medium);
assert!(result.is_err(), "expected Err for malformed profile: {content}");
}
let mut decoder = EntityDecoder::new("#1=IFCLSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,$,$,$,$,$);\n");
let processor = ProfileProcessor::new(IfcSchema::new());
let entity = decoder.decode_by_id(1).unwrap();
assert!(processor
.process(&entity, &mut decoder, TessellationQuality::Medium)
.is_ok());
}
#[test]
fn steel_fillets_drop_to_sharp_corners_below_medium() {
const I_BEAM: &str = "#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,180.,171.,6.,9.5,15.,$,$);\n";
for q in [TessellationQuality::Low, TessellationQuality::Lowest] {
let profile = process_content_at(I_BEAM, 1, q);
assert_eq!(
profile.outer.len(),
12,
"{q:?} I-shape should be the 12-point sharp section"
);
let expected = 180.0 * 171.0 - 174.0 * 152.0;
let area = outer_area(&profile);
assert!(
(area - expected).abs() < 1e-6,
"{q:?} sharp I area {area:.3} vs {expected:.3}"
);
let (mnx, mny, mxx, mxy) = outer_bbox(&profile);
assert!((mxx - mnx - 180.0).abs() < 1e-6 && (mxy - mny - 171.0).abs() < 1e-6);
}
}
#[test]
fn steel_fillets_unchanged_at_medium_and_above() {
const I_BEAM: &str = "#1=IFCISHAPEPROFILEDEF(.AREA.,$,$,180.,171.,6.,9.5,15.,$,$);\n";
let medium = process_content_at(I_BEAM, 1, TessellationQuality::Medium);
assert!(
medium.outer.len() > 12,
"Medium must keep the fillet arcs, got {} points",
medium.outer.len()
);
for q in [TessellationQuality::High, TessellationQuality::Highest] {
let profile = process_content_at(I_BEAM, 1, q);
assert_eq!(profile.outer.len(), medium.outer.len(), "{q:?} vertex count");
for (a, b) in profile.outer.iter().zip(medium.outer.iter()) {
assert_eq!((a.x, a.y), (b.x, b.y), "{q:?} must be byte-identical to Medium");
}
}
}
#[test]
fn asymmetric_steel_radii_drop_together_below_medium() {
let cases = [
("#1=IFCLSHAPEPROFILEDEF(.AREA.,$,$,100.,80.,10.,12.,8.,$,$,$);\n", 6),
("#1=IFCUSHAPEPROFILEDEF(.AREA.,$,$,200.,80.,10.,12.,12.,6.,$,$);\n", 8),
("#1=IFCTSHAPEPROFILEDEF(.AREA.,$,$,200.,100.,10.,15.,12.,6.,4.,$,$);\n", 8),
];
for (content, sharp_points) in cases {
for q in [TessellationQuality::Low, TessellationQuality::Lowest] {
let profile = process_content_at(content, 1, q);
assert_eq!(
profile.outer.len(),
sharp_points,
"{q:?} expected {sharp_points} sharp points for {content}"
);
}
assert!(
process_content(content, 1).outer.len() > sharp_points,
"Medium must still round {content}"
);
}
}