ifc_lite_geometry/analytic/
profile.rs1use super::{curve::CurveWalk, AnalyticCurveSegment, AnalyticPoint, AnalyticStatus};
8use crate::transform::parse_axis2_placement_2d;
9use ifc_lite_core::{DecodedEntity, EntityDecoder, IfcType};
10use serde::Serialize;
11use std::f64::consts::TAU;
12
13const MAX_LOOPS: usize = 100_000;
14const DEFAULT_TOPOLOGY_CLEARANCE: f64 = 1e-9;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
20#[serde(rename_all = "snake_case")]
21pub enum ProfileLoopKind { Outer, Inner }
22
23#[derive(Debug, Clone, PartialEq, Serialize)]
26pub struct AnalyticProfileLoop {
27 pub kind: ProfileLoopKind,
28 pub segments: Vec<AnalyticCurveSegment>,
29 pub signed_area: f64,
30 pub perimeter: f64,
32}
33
34#[derive(Debug, Clone, PartialEq, Serialize)]
38pub struct AnalyticProfile {
39 pub profile_id: u32,
40 pub ifc_type_name: String,
41 #[serde(rename = "ProfileType")]
42 pub profile_type: Option<String>,
43 #[serde(rename = "Position")]
44 pub position_id: Option<u32>,
45 pub profile_position: Option<[f64; 16]>,
46 pub loops: Vec<AnalyticProfileLoop>,
47 pub status: AnalyticStatus,
48}
49
50pub fn extract_analytic_profile(profile: &DecodedEntity, decoder: &mut EntityDecoder) -> AnalyticProfile {
53 let mut result = AnalyticProfile {
54 profile_id: profile.id, ifc_type_name: profile.ifc_type.name().to_string(),
55 profile_type: profile.get(0).and_then(|value| value.as_enum()).map(str::to_owned),
56 position_id: None, profile_position: None,
57 loops: Vec::new(), status: AnalyticStatus::Complete,
58 };
59 let parsed = parse_profile(profile, decoder, &mut result);
60 let validated = parsed.and_then(|()| {
61 if matches!(profile.ifc_type,
62 IfcType::IfcArbitraryClosedProfileDef | IfcType::IfcArbitraryProfileDefWithVoids)
63 {
64 let declared = decoder.geometric_context_precision_range()
65 .map_err(|error| format!("profile topology Precision: {error}"))?;
66 let clearance = declared.map_or(DEFAULT_TOPOLOGY_CLEARANCE, |(_, max)| {
67 max.max(DEFAULT_TOPOLOGY_CLEARANCE)
68 });
69 let join_precision = declared.map_or(DEFAULT_TOPOLOGY_CLEARANCE, |(min, _)| {
70 min.min(DEFAULT_TOPOLOGY_CLEARANCE)
71 });
72 super::profile_topology::validate(&result.loops, clearance, join_precision)
73 } else {
74 Ok(())
75 }
76 });
77 if let Err(reason) = validated {
78 result.loops.clear();
79 result.status = AnalyticStatus::Unsupported(reason);
80 }
81 result
82}
83
84fn parse_profile(
85 profile: &DecodedEntity, decoder: &mut EntityDecoder, result: &mut AnalyticProfile,
86) -> Result<(), String> {
87 result.loops = match profile.ifc_type {
88 IfcType::IfcRectangleProfileDef => {
89 parse_profile_position(profile, decoder, result)?;
90 let x = positive(profile.get_float(3), "XDim")? / 2.0;
91 let y = positive(profile.get_float(4), "YDim")? / 2.0;
92 let points = [[-x, -y, 0.0], [x, -y, 0.0], [x, y, 0.0], [-x, y, 0.0]];
93 vec![loop_from_segments(ProfileLoopKind::Outer, (0..4).map(|i| {
94 AnalyticCurveSegment::Line { start: points[i], end: points[(i + 1) % 4] }
95 }).collect())?]
96 }
97 IfcType::IfcCircleProfileDef => {
98 parse_profile_position(profile, decoder, result)?;
99 let radius = positive(profile.get_float(3), "Radius")?;
100 vec![loop_from_segments(ProfileLoopKind::Outer, vec![AnalyticCurveSegment::Arc {
101 center: [0.0; 3], normal: [0.0, 0.0, 1.0], x_axis: [1.0, 0.0, 0.0],
102 radius, start_angle: 0.0, sweep_angle: TAU,
103 }])?]
104 }
105 IfcType::IfcArbitraryClosedProfileDef | IfcType::IfcArbitraryProfileDefWithVoids => {
106 arbitrary_loops(profile, decoder)?
107 }
108 _ => return Err(format!("unsupported profile {}", profile.ifc_type.name())),
109 };
110 Ok(())
111}
112
113fn positive(value: Option<f64>, attribute: &str) -> Result<f64, String> {
114 value.filter(|value| value.is_finite() && *value > 0.0)
115 .ok_or_else(|| format!("invalid or missing {attribute}"))
116}
117
118fn parse_profile_position(
119 profile: &DecodedEntity, decoder: &mut EntityDecoder, result: &mut AnalyticProfile,
120) -> Result<(), String> {
121 let Some(position) = profile.get(2).filter(|value| !value.is_null()) else { return Ok(()) };
122 let id = position.as_entity_ref().ok_or("invalid profile Position reference")?;
123 result.position_id = Some(id);
124 let entity = decoder.decode_by_id(id).map_err(|error| format!("Position #{id}: {error}"))?;
125 if entity.ifc_type != IfcType::IfcAxis2Placement2D {
126 return Err(format!("Position #{id} is not IfcAxis2Placement2D"));
127 }
128 let matrix = parse_axis2_placement_2d(&entity, decoder)
129 .map_err(|error| format!("Position #{id}: {error}"))?;
130 if matrix.iter().any(|value| !value.is_finite()) {
131 return Err(format!("Position #{id} has non-finite coordinates"));
132 }
133 let mut values = [0.0; 16];
134 values.copy_from_slice(matrix.as_slice());
135 result.profile_position = Some(values);
136 Ok(())
137}
138
139fn arbitrary_loops(
140 profile: &DecodedEntity, decoder: &mut EntityDecoder,
141) -> Result<Vec<AnalyticProfileLoop>, String> {
142 let outer = profile.get_ref(2).ok_or("missing OuterCurve")?;
143 let mut loops = vec![curve_loop(outer, ProfileLoopKind::Outer, decoder)?];
144 let mut emitted_segments = loops[0].segments.len();
145 if profile.ifc_type == IfcType::IfcArbitraryProfileDefWithVoids {
146 let references = profile.get(3).and_then(|value| value.as_list())
147 .ok_or("missing or malformed InnerCurves")?;
148 if references.len() > MAX_LOOPS { return Err("InnerCurves exceed work budget".into()); }
149 for reference in references {
150 if emitted_segments >= MAX_LOOPS {
151 return Err("profile segments exceed aggregate work budget".into());
152 }
153 let id = reference.as_entity_ref().ok_or("invalid InnerCurves reference")?;
154 let loop_data = curve_loop(id, ProfileLoopKind::Inner, decoder)?;
155 if loop_data.segments.len() > MAX_LOOPS.saturating_sub(emitted_segments) {
156 return Err("profile segments exceed aggregate work budget".into());
157 }
158 emitted_segments += loop_data.segments.len();
159 loops.push(loop_data);
160 }
161 }
162 Ok(loops)
163}
164
165fn curve_loop(
166 id: u32, kind: ProfileLoopKind, decoder: &mut EntityDecoder,
167) -> Result<AnalyticProfileLoop, String> {
168 let curve = decoder.decode_by_id(id).map_err(|error| format!("curve #{id}: {error}"))?;
169 let segments = CurveWalk::default().extract(&curve, decoder)
170 .map_err(|error| format!("curve #{id}: {error}"))?
171 .ok_or_else(|| format!("curve #{id} is not exact line/arc geometry"))?;
172 loop_from_segments(kind, segments).map_err(|reason| format!("curve #{id}: {reason}"))
173}
174
175fn loop_from_segments(
176 kind: ProfileLoopKind, segments: Vec<AnalyticCurveSegment>,
177) -> Result<AnalyticProfileLoop, String> {
178 if segments.is_empty() || segments.len() > MAX_LOOPS {
179 return Err("profile loop is empty or exceeds work budget".into());
180 }
181 let frames: Vec<_> = segments.iter().map(|segment| segment.endpoint_frame()
182 .ok_or("profile loop has invalid segment geometry")).collect::<Result<_, _>>()?;
183 let origin = frames[0].start;
184 let mut area = 0.0;
185 let mut perimeter = 0.0;
186 for (index, segment) in segments.iter().enumerate() {
187 let frame = &frames[index];
188 let next = &frames[(index + 1) % frames.len()];
189 let tolerance = 1e-9_f64.max(relative_extent(frame.start, frame.end) * 1e-9);
190 if frame.end.distance_to(next.start).is_none_or(|gap| gap > tolerance) {
191 return Err(format!("profile loop has a gap after segment {index}"));
192 }
193 for point in [frame.start, frame.end] {
194 if (point.origin[2] + point.offset[2]).abs() > 1e-9 {
195 return Err("profile loop is not in its XY plane".into());
196 }
197 }
198 area += segment_area(segment, origin)?;
199 perimeter += segment.length().ok_or("profile segment has invalid length")?;
200 }
201 if !area.is_finite() || area == 0.0 || !perimeter.is_finite() || perimeter <= 0.0 {
202 return Err("profile loop has zero or invalid enclosed area".into());
203 }
204 Ok(AnalyticProfileLoop { kind, segments, signed_area: area, perimeter })
205}
206
207fn relative_extent(a: AnalyticPoint, b: AnalyticPoint) -> f64 {
208 a.distance_to(b).unwrap_or(f64::INFINITY)
209}
210
211fn segment_area(segment: &AnalyticCurveSegment, origin: AnalyticPoint) -> Result<f64, String> {
212 let frame = segment.endpoint_frame().ok_or("invalid segment frame")?;
213 let relative = |point: AnalyticPoint| -> [f64; 3] {
214 std::array::from_fn(|axis| (point.origin[axis] - origin.origin[axis])
215 + (point.offset[axis] - origin.offset[axis]))
216 };
217 let a = relative(frame.start);
218 let b = relative(frame.end);
219 match segment {
220 AnalyticCurveSegment::Line { .. } => Ok((a[0] * b[1] - b[0] * a[1]) * 0.5),
221 AnalyticCurveSegment::Arc { center, normal, x_axis, radius, sweep_angle, .. } => {
222 if normal[0].abs() > 1e-9 || normal[1].abs() > 1e-9
223 || (normal[2].abs() - 1.0).abs() > 1e-9 || x_axis[2].abs() > 1e-9 {
224 return Err("profile arc is not planar in XY".into());
225 }
226 let c = [center[0] - origin.origin[0] - origin.offset[0],
227 center[1] - origin.origin[1] - origin.offset[1]];
228 Ok((c[0] * (b[1] - a[1]) - c[1] * (b[0] - a[0])
229 + normal[2] * radius * radius * sweep_angle) * 0.5)
230 }
231 }
232}
233
234#[cfg(test)]
235#[path = "profile_tests.rs"]
236mod tests;