#![allow(clippy::unwrap_used)]
use cadmpeg_ir::examples::unit_cube;
use cadmpeg_ir::geometry::{
Curve, CurveGeometry, NurbsCurve, NurbsSurface, Surface, SurfaceGeometry,
};
use cadmpeg_ir::ids::{CurveId, ProceduralCurveId, SurfaceId};
use cadmpeg_ir::math::{Point3, Vector3};
use cadmpeg_ir::units::{LengthUnit, Units};
use cadmpeg_ir::CadIr;
use crate::{write_step, StepWriteOptions};
fn export(ir: &CadIr) -> String {
let mut buf = Vec::new();
write_step(ir, &mut buf, &StepWriteOptions::default()).expect("write");
String::from_utf8(buf).expect("utf8")
}
fn emit_surface_only(g: &SurfaceGeometry) -> String {
let mut e = crate::writer::Emitter::new();
crate::geometry::surface(&mut e, g);
e.into_lines().join("\n")
}
fn emit_curve_only(g: &CurveGeometry) -> String {
let mut e = crate::writer::Emitter::new();
crate::geometry::curve(&mut e, g);
e.into_lines().join("\n")
}
fn edgeless_doc() -> CadIr {
use cadmpeg_ir::ids::{
BodyId, CoedgeId, EdgeId, FaceId, LoopId, PointId, RegionId, ShellId, SurfaceId, VertexId,
};
use cadmpeg_ir::topology::{
Body, Coedge, Edge, Face, Loop, Point, Region, Sense, Shell, Vertex,
};
let mut ir = CadIr::empty(Units::default());
ir.model.points.push(Point {
id: PointId("p0".into()),
position: Point3::new(0.0, 0.0, 0.0),
});
ir.model.points.push(Point {
id: PointId("p1".into()),
position: Point3::new(1.0, 0.0, 0.0),
});
ir.model.vertices.push(Vertex {
id: VertexId("v0".into()),
point: PointId("p0".into()),
tolerance: None,
});
ir.model.vertices.push(Vertex {
id: VertexId("v1".into()),
point: PointId("p1".into()),
tolerance: None,
});
ir.model.edges.push(Edge {
id: EdgeId("e0".into()),
curve: None,
start: VertexId("v0".into()),
end: VertexId("v1".into()),
param_range: None,
tolerance: None,
});
ir.model.surfaces.push(Surface {
id: SurfaceId("s0".into()),
geometry: SurfaceGeometry::Plane {
origin: Point3::new(0.0, 0.0, 0.0),
normal: Vector3::new(0.0, 0.0, 1.0),
u_axis: Vector3::new(1.0, 0.0, 0.0),
},
});
ir.model.coedges.push(Coedge {
id: CoedgeId("ce0".into()),
owner_loop: LoopId("lp0".into()),
edge: EdgeId("e0".into()),
next: CoedgeId("ce0".into()),
previous: CoedgeId("ce0".into()),
radial_next: CoedgeId("ce0".into()),
sense: Sense::Forward,
pcurve: None,
});
ir.model.loops.push(Loop {
id: LoopId("lp0".into()),
face: FaceId("f0".into()),
coedges: vec![CoedgeId("ce0".into())],
});
ir.model.faces.push(Face {
id: FaceId("f0".into()),
shell: ShellId("sh0".into()),
surface: SurfaceId("s0".into()),
sense: Sense::Forward,
loops: vec![LoopId("lp0".into())],
name: None,
color: None,
tolerance: None,
});
ir.model.shells.push(Shell {
id: ShellId("sh0".into()),
region: RegionId("l0".into()),
faces: vec![FaceId("f0".into())],
wire_edges: Vec::new(),
free_vertices: Vec::new(),
});
ir.model.regions.push(Region {
id: RegionId("l0".into()),
body: BodyId("b0".into()),
shells: vec![ShellId("sh0".into())],
});
ir.model.bodies.push(Body {
id: BodyId("b0".into()),
kind: cadmpeg_ir::topology::BodyKind::Solid,
regions: vec![RegionId("l0".into())],
transform: None,
name: None,
color: None,
visible: None,
});
ir
}
#[test]
fn cube_has_valid_part21_envelope() {
let s = export(&unit_cube());
assert!(s.starts_with("ISO-10303-21;\n"));
assert!(s.contains("HEADER;"));
assert!(s.contains("FILE_SCHEMA(('AUTOMOTIVE_DESIGN { 1 0 10303 214 1 1 1 1 }'));"));
assert!(s.contains("\nDATA;\n"));
assert!(s.trim_end().ends_with("END-ISO-10303-21;"));
assert_eq!(s.matches("ENDSEC;").count(), 2);
}
#[test]
fn cube_emits_full_brep_hierarchy() {
let s = export(&unit_cube());
assert!(s.contains("MANIFOLD_SOLID_BREP"));
assert!(s.contains("CLOSED_SHELL"));
assert_eq!(s.matches("ADVANCED_FACE").count(), 6);
assert_eq!(s.matches("= PLANE(").count(), 6);
assert_eq!(s.matches("EDGE_CURVE").count(), 12);
assert_eq!(s.matches("VERTEX_POINT").count(), 8);
assert_eq!(s.matches("ORIENTED_EDGE").count(), 24);
assert_eq!(s.matches("= EDGE_LOOP(").count(), 6);
assert_eq!(s.matches("FACE_OUTER_BOUND").count(), 6);
assert_eq!(s.matches("= LINE(").count(), 12);
}
#[test]
fn cube_product_and_context_boilerplate_present() {
let s = export(&unit_cube());
for kw in [
"APPLICATION_CONTEXT",
"APPLICATION_PROTOCOL_DEFINITION",
"PRODUCT(",
"PRODUCT_DEFINITION(",
"PRODUCT_DEFINITION_SHAPE",
"SHAPE_DEFINITION_REPRESENTATION",
"ADVANCED_BREP_SHAPE_REPRESENTATION",
"GEOMETRIC_REPRESENTATION_CONTEXT",
"UNCERTAINTY_MEASURE_WITH_UNIT",
] {
assert!(s.contains(kw), "missing {kw}");
}
assert!(s.contains("SI_UNIT(.MILLI.,.METRE.)"));
}
#[test]
fn every_reference_resolves() {
let s = export(&unit_cube());
let mut declared = std::collections::HashSet::new();
for line in s.lines() {
if let Some(rest) = line.strip_prefix('#') {
if let Some(eq) = rest.find(" =") {
if let Ok(id) = rest[..eq].parse::<u64>() {
declared.insert(id);
}
}
}
}
assert!(!declared.is_empty());
for line in s.lines() {
let Some(eq) = line.find('=') else { continue };
let body = &line[eq + 1..];
let bytes = body.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'#' {
let start = i + 1;
let mut j = start;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j > start {
let id: u64 = body[start..j].parse().unwrap();
assert!(
declared.contains(&id),
"dangling reference #{id} in: {line}"
);
}
i = j;
} else {
i += 1;
}
}
}
}
#[test]
fn reports_entity_counts_and_no_geometry_loss_for_cube() {
let mut buf = Vec::new();
let report = write_step(&unit_cube(), &mut buf, &StepWriteOptions::default()).unwrap();
assert_eq!(report.total_entities, buf_line_count(&buf));
assert_eq!(report.entity_counts.get("ADVANCED_FACE"), Some(&6));
assert_eq!(report.entity_counts.get("VERTEX_POINT"), Some(&8));
assert_eq!(report.error_count(), 0);
}
fn buf_line_count(buf: &[u8]) -> usize {
String::from_utf8_lossy(buf)
.lines()
.filter(|l| l.starts_with('#'))
.count()
}
fn cylinder_surface_doc() -> CadIr {
let mut ir = CadIr::empty(Units::default());
ir.model.surfaces.push(Surface {
id: SurfaceId("cyl".into()),
geometry: SurfaceGeometry::Cylinder {
origin: Point3::new(0.0, 0.0, 0.0),
axis: Vector3::new(0.0, 0.0, 1.0),
ref_direction: Vector3::new(1.0, 0.0, 0.0),
radius: 5.0,
},
});
ir
}
#[test]
fn analytic_surfaces_map_to_their_step_entities() {
let cases: Vec<(SurfaceGeometry, &str)> = vec![
(
SurfaceGeometry::Cylinder {
origin: Point3::new(0.0, 0.0, 0.0),
axis: Vector3::new(0.0, 0.0, 1.0),
ref_direction: Vector3::new(1.0, 0.0, 0.0),
radius: 5.0,
},
"CYLINDRICAL_SURFACE",
),
(
SurfaceGeometry::Cone {
origin: Point3::new(0.0, 0.0, 0.0),
axis: Vector3::new(0.0, 0.0, 1.0),
ref_direction: Vector3::new(1.0, 0.0, 0.0),
radius: 2.0,
half_angle: 0.5,
},
"CONICAL_SURFACE",
),
(
SurfaceGeometry::Sphere {
center: Point3::new(1.0, 2.0, 3.0),
axis: Vector3::new(0.0, 0.0, 1.0),
ref_direction: Vector3::new(1.0, 0.0, 0.0),
radius: 4.0,
},
"SPHERICAL_SURFACE",
),
(
SurfaceGeometry::Torus {
center: Point3::new(0.0, 0.0, 0.0),
axis: Vector3::new(0.0, 0.0, 1.0),
ref_direction: Vector3::new(1.0, 0.0, 0.0),
major_radius: 3.0,
minor_radius: 1.0,
},
"TOROIDAL_SURFACE",
),
];
for (geom, kw) in cases {
let mut ir = CadIr::empty(Units::default());
ir.model.surfaces.push(Surface {
id: SurfaceId("s".into()),
geometry: geom,
});
let s = emit_surface_only(&ir.model.surfaces[0].geometry);
assert!(s.contains(kw), "missing {kw} in {s}");
}
}
#[test]
fn analytic_surface_placements_preserve_orientation() {
let geometry = SurfaceGeometry::Sphere {
center: Point3::new(1.0, 2.0, 3.0),
axis: Vector3::new(0.0, 1.0, 0.0),
ref_direction: Vector3::new(0.0, 0.0, 1.0),
radius: 4.0,
};
let s = emit_surface_only(&geometry);
assert!(s.contains("DIRECTION('',(0.,1.,0.))"));
assert!(s.contains("DIRECTION('',(0.,0.,1.))"));
}
#[test]
fn parabola_and_hyperbola_map_to_step_conics() {
let parabola = emit_curve_only(&CurveGeometry::Parabola {
vertex: Point3::new(1.0, 2.0, 3.0),
axis: Vector3::new(0.0, 0.0, 1.0),
major_direction: Vector3::new(0.0, 1.0, 0.0),
focal_distance: 2.5,
});
assert!(parabola.contains("= PARABOLA("));
assert!(parabola.contains(",2.5)"));
let hyperbola = emit_curve_only(&CurveGeometry::Hyperbola {
center: Point3::new(1.0, 2.0, 3.0),
axis: Vector3::new(0.0, 0.0, 1.0),
major_direction: Vector3::new(0.0, 1.0, 0.0),
major_radius: 4.0,
minor_radius: 1.5,
});
assert!(hyperbola.contains("= HYPERBOLA("));
assert!(hyperbola.contains(",4.,1.5)"));
}
#[test]
fn nurbs_curve_non_rational_uses_with_knots() {
let n = NurbsCurve {
degree: 2,
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
control_points: vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(2.0, 0.0, 0.0),
],
weights: None,
periodic: false,
};
let s = emit_curve_only(&CurveGeometry::Nurbs(n));
assert!(s.contains("B_SPLINE_CURVE_WITH_KNOTS"));
assert!(s.contains("(3,3)"), "knot multiplicities: {s}");
assert!(!s.contains("RATIONAL"));
}
#[test]
fn nurbs_curve_rational_uses_complex_form() {
let n = NurbsCurve {
degree: 2,
knots: vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0],
control_points: vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(2.0, 0.0, 0.0),
],
weights: Some(vec![1.0, 0.5, 1.0]),
periodic: false,
};
let s = emit_curve_only(&CurveGeometry::Nurbs(n));
assert!(s.contains("RATIONAL_B_SPLINE_CURVE"));
assert!(s.contains("BOUNDED_CURVE()"));
}
#[test]
fn nurbs_surface_grid_orientation_is_u_major() {
let n = NurbsSurface {
u_degree: 1,
v_degree: 1,
u_knots: vec![0.0, 0.0, 1.0, 1.0],
v_knots: vec![0.0, 0.0, 1.0, 1.0],
u_count: 2,
v_count: 2,
control_points: vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
],
weights: None,
u_periodic: false,
v_periodic: false,
};
let s = emit_surface_only(&SurfaceGeometry::Nurbs(n));
assert!(s.contains("B_SPLINE_SURFACE_WITH_KNOTS"));
}
#[test]
fn v1_document_uses_canonical_millimeter_unit() {
let ir = unit_cube();
assert_eq!(ir.units.length, LengthUnit::Millimeter);
let s = export(&ir);
assert!(s.contains("SI_UNIT(.MILLI.,.METRE.)"));
assert!(!s.contains("CONVERSION_BASED_UNIT"));
}
#[test]
fn real_formatting_always_has_decimal_point() {
let s = export(&unit_cube());
assert!(s.contains("10.")); assert!(!s.contains("(10,")); }
#[test]
fn edge_without_curve_is_reported_and_omitted() {
let _ = cylinder_surface_doc(); let ir = edgeless_doc();
let mut buf = Vec::new();
let report = write_step(&ir, &mut buf, &StepWriteOptions::default()).unwrap();
let curve = Curve {
id: CurveId("unused".into()),
geometry: CurveGeometry::Line {
origin: Point3::new(0.0, 0.0, 0.0),
direction: Vector3::new(1.0, 0.0, 0.0),
},
};
let _ = curve; assert!(report
.losses
.iter()
.any(|l| l.message.contains("edge(s) have no typed 3D curve")));
}
#[test]
fn face_on_unknown_surface_is_skipped_and_reported() {
let mut ir = unit_cube();
let target = ir.model.faces[0].surface.0.clone();
for s in &mut ir.model.surfaces {
if s.id.0 == target {
s.geometry = SurfaceGeometry::Unknown { record: None };
}
}
let mut buf = Vec::new();
let report = write_step(&ir, &mut buf, &StepWriteOptions::default()).unwrap();
let s = String::from_utf8(buf).unwrap();
assert_eq!(
s.matches("ADVANCED_FACE").count(),
5,
"the unknown-surface face should be omitted"
);
let unknown_notes: Vec<_> = report
.losses
.iter()
.filter(|l| l.message.contains("rest on an unknown"))
.collect();
assert_eq!(
unknown_notes.len(),
1,
"loss must be aggregated into a single counted note, got: {:?}",
report.losses
);
assert!(unknown_notes[0].message.contains("1 face(s)"));
}
#[test]
fn signed_analytic_radius_normalization_is_reported() {
let mut ir = unit_cube();
ir.model.surfaces[0].geometry = SurfaceGeometry::Sphere {
center: Point3::new(0.0, 0.0, 0.0),
axis: Vector3::new(0.0, 0.0, 1.0),
ref_direction: Vector3::new(1.0, 0.0, 0.0),
radius: -2.0,
};
let mut buf = Vec::new();
let report = write_step(&ir, &mut buf, &StepWriteOptions::default()).unwrap();
assert!(report.losses.iter().any(|loss| {
loss.category == cadmpeg_ir::LossCategory::Geometry
&& loss.message.contains("normalized to positive STEP radii")
}));
}
#[test]
fn procedural_construction_reduction_is_reported() {
let mut ir = unit_cube();
ir.model
.procedural_curves
.push(cadmpeg_ir::geometry::ProceduralCurve {
id: ProceduralCurveId("generated_int_cur".into()),
curve: ir.model.curves[0].id.clone(),
definition: cadmpeg_ir::geometry::ProceduralCurveDefinition::Intersection {
context: cadmpeg_ir::geometry::IntcurveSupportContext {
sides: std::array::from_fn(|_| cadmpeg_ir::geometry::IntcurveSupportSide {
surface: None,
pcurve: None,
}),
parameter_range: [0.0, 1.0],
discontinuities: std::array::from_fn(|_| Vec::new()),
},
},
cache_fit_tolerance: Some(0.01),
});
let mut buf = Vec::new();
let report = write_step(&ir, &mut buf, &StepWriteOptions::default()).unwrap();
assert!(report.losses.iter().any(|loss| loss
.message
.contains("reduced to their solved STEP carriers")));
}
#[test]
fn parametric_history_reduction_is_reported() {
let mut ir = unit_cube();
ir.native
.f3d
.get_or_insert_with(cadmpeg_ir::native::F3dNative::default)
.asm_histories
.push(cadmpeg_ir::history::AsmHistory {
id: "asm-history-0".into(),
byte_offset: 0,
stream_size: Some(0),
high_water_mark: Some(0),
states: Vec::new(),
});
let mut buf = Vec::new();
let report = write_step(&ir, &mut buf, &StepWriteOptions::default()).unwrap();
assert!(report.losses.iter().any(|loss| loss
.message
.contains("parametric design/history record(s) were not represented in STEP")));
}
#[test]
fn hidden_body_is_omitted_and_reported() {
let mut ir = unit_cube();
ir.model.bodies[0].visible = Some(false);
let mut buf = Vec::new();
let report = write_step(&ir, &mut buf, &StepWriteOptions::default()).unwrap();
let s = String::from_utf8(buf).unwrap();
assert!(!s.contains("MANIFOLD_SOLID_BREP"));
assert!(!s.contains("ADVANCED_FACE"));
assert!(report
.losses
.iter()
.any(|l| l.message.contains("hidden body(ies) were omitted")));
let mut ir = unit_cube();
ir.model.bodies[0].visible = Some(true);
let s = export(&ir);
assert!(s.contains("MANIFOLD_SOLID_BREP"));
}