ifc_geometry/input/profile/
outline.rs1use 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
31const SAME_VERTEX: f64 = 1e-12;
36
37#[derive(Debug, Clone, PartialEq)]
43#[non_exhaustive]
44pub struct ProfileOutline {
45 pub profile: EntityId,
47 pub outer: Vec<[f64; 2]>,
49 pub inner: Vec<Vec<[f64; 2]>>,
52}
53
54pub 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
100fn 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
141fn 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
165fn 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}