use crate::error::GeometryResult;
use crate::resource::direction::Direction;
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model};
mod offset_2d_slot {
pub const BASIS_CURVE: usize = 0;
pub const DISTANCE: usize = 1;
pub const SELF_INTERSECT: usize = 2;
}
mod offset_3d_slot {
pub const BASIS_CURVE: usize = 0;
pub const DISTANCE: usize = 1;
pub const SELF_INTERSECT: usize = 2;
pub const REF_DIRECTION: usize = 3;
}
pub(crate) mod pcurve_slot {
pub const BASIS_SURFACE: usize = 0;
pub const REFERENCE_CURVE: usize = 1;
}
pub(crate) mod surface_curve_slot {
pub const CURVE_3D: usize = 0;
pub const ASSOCIATED_GEOMETRY: usize = 1;
pub const MASTER_REPRESENTATION: usize = 2;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PreferredSurfaceCurveRepresentation {
Curve3D,
PCurveS1,
PCurveS2,
}
impl PreferredSurfaceCurveRepresentation {
pub fn from_token(token: &str) -> Option<Self> {
match token.to_ascii_uppercase().as_str() {
"CURVE3D" => Some(Self::Curve3D),
"PCURVE_S1" => Some(Self::PCurveS1),
"PCURVE_S2" => Some(Self::PCurveS2),
_ => None,
}
}
pub fn pcurve_index(&self) -> Option<usize> {
match self {
Self::Curve3D => None,
Self::PCurveS1 => Some(0),
Self::PCurveS2 => Some(1),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct OffsetCurve2D<'m> {
slots: Slots<'m>,
}
impl<'m> OffsetCurve2D<'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(offset_2d_slot::BASIS_CURVE, "BasisCurve")
}
pub fn distance(&self) -> GeometryResult<f64> {
self.slots.req_f64(offset_2d_slot::DISTANCE, "Distance")
}
pub fn self_intersect(&self) -> Option<bool> {
self.slots.opt_bool(offset_2d_slot::SELF_INTERSECT)
}
}
#[derive(Debug, Clone, Copy)]
pub struct OffsetCurve3D<'m> {
slots: Slots<'m>,
}
impl<'m> OffsetCurve3D<'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(offset_3d_slot::BASIS_CURVE, "BasisCurve")
}
pub fn distance(&self) -> GeometryResult<f64> {
self.slots.req_f64(offset_3d_slot::DISTANCE, "Distance")
}
pub fn self_intersect(&self) -> Option<bool> {
self.slots.opt_bool(offset_3d_slot::SELF_INTERSECT)
}
pub fn ref_direction_ref(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(offset_3d_slot::REF_DIRECTION, "RefDirection")
}
pub fn ref_direction<'v>(&self, model: &'v Model) -> GeometryResult<Direction<'v>> {
resolve::direction(model, self.id(), self.ref_direction_ref()?)
}
}
#[derive(Debug, Clone, Copy)]
pub struct PCurve<'m> {
slots: Slots<'m>,
}
impl<'m> PCurve<'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_surface_ref(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(pcurve_slot::BASIS_SURFACE, "BasisSurface")
}
pub fn reference_curve_ref(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(pcurve_slot::REFERENCE_CURVE, "ReferenceCurve")
}
}
#[derive(Debug, Clone, Copy)]
pub struct SurfaceCurve<'m> {
slots: Slots<'m>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SurfaceCurveKind {
Plain,
Intersection,
Seam,
}
impl<'m> SurfaceCurve<'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 kind(&self) -> Option<SurfaceCurveKind> {
match self.slots.entity().type_name.to_ascii_uppercase().as_str() {
"IFCSURFACECURVE" => Some(SurfaceCurveKind::Plain),
"IFCINTERSECTIONCURVE" => Some(SurfaceCurveKind::Intersection),
"IFCSEAMCURVE" => Some(SurfaceCurveKind::Seam),
_ => None,
}
}
pub fn curve_3d_ref(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(surface_curve_slot::CURVE_3D, "Curve3D")
}
pub fn associated_pcurve_refs(&self) -> GeometryResult<Vec<EntityId>> {
let pcurves = self.slots.req_ref_list(
surface_curve_slot::ASSOCIATED_GEOMETRY,
"AssociatedGeometry",
)?;
if pcurves.is_empty() || pcurves.len() > 2 {
return Err(self.slots.degenerate(format!(
"AssociatedGeometry must hold 1 or 2 IfcPcurves, found {}",
pcurves.len()
)));
}
if matches!(
self.kind(),
Some(SurfaceCurveKind::Intersection) | Some(SurfaceCurveKind::Seam)
) && pcurves.len() != 2
{
return Err(self.slots.degenerate(format!(
"{} requires exactly 2 associated IfcPcurves, found {}",
self.slots.type_name(),
pcurves.len()
)));
}
Ok(pcurves)
}
pub fn master_representation(&self) -> PreferredSurfaceCurveRepresentation {
self.slots
.opt_enum(surface_curve_slot::MASTER_REPRESENTATION)
.and_then(PreferredSurfaceCurveRepresentation::from_token)
.unwrap_or(PreferredSurfaceCurveRepresentation::Curve3D)
}
pub fn master_curve_ref(&self) -> GeometryResult<EntityId> {
match self.master_representation().pcurve_index() {
None => self.curve_3d_ref(),
Some(index) => {
let pcurves = self.associated_pcurve_refs()?;
pcurves.get(index).copied().ok_or_else(|| {
self.slots.degenerate(format!(
"MasterRepresentation names p-curve {} but AssociatedGeometry holds {}",
index + 1,
pcurves.len()
))
})
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ifc_model::Value;
fn offset_2d(distance: f64) -> Entity {
Entity::new(
"IFCOFFSETCURVE2D",
vec![
Value::Ref(EntityId(30)),
Value::Real(distance),
Value::Bool(false),
],
)
}
fn offset_3d(distance: f64) -> Entity {
Entity::new(
"IFCOFFSETCURVE3D",
vec![
Value::Ref(EntityId(30)),
Value::Real(distance),
Value::Bool(false),
Value::Ref(EntityId(31)),
],
)
}
fn surface_curve(type_name: &str, pcurves: &[u64], preference: &str) -> Entity {
Entity::new(
type_name,
vec![
Value::Ref(EntityId(40)),
Value::List(pcurves.iter().map(|i| Value::Ref(EntityId(*i))).collect()),
Value::Enum(preference.into()),
],
)
}
#[test]
fn offset_distance_keeps_its_sign() {
let e = offset_2d(-0.5);
assert_eq!(
OffsetCurve2D::new(EntityId(1), &e).distance().unwrap(),
-0.5
);
}
#[test]
fn a_zero_offset_distance_is_accepted() {
let e = offset_2d(0.0);
assert_eq!(OffsetCurve2D::new(EntityId(1), &e).distance().unwrap(), 0.0);
}
#[test]
fn only_the_3d_offset_carries_a_reference_direction() {
let e = offset_3d(1.0);
let view = OffsetCurve3D::new(EntityId(1), &e);
assert_eq!(view.basis_curve_ref().unwrap(), EntityId(30));
assert_eq!(view.ref_direction_ref().unwrap(), EntityId(31));
assert_eq!(view.self_intersect(), Some(false));
}
#[test]
fn the_reference_direction_resolves_unnormalized() {
let mut model = Model::new();
let ratios = Value::List(vec![Value::Real(0.0), Value::Real(0.0), Value::Real(5.0)]);
model.insert(EntityId(31), Entity::new("IFCDIRECTION", vec![ratios]));
let e = offset_3d(1.0);
let direction = OffsetCurve3D::new(EntityId(1), &e)
.ref_direction(&model)
.unwrap();
assert_eq!(direction.ratios().unwrap(), vec![0.0, 0.0, 5.0]);
assert_eq!(direction.unit().unwrap(), [0.0, 0.0, 1.0]);
}
#[test]
fn a_reference_direction_that_is_a_point_is_a_typed_error() {
let mut model = Model::new();
model.insert(EntityId(31), Entity::new("IFCCARTESIANPOINT", vec![]));
let e = offset_3d(1.0);
let err = OffsetCurve3D::new(EntityId(1), &e)
.ref_direction(&model)
.unwrap_err();
assert!(matches!(
err,
crate::GeometryError::WrongEntityType {
entity: EntityId(31),
expected: "IfcDirection",
..
}
));
}
#[test]
fn a_3d_offset_missing_its_reference_direction_is_reported_by_name() {
let e = Entity::new(
"IFCOFFSETCURVE3D",
vec![
Value::Ref(EntityId(30)),
Value::Real(1.0),
Value::Bool(false),
],
);
let err = OffsetCurve3D::new(EntityId(1), &e)
.ref_direction_ref()
.unwrap_err();
assert!(err.to_string().contains("RefDirection"), "got: {err}");
}
#[test]
fn pcurve_names_its_surface_and_its_parameter_space_curve() {
let e = Entity::new(
"IFCPCURVE",
vec![Value::Ref(EntityId(50)), Value::Ref(EntityId(51))],
);
let view = PCurve::new(EntityId(1), &e);
assert_eq!(view.basis_surface_ref().unwrap(), EntityId(50));
assert_eq!(view.reference_curve_ref().unwrap(), EntityId(51));
}
#[test]
fn master_representation_resolves_to_the_right_geometry() {
let by_3d = surface_curve("IFCSURFACECURVE", &[60, 61], "CURVE3D");
assert_eq!(
SurfaceCurve::new(EntityId(1), &by_3d)
.master_curve_ref()
.unwrap(),
EntityId(40)
);
let by_s1 = surface_curve("IFCSURFACECURVE", &[60, 61], "PCURVE_S1");
assert_eq!(
SurfaceCurve::new(EntityId(1), &by_s1)
.master_curve_ref()
.unwrap(),
EntityId(60)
);
let by_s2 = surface_curve("IFCSURFACECURVE", &[60, 61], "PCURVE_S2");
assert_eq!(
SurfaceCurve::new(EntityId(1), &by_s2)
.master_curve_ref()
.unwrap(),
EntityId(61)
);
}
#[test]
fn naming_a_pcurve_the_file_does_not_have_fails_instead_of_indexing_out_of_bounds() {
let e = surface_curve("IFCSURFACECURVE", &[60], "PCURVE_S2");
let err = SurfaceCurve::new(EntityId(3), &e)
.master_curve_ref()
.unwrap_err();
assert!(err.to_string().contains("#3"), "got: {err}");
}
#[test]
fn an_absent_master_representation_defaults_to_the_3d_curve() {
let e = Entity::new(
"IFCSURFACECURVE",
vec![
Value::Ref(EntityId(40)),
Value::List(vec![Value::Ref(EntityId(60))]),
],
);
assert_eq!(
SurfaceCurve::new(EntityId(1), &e).master_representation(),
PreferredSurfaceCurveRepresentation::Curve3D
);
}
#[test]
fn intersection_and_seam_curves_require_exactly_two_pcurves() {
for type_name in ["IFCINTERSECTIONCURVE", "IFCSEAMCURVE"] {
let e = surface_curve(type_name, &[60], "CURVE3D");
let err = SurfaceCurve::new(EntityId(1), &e)
.associated_pcurve_refs()
.unwrap_err();
assert!(err.to_string().contains("exactly 2"), "{type_name}: {err}");
}
let plain = surface_curve("IFCSURFACECURVE", &[60], "CURVE3D");
assert_eq!(
SurfaceCurve::new(EntityId(1), &plain)
.associated_pcurve_refs()
.unwrap()
.len(),
1
);
}
#[test]
fn surface_curve_subtypes_are_classified_from_the_type_name() {
for (type_name, expected) in [
("IFCSURFACECURVE", SurfaceCurveKind::Plain),
("IFCINTERSECTIONCURVE", SurfaceCurveKind::Intersection),
("IFCSEAMCURVE", SurfaceCurveKind::Seam),
] {
let e = surface_curve(type_name, &[60, 61], "CURVE3D");
assert_eq!(SurfaceCurve::new(EntityId(1), &e).kind(), Some(expected));
}
let other = surface_curve("IFCPOLYLINE", &[60], "CURVE3D");
assert_eq!(SurfaceCurve::new(EntityId(1), &other).kind(), None);
}
#[test]
fn preferred_representation_tokens_map_to_list_positions() {
assert_eq!(
PreferredSurfaceCurveRepresentation::Curve3D.pcurve_index(),
None
);
assert_eq!(
PreferredSurfaceCurveRepresentation::PCurveS1.pcurve_index(),
Some(0)
);
assert_eq!(
PreferredSurfaceCurveRepresentation::PCurveS2.pcurve_index(),
Some(1)
);
assert_eq!(
PreferredSurfaceCurveRepresentation::from_token("NOPE"),
None
);
}
}