1use crate::error::GeometryResult;
23use crate::slots::Slots;
24use ifc_model::{Entity, EntityId};
25
26pub(crate) mod curve_slot {
32 pub const SEGMENTS: usize = 0;
34 pub const SELF_INTERSECT: usize = 1;
36}
37
38pub(crate) mod segment_slot {
43 pub const TRANSITION: usize = 0;
45 pub const SAME_SENSE: usize = 1;
47 pub const PARENT_CURVE: usize = 2;
49 pub const PARAM_LENGTH: usize = 3;
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum TransitionCode {
59 Discontinuous,
61 Continuous,
63 ContSameGradient,
68 ContSameGradientSameCurvature,
70}
71
72impl TransitionCode {
73 pub fn from_token(token: &str) -> Option<Self> {
78 match token.to_ascii_uppercase().as_str() {
79 "DISCONTINUOUS" => Some(Self::Discontinuous),
80 "CONTINUOUS" => Some(Self::Continuous),
81 "CONTSAMEGRADIENT" => Some(Self::ContSameGradient),
82 "CONTSAMEGRADIENTSAMECURVATURE" => Some(Self::ContSameGradientSameCurvature),
83 _ => None,
84 }
85 }
86
87 pub fn token(self) -> &'static str {
89 match self {
90 Self::Discontinuous => "DISCONTINUOUS",
91 Self::Continuous => "CONTINUOUS",
92 Self::ContSameGradient => "CONTSAMEGRADIENT",
93 Self::ContSameGradientSameCurvature => "CONTSAMEGRADIENTSAMECURVATURE",
94 }
95 }
96
97 pub fn is_connected(&self) -> bool {
102 !matches!(self, Self::Discontinuous)
103 }
104}
105
106#[derive(Debug, Clone, Copy)]
111pub struct CompositeCurveSegment<'m> {
112 slots: Slots<'m>,
113}
114
115impl<'m> CompositeCurveSegment<'m> {
116 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
118 Self {
119 slots: Slots::new(id, entity),
120 }
121 }
122
123 pub fn id(&self) -> EntityId {
125 self.slots.id()
126 }
127
128 pub fn parent_curve_ref(&self) -> GeometryResult<EntityId> {
133 self.slots
134 .req_ref(segment_slot::PARENT_CURVE, "ParentCurve")
135 }
136
137 pub fn same_sense(&self) -> GeometryResult<bool> {
141 self.slots.req_bool(segment_slot::SAME_SENSE, "SameSense")
142 }
143
144 pub fn transition(&self) -> GeometryResult<TransitionCode> {
149 let token = self
150 .slots
151 .opt_enum(segment_slot::TRANSITION)
152 .ok_or_else(|| {
153 self.slots
154 .degenerate("Transition is missing or is not an enumeration token")
155 })?;
156 TransitionCode::from_token(token).ok_or_else(|| {
157 self.slots
158 .degenerate(format!("unknown IfcTransitionCode .{token}."))
159 })
160 }
161
162 pub fn param_length(&self) -> GeometryResult<Option<f64>> {
169 let Some(value) = self.slots.opt_f64(segment_slot::PARAM_LENGTH) else {
170 return Ok(None);
171 };
172 if value > 0.0 {
173 Ok(Some(value))
174 } else {
175 Err(self
176 .slots
177 .degenerate(format!("ParamLength must be positive, found {value}")))
178 }
179 }
180
181 pub fn is_reparametrised(&self) -> bool {
183 self.slots
184 .entity()
185 .type_name
186 .eq_ignore_ascii_case("IFCREPARAMETRISEDCOMPOSITECURVESEGMENT")
187 }
188}
189
190#[derive(Debug, Clone, Copy)]
195pub struct CompositeCurve<'m> {
196 slots: Slots<'m>,
197}
198
199impl<'m> CompositeCurve<'m> {
200 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
202 Self {
203 slots: Slots::new(id, entity),
204 }
205 }
206
207 pub fn id(&self) -> EntityId {
209 self.slots.id()
210 }
211
212 pub fn segment_refs(&self) -> GeometryResult<Vec<EntityId>> {
217 let segments = self.slots.req_ref_list(curve_slot::SEGMENTS, "Segments")?;
218 if segments.is_empty() {
219 return Err(self
220 .slots
221 .degenerate("Segments is empty; LIST [1:?] requires at least one segment"));
222 }
223 Ok(segments)
224 }
225
226 pub fn self_intersect(&self) -> Option<bool> {
228 self.slots.opt_bool(curve_slot::SELF_INTERSECT)
229 }
230
231 pub fn is_on_surface(&self) -> bool {
237 matches!(
238 self.slots.entity().type_name.to_ascii_uppercase().as_str(),
239 "IFCCOMPOSITECURVEONSURFACE" | "IFCBOUNDARYCURVE" | "IFCOUTERBOUNDARYCURVE"
240 )
241 }
242
243 pub fn is_outer_boundary(&self) -> bool {
249 self.slots
250 .entity()
251 .type_name
252 .eq_ignore_ascii_case("IFCOUTERBOUNDARYCURVE")
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use ifc_model::Value;
260
261 fn segment(transition: &str, same_sense: bool) -> Entity {
262 Entity::new(
263 "IFCCOMPOSITECURVESEGMENT",
264 vec![
265 Value::Enum(transition.into()),
266 Value::Bool(same_sense),
267 Value::Ref(EntityId(20)),
268 ],
269 )
270 }
271
272 fn composite(type_name: &str, segments: &[u64]) -> Entity {
273 Entity::new(
274 type_name,
275 vec![
276 Value::List(segments.iter().map(|i| Value::Ref(EntityId(*i))).collect()),
277 Value::Bool(false),
278 ],
279 )
280 }
281
282 #[test]
283 fn a_reversed_segment_reports_same_sense_false_rather_than_being_normalised() {
284 let e = segment("CONTINUOUS", false);
285 let view = CompositeCurveSegment::new(EntityId(1), &e);
286 assert!(!view.same_sense().unwrap());
287 assert_eq!(view.parent_curve_ref().unwrap(), EntityId(20));
288 }
289
290 #[test]
291 fn every_transition_code_round_trips_from_its_file_token() {
292 for (token, expected) in [
293 ("DISCONTINUOUS", TransitionCode::Discontinuous),
294 ("CONTINUOUS", TransitionCode::Continuous),
295 ("CONTSAMEGRADIENT", TransitionCode::ContSameGradient),
296 (
297 "CONTSAMEGRADIENTSAMECURVATURE",
298 TransitionCode::ContSameGradientSameCurvature,
299 ),
300 ] {
301 let e = segment(token, true);
302 assert_eq!(
303 CompositeCurveSegment::new(EntityId(1), &e)
304 .transition()
305 .unwrap(),
306 expected,
307 "token {token}"
308 );
309 }
310 }
311
312 #[test]
315 fn only_discontinuous_reports_a_gap_at_the_joint() {
316 assert!(!TransitionCode::Discontinuous.is_connected());
317 assert!(TransitionCode::Continuous.is_connected());
318 assert!(TransitionCode::ContSameGradient.is_connected());
319 assert!(TransitionCode::ContSameGradientSameCurvature.is_connected());
320 }
321
322 #[test]
323 fn an_unknown_transition_token_is_rejected_rather_than_defaulted() {
324 assert_eq!(TransitionCode::from_token("SMOOTHISH"), None);
325 let e = segment("SMOOTHISH", true);
326 assert!(CompositeCurveSegment::new(EntityId(1), &e)
327 .transition()
328 .is_err());
329 }
330
331 #[test]
332 fn param_length_is_absent_on_a_plain_segment_and_read_on_the_reparametrised_one() {
333 let plain = segment("CONTINUOUS", true);
334 let view = CompositeCurveSegment::new(EntityId(1), &plain);
335 assert_eq!(view.param_length().unwrap(), None);
336 assert!(!view.is_reparametrised());
337
338 let reparam = Entity::new(
339 "IFCREPARAMETRISEDCOMPOSITECURVESEGMENT",
340 vec![
341 Value::Enum("CONTINUOUS".into()),
342 Value::Bool(true),
343 Value::Ref(EntityId(20)),
344 Value::Typed {
345 type_name: "IFCPARAMETERVALUE".into(),
346 value: Box::new(Value::Real(2.0)),
347 },
348 ],
349 );
350 let view = CompositeCurveSegment::new(EntityId(1), &reparam);
351 assert_eq!(view.param_length().unwrap(), Some(2.0));
352 assert!(view.is_reparametrised());
353 }
354
355 #[test]
356 fn a_non_positive_param_length_is_degenerate() {
357 let e = Entity::new(
358 "IFCREPARAMETRISEDCOMPOSITECURVESEGMENT",
359 vec![
360 Value::Enum("CONTINUOUS".into()),
361 Value::Bool(true),
362 Value::Ref(EntityId(20)),
363 Value::Real(0.0),
364 ],
365 );
366 assert!(CompositeCurveSegment::new(EntityId(1), &e)
367 .param_length()
368 .is_err());
369 }
370
371 #[test]
372 fn composite_curve_segments_keep_their_file_order() {
373 let e = composite("IFCCOMPOSITECURVE", &[1, 2, 3]);
374 assert_eq!(
375 CompositeCurve::new(EntityId(1), &e).segment_refs().unwrap(),
376 vec![EntityId(1), EntityId(2), EntityId(3)]
377 );
378 }
379
380 #[test]
381 fn a_composite_curve_with_no_segments_is_degenerate() {
382 let e = composite("IFCCOMPOSITECURVE", &[]);
383 assert!(CompositeCurve::new(EntityId(1), &e).segment_refs().is_err());
384 }
385
386 #[test]
389 fn boundary_curve_subtypes_are_classified_from_the_type_name() {
390 let plain = composite("IFCCOMPOSITECURVE", &[1]);
391 let on_surface = composite("IFCCOMPOSITECURVEONSURFACE", &[1]);
392 let boundary = composite("IFCBOUNDARYCURVE", &[1]);
393 let outer = composite("IFCOUTERBOUNDARYCURVE", &[1]);
394
395 assert!(!CompositeCurve::new(EntityId(1), &plain).is_on_surface());
396 assert!(CompositeCurve::new(EntityId(1), &on_surface).is_on_surface());
397 assert!(CompositeCurve::new(EntityId(1), &boundary).is_on_surface());
398 assert!(!CompositeCurve::new(EntityId(1), &boundary).is_outer_boundary());
399 assert!(CompositeCurve::new(EntityId(1), &outer).is_outer_boundary());
400 }
401}