Skip to main content

ifc_geometry/input/profile/
outline.rs

1//! The straight-edged outline of an arbitrary profile, in metres (#166).
2//!
3//! [`super::describe_profile`] names an arbitrary profile's boundary curves by
4//! id. A rule check that bounds a section -- openings in a wall whose outline
5//! is a free polyline at a mitred join -- needs the vertices themselves, and
6//! must not link a kernel to get them. This reads them for boundaries whose
7//! every edge is straight, which a vertex list states exactly.
8//!
9//! # Curved edges are refused, never chorded
10//!
11//! A vertex list cannot state an arc. Chording one would hand the caller a
12//! polygon that differs from the file by an error nobody chose, so an
13//! `IfcArcIndex` segment, and any curve family other than `IfcPolyline` and
14//! `IfcIndexedPolyCurve`, is [`GeometryError::Unsupported`] naming the curve.
15//!
16//! # Rings
17//!
18//! Each boundary is returned as a ring: authored order, the closing vertex not
19//! repeated. IFC closes a polyline by repeating its first point, and a closed
20//! indexed curve by ending on its first index; both are dropped here, as the
21//! profile lowering drops them, so the ring is the loop the kernel receives.
22
23use ifc_model::{EntityId, Model};
24
25use super::{describe_profile, ProfileParameters};
26use crate::curve::{IndexedPolyCurve, PolySegment, Polyline};
27use crate::error::{GeometryError, GeometryResult};
28use crate::resource::point::CartesianPointList;
29use crate::units::UnitScale;
30
31/// Two vertices closer than this, in metres, are the same vertex.
32///
33/// The value the profile lowering uses for the closing duplicate, so both
34/// agree on whether a ring repeats its first vertex.
35const SAME_VERTEX: f64 = 1e-12;
36
37/// An arbitrary profile's boundaries as rings of vertices.
38///
39/// Coordinates are in the profile's own 2D coordinate system, in metres.
40/// Arbitrary profiles have no `Position`, so these are the curve coordinates
41/// the file authored, converted once.
42#[derive(Debug, Clone, PartialEq)]
43#[non_exhaustive]
44pub struct ProfileOutline {
45    /// The `IfcProfileDef` entity.
46    pub profile: EntityId,
47    /// `OuterCurve`'s vertices, in authored order, the first not repeated.
48    pub outer: Vec<[f64; 2]>,
49    /// Each `IfcArbitraryProfileDefWithVoids.InnerCurves` ring, in file
50    /// order. Empty for an `IfcArbitraryClosedProfileDef`.
51    pub inner: Vec<Vec<[f64; 2]>>,
52}
53
54/// Read an arbitrary closed profile's outline, in metres.
55///
56/// Accepts `IfcArbitraryClosedProfileDef` and
57/// `IfcArbitraryProfileDefWithVoids` whose boundaries are `IfcPolyline`s or
58/// line-only `IfcIndexedPolyCurve`s. Refused, each naming the entity:
59///
60/// - any other profile family, and any other boundary curve family, as
61///   [`GeometryError::Unsupported`];
62/// - an `IfcArcIndex` segment, as [`GeometryError::Unsupported`]: the
63///   outline states straight edges only;
64/// - a 3D point, a ring of fewer than three distinct vertices, or indexed
65///   segments that do not join, as [`GeometryError::Degenerate`];
66/// - whatever [`describe_profile`] refuses for the profile itself.
67///
68/// Kernel-free.
69pub fn profile_outline(
70    model: &Model,
71    units: &UnitScale,
72    profile: EntityId,
73) -> GeometryResult<ProfileOutline> {
74    let description = describe_profile(model, units, profile)?;
75    let (outer, inner): (EntityId, &[EntityId]) = match &description.parameters {
76        ProfileParameters::ArbitraryClosed { outer_curve } => (*outer_curve, &[]),
77        ProfileParameters::ArbitraryWithVoids {
78            outer_curve,
79            inner_curves,
80        } => (*outer_curve, inner_curves),
81        _ => {
82            return Err(GeometryError::Unsupported {
83                entity: profile,
84                type_name: description.type_name,
85                detail: "only IfcArbitraryClosedProfileDef and IfcArbitraryProfileDefWithVoids \
86                         have an authored outline; describe_profile states the other families",
87            })
88        }
89    };
90    Ok(ProfileOutline {
91        profile,
92        outer: ring(model, units, outer)?,
93        inner: inner
94            .iter()
95            .map(|curve| ring(model, units, *curve))
96            .collect::<GeometryResult<_>>()?,
97    })
98}
99
100/// One boundary curve as a ring of distinct vertices, in metres.
101fn ring(model: &Model, units: &UnitScale, curve: EntityId) -> GeometryResult<Vec<[f64; 2]>> {
102    let entity = model.get(curve).ok_or(GeometryError::MissingEntity {
103        referrer: curve,
104        missing: curve,
105    })?;
106    let type_name = entity.type_name.to_ascii_uppercase();
107    let raw = match type_name.as_str() {
108        "IFCPOLYLINE" => polyline(model, curve, entity)?,
109        "IFCINDEXEDPOLYCURVE" => indexed(model, curve, entity)?,
110        _ => {
111            return Err(GeometryError::Unsupported {
112                entity: curve,
113                type_name,
114                detail: "outline vertices are read from IfcPolyline and line-only \
115                         IfcIndexedPolyCurve boundaries; other curve families may be curved",
116            })
117        }
118    };
119    let mut vertices: Vec<[f64; 2]> = raw
120        .into_iter()
121        .map(|[x, y]| [units.length(x), units.length(y)])
122        .collect();
123    if let (Some(first), Some(last)) = (vertices.first(), vertices.last()) {
124        if vertices.len() >= 2 && distance(*first, *last) < SAME_VERTEX {
125            vertices.pop();
126        }
127    }
128    if vertices.len() < 3 {
129        return Err(GeometryError::Degenerate {
130            entity: curve,
131            type_name,
132            detail: format!(
133                "a profile boundary needs at least 3 distinct vertices, found {}",
134                vertices.len()
135            ),
136        });
137    }
138    Ok(vertices)
139}
140
141/// An `IfcPolyline`'s points, in file units. Each must be 2D.
142fn polyline(
143    model: &Model,
144    curve: EntityId,
145    entity: &ifc_model::Entity,
146) -> GeometryResult<Vec<[f64; 2]>> {
147    Polyline::new(curve, entity)
148        .points(model)?
149        .iter()
150        .map(|point| match point.coordinates()?.as_slice() {
151            [x, y] => Ok([*x, *y]),
152            other => Err(GeometryError::Degenerate {
153                entity: point.id(),
154                type_name: "IFCCARTESIANPOINT".to_owned(),
155                detail: format!(
156                    "a profile boundary point is 2D (OuterCurve.Dim = 2), this one has {} \
157                     coordinates",
158                    other.len()
159                ),
160            }),
161        })
162        .collect()
163}
164
165/// A line-only `IfcIndexedPolyCurve`'s path, in file units.
166///
167/// Without `Segments` the path is every point in list order. With them, each
168/// `IfcLineIndex` run must start where the previous one ended
169/// (`IfcConsecutiveSegments`), and an `IfcArcIndex` is refused.
170fn indexed(
171    model: &Model,
172    curve: EntityId,
173    entity: &ifc_model::Entity,
174) -> GeometryResult<Vec<[f64; 2]>> {
175    let view = IndexedPolyCurve::new(curve, entity);
176    let list = match view.points(model)? {
177        CartesianPointList::TwoD(list) => list.coordinates()?,
178        CartesianPointList::ThreeD(list) => {
179            return Err(GeometryError::Degenerate {
180                entity: list.id(),
181                type_name: "IFCCARTESIANPOINTLIST3D".to_owned(),
182                detail: "a profile boundary is 2D (OuterCurve.Dim = 2), this point list is 3D"
183                    .to_owned(),
184            })
185        }
186    };
187    if !view.has_explicit_segments() {
188        return Ok(list);
189    }
190    let mut indices: Vec<usize> = Vec::new();
191    for segment in view.segments(list.len())? {
192        let PolySegment::Line(run) = segment else {
193            return Err(GeometryError::Unsupported {
194                entity: curve,
195                type_name: entity.type_name.to_ascii_uppercase(),
196                detail: "an IfcArcIndex segment is a circular arc, which a vertex outline \
197                         cannot state; it is refused rather than chorded",
198            });
199        };
200        match indices.last() {
201            None => indices.extend_from_slice(&run),
202            Some(end) if Some(end) == run.first() => indices.extend_from_slice(&run[1..]),
203            Some(_) => {
204                return Err(GeometryError::Degenerate {
205                    entity: curve,
206                    type_name: entity.type_name.to_ascii_uppercase(),
207                    detail: "Segments are not consecutive (WHERE rule Consecutive), so the \
208                             boundary is not one ring"
209                        .to_owned(),
210                })
211            }
212        }
213    }
214    Ok(indices.into_iter().map(|index| list[index]).collect())
215}
216
217fn distance(a: [f64; 2], b: [f64; 2]) -> f64 {
218    (a[0] - b[0]).hypot(a[1] - b[1])
219}