use axiolid_curve::{Curve2, Curve3, Elevated3};
use axiolid_model::{CurveRelation, GeometryGraphBuilder, GeometryNode};
use ifc_model::{EntityId, Model};
use super::assemble::{finish, LoweredAlignmentCurve};
use super::elevation::profile_law;
use crate::cant::CantLayout;
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::AlignmentUnits;
use crate::vertical::read_vertical_segment;
use crate::view::AlignmentView;
pub fn lower_gradient_curve(
model: &Model,
entity: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<LoweredAlignmentCurve> {
let view = AlignmentView::for_model(model)?;
let alignment = model
.get(entity)
.ok_or(AlignmentError::MissingEntity { entity })?;
if !view.schema.is_a(&alignment.type_name, "IfcAlignment") {
return Err(AlignmentError::WrongType {
entity,
expected: "IfcAlignment",
actual: alignment.type_name.to_string(),
});
}
let horizontal = sole_layout(&view, entity, "IfcAlignmentHorizontal")?;
let vertical = sole_layout(&view, entity, "IfcAlignmentVertical")?;
let cant = optional_cant(model, &view, entity, units)?;
let (curve, ids) = compose(model, &view, horizontal, vertical, units, cant.as_ref())?;
let mut builder = GeometryGraphBuilder::new();
let root =
builder
.push(GeometryNode::Curve3(curve))
.map_err(|error| AlignmentError::Graph {
detail: error.to_string(),
})?;
let mut sources = vec![horizontal, vertical];
sources.extend(ids);
finish(builder, root, sources)
}
fn sole_layout(
view: &AlignmentView,
alignment: EntityId,
expected: &'static str,
) -> AlignmentResult<EntityId> {
let children = view.nested_children(alignment, expected)?;
match children.as_slice() {
[only] => Ok(*only),
[] => Err(AlignmentError::SemanticViolation {
entity: Some(alignment),
rule: "a gradient curve needs both a horizontal and a vertical layout",
}),
_ => Err(AlignmentError::SemanticViolation {
entity: Some(alignment),
rule: "an alignment with several layouts of one kind is ambiguous to compose",
}),
}
}
fn sole_plan_curve(
model: &Model,
horizontal: EntityId,
units: AlignmentUnits,
cant: Option<&CantLayout>,
) -> AlignmentResult<Curve2> {
let lowered = super::assemble::lower_horizontal_layout(model, horizontal, units, cant)?;
let Some(GeometryNode::CurveRelation(CurveRelation::Composite { segments })) =
lowered.graph.get(lowered.root)
else {
return Err(AlignmentError::Unsupported {
entity: horizontal,
type_name: "IfcAlignmentHorizontal".to_owned(),
detail: "plan did not lower to a composite curve",
});
};
let [only] = segments.as_slice() else {
return Err(AlignmentError::Unsupported {
entity: horizontal,
type_name: "IfcAlignmentHorizontal".to_owned(),
detail: "a multi-segment plan has no single Curve2 to elevate; the neutral vocabulary has no composite Curve2",
});
};
if let Some(GeometryNode::CurveRelation(CurveRelation::Trimmed { basis, .. })) =
lowered.graph.get(only.curve)
{
if let Some(GeometryNode::Curve2(curve)) = lowered.graph.get(*basis) {
return Ok(curve.clone());
}
}
Err(AlignmentError::Unsupported {
entity: horizontal,
type_name: "IfcAlignmentHorizontal".to_owned(),
detail: "plan has no single basis curve to elevate",
})
}
fn compose(
model: &Model,
view: &AlignmentView<'_>,
horizontal: EntityId,
vertical: EntityId,
units: AlignmentUnits,
cant: Option<&CantLayout>,
) -> AlignmentResult<(Curve3, Vec<EntityId>)> {
let plan = sole_plan_curve(model, horizontal, units, cant)?;
let ids = view.segment_chain(vertical, "IfcAlignmentVerticalSegment")?;
let mut segments = Vec::with_capacity(ids.len());
for id in &ids {
segments.push(read_vertical_segment(model, *id, units)?);
}
let elevation = profile_law(&segments)?;
Ok((Curve3::Elevated(Elevated3::new(plan, elevation)), ids))
}
pub fn gradient_curve3(
model: &Model,
entity: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<Curve3> {
let view = AlignmentView::for_model(model)?;
let alignment = model
.get(entity)
.ok_or(AlignmentError::MissingEntity { entity })?;
if !view.schema.is_a(&alignment.type_name, "IfcAlignment") {
return Err(AlignmentError::WrongType {
entity,
expected: "IfcAlignment",
actual: alignment.type_name.to_string(),
});
}
let horizontal = sole_layout(&view, entity, "IfcAlignmentHorizontal")?;
let vertical = sole_layout(&view, entity, "IfcAlignmentVertical")?;
let cant = optional_cant(model, &view, entity, units)?;
let (curve, _) = compose(model, &view, horizontal, vertical, units, cant.as_ref())?;
Ok(curve)
}
fn optional_cant(
model: &Model,
view: &AlignmentView,
alignment: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<Option<CantLayout>> {
let children = view.nested_children(alignment, "IfcAlignmentCant")?;
match children.as_slice() {
[] => Ok(None),
[only] => CantLayout::resolve(model, *only, units).map(Some),
_ => Err(AlignmentError::SemanticViolation {
entity: Some(alignment),
rule: "an alignment with several cant layouts is ambiguous to compose",
}),
}
}