use axiolid_curve::Curve2;
use axiolid_model::GeometryNode;
use ifc_alignment::{
lower_horizontal_layout, lower_horizontal_layout_partial, AlignmentError, AlignmentUnits,
};
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_all_or_nothing_entry_point_still_refuses_a_spiral_layout() {
let model = model();
let error = lower_horizontal_layout(&model, HORIZONTAL, units(), None)
.expect_err("a CLOTHOID has no exact neutral primitive");
assert!(
matches!(&error, AlignmentError::Unsupported { type_name, .. } if type_name == "CLOTHOID"),
"expected a CLOTHOID refusal, got {error:?}"
);
}
#[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_refusal_splits_the_layout_into_separate_runs_rather_than_bridging_it() {
let model = model();
let result = lower_horizontal_layout_partial(&model, HORIZONTAL, units(), None)
.expect("layout is readable");
assert_eq!(result.runs.len(), 3);
let sources: Vec<Vec<u64>> = result
.runs
.iter()
.map(|run| run.sources.iter().map(|id| id.0).collect())
.collect();
assert_eq!(sources, vec![vec![101, 103], vec![105, 107], vec![109]]);
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()
}