use ifc_model::value::Value;
use ifc_model::{EntityId, Model};
use crate::error::{AlignmentError, AlignmentResult};
use crate::horizontal::AlignmentUnits;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CurveMeasure {
Length(f64),
Parameter(f64),
}
#[derive(Debug, Clone, PartialEq)]
pub struct PointByDistance {
pub entity: EntityId,
pub distance_along: CurveMeasure,
pub offset_lateral: Option<f64>,
pub offset_vertical: Option<f64>,
pub offset_longitudinal: Option<f64>,
pub basis_curve: EntityId,
}
#[derive(Debug, Clone, PartialEq)]
pub struct LinearPlacement {
pub entity: EntityId,
pub relative_placement: PointByDistance,
pub has_explicit_axes: bool,
}
pub fn resolve_point_by_distance(
model: &Model,
id: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<PointByDistance> {
let entity = model
.get(id)
.ok_or(AlignmentError::MissingEntity { entity: id })?;
if !entity.is_type("IFCPOINTBYDISTANCEEXPRESSION") {
return Err(AlignmentError::WrongType {
entity: id,
expected: "IFCPOINTBYDISTANCEEXPRESSION",
actual: entity.type_name.to_string(),
});
}
let values = &entity.attributes;
let distance_along = curve_measure(values, id, 0, "DistanceAlong", units)?;
let offset_lateral = optional_number(values, id, 1, "OffsetLateral")?.map(|v| length(v, units));
let offset_vertical =
optional_number(values, id, 2, "OffsetVertical")?.map(|v| length(v, units));
let offset_longitudinal =
optional_number(values, id, 3, "OffsetLongitudinal")?.map(|v| length(v, units));
let basis_curve = reference(values, id, 4, "BasisCurve")?;
if model.get(basis_curve).is_none() {
return Err(AlignmentError::DanglingReference {
entity: id,
attribute: "BasisCurve",
target: basis_curve,
});
}
Ok(PointByDistance {
entity: id,
distance_along,
offset_lateral,
offset_vertical,
offset_longitudinal,
basis_curve,
})
}
pub fn resolve_linear_placement(
model: &Model,
id: EntityId,
units: AlignmentUnits,
) -> AlignmentResult<LinearPlacement> {
let entity = model
.get(id)
.ok_or(AlignmentError::MissingEntity { entity: id })?;
if !entity.is_type("IFCLINEARPLACEMENT") {
return Err(AlignmentError::WrongType {
entity: id,
expected: "IFCLINEARPLACEMENT",
actual: entity.type_name.to_string(),
});
}
let values = &entity.attributes;
let relative_placement_id = reference(values, id, 1, "RelativePlacement")?;
let placement_entity =
model
.get(relative_placement_id)
.ok_or(AlignmentError::DanglingReference {
entity: id,
attribute: "RelativePlacement",
target: relative_placement_id,
})?;
if !placement_entity.is_type("IFCAXIS2PLACEMENTLINEAR") {
return Err(AlignmentError::WrongType {
entity: relative_placement_id,
expected: "IFCAXIS2PLACEMENTLINEAR",
actual: placement_entity.type_name.to_string(),
});
}
let placement_values = &placement_entity.attributes;
let location_id = reference(placement_values, relative_placement_id, 0, "Location")?;
let relative_placement = resolve_point_by_distance(model, location_id, units)?;
let has_explicit_axes = matches!(placement_values.get(1), Some(Value::Ref(_)))
|| matches!(placement_values.get(2), Some(Value::Ref(_)));
let axis_present = matches!(placement_values.get(1), Some(Value::Ref(_)));
let ref_direction_present = matches!(placement_values.get(2), Some(Value::Ref(_)));
if axis_present != ref_direction_present {
return Err(AlignmentError::InvalidSegment {
entity: relative_placement_id,
detail: "IfcAxis2PlacementLinear.Axis and RefDirection must both be present or both absent (WR2)",
});
}
Ok(LinearPlacement {
entity: id,
relative_placement,
has_explicit_axes,
})
}
fn curve_measure(
values: &[Value],
id: EntityId,
slot: usize,
name: &'static str,
units: AlignmentUnits,
) -> AlignmentResult<CurveMeasure> {
match values.get(slot) {
Some(Value::Typed { type_name, value }) => {
let raw = value.as_f64().ok_or(AlignmentError::InvalidAttribute {
entity: id,
index: slot,
name,
})?;
if type_name.eq_ignore_ascii_case("IFCLENGTHMEASURE") {
Ok(CurveMeasure::Length(raw * units.length_to_metres))
} else if type_name.eq_ignore_ascii_case("IFCPARAMETERVALUE") {
Ok(CurveMeasure::Parameter(raw))
} else {
Err(AlignmentError::InvalidAttribute {
entity: id,
index: slot,
name,
})
}
}
Some(Value::Real(value)) => Ok(CurveMeasure::Length(value * units.length_to_metres)),
Some(Value::Integer(value)) => {
Ok(CurveMeasure::Length(*value as f64 * units.length_to_metres))
}
_ => Err(AlignmentError::InvalidAttribute {
entity: id,
index: slot,
name,
}),
}
}
fn optional_number(
values: &[Value],
id: EntityId,
slot: usize,
name: &'static str,
) -> AlignmentResult<Option<f64>> {
match values.get(slot) {
Some(Value::Null) => Ok(None),
Some(value) => {
value
.unwrap_typed()
.as_f64()
.map(Some)
.ok_or(AlignmentError::InvalidAttribute {
entity: id,
index: slot,
name,
})
}
None => Err(AlignmentError::InvalidAttribute {
entity: id,
index: slot,
name,
}),
}
}
fn reference(
values: &[Value],
id: EntityId,
slot: usize,
name: &'static str,
) -> AlignmentResult<EntityId> {
values
.get(slot)
.and_then(|value| value.as_ref_id())
.ok_or(AlignmentError::InvalidAttribute {
entity: id,
index: slot,
name,
})
}
fn length(value: f64, units: AlignmentUnits) -> f64 {
value * units.length_to_metres
}