#![allow(clippy::unwrap_used)]
use cadmpeg_ir::examples::unit_cube;
use cadmpeg_ir::geometry::{Curve, CurveGeometry, SolvedCurveGeometry};
use cadmpeg_ir::ids::CurveId;
use cadmpeg_ir::CadIr;
use crate::export::write_step;
use crate::{StepSchema, StepWriteOptions};
#[derive(Clone, Copy, PartialEq)]
enum Ring {
Beside,
BetweenVertices,
Alone,
AloneWithPcurves,
AfterAClosedCircle,
CurvelessOnItsVertex,
CurvelessWithLength,
CurvelessBare,
}
fn cube_with_a_collapsed_edge(ring_kind: Ring) -> CadIr {
use cadmpeg_ir::geometry::analytic::PlaneSurface;
use cadmpeg_ir::geometry::analytic::{CircleCurve, DegenerateCurve};
use cadmpeg_ir::geometry::pcurve::{
Pcurve, PcurveGeometry, PcurveMetadata, PcurveNurbs, PcurveNurbsPoles,
};
use cadmpeg_ir::geometry::{DirectedParameterRange, SolvedSurfaceGeometry, SurfaceGeometry};
use cadmpeg_ir::ids::{CoedgeId, EdgeId, PcurveId};
use cadmpeg_ir::math::{Point2, Point3, Vector3};
use cadmpeg_ir::topology::{Coedge, Edge, EdgeCarrier, PcurveUse, Sense};
let same_vertex = ring_kind != Ring::BetweenVertices;
let curveless = matches!(
ring_kind,
Ring::CurvelessOnItsVertex | Ring::CurvelessWithLength | Ring::CurvelessBare
);
let mut source = unit_cube().expect("unit cube fixture is admitted");
let (loop_index, ring) = source
.model
.loops
.iter()
.enumerate()
.find(|(_, loop_)| loop_.coedges().len() >= 3)
.map(|(index, loop_)| (index, loop_.coedges().to_vec()))
.expect("unit cube has an edge loop");
let ends = |id: &CoedgeId| {
let coedge = source.model.coedges.iter().find(|c| c.id == *id).unwrap();
let edge = source
.model
.edges
.iter()
.find(|e| e.id == coedge.edge)
.unwrap();
match coedge.sense {
Sense::Forward => (edge.start.clone(), edge.end.clone()),
Sense::Reversed => (edge.end.clone(), edge.start.clone()),
}
};
let (other, corner) = ends(&ring[0]);
let position = {
let vertex = source
.model
.vertices
.iter()
.find(|v| v.id == corner)
.unwrap();
source
.model
.points
.iter()
.find(|p| p.id == vertex.point)
.unwrap()
.position()
};
let corner_vertex = corner.clone();
let curve = CurveId::mint("test:model:curve#collapsed").unwrap();
let edge = EdgeId::mint("test:model:edge#collapsed").unwrap();
let coedge = CoedgeId::mint("test:model:coedge#collapsed").unwrap();
if !curveless {
source.model.curves.push(Curve {
id: curve.clone(),
parameter_range: None,
source_object: None,
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Degenerate(
DegenerateCurve::try_new(position.get()).unwrap(),
)),
});
}
source.model.edges.push(Edge {
id: edge.clone(),
carrier: EdgeCarrier::unbounded((!curveless).then_some(curve)),
start: if same_vertex { corner.clone() } else { other },
end: corner.clone(),
tolerance: None,
});
source.model.coedges.push(Coedge {
id: coedge.clone(),
owner_loop: source.model.loops[loop_index].id.clone(),
edge,
radial_next: coedge.clone(),
sense: Sense::Forward,
use_curve: None,
pcurves: Vec::new(),
});
let mut coedges = ring;
coedges.insert(1, coedge.clone());
if matches!(ring_kind, Ring::Alone | Ring::AloneWithPcurves) {
let twin = CoedgeId::mint("test:model:coedge#collapsed-twin").unwrap();
let mut second = source.model.coedges.last().unwrap().clone();
second.id = twin.clone();
second.radial_next = twin.clone();
source.model.coedges.push(second);
coedges = vec![coedge.clone(), twin];
}
if ring_kind == Ring::AloneWithPcurves {
let pcurve = PcurveId::mint("test:model:pcurve#collapsed").unwrap();
source.model.pcurves.push(Pcurve {
id: pcurve.clone(),
geometry: PcurveGeometry::Nurbs {
nurbs: PcurveNurbs::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
PcurveNurbsPoles::Polynomial {
points: vec![Point2::new(0.0, 0.0), Point2::new(1.0, 0.0)],
},
false,
)
.unwrap(),
},
metadata: PcurveMetadata::default(),
});
for coedge in source
.model
.coedges
.iter_mut()
.filter(|coedge| coedges.contains(&coedge.id))
{
coedge.pcurves.push(PcurveUse {
pcurve: pcurve.clone(),
isoparametric: None,
parameter_range: Some(DirectedParameterRange::new([0.0, 1.0]).unwrap()),
});
}
}
if matches!(
ring_kind,
Ring::CurvelessOnItsVertex | Ring::CurvelessWithLength
) {
let face_id = source.model.loops[loop_index].face.clone();
let surface_id = source
.model
.faces
.iter()
.find(|face| face.id == face_id)
.unwrap()
.surface
.clone();
let surface = source
.model
.surfaces
.iter_mut()
.find(|surface| surface.id == surface_id)
.unwrap();
let SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(plane)) = surface.geometry else {
panic!("a unit cube face is planar");
};
surface.geometry = SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(
PlaneSurface::new(position, *plane.frame()),
));
let end = if ring_kind == Ring::CurvelessWithLength {
Point2::new(1.0, 0.0)
} else {
Point2::new(0.0, 0.0)
};
let pcurve = PcurveId::mint("test:model:pcurve#collapsed").unwrap();
source.model.pcurves.push(Pcurve {
id: pcurve.clone(),
geometry: PcurveGeometry::Nurbs {
nurbs: PcurveNurbs::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
PcurveNurbsPoles::Polynomial {
points: vec![Point2::new(0.0, 0.0), end],
},
false,
)
.unwrap(),
},
metadata: PcurveMetadata::default(),
});
let collapsed = source
.model
.coedges
.iter_mut()
.find(|candidate| candidate.id == coedge)
.unwrap();
collapsed.pcurves.push(PcurveUse {
pcurve,
isoparametric: None,
parameter_range: Some(DirectedParameterRange::new([0.0, 1.0]).unwrap()),
});
}
if ring_kind == Ring::AfterAClosedCircle {
let circle = CurveId::mint("test:model:curve#closed-circle").unwrap();
let circle_edge = EdgeId::mint("test:model:edge#closed-circle").unwrap();
let circle_coedge = CoedgeId::mint("test:model:coedge#closed-circle").unwrap();
source.model.curves.push(Curve {
id: circle.clone(),
parameter_range: None,
source_object: None,
geometry: CurveGeometry::Solved(SolvedCurveGeometry::Circle(
CircleCurve::try_new(
Point3::new(position.get().x - 0.25, position.get().y, position.get().z),
Vector3::new(0.0, 0.0, 1.0),
Vector3::new(1.0, 0.0, 0.0),
0.25,
)
.unwrap(),
)),
});
source.model.edges.push(Edge {
id: circle_edge.clone(),
carrier: EdgeCarrier::unbounded(Some(circle)),
start: corner_vertex.clone(),
end: corner_vertex,
tolerance: None,
});
source.model.coedges.push(Coedge {
id: circle_coedge.clone(),
owner_loop: source.model.loops[loop_index].id.clone(),
edge: circle_edge,
radial_next: circle_coedge.clone(),
sense: Sense::Forward,
use_curve: None,
pcurves: Vec::new(),
});
coedges = vec![circle_coedge, coedge];
}
let vertex_uses = source.model.loops[loop_index]
.anchored_vertex_uses()
.to_vec();
source.model.loops[loop_index]
.replace_ring(coedges, vertex_uses)
.expect("the ring with a collapsed edge is valid");
source
}
#[derive(Debug, PartialEq)]
struct Written {
oriented_edges: usize,
faces: usize,
vertex_loops: usize,
losses: Vec<String>,
}
fn written(source: &CadIr) -> Written {
let mut bytes = Vec::new();
let report = write_step(
source,
&mut bytes,
StepSchema::Ap214,
&StepWriteOptions::default(),
)
.expect("the cube writes");
let text = String::from_utf8(bytes).expect("STEP is UTF-8");
Written {
oriented_edges: text.matches("ORIENTED_EDGE(").count(),
faces: text.matches("ADVANCED_FACE(").count(),
vertex_loops: text.matches("VERTEX_LOOP(").count(),
losses: report
.losses
.iter()
.map(|loss| loss.code.to_string())
.collect(),
}
}
#[test]
fn writer_leaves_an_edge_collapsed_to_one_vertex_out_of_its_loop() {
let plain = written(&unit_cube().expect("unit cube fixture is admitted"));
assert_eq!((plain.faces, plain.vertex_loops), (6, 0));
assert!(plain.losses.is_empty(), "{:?}", plain.losses);
assert_eq!(written(&cube_with_a_collapsed_edge(Ring::Beside)), plain);
let kept = written(&cube_with_a_collapsed_edge(Ring::BetweenVertices));
assert_ne!(
kept, plain,
"an edge between two vertices is written or reported"
);
}
#[test]
fn writer_leaves_a_curveless_edge_whose_pcurve_stays_at_its_vertex_out_without_a_loss() {
let plain = written(&unit_cube().expect("unit cube fixture is admitted"));
assert_eq!(
written(&cube_with_a_collapsed_edge(Ring::CurvelessOnItsVertex)),
Written {
losses: vec!["step/pcurve.use-native-metadata".to_owned()],
..plain
}
);
}
#[test]
fn writer_reports_a_curveless_edge_that_may_have_length() {
let plain = written(&unit_cube().expect("unit cube fixture is admitted"));
for ring in [Ring::CurvelessWithLength, Ring::CurvelessBare] {
let reported = written(&cube_with_a_collapsed_edge(ring));
assert_eq!(
(reported.oriented_edges, reported.faces),
(plain.oriented_edges, plain.faces)
);
for code in [
"step/topology.edge-no-3d-curve",
"step/geometry.curveless-edge-omitted",
] {
assert!(
reported.losses.contains(&code.to_owned()),
"{:?}",
reported.losses
);
}
}
}
#[test]
fn writer_writes_a_ring_of_bare_collapsed_edges_as_a_vertex_loop() {
let plain = written(&unit_cube().expect("unit cube fixture is admitted"));
let alone = written(&cube_with_a_collapsed_edge(Ring::Alone));
assert_eq!(
(alone.oriented_edges, alone.faces, alone.vertex_loops),
(plain.oriented_edges - 4, plain.faces, 1)
);
}
#[test]
fn writer_keeps_a_ring_of_collapsed_edges_with_pcurves() {
let plain = written(&unit_cube().expect("unit cube fixture is admitted"));
let alone = written(&cube_with_a_collapsed_edge(Ring::AloneWithPcurves));
assert_eq!(
(alone.oriented_edges, alone.faces, alone.vertex_loops),
(plain.oriented_edges - 4 + 2, plain.faces, 0)
);
}
#[test]
fn writer_leaves_a_collapsed_edge_out_beside_a_closed_circle() {
let plain = written(&unit_cube().expect("unit cube fixture is admitted"));
let circle = written(&cube_with_a_collapsed_edge(Ring::AfterAClosedCircle));
assert_eq!(
(circle.oriented_edges, circle.faces, circle.vertex_loops),
(plain.oriented_edges - 4 + 1, plain.faces, 0)
);
}
#[derive(Clone, Copy, PartialEq)]
enum SeamRing {
Alone,
BesideALoop,
BesideAnUnwritableLoop,
BesideALoopWithALongPole,
}
fn cube_with_a_seam_ring(case: SeamRing) -> CadIr {
use cadmpeg_ir::geometry::analytic::PlaneSurface;
use cadmpeg_ir::geometry::pcurve::{
Pcurve, PcurveGeometry, PcurveMetadata, PcurveNurbs, PcurveNurbsPoles,
};
use cadmpeg_ir::geometry::{DirectedParameterRange, SolvedSurfaceGeometry, SurfaceGeometry};
use cadmpeg_ir::ids::{CoedgeId, EdgeId, LoopId, PcurveId};
use cadmpeg_ir::math::Point2;
use cadmpeg_ir::topology::{
Coedge, Edge, EdgeCarrier, FaceLoops, Loop, LoopBoundary, LoopRing, PcurveUse, Sense,
};
let mut source = unit_cube().expect("unit cube fixture is admitted");
let loop_index = source
.model
.loops
.iter()
.position(|loop_| loop_.coedges().len() >= 3)
.expect("unit cube has an edge loop");
let first = source.model.loops[loop_index].coedges()[0].clone();
let line = {
let coedge = source.model.coedges.iter().find(|c| c.id == first).unwrap();
source
.model
.edges
.iter()
.find(|e| e.id == coedge.edge)
.unwrap()
.clone()
};
let face_id = source.model.loops[loop_index].face.clone();
let pole_position = {
let vertex = source
.model
.vertices
.iter()
.find(|v| v.id == line.end)
.unwrap();
source
.model
.points
.iter()
.find(|p| p.id == vertex.point)
.unwrap()
.position()
};
let surface_id = source
.model
.faces
.iter()
.find(|face| face.id == face_id)
.unwrap()
.surface
.clone();
let surface = source
.model
.surfaces
.iter_mut()
.find(|surface| surface.id == surface_id)
.unwrap();
let SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(plane)) = surface.geometry else {
panic!("a unit cube face is planar");
};
surface.geometry = SurfaceGeometry::Solved(SolvedSurfaceGeometry::Plane(PlaneSurface::new(
pole_position,
*plane.frame(),
)));
let beside = case != SeamRing::Alone;
let loop_id = if beside {
LoopId::mint("test:model:loop#seam").unwrap()
} else {
source.model.loops[loop_index].id.clone()
};
let seam = EdgeId::mint("test:model:edge#seam").unwrap();
let pole = EdgeId::mint("test:model:edge#pole").unwrap();
source.model.edges.push(Edge {
id: seam.clone(),
carrier: EdgeCarrier::unbounded(line.curve().cloned()),
start: line.start.clone(),
end: line.end.clone(),
tolerance: None,
});
source.model.edges.push(Edge {
id: pole.clone(),
carrier: EdgeCarrier::unbounded(None),
start: line.end.clone(),
end: line.end,
tolerance: None,
});
let pole_end = if case == SeamRing::BesideALoopWithALongPole {
Point2::new(1.0, 0.0)
} else {
Point2::new(0.0, 0.0)
};
let pcurve = PcurveId::mint("test:model:pcurve#pole").unwrap();
source.model.pcurves.push(Pcurve {
id: pcurve.clone(),
geometry: PcurveGeometry::Nurbs {
nurbs: PcurveNurbs::new(
1,
vec![0.0, 0.0, 1.0, 1.0],
PcurveNurbsPoles::Polynomial {
points: vec![Point2::new(0.0, 0.0), pole_end],
},
false,
)
.unwrap(),
},
metadata: PcurveMetadata::default(),
});
let ring: Vec<CoedgeId> = [
("up", &seam, Sense::Forward, None),
("pole", &pole, Sense::Forward, Some(pcurve)),
("down", &seam, Sense::Reversed, None),
]
.into_iter()
.map(|(name, edge, sense, pcurve)| {
let id = CoedgeId::mint(format!("test:model:coedge#{name}")).unwrap();
source.model.coedges.push(Coedge {
id: id.clone(),
owner_loop: loop_id.clone(),
edge: edge.clone(),
radial_next: id.clone(),
sense,
use_curve: None,
pcurves: pcurve
.map(|pcurve| PcurveUse {
pcurve,
isoparametric: None,
parameter_range: Some(DirectedParameterRange::new([0.0, 1.0]).unwrap()),
})
.into_iter()
.collect(),
});
id
})
.collect();
if case == SeamRing::BesideAnUnwritableLoop {
let first_loop = source.model.loops[loop_index].coedges().to_vec();
let edges: Vec<EdgeId> = source
.model
.coedges
.iter()
.filter(|coedge| first_loop.contains(&coedge.id))
.map(|coedge| coedge.edge.clone())
.collect();
for edge in source
.model
.edges
.iter_mut()
.filter(|edge| edges.contains(&edge.id))
{
edge.carrier = EdgeCarrier::unbounded(None);
}
}
if beside {
source.model.loops.push(Loop {
id: loop_id.clone(),
face: face_id.clone(),
boundary: LoopBoundary::Ring(LoopRing::new(ring, Vec::new()).unwrap()),
});
let face = source
.model
.faces
.iter_mut()
.find(|face| face.id == face_id)
.unwrap();
let mut loops = face.loops.to_vec();
loops.push(loop_id);
face.loops = FaceLoops::unspecified(loops);
} else {
source.model.loops[loop_index]
.replace_ring(ring, Vec::new())
.expect("the seam ring is valid");
}
source
}
#[test]
fn writer_writes_a_seam_ring_beside_another_bound_as_a_vertex_loop() {
let plain = written(&unit_cube().expect("unit cube fixture is admitted"));
let seam = written(&cube_with_a_seam_ring(SeamRing::BesideALoop));
assert_eq!(
seam,
Written {
vertex_loops: 1,
losses: vec!["step/pcurve.use-native-metadata".to_owned()],
..plain
}
);
}
#[test]
fn writer_keeps_a_seam_ring_that_is_the_only_bound() {
let plain = written(&unit_cube().expect("unit cube fixture is admitted"));
let alone = written(&cube_with_a_seam_ring(SeamRing::Alone));
assert_eq!(
(alone.oriented_edges, alone.faces, alone.vertex_loops),
(plain.oriented_edges - 4 + 2, plain.faces, 0)
);
let unwritable = written(&cube_with_a_seam_ring(SeamRing::BesideAnUnwritableLoop));
assert_eq!(unwritable.vertex_loops, 0);
assert!(unwritable.oriented_edges >= 2);
}
#[test]
fn writer_keeps_a_seam_ring_whose_curveless_edge_has_length() {
let long = written(&cube_with_a_seam_ring(SeamRing::BesideALoopWithALongPole));
assert_eq!(long.vertex_loops, 0);
assert!(
long.losses
.contains(&"step/topology.edge-no-3d-curve".to_owned()),
"{:?}",
long.losses
);
}