use axiolid_core::{Frame2, Point2, Vec2};
use axiolid_curve::{Circle2, Curve2, 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::spiral::{is_exactly_lowerable, spiral_curve};
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::{
read_horizontal_segment, AlignmentUnits, HorizontalSegment, HorizontalSegmentType,
};
use crate::vertical::{read_vertical_segment, VerticalSegment, VerticalSegmentType};
use crate::view::AlignmentView;
#[derive(Debug, Clone, PartialEq)]
pub struct LoweredAlignmentCurve {
pub graph: GeometryGraph,
pub root: NodeId,
pub sources: Vec<EntityId>,
}
fn observed_transition(end: Point2, next_start: Point2, tolerance: f64) -> Option<Transition> {
if end.distance(next_start) <= tolerance {
Some(Transition::Continuous)
} else {
None
}
}
fn closed_form_end_point(segment: &HorizontalSegment) -> Option<Point2> {
match segment.segment_type {
HorizontalSegmentType::Line => {
let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
Some(segment.start_point + direction * segment.segment_length)
}
HorizontalSegmentType::CircularArc if segment.start_radius != 0.0 => {
let direction = Vec2::new(segment.start_direction.cos(), segment.start_direction.sin());
let left = Vec2::new(-direction.y, direction.x);
let centre = segment.start_point + left * segment.start_radius;
let sweep = segment.segment_length / segment.start_radius;
let radial = segment.start_point - centre;
let (sin_s, cos_s) = sweep.sin_cos();
Some(
centre
+ Vec2::new(
radial.x * cos_s - radial.y * sin_s,
radial.x * sin_s + radial.y * cos_s,
),
)
}
_ => None,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RefusedSegment {
pub entity: EntityId,
pub type_name: String,
pub reason: AlignmentError,
}
#[derive(Debug, Clone, PartialEq)]
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)?;
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 is_exactly_lowerable(name, false) => {
push_spiral(&mut builder, &segment, name, None, 0.0)?
}
kind => return Err(AlignmentError::Unsupported {
entity: id,
type_name: kind.source_name().to_owned(),
detail:
"the pinned neutral curve vocabulary has no exact transition-curve primitive",
}),
};
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)?;
let mut builder = GeometryGraphBuilder::new();
let root = push_constant_gradient(&mut builder, &segment)?;
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_constant_gradient(
builder: &mut GeometryGraphBuilder,
segment: &VerticalSegment,
) -> AlignmentResult<NodeId> {
if !matches!(
segment.predefined_type,
VerticalSegmentType::ConstantGradient
) {
return Err(AlignmentError::Unsupported {
entity: segment.entity,
type_name: segment.predefined_type.source_name().to_owned(),
detail: "exact neutral vertical lowering is currently limited to constant gradient",
});
}
if segment.radius_of_curvature.is_some() || segment.start_gradient != segment.end_gradient {
return Err(AlignmentError::InvalidSegment {
entity: segment.entity,
detail: "CONSTANTGRADIENT requires equal gradients and no curvature radius",
});
}
let basis = push(
builder,
GeometryNode::Curve2(Curve2::Line(Line2 {
origin: Point2::new(segment.start_dist_along, segment.start_height),
direction: Vec2::new(1.0, segment.start_gradient),
})),
)?;
push(
builder,
GeometryNode::CurveRelation(CurveRelation::Trimmed {
basis,
start: vec![TrimSelector::Parameter(0.0)],
end: vec![TrimSelector::Parameter(segment.horizontal_length)],
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(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,
})
}
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 mut builder = GeometryGraphBuilder::new();
let mut composite_segments = Vec::with_capacity(segments.len());
let mut station = 0.0_f64;
for (index, segment) in segments.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 is_exactly_lowerable(name, cant.is_some()) =>
{
push_spiral(&mut builder, segment, name, cant, station)?
}
kind => return Err(AlignmentError::Unsupported {
entity: segment.entity,
type_name: kind.source_name().to_owned(),
detail:
"the pinned neutral curve vocabulary has no exact transition-curve primitive",
}),
};
let transition = if index == 0 {
Transition::Discontinuous
} else {
let previous = &segments[index - 1];
let previous_end =
closed_form_end_point(previous).ok_or(AlignmentError::Unsupported {
entity: previous.entity,
type_name: previous.segment_type.source_name().to_owned(),
detail: "continuity across a transition spiral is not provable in closed form",
})?;
observed_transition(previous_end, segment.start_point, 1e-6).ok_or(
AlignmentError::SemanticViolation {
entity: Some(segment.entity),
rule: "consecutive horizontal segments must share an endpoint exactly",
},
)?
};
composite_segments.push(CurveSegment {
curve,
same_sense: true,
transition,
});
station += segment.segment_length;
}
let root = push(
&mut builder,
GeometryNode::CurveRelation(CurveRelation::Composite {
segments: composite_segments,
}),
)?;
finish(builder, root, ids)
}
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 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 station = 0.0_f64;
for (index, segment) in segments.iter().enumerate() {
if let Some((previous_index, _)) = pending.last() {
if closed_form_end_point(&segments[*previous_index]).is_none() {
flush_run(&mut runs, &mut builder, &mut pending, &mut pending_ids)?;
}
}
let lowered = match &segment.segment_type {
HorizontalSegmentType::Line => push_line(&mut builder, segment),
HorizontalSegmentType::CircularArc => push_arc(&mut builder, segment),
HorizontalSegmentType::Transition(name)
if is_exactly_lowerable(name, cant.is_some()) =>
{
push_spiral(&mut builder, segment, name, cant, station)
}
kind => Err(AlignmentError::Unsupported {
entity: segment.entity,
type_name: kind.source_name().to_owned(),
detail:
"the pinned neutral curve vocabulary has no exact transition-curve primitive",
}),
};
station += segment.segment_length;
let curve = match lowered {
Ok(curve) => curve,
Err(reason) => {
flush_run(&mut runs, &mut builder, &mut pending, &mut pending_ids)?;
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 previous = &segments[*previous_index];
let previous_end = closed_form_end_point(previous)
.expect("run boundary guarantees a closed-form predecessor end point");
match observed_transition(previous_end, segment.start_point, 1e-6) {
Some(transition) => transition,
None => {
return Err(AlignmentError::SemanticViolation {
entity: Some(segment.entity),
rule: "consecutive horizontal segments must share an endpoint exactly",
})
}
}
}
};
pending.push((
index,
CurveSegment {
curve,
same_sense: true,
transition,
},
));
pending_ids.push(segment.entity);
}
flush_run(&mut runs, &mut builder, &mut pending, &mut pending_ids)?;
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>,
) -> AlignmentResult<()> {
if pending.is_empty() {
*builder = GeometryGraphBuilder::new();
pending_ids.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);
runs.push(finish(finished, root, sources)?);
Ok(())
}