use axiolid_core::{Frame2, Point2, Vec2};
use axiolid_curve::{BSplineCurve2, Circle2, Curve2, ElevationLaw, KnotSpec, Line2};
use axiolid_model::{
CurveRelation, CurveSegment, GeometryGraph, GeometryGraphBuilder, GeometryNode, NodeId,
Transition, TrimSelector, TrimmingPreference,
};
use ifc_model::{EntityId, Model};
use crate::cant::CantLayout;
use crate::curve::cubic::{cubic_curve, start_frame, CUBIC};
use crate::curve::elevation::elevation_law;
use crate::curve::seam::{check_position, HorizontalSeam, SeamCheck};
use crate::curve::spiral::{is_exactly_lowerable, refuse_unlowerable, spiral_curve};
use crate::curve::terminal::split_closing;
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::{
read_horizontal_segment, AlignmentUnits, HorizontalSegment, HorizontalSegmentType,
};
use crate::vertical::{read_vertical_segment, VerticalSegment};
use crate::view::AlignmentView;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct LoweredAlignmentCurve {
pub graph: GeometryGraph,
pub root: NodeId,
pub sources: Vec<EntityId>,
pub seams: Vec<HorizontalSeam>,
}
fn seam_transition(seam: &HorizontalSeam) -> Transition {
match seam.position {
SeamCheck::Verified => Transition::Continuous,
_ => Transition::Discontinuous,
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct RefusedSegment {
pub entity: EntityId,
pub type_name: String,
pub reason: AlignmentError,
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct PartialHorizontalLayout {
pub runs: Vec<LoweredAlignmentCurve>,
pub refused: Vec<RefusedSegment>,
pub segment_count: usize,
}
impl PartialHorizontalLayout {
#[must_use]
pub fn is_complete(&self) -> bool {
self.refused.is_empty()
}
#[must_use]
pub fn lowered_count(&self) -> usize {
self.segment_count - self.refused.len()
}
}
pub fn lower_horizontal_segment(
model: &Model,
id: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<LoweredAlignmentCurve> {
let segment = read_horizontal_segment(model, id, units)?;
if segment.segment_length == 0.0 {
return Err(no_geometry(id));
}
let mut builder = GeometryGraphBuilder::new();
let root = match &segment.segment_type {
HorizontalSegmentType::Line => push_line(&mut builder, &segment)?,
HorizontalSegmentType::CircularArc => push_arc(&mut builder, &segment)?,
HorizontalSegmentType::Transition(name) if name == CUBIC => {
push_cubic(&mut builder, &segment)?
}
HorizontalSegmentType::Transition(name) if is_exactly_lowerable(name, false) => {
push_spiral(&mut builder, &segment, name, None, 0.0)?
}
_ => return Err(refuse_unlowerable(&segment)),
};
finish(builder, root, vec![id])
}
pub fn lower_vertical_segment(
model: &Model,
id: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<LoweredAlignmentCurve> {
let segment = read_vertical_segment(model, id, units)?;
if segment.horizontal_length == 0.0 {
return Err(no_geometry(id));
}
let law = elevation_law(&segment)?;
let mut builder = GeometryGraphBuilder::new();
let root = push_vertical_law(&mut builder, &segment, &law)?;
finish(builder, root, vec![id])
}
fn push_line(
builder: &mut GeometryGraphBuilder,
segment: &HorizontalSegment,
) -> AlignmentResult<NodeId> {
if segment.start_radius != 0.0 || segment.end_radius != 0.0 {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "LINE requires zero start and end radii",
});
}
let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
let basis = push(
builder,
GeometryNode::Curve2(Curve2::Line(Line2 {
origin: segment.start_point,
direction,
})),
)?;
push(
builder,
GeometryNode::CurveRelation(CurveRelation::Trimmed {
basis,
start: vec![TrimSelector::Parameter(0.0)],
end: vec![TrimSelector::Parameter(segment.segment_length)],
sense_agreement: true,
preference: TrimmingPreference::Parameter,
}),
)
}
fn push_arc(
builder: &mut GeometryGraphBuilder,
segment: &HorizontalSegment,
) -> AlignmentResult<NodeId> {
if segment.start_radius == 0.0
|| segment.start_radius != segment.end_radius
|| !segment.start_radius.is_finite()
{
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "CIRCULARARC requires equal, finite, non-zero start and end radii",
});
}
let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
let left = Vec2::new(-direction.y, direction.x);
let signed_radius = segment.start_radius;
let radius = signed_radius.abs();
let centre = segment.start_point + left * signed_radius;
let x = left * -signed_radius.signum();
let y = Vec2::new(-x.y, x.x);
let sweep = segment.segment_length / signed_radius;
if [centre.x, centre.y, x.x, x.y, y.x, y.y, radius, sweep]
.iter()
.any(|value| !value.is_finite())
{
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "CIRCULARARC derived frame and trim parameters must be finite",
});
}
let basis = push(
builder,
GeometryNode::Curve2(Curve2::Circle(Circle2 {
frame: Frame2 {
origin: centre,
x,
y,
},
radius,
})),
)?;
push(
builder,
GeometryNode::CurveRelation(CurveRelation::Trimmed {
basis,
start: vec![TrimSelector::Parameter(0.0)],
end: vec![TrimSelector::Parameter(sweep)],
sense_agreement: true,
preference: TrimmingPreference::Parameter,
}),
)
}
fn push_vertical_law(
builder: &mut GeometryGraphBuilder,
segment: &VerticalSegment,
law: &ElevationLaw,
) -> AlignmentResult<NodeId> {
let unsupported = |detail| AlignmentError::Unsupported {
entity: segment.entity,
type_name: segment.predefined_type.source_name().to_owned(),
detail,
};
let coefficients = match law {
ElevationLaw::Polynomial { coefficients } => coefficients,
ElevationLaw::CircularArc { grade, radius, .. } => {
return push_vertical_arc(builder, segment, *grade, *radius)
}
_ => {
return Err(unsupported(
"a single vertical segment lowers from one polynomial or circular piece",
))
}
};
let start = segment.start_dist_along;
let length = segment.horizontal_length;
let coefficient = |power: usize| coefficients.get(power).copied().unwrap_or(0.0);
let curve = match coefficients.len() {
0..=2 => Curve2::Line(Line2 {
origin: Point2::new(start, coefficient(0)),
direction: Vec2::new(1.0, coefficient(1)),
}),
3 => {
let (z0, g0, c2) = (coefficient(0), coefficient(1), coefficient(2));
let control_points = vec![
Point2::new(start, z0),
Point2::new(start + 0.5 * length, z0 + 0.5 * g0 * length),
Point2::new(start + length, z0 + g0 * length + c2 * length * length),
];
if control_points
.iter()
.any(|p| !p.x.is_finite() || !p.y.is_finite())
{
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "vertical control points must be finite",
});
}
Curve2::BSpline(BSplineCurve2 {
degree: 2,
control_points,
knots: vec![0.0, length],
multiplicities: vec![3, 3],
weights: None,
closed: false,
self_intersect: Some(false),
knot_spec: KnotSpec::PiecewiseBezier,
})
}
_ => {
return Err(unsupported(
"the vertical law has no exact neutral curve of its degree",
))
}
};
let basis = push(builder, GeometryNode::Curve2(curve))?;
push(
builder,
GeometryNode::CurveRelation(CurveRelation::Trimmed {
basis,
start: vec![TrimSelector::Parameter(0.0)],
end: vec![TrimSelector::Parameter(length)],
sense_agreement: true,
preference: TrimmingPreference::Parameter,
}),
)
}
fn push_vertical_arc(
builder: &mut GeometryGraphBuilder,
segment: &VerticalSegment,
grade: f64,
radius: f64,
) -> AlignmentResult<NodeId> {
let norm = grade.hypot(1.0);
let (sin0, cos0) = (grade / norm, 1.0 / norm);
let left = Vec2::new(-sin0, cos0);
let start = Point2::new(segment.start_dist_along, segment.start_height);
let centre = start + left * radius;
let x = left * -radius.signum();
let y = Vec2::new(-x.y, x.x);
let sin1 = sin0 + segment.horizontal_length / radius;
let sweep = sin1.asin() - sin0.atan2(cos0);
if [centre.x, centre.y, sweep].iter().any(|v| !v.is_finite()) || sin1.abs() >= 1.0 {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "CIRCULARARC derived frame and trim angle must be finite",
});
}
let basis = push(
builder,
GeometryNode::Curve2(Curve2::Circle(Circle2 {
frame: Frame2 {
origin: centre,
x,
y,
},
radius: radius.abs(),
})),
)?;
push(
builder,
GeometryNode::CurveRelation(CurveRelation::Trimmed {
basis,
start: vec![TrimSelector::Parameter(0.0)],
end: vec![TrimSelector::Parameter(sweep)],
sense_agreement: true,
preference: TrimmingPreference::Parameter,
}),
)
}
fn push_spiral(
builder: &mut GeometryGraphBuilder,
segment: &HorizontalSegment,
name: &str,
cant: Option<&CantLayout>,
start_distance: f64,
) -> AlignmentResult<NodeId> {
let curve = spiral_curve(segment, name, cant, start_distance)?;
let basis = push(builder, GeometryNode::Curve2(curve))?;
push(
builder,
GeometryNode::CurveRelation(CurveRelation::Trimmed {
basis,
start: vec![TrimSelector::Parameter(0.0)],
end: vec![TrimSelector::Parameter(segment.segment_length)],
sense_agreement: true,
preference: TrimmingPreference::Parameter,
}),
)
}
fn push_cubic(
builder: &mut GeometryGraphBuilder,
segment: &HorizontalSegment,
) -> AlignmentResult<NodeId> {
let curve = cubic_curve(segment, start_frame(segment))?;
let basis = push(builder, GeometryNode::Curve2(curve))?;
push(
builder,
GeometryNode::CurveRelation(CurveRelation::Trimmed {
basis,
start: vec![TrimSelector::Parameter(0.0)],
end: vec![TrimSelector::ArcLength(segment.segment_length)],
sense_agreement: true,
preference: TrimmingPreference::Parameter,
}),
)
}
fn no_geometry(entity: EntityId) -> AlignmentError {
AlignmentError::InvalidSegment {
entity,
detail: "a zero-length segment has no geometry to lower; IFC4.3 allows it only as the \
closing segment of a layout",
}
}
fn push(builder: &mut GeometryGraphBuilder, node: GeometryNode) -> AlignmentResult<NodeId> {
builder.push(node).map_err(|error| AlignmentError::Graph {
detail: error.to_string(),
})
}
pub(super) fn finish(
builder: GeometryGraphBuilder,
root: NodeId,
sources: Vec<EntityId>,
) -> AlignmentResult<LoweredAlignmentCurve> {
let graph = builder
.finish(vec![root])
.map_err(|error| AlignmentError::Graph {
detail: error.to_string(),
})?;
Ok(LoweredAlignmentCurve {
graph,
root,
sources,
seams: Vec::new(),
})
}
pub fn lower_horizontal_layout(
model: &Model,
entity: EntityId,
units: AlignmentUnits,
cant: Option<&CantLayout>,
) -> AlignmentResult<LoweredAlignmentCurve> {
let view = AlignmentView::for_model(model)?;
let horizontal_entity = model
.get(entity)
.ok_or(AlignmentError::MissingEntity { entity })?;
if !view
.schema
.is_a(&horizontal_entity.type_name, "IfcAlignmentHorizontal")
{
return Err(AlignmentError::WrongType {
entity,
expected: "IfcAlignmentHorizontal",
actual: horizontal_entity.type_name.to_string(),
});
}
let ids = view.segment_chain(entity, "IfcAlignmentHorizontalSegment")?;
if ids.is_empty() {
return Err(AlignmentError::SemanticViolation {
entity: Some(entity),
rule: "IfcAlignmentHorizontal must nest at least one IfcAlignmentSegment",
});
}
let mut segments = Vec::with_capacity(ids.len());
for id in &ids {
segments.push(read_horizontal_segment(model, *id, units)?);
}
let (body, closing) = split_closing(&segments, |s| s.segment_length, |s| s.entity)?;
let mut builder = GeometryGraphBuilder::new();
let mut composite_segments = Vec::with_capacity(body.len());
let mut seams = Vec::with_capacity(segments.len().saturating_sub(1));
let mut station = 0.0_f64;
for (index, segment) in body.iter().enumerate() {
let curve = match &segment.segment_type {
HorizontalSegmentType::Line => push_line(&mut builder, segment)?,
HorizontalSegmentType::CircularArc => push_arc(&mut builder, segment)?,
HorizontalSegmentType::Transition(name) if name == CUBIC => {
push_cubic(&mut builder, segment)?
}
HorizontalSegmentType::Transition(name)
if is_exactly_lowerable(name, cant.is_some()) =>
{
push_spiral(&mut builder, segment, name, cant, station)?
}
_ => return Err(refuse_unlowerable(segment)),
};
let transition = if index == 0 {
Transition::Discontinuous
} else {
let seam = check_position(&body[index - 1], segment, station)?;
let transition = seam_transition(&seam);
seams.push(seam);
transition
};
composite_segments.push(CurveSegment {
curve,
same_sense: true,
transition,
});
station += segment.segment_length;
}
if let (Some(closing), Some(last)) = (closing, body.last()) {
seams.push(check_position(last, closing, station)?);
}
let root = push(
&mut builder,
GeometryNode::CurveRelation(CurveRelation::Composite {
segments: composite_segments,
}),
)?;
let mut lowered = finish(builder, root, ids)?;
lowered.seams = seams;
Ok(lowered)
}
pub fn lower_horizontal_layout_partial(
model: &Model,
entity: EntityId,
units: AlignmentUnits,
cant: Option<&CantLayout>,
) -> AlignmentResult<PartialHorizontalLayout> {
let view = AlignmentView::for_model(model)?;
let horizontal_entity = model
.get(entity)
.ok_or(AlignmentError::MissingEntity { entity })?;
if !view
.schema
.is_a(&horizontal_entity.type_name, "IfcAlignmentHorizontal")
{
return Err(AlignmentError::WrongType {
entity,
expected: "IfcAlignmentHorizontal",
actual: horizontal_entity.type_name.to_string(),
});
}
let ids = view.segment_chain(entity, "IfcAlignmentHorizontalSegment")?;
if ids.is_empty() {
return Err(AlignmentError::SemanticViolation {
entity: Some(entity),
rule: "IfcAlignmentHorizontal must nest at least one IfcAlignmentSegment",
});
}
let mut segments = Vec::with_capacity(ids.len());
for id in &ids {
segments.push(read_horizontal_segment(model, *id, units)?);
}
let (body, closing) = split_closing(&segments, |s| s.segment_length, |s| s.entity)?;
let mut runs = Vec::new();
let mut refused = Vec::new();
let mut builder = GeometryGraphBuilder::new();
let mut pending: Vec<(usize, CurveSegment)> = Vec::new();
let mut pending_ids: Vec<EntityId> = Vec::new();
let mut pending_seams: Vec<HorizontalSeam> = Vec::new();
let mut station = 0.0_f64;
for (index, segment) in body.iter().enumerate() {
let lowered = match &segment.segment_type {
HorizontalSegmentType::Line => push_line(&mut builder, segment),
HorizontalSegmentType::CircularArc => push_arc(&mut builder, segment),
HorizontalSegmentType::Transition(name) if name == CUBIC => {
push_cubic(&mut builder, segment)
}
HorizontalSegmentType::Transition(name)
if is_exactly_lowerable(name, cant.is_some()) =>
{
push_spiral(&mut builder, segment, name, cant, station)
}
_ => Err(refuse_unlowerable(segment)),
};
let start_station = station;
station += segment.segment_length;
let curve = match lowered {
Ok(curve) => curve,
Err(reason) => {
flush_run(
&mut runs,
&mut builder,
&mut pending,
&mut pending_ids,
&mut pending_seams,
)?;
refused.push(RefusedSegment {
entity: segment.entity,
type_name: segment.segment_type.source_name().to_owned(),
reason,
});
continue;
}
};
let transition = match pending.last() {
None => Transition::Discontinuous,
Some((previous_index, _)) => {
let seam = check_position(&body[*previous_index], segment, start_station)?;
let transition = seam_transition(&seam);
pending_seams.push(seam);
transition
}
};
pending.push((
index,
CurveSegment {
curve,
same_sense: true,
transition,
},
));
pending_ids.push(segment.entity);
}
if let (Some(closing), Some((previous_index, _))) = (closing, pending.last()) {
pending_seams.push(check_position(&body[*previous_index], closing, station)?);
}
flush_run(
&mut runs,
&mut builder,
&mut pending,
&mut pending_ids,
&mut pending_seams,
)?;
Ok(PartialHorizontalLayout {
runs,
refused,
segment_count: segments.len(),
})
}
fn flush_run(
runs: &mut Vec<LoweredAlignmentCurve>,
builder: &mut GeometryGraphBuilder,
pending: &mut Vec<(usize, CurveSegment)>,
pending_ids: &mut Vec<EntityId>,
pending_seams: &mut Vec<HorizontalSeam>,
) -> AlignmentResult<()> {
if pending.is_empty() {
*builder = GeometryGraphBuilder::new();
pending_ids.clear();
pending_seams.clear();
return Ok(());
}
let mut finished = GeometryGraphBuilder::new();
core::mem::swap(builder, &mut finished);
let composite_segments: Vec<CurveSegment> =
pending.drain(..).map(|(_, segment)| segment).collect();
let root = push(
&mut finished,
GeometryNode::CurveRelation(CurveRelation::Composite {
segments: composite_segments,
}),
)?;
let sources = core::mem::take(pending_ids);
let mut run = finish(finished, root, sources)?;
run.seams = core::mem::take(pending_seams);
runs.push(run);
Ok(())
}