use super::{directrix_slot, disk_slot, spine_slot};
use crate::error::GeometryResult;
use crate::slots::Slots;
use crate::solid::swept::area::SweptAreaSolid;
use ifc_model::{Entity, EntityId};
#[derive(Debug, Clone, Copy)]
pub struct SurfaceCurveSweptAreaSolid<'m> {
slots: Slots<'m>,
}
impl<'m> SurfaceCurveSweptAreaSolid<'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 base(&self) -> SweptAreaSolid<'m> {
SweptAreaSolid::from_slots(self.slots)
}
pub fn directrix(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(directrix_slot::DIRECTRIX, "Directrix")
}
pub fn start_param(&self) -> Option<f64> {
self.slots.opt_f64(directrix_slot::START_PARAM)
}
pub fn end_param(&self) -> Option<f64> {
self.slots.opt_f64(directrix_slot::END_PARAM)
}
pub fn reference_surface(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(directrix_slot::REFERENCE_SURFACE, "ReferenceSurface")
}
}
#[derive(Debug, Clone, Copy)]
pub struct FixedReferenceSweptAreaSolid<'m> {
slots: Slots<'m>,
}
impl<'m> FixedReferenceSweptAreaSolid<'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 base(&self) -> SweptAreaSolid<'m> {
SweptAreaSolid::from_slots(self.slots)
}
pub fn directrix(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(directrix_slot::DIRECTRIX, "Directrix")
}
pub fn start_param(&self) -> Option<f64> {
self.slots.opt_f64(directrix_slot::START_PARAM)
}
pub fn end_param(&self) -> Option<f64> {
self.slots.opt_f64(directrix_slot::END_PARAM)
}
pub fn fixed_reference(&self) -> GeometryResult<EntityId> {
self.slots
.req_ref(directrix_slot::FIXED_REFERENCE, "FixedReference")
}
}
#[derive(Debug, Clone, Copy)]
pub struct SweptDiskSolid<'m> {
slots: Slots<'m>,
}
impl<'m> SweptDiskSolid<'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 directrix(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(disk_slot::DIRECTRIX, "Directrix")
}
pub fn radius(&self) -> GeometryResult<f64> {
self.slots.req_f64(disk_slot::RADIUS, "Radius")
}
pub fn inner_radius(&self) -> Option<f64> {
self.slots.opt_f64(disk_slot::INNER_RADIUS)
}
pub fn start_param(&self) -> Option<f64> {
self.slots.opt_f64(disk_slot::START_PARAM)
}
pub fn end_param(&self) -> Option<f64> {
self.slots.opt_f64(disk_slot::END_PARAM)
}
pub fn checked_radii(&self) -> GeometryResult<(f64, Option<f64>)> {
let radius = self.radius()?;
if radius <= 0.0 {
return Err(self
.slots
.degenerate(format!("Radius must be positive, found {radius}")));
}
let inner = self.inner_radius();
if let Some(inner) = inner {
if inner >= radius {
return Err(self.slots.degenerate(format!(
"InnerRadius {inner} must be smaller than Radius {radius}"
)));
}
}
Ok((radius, inner))
}
}
#[derive(Debug, Clone, Copy)]
pub struct SweptDiskSolidPolygonal<'m> {
slots: Slots<'m>,
}
impl<'m> SweptDiskSolidPolygonal<'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 base(&self) -> SweptDiskSolid<'m> {
SweptDiskSolid { slots: self.slots }
}
pub fn fillet_radius(&self) -> Option<f64> {
self.slots.opt_f64(disk_slot::FILLET_RADIUS)
}
}
#[derive(Debug, Clone, Copy)]
pub struct SectionedSpine<'m> {
slots: Slots<'m>,
}
impl<'m> SectionedSpine<'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 spine_curve(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(spine_slot::SPINE_CURVE, "SpineCurve")
}
pub fn cross_sections(&self) -> GeometryResult<Vec<EntityId>> {
self.slots
.req_ref_list(spine_slot::CROSS_SECTIONS, "CrossSections")
}
pub fn cross_section_positions(&self) -> GeometryResult<Vec<EntityId>> {
self.slots
.req_ref_list(spine_slot::CROSS_SECTION_POSITIONS, "CrossSectionPositions")
}
pub fn checked_sections(&self) -> GeometryResult<Vec<(EntityId, EntityId)>> {
let sections = self.cross_sections()?;
let positions = self.cross_section_positions()?;
if sections.len() != positions.len() {
return Err(self.slots.degenerate(format!(
"CrossSections has {} entries but CrossSectionPositions has {}",
sections.len(),
positions.len()
)));
}
Ok(sections.into_iter().zip(positions).collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::solid::testkit::{entity, list, n, r};
use ifc_model::Value;
#[test]
fn surface_curve_sweep_exposes_directrix_surface_and_optional_trim() {
let e = entity(
"IFCSURFACECURVESWEPTAREASOLID",
vec![r(10), r(20), r(30), n(0.0), n(1.0), r(60)],
);
let view = SurfaceCurveSweptAreaSolid::new(EntityId(1), &e);
assert_eq!(view.base().swept_area().unwrap(), EntityId(10));
assert_eq!(view.directrix().unwrap(), EntityId(30));
assert_eq!(view.start_param(), Some(0.0));
assert_eq!(view.end_param(), Some(1.0));
assert_eq!(view.reference_surface().unwrap(), EntityId(60));
}
#[test]
fn untrimmed_directrix_reports_absent_parameters_rather_than_zero() {
let e = entity(
"IFCSURFACECURVESWEPTAREASOLID",
vec![r(10), r(20), r(30), Value::Null, Value::Null, r(60)],
);
let view = SurfaceCurveSweptAreaSolid::new(EntityId(1), &e);
assert_eq!(view.start_param(), None);
assert_eq!(view.end_param(), None);
}
#[test]
fn fixed_reference_sweep_reads_a_direction_where_surface_sweep_reads_a_surface() {
let attrs = vec![r(10), r(20), r(30), Value::Null, Value::Null, r(70)];
let fixed = entity("IFCFIXEDREFERENCESWEPTAREASOLID", attrs.clone());
let surface = entity("IFCSURFACECURVESWEPTAREASOLID", attrs);
assert_eq!(
FixedReferenceSweptAreaSolid::new(EntityId(1), &fixed)
.fixed_reference()
.unwrap(),
EntityId(70)
);
assert_eq!(
SurfaceCurveSweptAreaSolid::new(EntityId(1), &surface)
.reference_surface()
.unwrap(),
EntityId(70)
);
assert_eq!(
FixedReferenceSweptAreaSolid::new(EntityId(1), &fixed)
.directrix()
.unwrap(),
EntityId(30)
);
}
#[test]
fn swept_disk_directrix_is_slot_zero_with_no_inherited_profile() {
let e = entity(
"IFCSWEPTDISKSOLID",
vec![r(10), n(0.1), n(0.08), n(0.0), n(1.0)],
);
let view = SweptDiskSolid::new(EntityId(1), &e);
assert_eq!(view.directrix().unwrap(), EntityId(10));
assert_eq!(view.radius().unwrap(), 0.1);
assert_eq!(view.inner_radius(), Some(0.08));
assert_eq!(view.start_param(), Some(0.0));
assert_eq!(view.end_param(), Some(1.0));
}
#[test]
fn inner_radius_not_smaller_than_outer_is_degenerate() {
let bad = entity("IFCSWEPTDISKSOLID", vec![r(10), n(0.1), n(0.1)]);
let err = SweptDiskSolid::new(EntityId(5), &bad)
.checked_radii()
.unwrap_err();
assert_eq!(err.entity(), Some(EntityId(5)));
let tube = entity("IFCSWEPTDISKSOLID", vec![r(10), n(0.1), n(0.09)]);
assert_eq!(
SweptDiskSolid::new(EntityId(5), &tube)
.checked_radii()
.unwrap(),
(0.1, Some(0.09))
);
let rod = entity("IFCSWEPTDISKSOLID", vec![r(10), n(0.1)]);
assert_eq!(
SweptDiskSolid::new(EntityId(5), &rod)
.checked_radii()
.unwrap(),
(0.1, None)
);
}
#[test]
fn polygonal_disk_fillet_radius_is_optional() {
let with_fillet = entity(
"IFCSWEPTDISKSOLIDPOLYGONAL",
vec![
r(10),
n(0.1),
Value::Null,
Value::Null,
Value::Null,
n(0.15),
],
);
let view = SweptDiskSolidPolygonal::new(EntityId(1), &with_fillet);
assert_eq!(view.fillet_radius(), Some(0.15));
assert_eq!(view.base().radius().unwrap(), 0.1);
let sharp = entity("IFCSWEPTDISKSOLIDPOLYGONAL", vec![r(10), n(0.1)]);
assert_eq!(
SweptDiskSolidPolygonal::new(EntityId(1), &sharp).fillet_radius(),
None
);
}
#[test]
fn sectioned_spine_pairs_each_section_with_its_own_placement() {
let e = entity(
"IFCSECTIONEDSPINE",
vec![
r(1),
list(vec![r(10), r(11), r(12)]),
list(vec![r(20), r(21), r(22)]),
],
);
let view = SectionedSpine::new(EntityId(9), &e);
assert_eq!(view.spine_curve().unwrap(), EntityId(1));
assert_eq!(
view.checked_sections().unwrap(),
vec![
(EntityId(10), EntityId(20)),
(EntityId(11), EntityId(21)),
(EntityId(12), EntityId(22)),
]
);
}
#[test]
fn mismatched_spine_list_lengths_are_reported_not_truncated() {
let e = entity(
"IFCSECTIONEDSPINE",
vec![
r(1),
list(vec![r(10), r(11), r(12)]),
list(vec![r(20), r(21)]),
],
);
let view = SectionedSpine::new(EntityId(9), &e);
let err = view.checked_sections().unwrap_err();
assert_eq!(err.entity(), Some(EntityId(9)));
assert!(err.to_string().contains('3'));
}
}