ifc_lite_geometry/analytic/
mod.rs1mod curve;
9mod frame;
10mod helpers;
11mod profile;
12mod profile_topology;
13mod topology_interval;
14pub use profile::{extract_analytic_profile, AnalyticProfile, AnalyticProfileLoop, ProfileLoopKind};
15mod extrusion;
16pub use extrusion::{extract_analytic_extrusion, AnalyticExtrusion};
17#[cfg(test)]
18mod tests;
19
20use crate::{Error, Result};
21use ifc_lite_core::{AttributeValue, DecodedEntity, EntityDecoder, IfcType};
22pub use frame::{AnalyticPoint, AnalyticSegmentFrame};
23
24#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
26#[serde(tag = "type", rename_all = "snake_case")]
27pub enum AnalyticCurveSegment {
28 Line {
29 start: [f64; 3],
30 end: [f64; 3],
31 },
32 Arc {
33 center: [f64; 3],
34 normal: [f64; 3],
35 x_axis: [f64; 3],
36 radius: f64,
37 start_angle: f64,
38 sweep_angle: f64,
39 },
40}
41
42impl AnalyticCurveSegment {
43 fn is_finite(&self) -> bool {
44 match self {
45 Self::Line { start, end } => start.iter().chain(end).all(|v| v.is_finite()),
46 Self::Arc {
47 center,
48 normal,
49 x_axis,
50 radius,
51 start_angle,
52 sweep_angle,
53 } => {
54 center
55 .iter()
56 .chain(normal)
57 .chain(x_axis)
58 .all(|v| v.is_finite())
59 && radius.is_finite()
60 && *radius > 0.0
61 && start_angle.is_finite()
62 && sweep_angle.is_finite()
63 }
64 }
65 }
66
67 pub fn reversed(&self) -> Self {
69 match *self {
70 Self::Line { start, end } => Self::Line {
71 start: end,
72 end: start,
73 },
74 Self::Arc {
75 center,
76 normal,
77 x_axis,
78 radius,
79 start_angle,
80 sweep_angle,
81 } => Self::Arc {
82 center,
83 normal,
84 x_axis,
85 radius,
86 start_angle: start_angle + sweep_angle,
87 sweep_angle: -sweep_angle,
88 },
89 }
90 }
91
92 pub fn subsegment(&self, start: f64, end: f64) -> Self {
94 match *self {
95 Self::Line { start: a, end: b } => {
96 let p = |t: f64| std::array::from_fn(|i| a[i] + (b[i] - a[i]) * t);
97 Self::Line {
98 start: p(start),
99 end: p(end),
100 }
101 }
102 Self::Arc {
103 center,
104 normal,
105 x_axis,
106 radius,
107 start_angle,
108 sweep_angle,
109 } => Self::Arc {
110 center,
111 normal,
112 x_axis,
113 radius,
114 start_angle: start_angle + sweep_angle * start,
115 sweep_angle: sweep_angle * (end - start),
116 },
117 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
123#[serde(tag = "type", content = "reason", rename_all = "snake_case")]
124pub enum AnalyticStatus {
125 Complete,
126 Unsupported(String),
127}
128
129#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
131pub struct AnalyticSweptDisk {
132 pub directrix_id: u32,
133 pub radius: f64,
134 pub inner_radius: Option<f64>,
135 pub segments: Vec<AnalyticCurveSegment>,
136 pub status: AnalyticStatus,
137}
138
139pub fn extract_swept_disk(
142 entity: &DecodedEntity,
143 decoder: &mut EntityDecoder,
144) -> Result<AnalyticSweptDisk> {
145 if entity.ifc_type != IfcType::IfcSweptDiskSolid {
146 return Err(Error::geometry("expected IfcSweptDiskSolid".to_string()));
147 }
148 let directrix_id = entity
149 .get_ref(0)
150 .ok_or_else(|| Error::geometry("IfcSweptDiskSolid missing Directrix".to_string()))?;
151 let radius = read_optional_numeric(entity.get(1), "Radius")?
152 .filter(|r| r.is_finite() && *r > 0.0)
153 .ok_or_else(|| Error::geometry("IfcSweptDiskSolid has invalid Radius".to_string()))?;
154 let inner_radius = read_optional_numeric(entity.get(2), "InnerRadius")?;
155 if inner_radius.is_some_and(|r| !r.is_finite() || r <= 0.0 || r >= radius) {
156 return Err(Error::geometry(
157 "IfcSweptDiskSolid has invalid InnerRadius".to_string(),
158 ));
159 }
160 let directrix = decoder
161 .resolve_ref(entity.get(0).unwrap())?
162 .ok_or_else(|| {
163 Error::geometry("IfcSweptDiskSolid Directrix cannot be resolved".to_string())
164 })?;
165 let mut walk = curve::CurveWalk::default();
166 let start = read_optional_numeric(entity.get(3), "StartParam")?;
167 let end = read_optional_numeric(entity.get(4), "EndParam")?;
168 let has_trim = start.is_some() || end.is_some();
169 let extracted = if directrix.ifc_type == IfcType::IfcLine && (start.is_none() || end.is_none())
170 {
171 None } else if has_trim && directrix.ifc_type.is_subtype_of(IfcType::IfcCompositeCurve) {
173 walk.composite_trimmed(&directrix, decoder, start, end)?
174 } else {
175 walk.extract(&directrix, decoder)?
176 };
177 let (mut segments, mut status) = match extracted {
178 Some(segments) => (segments, AnalyticStatus::Complete),
179 None => (
180 Vec::new(),
181 AnalyticStatus::Unsupported(format!(
182 "unsupported directrix {}",
183 directrix.ifc_type.as_str()
184 )),
185 ),
186 };
187 if start.is_some_and(|v| !v.is_finite()) || end.is_some_and(|v| !v.is_finite()) {
188 return Err(Error::geometry(
189 "IfcSweptDiskSolid has non-finite trim parameter".to_string(),
190 ));
191 }
192 if matches!(status, AnalyticStatus::Complete) && has_trim {
193 match directrix.ifc_type {
194 IfcType::IfcPolyline => {
195 segments = helpers::trim_indexed_segments(&segments, start, end)?;
196 }
197 IfcType::IfcCompositeCurve | IfcType::IfcCompositeCurveOnSurface => {}
198 IfcType::IfcTrimmedCurve => {
199 if !trimmed_line_solid_bounds_redundant(&directrix, &segments, start, end, decoder)?
200 {
201 status = AnalyticStatus::Unsupported(
202 "solid-level trim on trimmed directrix is not redundant".to_string(),
203 );
204 }
205 }
206 IfcType::IfcCircle => {
207 let scale = decoder.plane_angle_to_radians();
208 if !scale.is_finite() || scale <= 0.0 {
209 return Err(Error::geometry(
210 "invalid plane angle unit scale".to_string(),
211 ));
212 }
213 let begin = start.unwrap_or(0.0) * scale;
214 let finish = end.unwrap_or(std::f64::consts::TAU / scale) * scale;
215 let mut sweep = finish - begin;
216 if sweep < 0.0 {
217 sweep += std::f64::consts::TAU;
218 }
219 if !begin.is_finite()
220 || !sweep.is_finite()
221 || sweep <= 0.0
222 || sweep > std::f64::consts::TAU + 1e-9
223 {
224 status = AnalyticStatus::Unsupported(
225 "invalid circular directrix parameter range".to_string(),
226 );
227 } else if let Some(AnalyticCurveSegment::Arc {
228 start_angle,
229 sweep_angle,
230 ..
231 }) = segments.first_mut()
232 {
233 *start_angle = begin;
234 *sweep_angle = sweep;
235 }
236 }
237 IfcType::IfcLine => {
238 let line = segments
241 .first()
242 .cloned()
243 .ok_or_else(|| Error::geometry("empty line".to_string()))?;
244 segments = vec![line.subsegment(start.unwrap_or(0.0), end.unwrap_or(1.0))];
245 }
246 _ => {
247 status = AnalyticStatus::Unsupported(
248 "solid-level trim for this directrix is unsupported".to_string(),
249 )
250 }
251 }
252 }
253 if matches!(status, AnalyticStatus::Complete)
254 && segments.iter().any(|segment| !segment.is_finite())
255 {
256 return Err(Error::geometry(
257 "analytic directrix has non-finite geometry".to_string(),
258 ));
259 }
260 if matches!(status, AnalyticStatus::Complete) && segments.is_empty() {
261 status = AnalyticStatus::Unsupported("directrix has no segments".to_string());
262 }
263 if !matches!(status, AnalyticStatus::Complete) {
264 segments.clear();
265 }
266 Ok(AnalyticSweptDisk {
267 directrix_id,
268 radius,
269 inner_radius,
270 segments,
271 status,
272 })
273}
274
275fn trimmed_line_solid_bounds_redundant(
279 directrix: &DecodedEntity,
280 segments: &[AnalyticCurveSegment],
281 start: Option<f64>,
282 end: Option<f64>,
283 decoder: &mut EntityDecoder,
284) -> Result<bool> {
285 let (Some(start), Some(end), [AnalyticCurveSegment::Line { start: a, end: b }]) =
286 (start, end, segments)
287 else {
288 return Ok(false);
289 };
290 let Some(basis_attr) = directrix.get(0) else {
291 return Ok(false);
292 };
293 let Some(basis) = decoder.resolve_ref(basis_attr)? else {
294 return Ok(false);
295 };
296 if basis.ifc_type != IfcType::IfcLine {
297 return Ok(false);
298 }
299 let (_, direction) = helpers::line_basis(&basis, decoder)?;
300 let magnitude = (direction.iter().map(|v| v * v).sum::<f64>()).sqrt();
301 if magnitude <= 1e-12 {
302 return Ok(false);
303 }
304 let span = a
305 .iter()
306 .zip(b)
307 .map(|(x, y)| (y - x) * (y - x))
308 .sum::<f64>()
309 .sqrt()
310 / magnitude;
311 let close = |x: f64, y: f64| (x - y).abs() <= 1e-8 * x.abs().max(y.abs()).max(1.0);
312 Ok(close(start, 0.0) && close(end, span))
315}
316
317fn read_optional_numeric(value: Option<&AttributeValue>, field: &str) -> Result<Option<f64>> {
321 match value {
322 None | Some(AttributeValue::Null | AttributeValue::Derived) => Ok(None),
323 Some(value) => value
324 .as_float()
325 .map(Some)
326 .ok_or_else(|| Error::geometry(format!("IfcSweptDiskSolid has invalid {field}"))),
327 }
328}
329
330pub(crate) fn composite_parent_parameter_span(
338 parent: &DecodedEntity,
339 decoder: &mut EntityDecoder,
340) -> Result<Option<f64>> {
341 let mut walk = curve::CurveWalk::default();
342 let Some(pieces) = walk.extract(parent, decoder)? else {
343 return Ok(None);
344 };
345 let Some(spans) = helpers::parent_piece_spans(parent, &pieces, decoder)? else {
346 return Ok(None);
347 };
348 let span: f64 = spans.iter().sum();
349 Ok(span.is_finite().then_some(span))
350}