#![allow(clippy::unwrap_used)]
#![allow(clippy::default_trait_access)]
use std::io::Cursor;
use cadmpeg_ir::codec::{Codec, DecodeOptions};
use cadmpeg_ir::examples::unit_cube;
use cadmpeg_ir::geometry::{CurveGeometry, SurfaceGeometry};
use cadmpeg_ir::math::{Point3, Vector3};
use crate::ids::StepIdentity;
use crate::loss::StepLossCode;
use crate::test_support::export;
use crate::{write_step, StepCodec, StepWriteOptions};
#[test]
fn base_face_with_polygon_loop_gets_an_inferred_plane() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace(
"#25=EDGE_LOOP('',(#22,#23,#24));",
"#25=POLY_LOOP('',(#3,#4,#5));",
)
.replace(
"#29=ADVANCED_FACE('',(#26),#28,.T.);",
"#29=FACE('',(#26));",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode base face");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert_eq!(decoded.ir().model.faces.len(), 1);
let surface = decoded
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#implicit-face-29")
.expect("implicit face plane");
let SurfaceGeometry::Plane {
origin,
normal,
u_axis,
} = &surface.geometry
else {
panic!("implicit face did not produce a plane");
};
assert_eq!(*normal, Vector3::new(0.0, 0.0, 1.0));
assert_eq!(*origin, Point3::new(10.0 / 3.0, 10.0 / 3.0, 0.0));
assert_eq!(*u_axis, Vector3::new(1.0, 0.0, 0.0));
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn implicit_face_plane_uses_poly_loop_orientation_and_rejects_non_planar_points() {
let source = String::from_utf8(include_bytes!("data/tp06_implicit_face_plane.p21").to_vec())
.expect("fixture is UTF-8");
let base = StepCodec::default()
.decode(&mut Cursor::new(source.clone()), &DecodeOptions::default())
.expect("decode implicit face witness");
let base_surface = base
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#implicit-face-8")
.expect("base implicit plane");
let SurfaceGeometry::Plane { normal, origin, .. } = base_surface.geometry else {
panic!("base face did not produce a plane");
};
assert_eq!(normal, Vector3::new(0.0, 0.0, 1.0));
assert_eq!(origin, Point3::new(2.0, 1.5, 0.0));
let rotated = source.replace(
"#6=POLY_LOOP('outer',(#3,#4,#5,#11));",
"#6=POLY_LOOP('outer',(#4,#5,#11,#3));",
);
let rotated = StepCodec::default()
.decode(&mut Cursor::new(rotated), &DecodeOptions::default())
.expect("decode rotated implicit face witness");
let rotated_surface = rotated
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#implicit-face-8")
.expect("rotated implicit plane");
assert_eq!(rotated_surface.geometry, base_surface.geometry);
let reversed = source.replace(
"#7=FACE_OUTER_BOUND('outer',#6,.T.);",
"#7=FACE_OUTER_BOUND('outer',#6,.F.);",
);
let reversed = StepCodec::default()
.decode(&mut Cursor::new(reversed), &DecodeOptions::default())
.expect("decode reversed implicit face witness");
let reversed_surface = reversed
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#implicit-face-8")
.expect("reversed implicit plane");
let SurfaceGeometry::Plane { normal, .. } = reversed_surface.geometry else {
panic!("reversed face did not produce a plane");
};
assert_eq!(normal, Vector3::new(0.0, 0.0, -1.0));
let non_planar = source.replace(
"#5=CARTESIAN_POINT('c',(4.,3.,0.));",
"#5=CARTESIAN_POINT('c',(4.,3.,1.));",
);
let non_planar = StepCodec::default()
.decode(&mut Cursor::new(non_planar), &DecodeOptions::default())
.expect("decode non-planar implicit face witness");
assert!(non_planar.ir().model.bodies.is_empty());
assert!(!non_planar
.ir()
.model
.surfaces
.iter()
.any(|surface| surface.id.as_str() == "step:data:surface#implicit-face-8"));
assert!(non_planar.report().losses.iter().any(|loss| {
loss.code == StepLossCode::TopologyRootRejected.kind()
&& loss.message.contains("implicit face plane")
}));
}
#[test]
fn complex_face_bound_partials_keep_attributes_when_reordered() {
let source = String::from_utf8(include_bytes!("data/tp08_face_bound_partial.p21").to_vec())
.expect("fixture is UTF-8");
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source.clone()), &DecodeOptions::default())
.expect("decode complex face-bound witness");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert_eq!(decoded.ir().model.faces.len(), 1);
assert_eq!(decoded.ir().model.loops.len(), 1);
assert_eq!(
decoded.ir().model.loops[0].boundary_role,
cadmpeg_ir::topology::LoopBoundaryRole::Outer
);
assert!(decoded.ir().model.surfaces.iter().any(|surface| {
surface.id.as_str() == "step:data:surface#implicit-face-8"
&& matches!(surface.geometry, SurfaceGeometry::Plane { .. })
}));
let reordered = source.replace(
"#7=(FACE_BOUND('outer',#6,.T.) FACE_OUTER_BOUND());",
"#7=(FACE_OUTER_BOUND() FACE_BOUND('outer',#6,.T.));",
);
let reordered = StepCodec::default()
.decode(&mut Cursor::new(reordered), &DecodeOptions::default())
.expect("decode reordered complex face-bound witness");
assert!(reordered
.report()
.losses
.iter()
.any(|loss| loss.code == StepLossCode::ParseNoncanonicalSyntax.kind()));
assert_eq!(reordered.ir().model.bodies.len(), 1);
assert_eq!(reordered.ir().model.loops.len(), 1);
assert_eq!(
reordered.ir().model.loops[0].boundary_role,
cadmpeg_ir::topology::LoopBoundaryRole::Outer
);
assert!(reordered.ir().model.surfaces.iter().any(|surface| {
surface.id.as_str() == "step:data:surface#implicit-face-8"
&& matches!(surface.geometry, SurfaceGeometry::Plane { .. })
}));
let validation =
cadmpeg_ir::validate_neutral(reordered.ir(), reordered.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn edge_loop_without_explicit_surface_rejects_implicit_plane() {
let source = String::from_utf8(include_bytes!("data/tp06_edge_loop_base_face.p21").to_vec())
.expect("fixture is UTF-8");
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode edge-loop base-face witness");
assert!(decoded.ir().model.bodies.is_empty());
assert!(decoded
.ir()
.model
.surfaces
.iter()
.all(|surface| surface.id.as_str() != "step:data:surface#implicit-face-26"));
assert!(decoded.report().losses.iter().any(|loss| {
loss.code == StepLossCode::TopologyRootRejected.kind()
&& loss.message.contains("implicit face plane")
}));
}
#[test]
fn non_planar_base_face_is_rejected_without_an_inferred_surface() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace(
"#25=EDGE_LOOP('',(#22,#23,#24));",
"#25=POLY_LOOP('',(#3,#4,#5));",
)
.replace(
"#29=ADVANCED_FACE('',(#26),#28,.T.);",
"#29=FACE('',(#26));",
)
.replace(
"#25=POLY_LOOP('',(#3,#4,#5));",
"#70=CARTESIAN_POINT('',(5.,5.,1.));\n#25=POLY_LOOP('',(#3,#4,#5,#70));",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode non-planar base face");
assert!(decoded.ir().model.bodies.is_empty());
assert!(!decoded
.ir()
.model
.surfaces
.iter()
.any(|surface| surface.id.as_str() == "step:data:surface#implicit-face-29"));
assert!(decoded.report().losses.iter().any(|loss| {
loss.code == StepLossCode::TopologyRootRejected.kind()
&& loss.severity == cadmpeg_ir::Severity::Error
}));
}
#[test]
fn complex_outer_face_bound_uses_inherited_attributes() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace(
"#26=FACE_OUTER_BOUND('',#25,.T.);",
"#26=(FACE_BOUND('',#25,.T.) FACE_OUTER_BOUND());",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode complex face bound");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert_eq!(decoded.ir().model.faces.len(), 1);
let surface = decoded
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#28")
.expect("explicit face plane");
assert!(matches!(surface.geometry, SurfaceGeometry::Plane { .. }));
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn implicit_face_plane_uses_all_coplanar_poly_loops() {
let source = String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace(
"#25=EDGE_LOOP('',(#22,#23,#24));",
"#25=POLY_LOOP('',(#3,#4,#5));",
)
.replace(
"#29=ADVANCED_FACE('',(#26),#28,.T.);",
"#70=CARTESIAN_POINT('',(2.,2.,0.));\n#71=CARTESIAN_POINT('',(2.,3.,0.));\n#72=CARTESIAN_POINT('',(3.,2.,0.));\n#73=POLY_LOOP('',(#70,#71,#72));\n#74=FACE_BOUND('',#73,.F.);\n#29=FACE('',(#74,#26));",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode base face with a hole");
let surface = decoded
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#implicit-face-29")
.expect("implicit face plane");
let SurfaceGeometry::Plane { normal, origin, .. } = surface.geometry else {
panic!("implicit face did not produce a plane");
};
assert_eq!(normal, Vector3::new(0.0, 0.0, 1.0));
assert_eq!(origin, Point3::new(17.0 / 6.0, 17.0 / 6.0, 0.0));
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn implicit_face_plane_is_independent_of_coplanar_bound_set_order() {
let first = StepCodec::default()
.decode(
&mut Cursor::new(include_bytes!("data/tp06_coplanar_bounds_first.p21")),
&DecodeOptions::default(),
)
.expect("decode coplanar first-order witness");
let reordered = StepCodec::default()
.decode(
&mut Cursor::new(include_bytes!("data/tp06_coplanar_bounds_reordered.p21")),
&DecodeOptions::default(),
)
.expect("decode coplanar reordered witness");
let first_surface = first
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#implicit-face-14")
.expect("first implicit face plane");
let reordered_surface = reordered
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#implicit-face-14")
.expect("reordered implicit face plane");
assert_eq!(first_surface.geometry, reordered_surface.geometry);
assert_eq!(first.ir().model.bodies.len(), 1);
assert_eq!(reordered.ir().model.bodies.len(), 1);
assert!(cadmpeg_ir::validate_neutral(first.ir(), first.report().losses.clone()).is_ok());
assert!(
cadmpeg_ir::validate_neutral(reordered.ir(), reordered.report().losses.clone()).is_ok()
);
}
#[test]
fn implicit_face_plane_rejects_non_coplanar_poly_loop_bounds_in_any_order() {
use std::collections::BTreeSet;
for input in [
include_bytes!("data/tp06_non_coplanar_bounds_first.p21").as_slice(),
include_bytes!("data/tp06_non_coplanar_bounds_reordered.p21").as_slice(),
] {
let decoded = StepCodec::default()
.decode(&mut Cursor::new(input), &DecodeOptions::default())
.expect("decode non-coplanar witness");
assert!(decoded.ir().model.bodies.is_empty());
assert!(decoded.ir().model.faces.is_empty());
assert!(decoded.ir().model.surfaces.is_empty());
assert!(decoded.report().losses.iter().any(|loss| {
loss.code == StepLossCode::TopologyRootRejected.kind()
&& loss.message.contains("implicit face plane")
}));
let unknown_ids = decoded
.ir()
.native_unknowns("step")
.expect("STEP native namespace")
.iter()
.map(|record| record.id.0.clone())
.collect::<BTreeSet<_>>();
assert_eq!(
unknown_ids,
BTreeSet::from([
"step:data:face#14".to_string(),
"step:data:face_bound#8".to_string(),
"step:data:face_bound#13".to_string(),
"step:data:open_shell#15".to_string(),
"step:data:poly_loop#7".to_string(),
"step:data:poly_loop#12".to_string(),
"step:data:shell_based_surface_model#16".to_string(),
])
);
}
}
#[test]
fn nearly_collinear_implicit_face_is_rejected_without_a_fabricated_plane() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace(
"#5=CARTESIAN_POINT('',(0.,10.,0.));",
"#5=CARTESIAN_POINT('',(20.,0.0000000000002,0.));",
)
.replace(
"#29=ADVANCED_FACE('',(#26),#28,.T.);",
"#29=FACE('',(#26));",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode nearly collinear base face");
assert!(decoded.ir().model.bodies.is_empty());
assert!(decoded.ir().model.surfaces.is_empty());
assert!(decoded.report().losses.iter().any(|loss| {
loss.code == StepLossCode::TopologyRootRejected.kind()
&& loss.message.contains("implicit face plane")
}));
}
#[test]
fn implicit_face_plane_keeps_base_orientation_across_oriented_face() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace(
"#25=EDGE_LOOP('',(#22,#23,#24));",
"#25=POLY_LOOP('',(#3,#4,#5));",
)
.replace(
"#29=ADVANCED_FACE('',(#26),#28,.T.);",
"#29=FACE('',(#26));",
)
.replace("#30=OPEN_SHELL('',(#29));", "#30=OPEN_SHELL('',(#34));")
.replace(
"#31=SHELL_BASED_SURFACE_MODEL('',(#33));",
"#31=SHELL_BASED_SURFACE_MODEL('',(#33));\n#34=ORIENTED_FACE('',#29,.F.);",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode oriented base face");
let surface = decoded
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#implicit-face-34")
.expect("implicit face plane");
let SurfaceGeometry::Plane { normal, .. } = surface.geometry else {
panic!("implicit face did not produce a plane");
};
assert_eq!(normal, Vector3::new(0.0, 0.0, 1.0));
assert_eq!(
decoded.ir().model.faces[0].sense,
cadmpeg_ir::topology::Sense::Forward
);
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn oriented_face_subtype_composes_face_orientation() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace("#30=OPEN_SHELL('',(#29));", "#30=OPEN_SHELL('',(#34));")
.replace(
"#31=SHELL_BASED_SURFACE_MODEL('',(#33));",
"#31=SHELL_BASED_SURFACE_MODEL('',(#33));\n#34=ORIENTED_FACE('',#29,.F.);",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode oriented face");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert_eq!(decoded.ir().model.faces.len(), 1);
assert!(decoded
.ir()
.model
.coedges
.iter()
.all(|coedge| coedge.sense == cadmpeg_ir::topology::Sense::Reversed));
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn nested_oriented_faces_compose_back_to_the_base_orientation() {
let source = String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace("#30=OPEN_SHELL('',(#29));", "#30=OPEN_SHELL('',(#35));")
.replace(
"#31=SHELL_BASED_SURFACE_MODEL('',(#33));",
"#31=SHELL_BASED_SURFACE_MODEL('',(#33));\n#34=ORIENTED_FACE('',#29,.F.);\n#35=ORIENTED_FACE('',#34,.F.);",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode nested oriented faces");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert_eq!(decoded.ir().model.faces.len(), 1);
assert_eq!(
decoded.ir().model.faces[0].sense,
cadmpeg_ir::topology::Sense::Reversed
);
assert!(decoded
.ir()
.model
.coedges
.iter()
.all(|coedge| coedge.sense == cadmpeg_ir::topology::Sense::Forward));
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn subface_subtype_reuses_parent_surface_and_own_bounds() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace("#30=OPEN_SHELL('',(#29));", "#30=OPEN_SHELL('',(#34));")
.replace(
"#31=SHELL_BASED_SURFACE_MODEL('',(#33));",
"#31=SHELL_BASED_SURFACE_MODEL('',(#33));\n#34=SUBFACE('',(#26),#29);",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode subface");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert_eq!(decoded.ir().model.faces.len(), 1);
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn complex_advanced_face_uses_its_explicit_surface_carrier() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace("#28=PLANE('',#27);", "#28=CYLINDRICAL_SURFACE('',#27,5.);")
.replace(
"#29=ADVANCED_FACE('',(#26),#28,.T.);",
"#29=(FACE('',(#26)) FACE_SURFACE('',#28,.T.) ADVANCED_FACE());",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode complex advanced face");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert!(decoded.ir().model.surfaces.iter().any(|surface| {
surface.id.as_str() == "step:data:surface#28"
&& matches!(surface.geometry, SurfaceGeometry::Cylinder { .. })
}));
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn connected_face_sub_set_validates_and_uses_its_own_members() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace(
"#31=SHELL_BASED_SURFACE_MODEL('',(#33));",
"#31=FACE_BASED_SURFACE_MODEL('',(#34));",
)
.replace(
"#30=OPEN_SHELL('',(#29));",
"#30=CONNECTED_FACE_SET('',(#29));",
)
.replace(
"#33=ORIENTED_OPEN_SHELL('',*,#30,.F.);",
"#34=CONNECTED_FACE_SUB_SET('',(#29),#30);",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode connected face subset");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert_eq!(decoded.ir().model.faces.len(), 1);
assert!(!decoded
.report()
.losses
.iter()
.any(|loss| loss.message.contains("CONNECTED_FACE_SUB_SET #34")));
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
pub(crate) fn face_outer_bound_is_canonicalized_ahead_of_inner_bounds() {
use cadmpeg_ir::ids::LoopId;
use cadmpeg_ir::topology::Loop;
let mut ir = unit_cube();
let face = ir.model.faces[0].id.clone();
let vertex = ir.model.vertices[0].id.clone();
let inner = LoopId("zzzz:test:loop#inner".into());
ir.model.loops.push(Loop {
id: inner.clone(),
face: face.clone(),
boundary_role: cadmpeg_ir::topology::LoopBoundaryRole::Inner,
coedges: Vec::new(),
vertex_uses: vec![cadmpeg_ir::topology::VertexUse {
vertex,
after: None,
pcurves: Vec::new(),
}],
});
ir.model.faces[0].loops.push(inner);
let output = export(&ir);
let (exchange, diagnostics) = crate::parse::parse(output.as_bytes()).unwrap();
assert!(diagnostics.is_empty());
let (face_step, outer_bound, inner_bound, outer_loop) = exchange
.records
.iter()
.find_map(|(&face_step, record)| {
let partial = record.partials.first()?;
if partial.name != "ADVANCED_FACE" {
return None;
}
let crate::parse::Value::List(bounds) = partial.parameters.get(1)? else {
return None;
};
if bounds.len() != 2 {
return None;
}
let crate::parse::Value::Reference(first) = bounds[0] else {
return None;
};
let crate::parse::Value::Reference(second) = bounds[1] else {
return None;
};
let first_record = exchange.records.get(&first)?.partials.first()?;
let second_record = exchange.records.get(&second)?.partials.first()?;
let (outer, inner) = if first_record.name == "FACE_OUTER_BOUND" {
(first, second)
} else if second_record.name == "FACE_OUTER_BOUND" {
(second, first)
} else {
return None;
};
let crate::parse::Value::Reference(outer_loop) = exchange.records.get(&outer)?.partials
[0]
.parameters
.get(1)?
else {
return None;
};
Some((face_step, outer, inner, outer_loop))
})
.expect("face with outer and inner bounds");
let ordered = format!("(#{outer_bound},#{inner_bound})");
let reversed = format!("(#{inner_bound},#{outer_bound})");
let reordered = output.replacen(&ordered, &reversed, 1);
assert_ne!(reordered, output);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(reordered), &DecodeOptions::default())
.expect("decode reversed face bounds");
let face = decoded
.ir()
.model
.faces
.iter()
.find(|face| face.id.as_str() == StepIdentity::data("face", face_step))
.expect("decoded face");
assert_eq!(
face.loops[0].as_str(),
StepIdentity::data("loop", format!("{outer_loop}-face-{face_step}"))
);
}
#[test]
fn duplicate_face_outer_bounds_reject_the_containing_topology_in_any_order() {
use cadmpeg_ir::ids::LoopId;
use cadmpeg_ir::topology::Loop;
for duplicate_first in [false, true] {
let mut ir = unit_cube();
let face = ir.model.faces[0].id.clone();
let duplicate = LoopId("synthetic:test:loop#duplicate-outer".into());
ir.model.loops.push(Loop {
id: duplicate.clone(),
face,
boundary_role: cadmpeg_ir::topology::LoopBoundaryRole::Outer,
coedges: Vec::new(),
vertex_uses: vec![cadmpeg_ir::topology::VertexUse {
vertex: ir.model.vertices[0].id.clone(),
after: None,
pcurves: Vec::new(),
}],
});
if duplicate_first {
ir.model.faces[0].loops.insert(0, duplicate);
} else {
ir.model.faces[0].loops.push(duplicate);
}
let output = export(&ir);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(output), &DecodeOptions::default())
.expect("decode duplicate outer bounds");
assert!(decoded.report().losses.iter().any(|loss| {
loss.code == StepLossCode::FaceMultipleOuterBounds.kind()
&& loss.message.contains("violates the STEP face-bound rule")
&& loss
.message
.contains("omitting the containing topology shell")
}));
assert!(decoded.report().losses.iter().any(|loss| {
loss.code == StepLossCode::TopologyRootRejected.kind()
&& loss.severity == cadmpeg_ir::Severity::Error
&& loss.message.contains("face with multiple outer bounds")
}));
assert!(decoded.ir().model.bodies.is_empty());
assert!(decoded.ir().model.faces.is_empty());
assert!(decoded
.ir()
.native_unknowns("step")
.expect("STEP unknown arena")
.iter()
.any(|record| record.id.0.contains("advanced_face")));
assert!(cadmpeg_ir::validate_neutral(decoded.ir(), Vec::new()).is_ok());
}
}
#[test]
fn duplicate_face_outer_bound_witnesses_reject_topology_in_any_order() {
use std::collections::{BTreeMap, BTreeSet};
for input in [
include_bytes!("data/tp10_duplicate_outer_first.p21").as_slice(),
include_bytes!("data/tp10_duplicate_outer_reordered.p21").as_slice(),
] {
let decoded = StepCodec::default()
.decode(&mut Cursor::new(input), &DecodeOptions::default())
.expect("decode duplicate outer-bound witness");
assert!(decoded.report().losses.iter().any(|loss| {
loss.code == StepLossCode::FaceMultipleOuterBounds.kind()
&& loss.message.contains("face #10")
&& loss
.message
.contains("omitting the containing topology shell")
}));
assert!(decoded.report().losses.iter().any(|loss| {
loss.code == StepLossCode::TopologyRootRejected.kind()
&& loss.message.contains("face with multiple outer bounds")
}));
assert!(decoded.ir().model.bodies.is_empty());
assert!(decoded.ir().model.faces.is_empty());
assert!(decoded.ir().model.surfaces.is_empty());
let unknowns = decoded
.ir()
.native_unknowns("step")
.expect("STEP native namespace");
let ids = unknowns
.iter()
.map(|record| record.id.0.clone())
.collect::<BTreeSet<_>>();
assert_eq!(
ids,
BTreeSet::from([
"step:data:face#10".to_string(),
"step:data:face_outer_bound#7".to_string(),
"step:data:face_outer_bound#9".to_string(),
"step:data:manifold_surface_shape_representation#13".to_string(),
"step:data:open_shell#11".to_string(),
"step:data:poly_loop#6".to_string(),
"step:data:poly_loop#8".to_string(),
"step:data:shell_based_surface_model#12".to_string(),
])
);
let links = unknowns
.iter()
.map(|record| (record.id.0.clone(), record.links.clone()))
.collect::<BTreeMap<_, _>>();
assert_eq!(
links,
BTreeMap::from([
(
"step:data:face#10".to_string(),
vec![
"step:data:face_outer_bound#7".to_string(),
"step:data:face_outer_bound#9".to_string(),
],
),
(
"step:data:face_outer_bound#7".to_string(),
vec!["step:data:poly_loop#6".to_string()],
),
(
"step:data:face_outer_bound#9".to_string(),
vec!["step:data:poly_loop#8".to_string()],
),
(
"step:data:manifold_surface_shape_representation#13".to_string(),
vec!["step:data:shell_based_surface_model#12".to_string()],
),
(
"step:data:open_shell#11".to_string(),
vec!["step:data:face#10".to_string()],
),
(
"step:data:poly_loop#6".to_string(),
vec![
"step:data:point#3".to_string(),
"step:data:point#4".to_string(),
"step:data:point#5".to_string(),
],
),
(
"step:data:poly_loop#8".to_string(),
vec![
"step:data:point#3".to_string(),
"step:data:point#4".to_string(),
"step:data:point#5".to_string(),
],
),
(
"step:data:shell_based_surface_model#12".to_string(),
vec!["step:data:open_shell#11".to_string()],
),
])
);
}
}
#[test]
fn failed_face_bounds_do_not_duplicate_the_shared_surface() {
let mut ir = unit_cube();
ir.model.faces[0].surface = ir.model.faces[1].surface.clone();
ir.model.faces[0].loops.clear();
let output = export(&ir);
assert_eq!(output.matches("= PLANE(").count(), 6);
}
#[test]
fn advanced_face_name_transfers_through_inherited_representation_item() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace(
"#29=ADVANCED_FACE('',(#26),#28,.T.);",
"#29=ADVANCED_FACE('named face',(#26),#28,.T.);",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode named face");
assert_eq!(
decoded.ir().model.faces[0].name.as_deref(),
Some("named face")
);
let mut output = Vec::new();
write_step(decoded.ir(), &mut output, &StepWriteOptions::default()).expect("write named face");
let roundtrip = StepCodec::default()
.decode(&mut Cursor::new(output), &DecodeOptions::default())
.expect("decode written named face");
assert_eq!(
roundtrip.ir().model.faces[0].name.as_deref(),
Some("named face")
);
}
#[test]
fn complex_advanced_face_name_uses_representation_item_partial() {
let source = String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace(
"#29=ADVANCED_FACE('',(#26),#28,.T.);",
"#29=(FACE('',(#26)) FACE_SURFACE('',#28,.T.) ADVANCED_FACE() REPRESENTATION_ITEM('complex named face'));",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode complex named face");
assert_eq!(
decoded.ir().model.faces[0].name.as_deref(),
Some("complex named face")
);
}
#[test]
fn unsupported_mandatory_carriers_preserve_topology_as_unknown() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace("#16=LINE('',#3,#13);", "#16=UNSUPPORTED_CURVE('',#3);")
.replace("#28=PLANE('',#27);", "#28=UNSUPPORTED_SURFACE('',#27);");
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode sheet with unknown mandatory carriers");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert_eq!(decoded.ir().model.faces.len(), 1);
assert_eq!(decoded.ir().model.edges.len(), 3);
assert!(matches!(
decoded
.ir()
.model
.curves
.iter()
.find(|curve| curve.id.as_str() == "step:data:curve#16")
.map(|curve| &curve.geometry),
Some(CurveGeometry::Unknown { record: Some(record) })
if record.as_str() == "step:data:unsupported_curve#16"
));
assert!(matches!(
decoded
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#28")
.map(|surface| &surface.geometry),
Some(SurfaceGeometry::Unknown { record: Some(record) })
if record.as_str() == "step:data:unsupported_surface#28"
));
assert!(decoded
.report()
.losses
.iter()
.all(|loss| !loss.message.contains("conflicts with decoded topology")));
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}
#[test]
fn unsupported_surface_carrier_on_face_surface_preserves_topology_as_unknown() {
let source =
String::from_utf8(include_bytes!("../../../../tests/fixtures/ap214_sheet.p21").to_vec())
.expect("fixture is UTF-8")
.replace("#28=PLANE('',#27);", "#28=UNSUPPORTED_SURFACE('',#27);")
.replace(
"#29=ADVANCED_FACE('',(#26),#28,.T.);",
"#29=FACE_SURFACE('',(#26),#28,.T.);",
);
let decoded = StepCodec::default()
.decode(&mut Cursor::new(source), &DecodeOptions::default())
.expect("decode FACE_SURFACE with unknown carrier");
assert_eq!(decoded.ir().model.bodies.len(), 1);
assert!(matches!(
decoded
.ir()
.model
.surfaces
.iter()
.find(|surface| surface.id.as_str() == "step:data:surface#28")
.map(|surface| &surface.geometry),
Some(SurfaceGeometry::Unknown { record: Some(record) })
if record.as_str() == "step:data:unsupported_surface#28"
));
let validation = cadmpeg_ir::validate_neutral(decoded.ir(), decoded.report().losses.clone());
assert!(validation.is_ok(), "{:#?}", validation.findings);
}