1use ifc_model::{Entity, EntityId, Transaction, Value};
23
24use crate::curve::bspline::slot as bspline_slot;
25use crate::curve::composite::{curve_slot, segment_slot};
26use crate::curve::conic::{circle_slot, ellipse_slot};
27use crate::curve::line::slot as line_slot;
28use crate::curve::polyline::indexed_slot;
29use crate::curve::trimmed::{slot as trimmed_slot, Trim};
30use crate::curve::{TransitionCode, TrimmingPreference};
31use crate::error::GeometryError;
32
33use super::std_profile::positive;
34use super::{invalid, reals, refs, require_finite};
35
36pub fn vector(
43 tx: &mut Transaction,
44 orientation: EntityId,
45 magnitude: f64,
46) -> Result<EntityId, GeometryError> {
47 positive("IFCVECTOR", "Magnitude", magnitude)?;
48 let attrs = vec![Value::Ref(orientation), Value::Real(magnitude)];
49 Ok(tx.create(Entity::new("IFCVECTOR", attrs)))
50}
51
52pub fn line(tx: &mut Transaction, point: EntityId, dir: EntityId) -> EntityId {
57 let mut attrs = vec![Value::Null; 2];
58 attrs[line_slot::PNT] = Value::Ref(point);
59 attrs[line_slot::DIR] = Value::Ref(dir);
60 tx.create(Entity::new("IFCLINE", attrs))
61}
62
63pub fn circle(
73 tx: &mut Transaction,
74 position: EntityId,
75 radius: f64,
76) -> Result<EntityId, GeometryError> {
77 positive("IFCCIRCLE", "Radius", radius)?;
78 let mut attrs = vec![Value::Null; 2];
79 attrs[circle_slot::POSITION] = Value::Ref(position);
80 attrs[circle_slot::RADIUS] = Value::Real(radius);
81 Ok(tx.create(Entity::new("IFCCIRCLE", attrs)))
82}
83
84pub fn ellipse(
90 tx: &mut Transaction,
91 position: EntityId,
92 semi_axis_1: f64,
93 semi_axis_2: f64,
94) -> Result<EntityId, GeometryError> {
95 positive("IFCELLIPSE", "SemiAxis1", semi_axis_1)?;
96 positive("IFCELLIPSE", "SemiAxis2", semi_axis_2)?;
97 let mut attrs = vec![Value::Null; 3];
98 attrs[ellipse_slot::POSITION] = Value::Ref(position);
99 attrs[ellipse_slot::SEMI_AXIS_1] = Value::Real(semi_axis_1);
100 attrs[ellipse_slot::SEMI_AXIS_2] = Value::Real(semi_axis_2);
101 Ok(tx.create(Entity::new("IFCELLIPSE", attrs)))
102}
103
104fn trim_members(trim: Trim, which: &'static str) -> Result<Value, GeometryError> {
110 let mut set = Vec::with_capacity(2);
111 if let Some(point) = trim.cartesian {
112 set.push(Value::Ref(point));
113 }
114 if let Some(parameter) = trim.parameter {
115 require_finite("IFCTRIMMEDCURVE", which, &[parameter])?;
116 set.push(Value::Typed {
121 type_name: "IFCPARAMETERVALUE".into(),
122 value: Box::new(Value::Real(parameter)),
123 });
124 }
125 if set.is_empty() {
126 return Err(invalid(
127 "IFCTRIMMEDCURVE",
128 which,
129 "a trim needs at least a Cartesian point or a parameter",
130 ));
131 }
132 Ok(Value::List(set))
133}
134
135pub fn trimmed_curve(
147 tx: &mut Transaction,
148 basis: EntityId,
149 trim_1: Trim,
150 trim_2: Trim,
151 sense_agreement: bool,
152 master: TrimmingPreference,
153) -> Result<EntityId, GeometryError> {
154 let mut attrs = vec![Value::Null; 5];
155 attrs[trimmed_slot::BASIS_CURVE] = Value::Ref(basis);
156 attrs[trimmed_slot::TRIM_1] = trim_members(trim_1, "Trim1")?;
157 attrs[trimmed_slot::TRIM_2] = trim_members(trim_2, "Trim2")?;
158 attrs[trimmed_slot::SENSE_AGREEMENT] = Value::Bool(sense_agreement);
159 attrs[trimmed_slot::MASTER_REPRESENTATION] = Value::Enum(master.token().into());
160 Ok(tx.create(Entity::new("IFCTRIMMEDCURVE", attrs)))
161}
162
163pub fn composite_curve_segment(
168 tx: &mut Transaction,
169 transition: TransitionCode,
170 same_sense: bool,
171 parent_curve: EntityId,
172) -> EntityId {
173 let mut attrs = vec![Value::Null; 3];
174 attrs[segment_slot::TRANSITION] = Value::Enum(transition.token().into());
175 attrs[segment_slot::SAME_SENSE] = Value::Bool(same_sense);
176 attrs[segment_slot::PARENT_CURVE] = Value::Ref(parent_curve);
177 tx.create(Entity::new("IFCCOMPOSITECURVESEGMENT", attrs))
178}
179
180pub fn composite_curve(
188 tx: &mut Transaction,
189 segments: &[EntityId],
190 self_intersect: Option<bool>,
191) -> Result<EntityId, GeometryError> {
192 if segments.is_empty() {
193 return Err(invalid(
194 "IFCCOMPOSITECURVE",
195 "Segments",
196 "expected at least one segment",
197 ));
198 }
199 let mut attrs = vec![Value::Null; 2];
200 attrs[curve_slot::SEGMENTS] = refs(segments);
201 attrs[curve_slot::SELF_INTERSECT] = match self_intersect {
202 Some(value) => Value::Bool(value),
203 None => Value::LogicalUnknown,
206 };
207 Ok(tx.create(Entity::new("IFCCOMPOSITECURVE", attrs)))
208}
209
210pub fn offset_curve_2d(
219 tx: &mut Transaction,
220 basis: EntityId,
221 distance: f64,
222 self_intersect: Option<bool>,
223) -> Result<EntityId, GeometryError> {
224 require_finite("IFCOFFSETCURVE2D", "Distance", &[distance])?;
225 let attrs = vec![
226 Value::Ref(basis),
227 Value::Real(distance),
228 logical(self_intersect),
229 ];
230 Ok(tx.create(Entity::new("IFCOFFSETCURVE2D", attrs)))
231}
232
233pub fn offset_curve_3d(
242 tx: &mut Transaction,
243 basis: EntityId,
244 distance: f64,
245 self_intersect: Option<bool>,
246 ref_direction: EntityId,
247) -> Result<EntityId, GeometryError> {
248 require_finite("IFCOFFSETCURVE3D", "Distance", &[distance])?;
249 let attrs = vec![
250 Value::Ref(basis),
251 Value::Real(distance),
252 logical(self_intersect),
253 Value::Ref(ref_direction),
254 ];
255 Ok(tx.create(Entity::new("IFCOFFSETCURVE3D", attrs)))
256}
257
258fn logical(value: Option<bool>) -> Value {
260 match value {
261 Some(value) => Value::Bool(value),
262 None => Value::LogicalUnknown,
263 }
264}
265
266#[derive(Debug, Clone, Copy)]
268pub struct KnotVector<'a> {
269 pub multiplicities: &'a [i64],
271 pub knots: &'a [f64],
273 pub spec: &'a str,
275}
276
277pub fn bspline_curve_with_knots(
298 tx: &mut Transaction,
299 degree: i64,
300 control_points: &[EntityId],
301 curve_form: &str,
302 knots: KnotVector<'_>,
303) -> Result<EntityId, GeometryError> {
304 const T: &str = "IFCBSPLINECURVEWITHKNOTS";
305 let attrs = bspline_attrs(T, degree, control_points, curve_form, knots)?;
306 Ok(tx.create(Entity::new(T, attrs)))
307}
308
309pub fn rational_bspline_curve_with_knots(
317 tx: &mut Transaction,
318 degree: i64,
319 control_points: &[EntityId],
320 curve_form: &str,
321 knots: KnotVector<'_>,
322 weights: &[f64],
323) -> Result<EntityId, GeometryError> {
324 const T: &str = "IFCRATIONALBSPLINECURVEWITHKNOTS";
325 if weights.len() != control_points.len() {
326 return Err(invalid(
327 T,
328 "WeightsData",
329 format!(
330 "{} weights for {} control points",
331 weights.len(),
332 control_points.len()
333 ),
334 ));
335 }
336 require_finite(T, "WeightsData", weights)?;
337 let mut attrs = bspline_attrs(T, degree, control_points, curve_form, knots)?;
338 attrs.push(Value::List(
339 weights.iter().copied().map(Value::Real).collect(),
340 ));
341 Ok(tx.create(Entity::new(T, attrs)))
342}
343
344fn bspline_attrs(
346 type_name: &'static str,
347 degree: i64,
348 control_points: &[EntityId],
349 curve_form: &str,
350 knots: KnotVector<'_>,
351) -> Result<Vec<Value>, GeometryError> {
352 if degree < 1 {
353 return Err(invalid(
354 type_name,
355 "Degree",
356 format!("expected a degree of at least 1, got {degree}"),
357 ));
358 }
359 if control_points.len() < 2 {
360 return Err(invalid(
361 type_name,
362 "ControlPointsList",
363 format!(
364 "expected at least 2 control points, got {}",
365 control_points.len()
366 ),
367 ));
368 }
369 if knots.multiplicities.len() != knots.knots.len() {
370 return Err(invalid(
371 type_name,
372 "KnotMultiplicities",
373 format!(
374 "{} multiplicities for {} knots",
375 knots.multiplicities.len(),
376 knots.knots.len()
377 ),
378 ));
379 }
380 if knots.knots.len() < 2 {
381 return Err(invalid(
382 type_name,
383 "Knots",
384 "expected at least 2 distinct knots",
385 ));
386 }
387 require_finite(type_name, "Knots", knots.knots)?;
388 if let Some(bad) = knots.multiplicities.iter().position(|m| *m < 1) {
389 return Err(invalid(
390 type_name,
391 "KnotMultiplicities",
392 format!("multiplicity at index {bad} is not positive"),
393 ));
394 }
395 if let Some(bad) = knots.knots.windows(2).position(|w| w[1] <= w[0]) {
396 return Err(invalid(
397 type_name,
398 "Knots",
399 format!("knots must strictly increase; index {bad} does not"),
400 ));
401 }
402
403 let total: i64 = knots.multiplicities.iter().sum();
406 let expected = degree + control_points.len() as i64 + 1;
407 if total != expected {
408 return Err(invalid(
409 type_name,
410 "KnotMultiplicities",
411 format!(
412 "multiplicities sum to {total}, but degree {degree} with {} control points requires {expected}",
413 control_points.len()
414 ),
415 ));
416 }
417
418 let mut attrs = vec![Value::Null; 8];
419 attrs[bspline_slot::DEGREE] = Value::Integer(degree);
420 attrs[bspline_slot::CONTROL_POINTS] = refs(control_points);
421 attrs[bspline_slot::CURVE_FORM] = Value::Enum(curve_form.into());
422 attrs[bspline_slot::CLOSED_CURVE] = Value::LogicalUnknown;
425 attrs[bspline_slot::SELF_INTERSECT] = Value::LogicalUnknown;
426 attrs[bspline_slot::KNOT_MULTIPLICITIES] = Value::List(
427 knots
428 .multiplicities
429 .iter()
430 .copied()
431 .map(Value::Integer)
432 .collect(),
433 );
434 attrs[bspline_slot::KNOTS] =
435 Value::List(knots.knots.iter().copied().map(Value::Real).collect());
436 attrs[bspline_slot::KNOT_SPEC] = Value::Enum(knots.spec.into());
437 Ok(attrs)
438}
439
440pub fn indexed_poly_curve(
451 tx: &mut Transaction,
452 points: EntityId,
453 segments: Option<&[PolyCurveSegment<'_>]>,
454 point_count: usize,
455 self_intersect: Option<bool>,
456) -> Result<EntityId, GeometryError> {
457 const T: &str = "IFCINDEXEDPOLYCURVE";
458 let mut attrs = vec![Value::Null; 3];
459 attrs[indexed_slot::POINTS] = Value::Ref(points);
460 if let Some(segments) = segments {
461 let mut list = Vec::with_capacity(segments.len());
462 for segment in segments {
463 list.push(segment.to_value(T, point_count)?);
464 }
465 attrs[indexed_slot::SEGMENTS] = Value::List(list);
466 }
467 attrs[indexed_slot::SELF_INTERSECT] = logical(self_intersect);
468 Ok(tx.create(Entity::new(T, attrs)))
469}
470
471#[derive(Debug, Clone, Copy)]
477pub enum PolyCurveSegment<'a> {
478 Line(&'a [usize]),
480 Arc([usize; 3]),
482}
483
484impl PolyCurveSegment<'_> {
485 fn to_value(self, type_name: &'static str, point_count: usize) -> Result<Value, GeometryError> {
487 let (label, indices): (&str, &[usize]) = match self {
488 Self::Line(indices) => ("IFCLINEINDEX", indices),
489 Self::Arc(ref indices) => ("IFCARCINDEX", indices),
490 };
491 if let Self::Line(indices) = self {
492 if indices.len() < 2 {
493 return Err(invalid(
494 type_name,
495 "Segments",
496 format!(
497 "a line index needs at least 2 points, got {}",
498 indices.len()
499 ),
500 ));
501 }
502 }
503 for index in indices {
504 if *index >= point_count {
505 return Err(invalid(
506 type_name,
507 "Segments",
508 format!("index {index} is past the {point_count} point list"),
509 ));
510 }
511 }
512 let encoded = indices
513 .iter()
514 .map(|index| Value::Integer(*index as i64 + 1))
515 .collect();
516 Ok(Value::Typed {
517 type_name: label.into(),
518 value: Box::new(Value::List(encoded)),
519 })
520 }
521}
522
523#[derive(Debug, Clone, Copy, Default)]
528pub struct PolynomialCoefficients<'a> {
529 pub x: Option<&'a [f64]>,
531 pub y: Option<&'a [f64]>,
533 pub z: Option<&'a [f64]>,
535}
536
537pub fn polynomial_curve(
551 tx: &mut Transaction,
552 position: EntityId,
553 coefficients: PolynomialCoefficients<'_>,
554 position_is_3d: bool,
555) -> Result<EntityId, GeometryError> {
556 const ENTITY: &str = "IFCPOLYNOMIALCURVE";
557 let PolynomialCoefficients { x, y, z } = coefficients;
558 if z.is_some() && !position_is_3d {
559 return Err(invalid(
560 ENTITY,
561 "CoefficientsZ",
562 "a 2D position cannot carry Z coefficients",
563 ));
564 }
565 let given = [x, y, z].iter().filter(|c| c.is_some()).count();
566 if given < 2 {
567 return Err(invalid(
568 ENTITY,
569 "Coefficients",
570 "expected at least two of X, Y, Z, per ValidCoefficients",
571 ));
572 }
573 for (values, attribute) in [
574 (x, "CoefficientsX"),
575 (y, "CoefficientsY"),
576 (z, "CoefficientsZ"),
577 ] {
578 let Some(values) = values else { continue };
579 if values.len() < 2 {
580 return Err(invalid(ENTITY, attribute, "expected LIST [2:?]"));
581 }
582 require_finite(ENTITY, attribute, values)?;
583 }
584 let attrs = vec![
585 Value::Ref(position),
586 x.map_or(Value::Null, reals),
587 y.map_or(Value::Null, reals),
588 z.map_or(Value::Null, reals),
589 ];
590 Ok(tx.create(Entity::new(ENTITY, attrs)))
591}
592
593pub fn offset_curve_by_distances(
605 tx: &mut Transaction,
606 basis: EntityId,
607 offset_values: &[EntityId],
608 tag: Option<&str>,
609) -> Result<EntityId, GeometryError> {
610 const ENTITY: &str = "IFCOFFSETCURVEBYDISTANCES";
611 if offset_values.is_empty() {
612 return Err(invalid(
613 ENTITY,
614 "OffsetValues",
615 "expected at least one offset, per LIST [1:?]",
616 ));
617 }
618 let attrs = vec![
619 Value::Ref(basis),
620 refs(offset_values),
621 tag.map_or(Value::Null, |t| Value::Text(t.into())),
622 ];
623 Ok(tx.create(Entity::new(ENTITY, attrs)))
624}
625
626pub fn segmented_reference_curve(
637 tx: &mut Transaction,
638 segments: &[EntityId],
639 self_intersect: Option<bool>,
640 base_curve: EntityId,
641 end_point: Option<EntityId>,
642) -> Result<EntityId, GeometryError> {
643 const ENTITY: &str = "IFCSEGMENTEDREFERENCECURVE";
644 if segments.is_empty() {
645 return Err(invalid(
646 ENTITY,
647 "Segments",
648 "expected at least one segment, per LIST [1:?]",
649 ));
650 }
651 let attrs = vec![
652 refs(segments),
653 logical(self_intersect),
654 Value::Ref(base_curve),
655 end_point.map_or(Value::Null, Value::Ref),
656 ];
657 Ok(tx.create(Entity::new(ENTITY, attrs)))
658}