use crate::error::GeometryResult;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId};
pub(crate) mod curve_slot {
pub const SEGMENTS: usize = 0;
pub const SELF_INTERSECT: usize = 1;
}
pub(crate) mod segment_slot {
pub const TRANSITION: usize = 0;
pub const SAME_SENSE: usize = 1;
pub const PARENT_CURVE: usize = 2;
pub const PARAM_LENGTH: usize = 3;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransitionCode {
Discontinuous,
Continuous,
ContSameGradient,
ContSameGradientSameCurvature,
}
impl TransitionCode {
pub fn from_token(token: &str) -> Option<Self> {
match token.to_ascii_uppercase().as_str() {
"DISCONTINUOUS" => Some(Self::Discontinuous),
"CONTINUOUS" => Some(Self::Continuous),
"CONTSAMEGRADIENT" => Some(Self::ContSameGradient),
"CONTSAMEGRADIENTSAMECURVATURE" => Some(Self::ContSameGradientSameCurvature),
_ => None,
}
}
pub fn token(self) -> &'static str {
match self {
Self::Discontinuous => "DISCONTINUOUS",
Self::Continuous => "CONTINUOUS",
Self::ContSameGradient => "CONTSAMEGRADIENT",
Self::ContSameGradientSameCurvature => "CONTSAMEGRADIENTSAMECURVATURE",
}
}
pub fn is_connected(&self) -> bool {
!matches!(self, Self::Discontinuous)
}
}
pub const CURVE_SEGMENT_UNREAD: &str = "an IfcCurveSegment is placed and trimmed by \
SegmentStart and SegmentLength and cannot be read as an \
IfcCompositeCurveSegment; lower it with lower::curve instead";
#[derive(Debug, Clone, Copy)]
pub struct CompositeCurveSegment<'m> {
slots: Slots<'m>,
}
impl<'m> CompositeCurveSegment<'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 parent_curve_ref(&self) -> GeometryResult<EntityId> {
self.refuse_curve_segment()?;
self.slots
.req_ref(segment_slot::PARENT_CURVE, "ParentCurve")
}
fn refuse_curve_segment(&self) -> GeometryResult<()> {
if self.slots.entity().is_type("IFCCURVESEGMENT") {
return Err(self.slots.unsupported(CURVE_SEGMENT_UNREAD));
}
Ok(())
}
pub fn same_sense(&self) -> GeometryResult<bool> {
self.refuse_curve_segment()?;
self.slots.req_bool(segment_slot::SAME_SENSE, "SameSense")
}
pub fn transition(&self) -> GeometryResult<TransitionCode> {
let token = self
.slots
.opt_enum(segment_slot::TRANSITION)
.ok_or_else(|| {
self.slots
.degenerate("Transition is missing or is not an enumeration token")
})?;
TransitionCode::from_token(token).ok_or_else(|| {
self.slots
.degenerate(format!("unknown IfcTransitionCode .{token}."))
})
}
pub fn param_length(&self) -> GeometryResult<Option<f64>> {
self.refuse_curve_segment()?;
let Some(value) = self.slots.opt_f64(segment_slot::PARAM_LENGTH) else {
return Ok(None);
};
if value > 0.0 {
Ok(Some(value))
} else {
Err(self
.slots
.degenerate(format!("ParamLength must be positive, found {value}")))
}
}
pub fn is_reparametrised(&self) -> bool {
self.slots
.entity()
.type_name
.eq_ignore_ascii_case("IFCREPARAMETRISEDCOMPOSITECURVESEGMENT")
}
}
#[derive(Debug, Clone, Copy)]
pub struct CompositeCurve<'m> {
slots: Slots<'m>,
}
impl<'m> CompositeCurve<'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 segment_refs(&self) -> GeometryResult<Vec<EntityId>> {
let segments = self.slots.req_ref_list(curve_slot::SEGMENTS, "Segments")?;
if segments.is_empty() {
return Err(self
.slots
.degenerate("Segments is empty; LIST [1:?] requires at least one segment"));
}
Ok(segments)
}
pub fn self_intersect(&self) -> Option<bool> {
self.slots.opt_bool(curve_slot::SELF_INTERSECT)
}
pub fn is_on_surface(&self) -> bool {
matches!(
self.slots.entity().type_name.to_ascii_uppercase().as_str(),
"IFCCOMPOSITECURVEONSURFACE" | "IFCBOUNDARYCURVE" | "IFCOUTERBOUNDARYCURVE"
)
}
pub fn is_outer_boundary(&self) -> bool {
self.slots
.entity()
.type_name
.eq_ignore_ascii_case("IFCOUTERBOUNDARYCURVE")
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Value;
fn segment(transition: &str, same_sense: bool) -> Entity {
Entity::new(
"IFCCOMPOSITECURVESEGMENT",
vec![
Value::Enum(transition.into()),
Value::Bool(same_sense),
Value::Ref(EntityId(20)),
],
)
}
#[test]
fn an_ifc4x3_curve_segment_is_refused_by_name_not_misread() {
let length = |v: f64| Value::Typed {
type_name: "IFCLENGTHMEASURE".into(),
value: Box::new(Value::Real(v)),
};
let e = Entity::new(
"IFCCURVESEGMENT",
vec![
Value::Enum("DISCONTINUOUS".into()),
Value::Ref(EntityId(10)),
length(0.0),
length(50.0),
Value::Ref(EntityId(20)),
],
);
let view = CompositeCurveSegment::new(EntityId(7), &e);
for error in [
view.parent_curve_ref().map(|_| ()).unwrap_err(),
view.same_sense().map(|_| ()).unwrap_err(),
view.param_length().map(|_| ()).unwrap_err(),
] {
assert!(
matches!(
&error,
crate::GeometryError::Unsupported { type_name, detail, .. }
if type_name == "IFCCURVESEGMENT" && *detail == CURVE_SEGMENT_UNREAD
),
"{error}"
);
}
}
fn composite(type_name: &str, segments: &[u64]) -> Entity {
Entity::new(
type_name,
vec![
Value::List(segments.iter().map(|i| Value::Ref(EntityId(*i))).collect()),
Value::Bool(false),
],
)
}
#[test]
fn a_reversed_segment_reports_same_sense_false_rather_than_being_normalised() {
let e = segment("CONTINUOUS", false);
let view = CompositeCurveSegment::new(EntityId(1), &e);
assert!(!view.same_sense().unwrap());
assert_eq!(view.parent_curve_ref().unwrap(), EntityId(20));
}
#[test]
fn every_transition_code_round_trips_from_its_file_token() {
for (token, expected) in [
("DISCONTINUOUS", TransitionCode::Discontinuous),
("CONTINUOUS", TransitionCode::Continuous),
("CONTSAMEGRADIENT", TransitionCode::ContSameGradient),
(
"CONTSAMEGRADIENTSAMECURVATURE",
TransitionCode::ContSameGradientSameCurvature,
),
] {
let e = segment(token, true);
assert_eq!(
CompositeCurveSegment::new(EntityId(1), &e)
.transition()
.unwrap(),
expected,
"token {token}"
);
}
}
#[test]
fn only_discontinuous_reports_a_gap_at_the_joint() {
assert!(!TransitionCode::Discontinuous.is_connected());
assert!(TransitionCode::Continuous.is_connected());
assert!(TransitionCode::ContSameGradient.is_connected());
assert!(TransitionCode::ContSameGradientSameCurvature.is_connected());
}
#[test]
fn an_unknown_transition_token_is_rejected_rather_than_defaulted() {
assert_eq!(TransitionCode::from_token("SMOOTHISH"), None);
let e = segment("SMOOTHISH", true);
assert!(CompositeCurveSegment::new(EntityId(1), &e)
.transition()
.is_err());
}
#[test]
fn param_length_is_absent_on_a_plain_segment_and_read_on_the_reparametrised_one() {
let plain = segment("CONTINUOUS", true);
let view = CompositeCurveSegment::new(EntityId(1), &plain);
assert_eq!(view.param_length().unwrap(), None);
assert!(!view.is_reparametrised());
let reparam = Entity::new(
"IFCREPARAMETRISEDCOMPOSITECURVESEGMENT",
vec![
Value::Enum("CONTINUOUS".into()),
Value::Bool(true),
Value::Ref(EntityId(20)),
Value::Typed {
type_name: "IFCPARAMETERVALUE".into(),
value: Box::new(Value::Real(2.0)),
},
],
);
let view = CompositeCurveSegment::new(EntityId(1), &reparam);
assert_eq!(view.param_length().unwrap(), Some(2.0));
assert!(view.is_reparametrised());
}
#[test]
fn a_non_positive_param_length_is_degenerate() {
let e = Entity::new(
"IFCREPARAMETRISEDCOMPOSITECURVESEGMENT",
vec![
Value::Enum("CONTINUOUS".into()),
Value::Bool(true),
Value::Ref(EntityId(20)),
Value::Real(0.0),
],
);
assert!(CompositeCurveSegment::new(EntityId(1), &e)
.param_length()
.is_err());
}
#[test]
fn composite_curve_segments_keep_their_file_order() {
let e = composite("IFCCOMPOSITECURVE", &[1, 2, 3]);
assert_eq!(
CompositeCurve::new(EntityId(1), &e).segment_refs().unwrap(),
vec![EntityId(1), EntityId(2), EntityId(3)]
);
}
#[test]
fn a_composite_curve_with_no_segments_is_degenerate() {
let e = composite("IFCCOMPOSITECURVE", &[]);
assert!(CompositeCurve::new(EntityId(1), &e).segment_refs().is_err());
}
#[test]
fn boundary_curve_subtypes_are_classified_from_the_type_name() {
let plain = composite("IFCCOMPOSITECURVE", &[1]);
let on_surface = composite("IFCCOMPOSITECURVEONSURFACE", &[1]);
let boundary = composite("IFCBOUNDARYCURVE", &[1]);
let outer = composite("IFCOUTERBOUNDARYCURVE", &[1]);
assert!(!CompositeCurve::new(EntityId(1), &plain).is_on_surface());
assert!(CompositeCurve::new(EntityId(1), &on_surface).is_on_surface());
assert!(CompositeCurve::new(EntityId(1), &boundary).is_on_surface());
assert!(!CompositeCurve::new(EntityId(1), &boundary).is_outer_boundary());
assert!(CompositeCurve::new(EntityId(1), &outer).is_outer_boundary());
}
}