use crate::error::GeometryResult;
use crate::resource::point::CartesianPoint;
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model, Value};
pub(crate) mod slot {
pub const BASIS_CURVE: usize = 0;
pub const TRIM_1: usize = 1;
pub const TRIM_2: usize = 2;
pub const SENSE_AGREEMENT: usize = 3;
pub const MASTER_REPRESENTATION: usize = 4;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrimmingPreference {
Cartesian,
Parameter,
Unspecified,
}
impl TrimmingPreference {
pub fn from_token(token: &str) -> Option<Self> {
match token.to_ascii_uppercase().as_str() {
"CARTESIAN" => Some(Self::Cartesian),
"PARAMETER" => Some(Self::Parameter),
"UNSPECIFIED" => Some(Self::Unspecified),
_ => None,
}
}
pub fn token(self) -> &'static str {
match self {
Self::Cartesian => "CARTESIAN",
Self::Parameter => "PARAMETER",
Self::Unspecified => "UNSPECIFIED",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TrimPoint {
Cartesian(EntityId),
Parameter(f64),
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct Trim {
pub cartesian: Option<EntityId>,
pub parameter: Option<f64>,
}
impl Trim {
pub fn preferred(&self, preference: TrimmingPreference) -> Option<TrimPoint> {
let cartesian = self.cartesian.map(TrimPoint::Cartesian);
let parameter = self.parameter.map(TrimPoint::Parameter);
match preference {
TrimmingPreference::Cartesian => cartesian.or(parameter),
TrimmingPreference::Parameter | TrimmingPreference::Unspecified => {
parameter.or(cartesian)
}
}
}
pub fn is_over_specified(&self) -> bool {
self.cartesian.is_some() && self.parameter.is_some()
}
pub fn is_empty(&self) -> bool {
self.cartesian.is_none() && self.parameter.is_none()
}
pub fn cartesian_point<'v>(
&self,
model: &'v Model,
referrer: EntityId,
) -> GeometryResult<Option<CartesianPoint<'v>>> {
self.cartesian
.map(|id| resolve::cartesian_point(model, referrer, id))
.transpose()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TrimSpec {
pub basis_curve: EntityId,
pub trim1: Trim,
pub trim2: Trim,
pub sense_agreement: bool,
pub master_representation: TrimmingPreference,
}
impl TrimSpec {
pub fn endpoints(&self) -> (Trim, Trim) {
(self.trim1, self.trim2)
}
pub fn start(&self) -> Option<TrimPoint> {
self.trim1.preferred(self.master_representation)
}
pub fn end(&self) -> Option<TrimPoint> {
self.trim2.preferred(self.master_representation)
}
pub fn is_parametrically_complete(&self) -> bool {
self.trim1.parameter.is_some() && self.trim2.parameter.is_some()
}
}
#[derive(Debug, Clone, Copy)]
pub struct TrimmedCurve<'m> {
slots: Slots<'m>,
}
impl<'m> TrimmedCurve<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn basis_curve_ref(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(slot::BASIS_CURVE, "BasisCurve")
}
pub fn sense_agreement(&self) -> GeometryResult<bool> {
self.slots.req_bool(slot::SENSE_AGREEMENT, "SenseAgreement")
}
pub fn master_representation(&self) -> TrimmingPreference {
self.slots
.opt_enum(slot::MASTER_REPRESENTATION)
.and_then(TrimmingPreference::from_token)
.unwrap_or(TrimmingPreference::Unspecified)
}
pub fn trim1(&self) -> GeometryResult<Trim> {
self.read_trim(slot::TRIM_1, "Trim1")
}
pub fn trim2(&self) -> GeometryResult<Trim> {
self.read_trim(slot::TRIM_2, "Trim2")
}
pub fn spec(&self) -> GeometryResult<TrimSpec> {
Ok(TrimSpec {
basis_curve: self.basis_curve_ref()?,
trim1: self.trim1()?,
trim2: self.trim2()?,
sense_agreement: self.sense_agreement()?,
master_representation: self.master_representation(),
})
}
fn read_trim(&self, index: usize, name: &'static str) -> GeometryResult<Trim> {
let value = self.slots.req(index, name)?;
let members: &[Value] = match value {
Value::List(items) => items,
single => std::slice::from_ref(single),
};
let mut trim = Trim::default();
for member in members {
match member {
Value::Ref(id) => {
if trim.cartesian.replace(*id).is_some() {
return Err(self.slots.degenerate(format!(
"{name} holds two Cartesian trim points; \
IfcTrimmingSelect allows at most one of each kind"
)));
}
}
other => {
let Some(p) = other.unwrap_typed().as_f64() else {
return Err(self.slots.degenerate(format!(
"{name} member is neither an IfcCartesianPoint reference \
nor an IfcParameterValue"
)));
};
if trim.parameter.replace(p).is_some() {
return Err(self.slots.degenerate(format!(
"{name} holds two parameter values; \
IfcTrimmingSelect allows at most one of each kind"
)));
}
}
}
}
if trim.is_empty() {
return Err(self
.slots
.degenerate(format!("{name} is empty; SET [1:2] requires a member")));
}
Ok(trim)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn parameter(v: f64) -> Value {
Value::Typed {
type_name: "IFCPARAMETERVALUE".into(),
value: Box::new(Value::Real(v)),
}
}
fn curve(trim1: Value, trim2: Value, sense: bool, preference: &str) -> Entity {
Entity::new(
"IFCTRIMMEDCURVE",
vec![
Value::Ref(EntityId(50)),
trim1,
trim2,
Value::Bool(sense),
Value::Enum(preference.into()),
],
)
}
#[test]
fn a_trim_may_carry_a_point_and_a_parameter_at_once() {
let e = curve(
Value::List(vec![Value::Ref(EntityId(1)), parameter(0.0)]),
Value::List(vec![Value::Ref(EntityId(2)), parameter(90.0)]),
true,
"PARAMETER",
);
let spec = TrimmedCurve::new(EntityId(9), &e).spec().unwrap();
assert_eq!(spec.trim1.cartesian, Some(EntityId(1)));
assert_eq!(spec.trim1.parameter, Some(0.0));
assert!(spec.trim1.is_over_specified());
assert!(spec.is_parametrically_complete());
}
#[test]
fn a_cartesian_trim_resolves_to_its_point_and_a_parameter_trim_to_none() {
let mut model = Model::new();
let coords = Value::List(vec![Value::Real(1.0), Value::Real(0.0)]);
model.insert(EntityId(1), Entity::new("IFCCARTESIANPOINT", vec![coords]));
let e = curve(
Value::List(vec![Value::Ref(EntityId(1))]),
Value::List(vec![parameter(90.0)]),
true,
"CARTESIAN",
);
let spec = TrimmedCurve::new(EntityId(9), &e).spec().unwrap();
let point = spec.trim1.cartesian_point(&model, EntityId(9)).unwrap();
assert_eq!(point.unwrap().coordinates().unwrap(), vec![1.0, 0.0]);
assert!(spec
.trim2
.cartesian_point(&model, EntityId(9))
.unwrap()
.is_none());
}
#[test]
fn a_cartesian_trim_naming_a_non_point_is_a_typed_error() {
let mut model = Model::new();
model.insert(EntityId(1), Entity::new("IFCDIRECTION", vec![]));
let trim = Trim {
cartesian: Some(EntityId(1)),
parameter: None,
};
let err = trim.cartesian_point(&model, EntityId(9)).unwrap_err();
assert!(matches!(
err,
crate::GeometryError::WrongEntityType {
entity: EntityId(1),
..
}
));
let err = trim
.cartesian_point(&Model::new(), EntityId(9))
.unwrap_err();
assert_eq!(err.entity(), Some(EntityId(9)));
}
#[test]
fn master_representation_decides_between_the_two_trim_forms() {
let both = Value::List(vec![Value::Ref(EntityId(1)), parameter(0.25)]);
let cartesian = curve(both.clone(), both.clone(), true, "CARTESIAN");
assert_eq!(
TrimmedCurve::new(EntityId(1), &cartesian)
.spec()
.unwrap()
.start(),
Some(TrimPoint::Cartesian(EntityId(1)))
);
let param = curve(both.clone(), both.clone(), true, "PARAMETER");
assert_eq!(
TrimmedCurve::new(EntityId(1), ¶m)
.spec()
.unwrap()
.start(),
Some(TrimPoint::Parameter(0.25))
);
}
#[test]
fn unspecified_preference_falls_back_to_the_parameter_form() {
let both = Value::List(vec![Value::Ref(EntityId(1)), parameter(0.5)]);
let e = curve(both.clone(), both, true, "UNSPECIFIED");
assert_eq!(
TrimmedCurve::new(EntityId(1), &e).spec().unwrap().start(),
Some(TrimPoint::Parameter(0.5))
);
}
#[test]
fn a_preference_for_an_absent_form_falls_back_to_the_present_one() {
let only_point = Value::List(vec![Value::Ref(EntityId(1))]);
let e = curve(only_point.clone(), only_point, true, "PARAMETER");
assert_eq!(
TrimmedCurve::new(EntityId(1), &e).spec().unwrap().start(),
Some(TrimPoint::Cartesian(EntityId(1)))
);
}
#[test]
fn four_distinct_arcs_come_from_one_basis_curve_and_one_trim_pair() {
let a = Value::List(vec![parameter(0.0)]);
let b = Value::List(vec![parameter(90.0)]);
let mut specs = Vec::new();
for (t1, t2) in [(a.clone(), b.clone()), (b.clone(), a.clone())] {
for sense in [true, false] {
let e = curve(t1.clone(), t2.clone(), sense, "PARAMETER");
specs.push(TrimmedCurve::new(EntityId(1), &e).spec().unwrap());
}
}
assert_eq!(specs.len(), 4);
for i in 0..specs.len() {
for j in (i + 1)..specs.len() {
assert_ne!(
specs[i], specs[j],
"arcs {i} and {j} collapsed into one specification"
);
}
}
}
#[test]
fn sense_agreement_does_not_swap_the_endpoints() {
let a = Value::List(vec![parameter(10.0)]);
let b = Value::List(vec![parameter(350.0)]);
for sense in [true, false] {
let e = curve(a.clone(), b.clone(), sense, "PARAMETER");
let spec = TrimmedCurve::new(EntityId(1), &e).spec().unwrap();
assert_eq!(spec.start(), Some(TrimPoint::Parameter(10.0)));
assert_eq!(spec.end(), Some(TrimPoint::Parameter(350.0)));
assert_eq!(spec.sense_agreement, sense);
}
}
#[test]
fn descending_trim_parameters_are_preserved_not_sorted() {
let e = curve(
Value::List(vec![parameter(270.0)]),
Value::List(vec![parameter(45.0)]),
true,
"PARAMETER",
);
let spec = TrimmedCurve::new(EntityId(1), &e).spec().unwrap();
assert_eq!(spec.trim1.parameter, Some(270.0));
assert_eq!(spec.trim2.parameter, Some(45.0));
}
#[test]
fn an_empty_trim_set_is_rejected() {
let e = curve(
Value::List(vec![]),
Value::List(vec![parameter(1.0)]),
true,
"PARAMETER",
);
let err = TrimmedCurve::new(EntityId(3), &e).spec().unwrap_err();
assert!(err.to_string().contains("Trim1"), "got: {err}");
}
#[test]
fn two_parameters_in_one_trim_set_is_rejected() {
let e = curve(
Value::List(vec![parameter(1.0), parameter(2.0)]),
Value::List(vec![parameter(3.0)]),
true,
"PARAMETER",
);
assert!(TrimmedCurve::new(EntityId(1), &e).spec().is_err());
}
#[test]
fn a_single_unwrapped_trim_member_is_accepted() {
let e = curve(parameter(0.0), Value::Ref(EntityId(2)), true, "UNSPECIFIED");
let spec = TrimmedCurve::new(EntityId(1), &e).spec().unwrap();
assert_eq!(spec.trim1.parameter, Some(0.0));
assert_eq!(spec.trim2.cartesian, Some(EntityId(2)));
}
#[test]
fn an_absent_master_representation_reads_as_unspecified() {
let e = Entity::new(
"IFCTRIMMEDCURVE",
vec![
Value::Ref(EntityId(50)),
Value::List(vec![parameter(0.0)]),
Value::List(vec![parameter(1.0)]),
Value::Bool(true),
],
);
assert_eq!(
TrimmedCurve::new(EntityId(1), &e).master_representation(),
TrimmingPreference::Unspecified
);
}
#[test]
fn unknown_preference_tokens_are_not_silently_accepted() {
assert_eq!(TrimmingPreference::from_token("NONSENSE"), None);
assert_eq!(
TrimmingPreference::from_token("cartesian"),
Some(TrimmingPreference::Cartesian)
);
}
}