use axiolid_curve::Curve2;
use axiolid_model::{CurveRelation, GeometryNode, Transition};
use ifc_alignment::{
lower_horizontal_layout, lower_horizontal_layout_partial, AlignmentError, AlignmentUnits,
SeamCheck,
};
use ifc_model::{Codec, EntityId};
use ifc_step::StepCodec;
const HORIZONTAL: EntityId = EntityId(191);
fn load(name: &str) -> ifc_model::Model {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../test/fixtures/synthetic-surfaces")
.join(name);
StepCodec.read_path(&path).expect("fixture parses")
}
fn units() -> AlignmentUnits {
AlignmentUnits {
length_to_metres: 1.0,
angle_to_radians: 1.0,
}
}
fn model() -> ifc_model::Model {
load("synthetic_alignment_spiral.ifc")
}
#[test]
fn the_strict_entry_point_lowers_a_spiral_layout_with_its_seams_recorded() {
let model = model();
let lowered = lower_horizontal_layout(&model, HORIZONTAL, units(), None)
.expect("a spiral no longer stops the strict path");
assert_eq!(
lowered.sources.iter().map(|id| id.0).collect::<Vec<_>>(),
vec![101, 103, 105, 107, 109]
);
let checks: Vec<(u64, u64, f64, SeamCheck)> = lowered
.seams
.iter()
.map(|seam| {
(
seam.previous.0,
seam.next.0,
seam.distance_along,
seam.position,
)
})
.collect();
assert_eq!(
checks,
vec![
(101, 103, 100.0, SeamCheck::Verified),
(103, 105, 160.0, SeamCheck::Authored),
(105, 107, 280.0, SeamCheck::Verified),
(107, 109, 340.0, SeamCheck::Authored),
]
);
let Some(GeometryNode::CurveRelation(CurveRelation::Composite { segments })) =
lowered.graph.get(lowered.root)
else {
panic!("a layout lowers to a composite");
};
let transitions: Vec<Transition> = segments.iter().map(|s| s.transition).collect();
assert_eq!(
transitions,
vec![
Transition::Discontinuous,
Transition::Continuous,
Transition::Discontinuous,
Transition::Continuous,
Transition::Discontinuous,
]
);
}
#[test]
fn partial_lowering_keeps_the_exact_segments_and_names_the_refused_ones() {
let model = model();
let result = lower_horizontal_layout_partial(&model, HORIZONTAL, units(), None)
.expect("layout is readable");
assert_eq!(result.segment_count, 5);
assert_eq!(
result.lowered_count(),
5,
"every segment lowers exactly: clothoids are intrinsic curves"
);
assert!(result.is_complete());
assert!(
result.refused.is_empty(),
"clothoids are no longer refused: {:?}",
result.refused
);
assert!(result.is_complete());
for refusal in &result.refused {
assert!(matches!(refusal.reason, AlignmentError::Unsupported { .. }));
}
}
#[test]
fn a_spiral_seam_does_not_split_the_layout_into_runs() {
let model = model();
let result = lower_horizontal_layout_partial(&model, HORIZONTAL, units(), None)
.expect("layout is readable");
assert_eq!(result.runs.len(), 1);
assert_eq!(
result.runs[0]
.sources
.iter()
.map(|id| id.0)
.collect::<Vec<_>>(),
vec![101, 103, 105, 107, 109]
);
let authored: Vec<u64> = result.runs[0]
.seams
.iter()
.filter(|seam| seam.position == SeamCheck::Authored)
.map(|seam| seam.next.0)
.collect();
assert_eq!(authored, vec![105, 109], "the seams after each clothoid");
const FIXTURE_SPIRALS: [u64; 2] = [103, 107];
for spiral in FIXTURE_SPIRALS {
assert!(
result
.runs
.iter()
.any(|run| run.sources.iter().any(|id| id.0 == spiral)),
"spiral {spiral} must lower into a run, not be dropped"
);
}
let intrinsics: usize = result
.runs
.iter()
.flat_map(|run| run.graph.iter())
.filter(|(_, node)| matches!(node, GeometryNode::Curve2(Curve2::Intrinsic(_))))
.count();
assert_eq!(
intrinsics,
FIXTURE_SPIRALS.len(),
"each clothoid must lower to exactly one intrinsic curve"
);
let approximated = result
.runs
.iter()
.flat_map(|run| run.graph.iter())
.any(|(_, node)| {
matches!(
node,
GeometryNode::Curve2(Curve2::Polyline(_))
| GeometryNode::Curve2(Curve2::BSpline(_))
)
});
assert!(
!approximated,
"a spiral must never be discretised or fitted"
);
let lowered_total: usize = result.runs.iter().map(|run| run.sources.len()).sum();
assert_eq!(lowered_total, result.lowered_count());
assert_eq!(lowered_total + result.refused.len(), result.segment_count);
}
#[test]
fn a_fully_lowerable_layout_reports_complete_with_one_run() {
let model = load("synthetic_alignment_layout.ifc");
let result = lower_horizontal_layout_partial(&model, EntityId(121), units(), None)
.expect("layout is readable");
assert!(result.is_complete());
assert_eq!(result.refused, vec![]);
assert_eq!(result.runs.len(), 1);
assert_eq!(result.lowered_count(), 2);
let strict = lower_horizontal_layout(&model, EntityId(121), units(), None).expect("lowers");
assert_eq!(result.runs[0].sources, strict.sources);
let shape = |curve: &ifc_alignment::LoweredAlignmentCurve| -> Vec<String> {
let graph_id = regex_free_graph_id(&format!("{:?}", curve.graph));
curve
.graph
.iter()
.map(|(_, node)| format!("{node:?}").replace(&graph_id, "GraphId(N)"))
.collect()
};
assert_eq!(shape(&result.runs[0]), shape(&strict));
}
fn regex_free_graph_id(debug: &str) -> String {
let start = debug.find("GraphId(").expect("graph debug names its id");
let end = debug[start..].find(')').expect("graph id is closed") + start + 1;
debug[start..end].to_owned()
}