use crate::error::GeometryResult;
use crate::resource::point::{CartesianPoint, CartesianPointList};
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model, Value};
pub(crate) mod polyline_slot {
pub const POINTS: usize = 0;
}
pub(crate) mod indexed_slot {
pub const POINTS: usize = 0;
pub const SEGMENTS: usize = 1;
pub const SELF_INTERSECT: usize = 2;
}
#[derive(Debug, Clone, Copy)]
pub struct Polyline<'m> {
slots: Slots<'m>,
}
impl<'m> Polyline<'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 point_refs(&self) -> GeometryResult<Vec<EntityId>> {
let points = self.slots.req_ref_list(polyline_slot::POINTS, "Points")?;
if points.len() < 2 {
return Err(self.slots.degenerate(format!(
"Points must hold at least 2 entries, found {}",
points.len()
)));
}
Ok(points)
}
pub fn points<'v>(&self, model: &'v Model) -> GeometryResult<Vec<CartesianPoint<'v>>> {
resolve::cartesian_points(model, self.id(), &self.point_refs()?)
}
pub fn closes_by_repeating_first_point(&self) -> GeometryResult<bool> {
let points = self.point_refs()?;
Ok(points.first() == points.last())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PolySegment {
Line(Vec<usize>),
Arc {
start: usize,
mid: usize,
end: usize,
},
}
impl PolySegment {
pub fn indices(&self) -> Vec<usize> {
match self {
Self::Line(indices) => indices.clone(),
Self::Arc { start, mid, end } => vec![*start, *mid, *end],
}
}
pub fn is_arc(&self) -> bool {
matches!(self, Self::Arc { .. })
}
}
#[derive(Debug, Clone, Copy)]
pub struct IndexedPolyCurve<'m> {
slots: Slots<'m>,
}
impl<'m> IndexedPolyCurve<'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 points_ref(&self) -> GeometryResult<EntityId> {
self.slots.req_ref(indexed_slot::POINTS, "Points")
}
pub fn points<'v>(&self, model: &'v Model) -> GeometryResult<CartesianPointList<'v>> {
resolve::cartesian_point_list(model, self.id(), self.points_ref()?)
}
pub fn self_intersect(&self) -> Option<bool> {
self.slots.opt_bool(indexed_slot::SELF_INTERSECT)
}
pub fn has_explicit_segments(&self) -> bool {
self.slots.opt(indexed_slot::SEGMENTS).is_some()
}
pub fn segments(&self, point_count: usize) -> GeometryResult<Vec<PolySegment>> {
let Some(value) = self.slots.opt(indexed_slot::SEGMENTS) else {
return Ok(Vec::new());
};
let items = value.as_list().ok_or_else(|| {
self.slots
.degenerate("Segments must be a list of IfcSegmentIndexSelect")
})?;
items
.iter()
.map(|item| self.parse_segment(item, point_count))
.collect()
}
pub fn implicit_polyline_indices(point_count: usize) -> Vec<usize> {
(0..point_count).collect()
}
fn parse_segment(&self, item: &Value, point_count: usize) -> GeometryResult<PolySegment> {
let Value::Typed { type_name, value } = item else {
return Err(self.slots.degenerate(
"segment is not a typed IfcLineIndex/IfcArcIndex; the select tag is required \
to tell a line from an arc",
));
};
let raw = value.as_list().ok_or_else(|| {
self.slots
.degenerate(format!("{type_name} must wrap a list of indices"))
})?;
let mut indices = Vec::with_capacity(raw.len());
for v in raw {
let n = match v.unwrap_typed() {
Value::Integer(i) => *i,
other => {
return Err(self
.slots
.degenerate(format!("{type_name} index is not an integer: {other:?}")));
}
};
if n < 1 {
return Err(self.slots.degenerate(format!(
"{type_name} index {n} is not a positive integer; \
IfcPositiveInteger starts at 1"
)));
}
let zero_based = (n - 1) as usize;
if zero_based >= point_count {
return Err(self.slots.degenerate(format!(
"{type_name} index {n} is out of range for a point list of {point_count}"
)));
}
indices.push(zero_based);
}
match type_name.to_ascii_uppercase().as_str() {
"IFCLINEINDEX" => {
if indices.len() < 2 {
return Err(self.slots.degenerate(format!(
"IfcLineIndex needs at least 2 indices, found {}",
indices.len()
)));
}
Ok(PolySegment::Line(indices))
}
"IFCARCINDEX" => {
if indices.len() != 3 {
return Err(self.slots.degenerate(format!(
"IfcArcIndex needs exactly 3 indices (start, on-arc, end), found {}",
indices.len()
)));
}
Ok(PolySegment::Arc {
start: indices[0],
mid: indices[1],
end: indices[2],
})
}
other => Err(self
.slots
.degenerate(format!("{other} is not an IfcSegmentIndexSelect member"))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn refs(ids: &[u64]) -> Value {
Value::List(ids.iter().map(|i| Value::Ref(EntityId(*i))).collect())
}
fn index_select(name: &str, indices: &[i64]) -> Value {
Value::Typed {
type_name: name.into(),
value: Box::new(Value::List(
indices.iter().map(|i| Value::Integer(*i)).collect(),
)),
}
}
fn indexed(segments: Value) -> Entity {
Entity::new(
"IFCINDEXEDPOLYCURVE",
vec![Value::Ref(EntityId(100)), segments, Value::Bool(false)],
)
}
fn point(values: &[f64]) -> Entity {
Entity::new(
"IFCCARTESIANPOINT",
vec![Value::List(
values.iter().copied().map(Value::Real).collect(),
)],
)
}
#[test]
fn polyline_points_resolve_in_file_order() {
let mut model = Model::new();
model.insert(EntityId(1), point(&[0.0, 0.0]));
model.insert(EntityId(2), point(&[1.0, 0.0]));
let e = Entity::new("IFCPOLYLINE", vec![refs(&[2, 1])]);
let points = Polyline::new(EntityId(9), &e).points(&model).unwrap();
let ids: Vec<_> = points.iter().map(|p| p.id()).collect();
assert_eq!(ids, vec![EntityId(2), EntityId(1)]);
assert_eq!(points[0].coordinates().unwrap(), vec![1.0, 0.0]);
}
#[test]
fn a_polyline_vertex_of_the_wrong_type_fails_the_whole_list() {
let mut model = Model::new();
model.insert(EntityId(1), point(&[0.0, 0.0]));
model.insert(EntityId(2), Entity::new("IFCDIRECTION", vec![]));
let e = Entity::new("IFCPOLYLINE", vec![refs(&[1, 2])]);
let err = Polyline::new(EntityId(9), &e).points(&model).unwrap_err();
assert!(matches!(
err,
crate::GeometryError::WrongEntityType {
entity: EntityId(2),
..
}
));
}
#[test]
fn indexed_points_resolve_to_the_declared_list_subtype() {
let mut model = Model::new();
model.insert(
EntityId(100),
Entity::new(
"IFCCARTESIANPOINTLIST3D",
vec![Value::List(vec![Value::List(vec![
Value::Real(0.0),
Value::Real(0.0),
Value::Real(1.0),
])])],
),
);
let e = indexed(Value::Null);
let list = IndexedPolyCurve::new(EntityId(9), &e)
.points(&model)
.unwrap();
assert_eq!(list.id(), EntityId(100));
assert_eq!(list.dimension(), 3);
}
#[test]
fn indexed_points_reject_a_single_point_and_a_dangling_list() {
let mut model = Model::new();
model.insert(EntityId(100), point(&[0.0, 0.0]));
let e = indexed(Value::Null);
let view = IndexedPolyCurve::new(EntityId(9), &e);
assert!(matches!(
view.points(&model).unwrap_err(),
crate::GeometryError::WrongEntityType { .. }
));
let err = view.points(&Model::new()).unwrap_err();
assert_eq!(err.entity(), Some(EntityId(9)));
}
#[test]
fn polyline_reads_its_points_in_file_order() {
let e = Entity::new("IFCPOLYLINE", vec![refs(&[1, 2, 3])]);
let view = Polyline::new(EntityId(1), &e);
assert_eq!(
view.point_refs().unwrap(),
vec![EntityId(1), EntityId(2), EntityId(3)]
);
}
#[test]
fn polyline_with_fewer_than_two_points_is_degenerate() {
let e = Entity::new("IFCPOLYLINE", vec![refs(&[1])]);
let err = Polyline::new(EntityId(4), &e).point_refs().unwrap_err();
assert!(err.to_string().contains("#4"), "got: {err}");
}
#[test]
fn polyline_closure_is_detected_from_a_repeated_first_reference() {
let closed = Entity::new("IFCPOLYLINE", vec![refs(&[1, 2, 3, 1])]);
let open = Entity::new("IFCPOLYLINE", vec![refs(&[1, 2, 3])]);
assert!(Polyline::new(EntityId(1), &closed)
.closes_by_repeating_first_point()
.unwrap());
assert!(!Polyline::new(EntityId(1), &open)
.closes_by_repeating_first_point()
.unwrap());
}
#[test]
fn one_based_file_indices_become_zero_based_rust_indices() {
let e = indexed(Value::List(vec![index_select("IFCLINEINDEX", &[1, 2, 3])]));
let segments = IndexedPolyCurve::new(EntityId(1), &e).segments(5).unwrap();
assert_eq!(segments, vec![PolySegment::Line(vec![0, 1, 2])]);
}
#[test]
fn arc_index_names_its_middle_point_as_on_arc_not_as_a_centre() {
let e = indexed(Value::List(vec![index_select("IFCARCINDEX", &[3, 4, 5])]));
let segments = IndexedPolyCurve::new(EntityId(1), &e).segments(9).unwrap();
assert_eq!(
segments,
vec![PolySegment::Arc {
start: 2,
mid: 3,
end: 4
}]
);
assert!(segments[0].is_arc());
}
#[test]
fn an_index_past_the_end_of_the_point_list_is_rejected() {
let e = indexed(Value::List(vec![index_select("IFCLINEINDEX", &[1, 9])]));
let err = IndexedPolyCurve::new(EntityId(2), &e)
.segments(3)
.unwrap_err();
assert!(err.to_string().contains("out of range"), "got: {err}");
}
#[test]
fn index_zero_is_rejected_rather_than_underflowing() {
let e = indexed(Value::List(vec![index_select("IFCLINEINDEX", &[0, 1])]));
assert!(IndexedPolyCurve::new(EntityId(1), &e).segments(3).is_err());
}
#[test]
fn arc_index_with_the_wrong_cardinality_is_rejected() {
let e = indexed(Value::List(vec![index_select("IFCARCINDEX", &[1, 2])]));
let err = IndexedPolyCurve::new(EntityId(1), &e)
.segments(5)
.unwrap_err();
assert!(err.to_string().contains("exactly 3"), "got: {err}");
}
#[test]
fn line_index_needs_at_least_two_indices() {
let e = indexed(Value::List(vec![index_select("IFCLINEINDEX", &[2])]));
assert!(IndexedPolyCurve::new(EntityId(1), &e).segments(5).is_err());
}
#[test]
fn an_untagged_segment_is_rejected_rather_than_assumed_to_be_a_line() {
let e = indexed(Value::List(vec![Value::List(vec![
Value::Integer(1),
Value::Integer(2),
])]));
let err = IndexedPolyCurve::new(EntityId(1), &e)
.segments(5)
.unwrap_err();
assert!(err.to_string().contains("select tag"), "got: {err}");
}
#[test]
fn absent_segments_is_distinguishable_from_an_empty_segment_list() {
let absent = Entity::new("IFCINDEXEDPOLYCURVE", vec![Value::Ref(EntityId(100))]);
let view = IndexedPolyCurve::new(EntityId(1), &absent);
assert!(!view.has_explicit_segments());
assert!(view.segments(4).unwrap().is_empty());
assert_eq!(
IndexedPolyCurve::implicit_polyline_indices(4),
vec![0, 1, 2, 3]
);
let empty = indexed(Value::List(vec![]));
assert!(IndexedPolyCurve::new(EntityId(1), &empty).has_explicit_segments());
}
#[test]
fn mixed_line_and_arc_segments_keep_their_file_order() {
let e = indexed(Value::List(vec![
index_select("IFCLINEINDEX", &[1, 2]),
index_select("IFCARCINDEX", &[2, 3, 4]),
index_select("IFCLINEINDEX", &[4, 5]),
]));
let segments = IndexedPolyCurve::new(EntityId(1), &e).segments(5).unwrap();
assert_eq!(segments.len(), 3);
assert!(!segments[0].is_arc());
assert!(segments[1].is_arc());
assert_eq!(segments[2].indices(), vec![3, 4]);
}
}