use crate::error::{GeometryError, GeometryResult};
use crate::select::subtype::is_a;
use ifc_model::{Entity, EntityId, Model, Value};
fn classify<T>(
model: &Model,
referrer: EntityId,
target: EntityId,
expected: &'static str,
f: impl Fn(&str) -> Option<T>,
) -> GeometryResult<T> {
let entity: &Entity = model.get(target).ok_or(GeometryError::MissingEntity {
referrer,
missing: target,
})?;
f(&entity.type_name).ok_or_else(|| GeometryError::WrongEntityType {
entity: target,
actual: entity.type_name.to_string(),
expected,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CurveOnSurface {
Composite(EntityId),
PCurve(EntityId),
SurfaceCurve(EntityId),
}
impl CurveOnSurface {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
classify(model, referrer, target, "IfcCurveOnSurface", |t| {
if is_a(t, "IFCCOMPOSITECURVEONSURFACE") {
Some(Self::Composite(target))
} else if is_a(t, "IFCPCURVE") {
Some(Self::PCurve(target))
} else if is_a(t, "IFCSURFACECURVE") {
Some(Self::SurfaceCurve(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::Composite(id) | Self::PCurve(id) | Self::SurfaceCurve(id) => *id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CurveOrEdgeCurve {
Bounded(EntityId),
EdgeCurve(EntityId),
}
impl CurveOrEdgeCurve {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
classify(model, referrer, target, "IfcCurveOrEdgeCurve", |t| {
if is_a(t, "IFCEDGECURVE") {
Some(Self::EdgeCurve(target))
} else if is_a(t, "IFCBOUNDEDCURVE") {
Some(Self::Bounded(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::Bounded(id) | Self::EdgeCurve(id) => *id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeometricSetSelect {
Curve(EntityId),
Point(EntityId),
Surface(EntityId),
}
impl GeometricSetSelect {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
classify(model, referrer, target, "IfcGeometricSetSelect", |t| {
if is_a(t, "IFCCURVE") {
Some(Self::Curve(target))
} else if is_a(t, "IFCPOINT") {
Some(Self::Point(target))
} else if is_a(t, "IFCSURFACE") {
Some(Self::Surface(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::Curve(id) | Self::Point(id) | Self::Surface(id) => *id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SurfaceOrFaceSurface {
FaceBasedSurfaceModel(EntityId),
FaceSurface(EntityId),
Surface(EntityId),
}
impl SurfaceOrFaceSurface {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
classify(model, referrer, target, "IfcSurfaceOrFaceSurface", |t| {
if is_a(t, "IFCFACEBASEDSURFACEMODEL") {
Some(Self::FaceBasedSurfaceModel(target))
} else if is_a(t, "IFCFACESURFACE") {
Some(Self::FaceSurface(target))
} else if is_a(t, "IFCSURFACE") {
Some(Self::Surface(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::FaceBasedSurfaceModel(id) | Self::FaceSurface(id) | Self::Surface(id) => *id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PointOrVertexPoint {
Point(EntityId),
VertexPoint(EntityId),
}
impl PointOrVertexPoint {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
classify(model, referrer, target, "IfcPointOrVertexPoint", |t| {
if is_a(t, "IFCVERTEXPOINT") {
Some(Self::VertexPoint(target))
} else if is_a(t, "IFCPOINT") {
Some(Self::Point(target))
} else {
None
}
})
}
pub fn id(&self) -> EntityId {
match self {
Self::Point(id) | Self::VertexPoint(id) => *id,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GridPlacementDirectionSelect {
Direction(EntityId),
GridIntersection(EntityId),
}
impl GridPlacementDirectionSelect {
pub fn resolve(model: &Model, referrer: EntityId, target: EntityId) -> GeometryResult<Self> {
classify(
model,
referrer,
target,
"IfcGridPlacementDirectionSelect",
|t| {
if is_a(t, "IFCVIRTUALGRIDINTERSECTION") {
Some(Self::GridIntersection(target))
} else if is_a(t, "IFCDIRECTION") {
Some(Self::Direction(target))
} else {
None
}
},
)
}
pub fn id(&self) -> EntityId {
match self {
Self::Direction(id) | Self::GridIntersection(id) => *id,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LineIndex(Vec<usize>);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ArcIndex([usize; 3]);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SegmentIndexSelect {
Line(LineIndex),
Arc(ArcIndex),
}
impl LineIndex {
pub fn as_zero_based(&self) -> &[usize] {
&self.0
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
}
impl ArcIndex {
pub fn as_zero_based(&self) -> [usize; 3] {
self.0
}
}
impl SegmentIndexSelect {
pub fn from_value(entity: EntityId, value: &Value) -> GeometryResult<Self> {
let (tag, inner) = match value {
Value::Typed { type_name, value } => (type_name.to_ascii_uppercase(), value.as_ref()),
_ => {
return Err(GeometryError::Degenerate {
entity,
type_name: "IFCSEGMENTINDEXSELECT".into(),
detail: "segment is not tagged IFCLINEINDEX or IFCARCINDEX, so its \
kind cannot be determined"
.into(),
})
}
};
let raw = inner.as_list().ok_or_else(|| GeometryError::Degenerate {
entity,
type_name: tag.clone(),
detail: "segment index is not a list".into(),
})?;
let mut indices = Vec::with_capacity(raw.len());
for value in raw {
let n = match value.unwrap_typed() {
Value::Integer(i) => *i,
other => {
return Err(GeometryError::Degenerate {
entity,
type_name: tag.clone(),
detail: format!("segment index is not an integer: {other:?}"),
})
}
};
if n < 1 {
return Err(GeometryError::Degenerate {
entity,
type_name: tag.clone(),
detail: format!("index {n} is not a positive integer (EXPRESS is 1-based)"),
});
}
indices.push((n - 1) as usize);
}
match tag.as_str() {
"IFCARCINDEX" => {
let three: [usize; 3] =
indices
.as_slice()
.try_into()
.map_err(|_| GeometryError::Degenerate {
entity,
type_name: tag.clone(),
detail: format!(
"IfcArcIndex requires exactly 3 indices, found {}",
indices.len()
),
})?;
Ok(Self::Arc(ArcIndex(three)))
}
"IFCLINEINDEX" => {
if indices.len() < 2 {
return Err(GeometryError::Degenerate {
entity,
type_name: tag,
detail: format!(
"IfcLineIndex requires at least 2 indices, found {}",
indices.len()
),
});
}
Ok(Self::Line(LineIndex(indices)))
}
other => Err(GeometryError::Degenerate {
entity,
type_name: other.to_string(),
detail: "not a member of IfcSegmentIndexSelect".into(),
}),
}
}
pub fn indices(&self) -> Vec<usize> {
match self {
Self::Line(l) => l.0.clone(),
Self::Arc(a) => a.0.to_vec(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct DimensionCount(u8);
impl DimensionCount {
pub fn new(count: i64) -> Option<Self> {
(1..=3).contains(&count).then_some(Self(count as u8))
}
pub fn get(&self) -> usize {
self.0 as usize
}
}
#[cfg(test)]
mod tests {
use super::*;
fn model_with(id: u64, type_name: &str) -> Model {
let mut model = Model::new();
model.insert(EntityId(id), Entity::new(type_name, vec![Value::Null; 4]));
model
}
fn tagged(tag: &str, values: &[i64]) -> Value {
Value::Typed {
type_name: tag.into(),
value: Box::new(Value::List(
values.iter().map(|v| Value::Integer(*v)).collect(),
)),
}
}
#[test]
fn one_based_express_indices_become_zero_based_exactly_once() {
let seg = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCLINEINDEX", &[1, 2, 3]))
.unwrap();
assert_eq!(
seg.indices(),
vec![0, 1, 2],
"IfcLineIndex((1,2,3)) addresses points 0,1,2"
);
}
#[test]
fn arc_indices_require_exactly_three_points() {
let ok = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCARCINDEX", &[3, 4, 5]));
assert_eq!(ok.unwrap().indices(), vec![2, 3, 4]);
let short = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCARCINDEX", &[3, 4]));
assert!(short.is_err(), "two indices cannot define an arc");
}
#[test]
fn line_indices_require_at_least_two_points() {
assert!(
SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCLINEINDEX", &[7])).is_err()
);
}
#[test]
fn a_zero_index_is_rejected_rather_than_underflowing() {
let err = SegmentIndexSelect::from_value(EntityId(1), &tagged("IFCLINEINDEX", &[0, 1]))
.unwrap_err();
assert!(err.to_string().contains("1-based"), "got {err}");
}
#[test]
fn an_untagged_segment_is_rejected_rather_than_guessed() {
let bare = Value::List(vec![Value::Integer(1), Value::Integer(2)]);
assert!(SegmentIndexSelect::from_value(EntityId(1), &bare).is_err());
}
#[test]
fn dimension_count_enforces_its_where_rule() {
assert_eq!(DimensionCount::new(3).map(|d| d.get()), Some(3));
assert_eq!(DimensionCount::new(1).map(|d| d.get()), Some(1));
assert_eq!(DimensionCount::new(0), None, "0 < SELF is required");
assert_eq!(DimensionCount::new(4), None, "SELF <= 3 is required");
assert_eq!(DimensionCount::new(-1), None);
}
#[test]
fn geometric_set_members_classify_by_family() {
let curve = model_with(5, "IFCPOLYLINE");
assert_eq!(
GeometricSetSelect::resolve(&curve, EntityId(1), EntityId(5)).unwrap(),
GeometricSetSelect::Curve(EntityId(5))
);
let surface = model_with(5, "IFCPLANE");
assert_eq!(
GeometricSetSelect::resolve(&surface, EntityId(1), EntityId(5)).unwrap(),
GeometricSetSelect::Surface(EntityId(5))
);
let point = model_with(5, "IFCCARTESIANPOINT");
assert_eq!(
GeometricSetSelect::resolve(&point, EntityId(1), EntityId(5)).unwrap(),
GeometricSetSelect::Point(EntityId(5))
);
}
#[test]
fn curve_on_surface_picks_the_most_derived_branch() {
let model = model_with(5, "IFCCOMPOSITECURVEONSURFACE");
assert_eq!(
CurveOnSurface::resolve(&model, EntityId(1), EntityId(5)).unwrap(),
CurveOnSurface::Composite(EntityId(5))
);
}
#[test]
fn a_face_surface_is_not_classified_as_a_plain_surface() {
let model = model_with(1, "IFCFACESURFACE");
let resolved = SurfaceOrFaceSurface::resolve(&model, EntityId(9), EntityId(1))
.expect("face surface resolves");
assert_eq!(resolved, SurfaceOrFaceSurface::FaceSurface(EntityId(1)));
let plain = model_with(2, "IFCPLANE");
let resolved = SurfaceOrFaceSurface::resolve(&plain, EntityId(9), EntityId(2))
.expect("plane resolves");
assert_eq!(resolved, SurfaceOrFaceSurface::Surface(EntityId(2)));
}
#[test]
fn a_non_surface_is_rejected_by_the_surface_select() {
let model = model_with(1, "IFCPOLYLINE");
let error = SurfaceOrFaceSurface::resolve(&model, EntityId(9), EntityId(1))
.expect_err("a curve is not a surface");
assert_eq!(error.entity(), Some(EntityId(1)));
}
#[test]
fn a_vertex_point_keeps_its_topological_identity() {
let model = model_with(1, "IFCVERTEXPOINT");
let resolved = PointOrVertexPoint::resolve(&model, EntityId(9), EntityId(1))
.expect("vertex point resolves");
assert_eq!(resolved, PointOrVertexPoint::VertexPoint(EntityId(1)));
let plain = model_with(2, "IFCCARTESIANPOINT");
let resolved = PointOrVertexPoint::resolve(&plain, EntityId(9), EntityId(2))
.expect("cartesian point resolves");
assert_eq!(resolved, PointOrVertexPoint::Point(EntityId(2)));
}
#[test]
fn an_edge_curve_is_not_classified_as_a_bounded_curve() {
let model = model_with(1, "IFCEDGECURVE");
let resolved = CurveOrEdgeCurve::resolve(&model, EntityId(9), EntityId(1))
.expect("edge curve resolves");
assert_eq!(resolved, CurveOrEdgeCurve::EdgeCurve(EntityId(1)));
let plain = model_with(2, "IFCPOLYLINE");
let resolved =
CurveOrEdgeCurve::resolve(&plain, EntityId(9), EntityId(2)).expect("polyline resolves");
assert_eq!(resolved, CurveOrEdgeCurve::Bounded(EntityId(2)));
}
#[test]
fn a_surface_is_rejected_by_the_curve_select() {
let model = model_with(1, "IFCPLANE");
let error = CurveOrEdgeCurve::resolve(&model, EntityId(9), EntityId(1))
.expect_err("a plane is not a curve");
assert_eq!(error.entity(), Some(EntityId(1)));
}
}