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
106pub const CURVE_SEGMENT_UNREAD: &str = "an IfcCurveSegment is placed and trimmed by \
117 SegmentStart and SegmentLength and cannot be read as an \
118 IfcCompositeCurveSegment; lower it with lower::curve instead";
119
120#[derive(Debug, Clone, Copy)]
127pub struct CompositeCurveSegment<'m> {
128 slots: Slots<'m>,
129}
130
131impl<'m> CompositeCurveSegment<'m> {
132 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
134 Self {
135 slots: Slots::new(id, entity),
136 }
137 }
138
139 pub fn id(&self) -> EntityId {
141 self.slots.id()
142 }
143
144 pub fn parent_curve_ref(&self) -> GeometryResult<EntityId> {
149 self.refuse_curve_segment()?;
150 self.slots
151 .req_ref(segment_slot::PARENT_CURVE, "ParentCurve")
152 }
153
154 fn refuse_curve_segment(&self) -> GeometryResult<()> {
156 if self.slots.entity().is_type("IFCCURVESEGMENT") {
157 return Err(self.slots.unsupported(CURVE_SEGMENT_UNREAD));
158 }
159 Ok(())
160 }
161
162 pub fn same_sense(&self) -> GeometryResult<bool> {
166 self.refuse_curve_segment()?;
167 self.slots.req_bool(segment_slot::SAME_SENSE, "SameSense")
168 }
169
170 pub fn transition(&self) -> GeometryResult<TransitionCode> {
175 let token = self
176 .slots
177 .opt_enum(segment_slot::TRANSITION)
178 .ok_or_else(|| {
179 self.slots
180 .degenerate("Transition is missing or is not an enumeration token")
181 })?;
182 TransitionCode::from_token(token).ok_or_else(|| {
183 self.slots
184 .degenerate(format!("unknown IfcTransitionCode .{token}."))
185 })
186 }
187
188 pub fn param_length(&self) -> GeometryResult<Option<f64>> {
195 self.refuse_curve_segment()?;
196 let Some(value) = self.slots.opt_f64(segment_slot::PARAM_LENGTH) else {
197 return Ok(None);
198 };
199 if value > 0.0 {
200 Ok(Some(value))
201 } else {
202 Err(self
203 .slots
204 .degenerate(format!("ParamLength must be positive, found {value}")))
205 }
206 }
207
208 pub fn is_reparametrised(&self) -> bool {
210 self.slots
211 .entity()
212 .type_name
213 .eq_ignore_ascii_case("IFCREPARAMETRISEDCOMPOSITECURVESEGMENT")
214 }
215}
216
217#[derive(Debug, Clone, Copy)]
222pub struct CompositeCurve<'m> {
223 slots: Slots<'m>,
224}
225
226impl<'m> CompositeCurve<'m> {
227 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
229 Self {
230 slots: Slots::new(id, entity),
231 }
232 }
233
234 pub fn id(&self) -> EntityId {
236 self.slots.id()
237 }
238
239 pub fn segment_refs(&self) -> GeometryResult<Vec<EntityId>> {
244 let segments = self.slots.req_ref_list(curve_slot::SEGMENTS, "Segments")?;
245 if segments.is_empty() {
246 return Err(self
247 .slots
248 .degenerate("Segments is empty; LIST [1:?] requires at least one segment"));
249 }
250 Ok(segments)
251 }
252
253 pub fn self_intersect(&self) -> Option<bool> {
255 self.slots.opt_bool(curve_slot::SELF_INTERSECT)
256 }
257
258 pub fn is_on_surface(&self) -> bool {
264 matches!(
265 self.slots.entity().type_name.to_ascii_uppercase().as_str(),
266 "IFCCOMPOSITECURVEONSURFACE" | "IFCBOUNDARYCURVE" | "IFCOUTERBOUNDARYCURVE"
267 )
268 }
269
270 pub fn is_outer_boundary(&self) -> bool {
276 self.slots
277 .entity()
278 .type_name
279 .eq_ignore_ascii_case("IFCOUTERBOUNDARYCURVE")
280 }
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use ifc_model::Value;
287
288 fn segment(transition: &str, same_sense: bool) -> Entity {
289 Entity::new(
290 "IFCCOMPOSITECURVESEGMENT",
291 vec![
292 Value::Enum(transition.into()),
293 Value::Bool(same_sense),
294 Value::Ref(EntityId(20)),
295 ],
296 )
297 }
298
299 #[test]
302 fn an_ifc4x3_curve_segment_is_refused_by_name_not_misread() {
303 let length = |v: f64| Value::Typed {
304 type_name: "IFCLENGTHMEASURE".into(),
305 value: Box::new(Value::Real(v)),
306 };
307 let e = Entity::new(
308 "IFCCURVESEGMENT",
309 vec![
310 Value::Enum("DISCONTINUOUS".into()),
311 Value::Ref(EntityId(10)),
312 length(0.0),
313 length(50.0),
314 Value::Ref(EntityId(20)),
315 ],
316 );
317 let view = CompositeCurveSegment::new(EntityId(7), &e);
318 for error in [
319 view.parent_curve_ref().map(|_| ()).unwrap_err(),
320 view.same_sense().map(|_| ()).unwrap_err(),
321 view.param_length().map(|_| ()).unwrap_err(),
322 ] {
323 assert!(
324 matches!(
325 &error,
326 crate::GeometryError::Unsupported { type_name, detail, .. }
327 if type_name == "IFCCURVESEGMENT" && *detail == CURVE_SEGMENT_UNREAD
328 ),
329 "{error}"
330 );
331 }
332 }
333
334 fn composite(type_name: &str, segments: &[u64]) -> Entity {
335 Entity::new(
336 type_name,
337 vec![
338 Value::List(segments.iter().map(|i| Value::Ref(EntityId(*i))).collect()),
339 Value::Bool(false),
340 ],
341 )
342 }
343
344 #[test]
345 fn a_reversed_segment_reports_same_sense_false_rather_than_being_normalised() {
346 let e = segment("CONTINUOUS", false);
347 let view = CompositeCurveSegment::new(EntityId(1), &e);
348 assert!(!view.same_sense().unwrap());
349 assert_eq!(view.parent_curve_ref().unwrap(), EntityId(20));
350 }
351
352 #[test]
353 fn every_transition_code_round_trips_from_its_file_token() {
354 for (token, expected) in [
355 ("DISCONTINUOUS", TransitionCode::Discontinuous),
356 ("CONTINUOUS", TransitionCode::Continuous),
357 ("CONTSAMEGRADIENT", TransitionCode::ContSameGradient),
358 (
359 "CONTSAMEGRADIENTSAMECURVATURE",
360 TransitionCode::ContSameGradientSameCurvature,
361 ),
362 ] {
363 let e = segment(token, true);
364 assert_eq!(
365 CompositeCurveSegment::new(EntityId(1), &e)
366 .transition()
367 .unwrap(),
368 expected,
369 "token {token}"
370 );
371 }
372 }
373
374 #[test]
377 fn only_discontinuous_reports_a_gap_at_the_joint() {
378 assert!(!TransitionCode::Discontinuous.is_connected());
379 assert!(TransitionCode::Continuous.is_connected());
380 assert!(TransitionCode::ContSameGradient.is_connected());
381 assert!(TransitionCode::ContSameGradientSameCurvature.is_connected());
382 }
383
384 #[test]
385 fn an_unknown_transition_token_is_rejected_rather_than_defaulted() {
386 assert_eq!(TransitionCode::from_token("SMOOTHISH"), None);
387 let e = segment("SMOOTHISH", true);
388 assert!(CompositeCurveSegment::new(EntityId(1), &e)
389 .transition()
390 .is_err());
391 }
392
393 #[test]
394 fn param_length_is_absent_on_a_plain_segment_and_read_on_the_reparametrised_one() {
395 let plain = segment("CONTINUOUS", true);
396 let view = CompositeCurveSegment::new(EntityId(1), &plain);
397 assert_eq!(view.param_length().unwrap(), None);
398 assert!(!view.is_reparametrised());
399
400 let reparam = Entity::new(
401 "IFCREPARAMETRISEDCOMPOSITECURVESEGMENT",
402 vec![
403 Value::Enum("CONTINUOUS".into()),
404 Value::Bool(true),
405 Value::Ref(EntityId(20)),
406 Value::Typed {
407 type_name: "IFCPARAMETERVALUE".into(),
408 value: Box::new(Value::Real(2.0)),
409 },
410 ],
411 );
412 let view = CompositeCurveSegment::new(EntityId(1), &reparam);
413 assert_eq!(view.param_length().unwrap(), Some(2.0));
414 assert!(view.is_reparametrised());
415 }
416
417 #[test]
418 fn a_non_positive_param_length_is_degenerate() {
419 let e = Entity::new(
420 "IFCREPARAMETRISEDCOMPOSITECURVESEGMENT",
421 vec![
422 Value::Enum("CONTINUOUS".into()),
423 Value::Bool(true),
424 Value::Ref(EntityId(20)),
425 Value::Real(0.0),
426 ],
427 );
428 assert!(CompositeCurveSegment::new(EntityId(1), &e)
429 .param_length()
430 .is_err());
431 }
432
433 #[test]
434 fn composite_curve_segments_keep_their_file_order() {
435 let e = composite("IFCCOMPOSITECURVE", &[1, 2, 3]);
436 assert_eq!(
437 CompositeCurve::new(EntityId(1), &e).segment_refs().unwrap(),
438 vec![EntityId(1), EntityId(2), EntityId(3)]
439 );
440 }
441
442 #[test]
443 fn a_composite_curve_with_no_segments_is_degenerate() {
444 let e = composite("IFCCOMPOSITECURVE", &[]);
445 assert!(CompositeCurve::new(EntityId(1), &e).segment_refs().is_err());
446 }
447
448 #[test]
451 fn boundary_curve_subtypes_are_classified_from_the_type_name() {
452 let plain = composite("IFCCOMPOSITECURVE", &[1]);
453 let on_surface = composite("IFCCOMPOSITECURVEONSURFACE", &[1]);
454 let boundary = composite("IFCBOUNDARYCURVE", &[1]);
455 let outer = composite("IFCOUTERBOUNDARYCURVE", &[1]);
456
457 assert!(!CompositeCurve::new(EntityId(1), &plain).is_on_surface());
458 assert!(CompositeCurve::new(EntityId(1), &on_surface).is_on_surface());
459 assert!(CompositeCurve::new(EntityId(1), &boundary).is_on_surface());
460 assert!(!CompositeCurve::new(EntityId(1), &boundary).is_outer_boundary());
461 assert!(CompositeCurve::new(EntityId(1), &outer).is_outer_boundary());
462 }
463}