1use crate::curve::bspline::{KnotType, KnotVector};
29use crate::error::GeometryResult;
30use crate::slots::Slots;
31use ifc_model::{Entity, EntityId, Value};
32
33pub(crate) mod slot {
39 pub const U_DEGREE: usize = 0;
41 pub const V_DEGREE: usize = 1;
43 pub const CONTROL_POINTS: usize = 2;
45 pub const SURFACE_FORM: usize = 3;
47 pub const U_CLOSED: usize = 4;
49 pub const V_CLOSED: usize = 5;
51 pub const SELF_INTERSECT: usize = 6;
53 pub const U_MULTIPLICITIES: usize = 7;
55 pub const V_MULTIPLICITIES: usize = 8;
57 pub const U_KNOTS: usize = 9;
59 pub const V_KNOTS: usize = 10;
61 pub const KNOT_SPEC: usize = 11;
63 pub const WEIGHTS_DATA: usize = 12;
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum BSplineSurfaceForm {
75 PlaneSurf,
77 CylindricalSurf,
79 ConicalSurf,
81 SphericalSurf,
83 ToroidalSurf,
85 SurfOfRevolution,
87 RuledSurf,
89 GeneralisedCone,
91 QuadricSurf,
93 SurfOfLinearExtrusion,
95 Unspecified,
97}
98
99impl BSplineSurfaceForm {
100 pub fn from_token(token: &str) -> Option<Self> {
102 match token.to_ascii_uppercase().as_str() {
103 "PLANE_SURF" => Some(Self::PlaneSurf),
104 "CYLINDRICAL_SURF" => Some(Self::CylindricalSurf),
105 "CONICAL_SURF" => Some(Self::ConicalSurf),
106 "SPHERICAL_SURF" => Some(Self::SphericalSurf),
107 "TOROIDAL_SURF" => Some(Self::ToroidalSurf),
108 "SURF_OF_REVOLUTION" => Some(Self::SurfOfRevolution),
109 "RULED_SURF" => Some(Self::RuledSurf),
110 "GENERALISED_CONE" => Some(Self::GeneralisedCone),
111 "QUADRIC_SURF" => Some(Self::QuadricSurf),
112 "SURF_OF_LINEAR_EXTRUSION" => Some(Self::SurfOfLinearExtrusion),
113 "UNSPECIFIED" => Some(Self::Unspecified),
114 _ => None,
115 }
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq)]
124pub struct ControlPointGrid {
125 rows: Vec<Vec<EntityId>>,
126}
127
128impl ControlPointGrid {
129 pub fn u_count(&self) -> usize {
131 self.rows.len()
132 }
133
134 pub fn v_count(&self) -> usize {
136 self.rows.first().map_or(0, Vec::len)
137 }
138
139 pub fn get(&self, u_index: usize, v_index: usize) -> Option<EntityId> {
141 self.rows.get(u_index)?.get(v_index).copied()
142 }
143
144 pub fn rows(&self) -> &[Vec<EntityId>] {
146 &self.rows
147 }
148}
149
150#[derive(Debug, Clone, Copy)]
152pub struct BSplineSurface<'m> {
153 slots: Slots<'m>,
154}
155
156impl<'m> BSplineSurface<'m> {
157 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
159 Self {
160 slots: Slots::new(id, entity),
161 }
162 }
163
164 pub fn id(&self) -> EntityId {
166 self.slots.id()
167 }
168
169 pub fn u_degree(&self) -> GeometryResult<usize> {
171 self.degree(slot::U_DEGREE, "UDegree", self.control_points()?.u_count())
172 }
173
174 pub fn v_degree(&self) -> GeometryResult<usize> {
176 self.degree(slot::V_DEGREE, "VDegree", self.control_points()?.v_count())
177 }
178
179 pub fn control_points(&self) -> GeometryResult<ControlPointGrid> {
186 let value = self.slots.req(slot::CONTROL_POINTS, "ControlPointsList")?;
187 let outer = value.as_list().ok_or_else(|| {
188 self.slots
189 .degenerate("ControlPointsList must be a list of lists")
190 })?;
191 if outer.len() < 2 {
192 return Err(self.slots.degenerate(format!(
193 "ControlPointsList needs at least 2 rows along u, found {}",
194 outer.len()
195 )));
196 }
197
198 let mut rows: Vec<Vec<EntityId>> = Vec::with_capacity(outer.len());
199 for (u_index, row_value) in outer.iter().enumerate() {
200 let inner = row_value.as_list().ok_or_else(|| {
201 self.slots
202 .degenerate(format!("ControlPointsList row {u_index} is not a list"))
203 })?;
204 let mut row = Vec::with_capacity(inner.len());
205 for (v_index, point) in inner.iter().enumerate() {
206 let id = point.as_ref_id().ok_or_else(|| {
207 self.slots.degenerate(format!(
208 "ControlPointsList[{u_index}][{v_index}] is not an entity reference"
209 ))
210 })?;
211 row.push(id);
212 }
213 rows.push(row);
214 }
215
216 let v_count = rows[0].len();
217 if v_count < 2 {
218 return Err(self.slots.degenerate(format!(
219 "ControlPointsList needs at least 2 columns along v, found {v_count}"
220 )));
221 }
222 for (u_index, row) in rows.iter().enumerate() {
223 if row.len() != v_count {
224 return Err(self.slots.degenerate(format!(
225 "ControlPointsList row {u_index} has {} points but row 0 has {v_count}; \
226 the grid must be rectangular",
227 row.len()
228 )));
229 }
230 }
231 Ok(ControlPointGrid { rows })
232 }
233
234 pub fn surface_form(&self) -> BSplineSurfaceForm {
236 self.slots
237 .opt_enum(slot::SURFACE_FORM)
238 .and_then(BSplineSurfaceForm::from_token)
239 .unwrap_or(BSplineSurfaceForm::Unspecified)
240 }
241
242 pub fn u_closed(&self) -> Option<bool> {
244 self.slots.opt_bool(slot::U_CLOSED)
245 }
246
247 pub fn v_closed(&self) -> Option<bool> {
249 self.slots.opt_bool(slot::V_CLOSED)
250 }
251
252 pub fn self_intersect(&self) -> Option<bool> {
254 self.slots.opt_bool(slot::SELF_INTERSECT)
255 }
256
257 pub fn knot_spec(&self) -> KnotType {
259 self.slots
260 .opt_enum(slot::KNOT_SPEC)
261 .and_then(KnotType::from_token)
262 .unwrap_or(KnotType::Unspecified)
263 }
264
265 pub fn has_knots(&self) -> bool {
267 self.slots.opt(slot::U_KNOTS).is_some()
268 }
269
270 pub fn is_rational(&self) -> bool {
272 self.slots
273 .type_name()
274 .eq_ignore_ascii_case("IFCRATIONALBSPLINESURFACEWITHKNOTS")
275 }
276
277 pub fn u_knots(&self) -> GeometryResult<Option<KnotVector>> {
281 if !self.has_knots() {
282 return Ok(None);
283 }
284 let expected = self
285 .control_points()?
286 .u_count()
287 .checked_add(self.u_degree()?)
288 .and_then(|value| value.checked_add(1))
289 .ok_or_else(|| self.slots.degenerate("u knot count overflows usize"))?;
290 self.knot_vector(
291 slot::U_KNOTS,
292 "UKnots",
293 slot::U_MULTIPLICITIES,
294 "UMultiplicities",
295 expected,
296 )
297 .map(Some)
298 }
299
300 pub fn v_knots(&self) -> GeometryResult<Option<KnotVector>> {
304 if !self.has_knots() {
305 return Ok(None);
306 }
307 let expected = self
308 .control_points()?
309 .v_count()
310 .checked_add(self.v_degree()?)
311 .and_then(|value| value.checked_add(1))
312 .ok_or_else(|| self.slots.degenerate("v knot count overflows usize"))?;
313 self.knot_vector(
314 slot::V_KNOTS,
315 "VKnots",
316 slot::V_MULTIPLICITIES,
317 "VMultiplicities",
318 expected,
319 )
320 .map(Some)
321 }
322
323 pub fn weights(&self) -> GeometryResult<Option<Vec<Vec<f64>>>> {
329 let supplied = self.slots.opt(slot::WEIGHTS_DATA).is_some();
330 match (self.is_rational(), supplied) {
331 (false, false) => return Ok(None),
332 (true, false) => {
333 return Err(self
334 .slots
335 .degenerate("rational B-spline surface is missing WeightsData"));
336 }
337 (false, true) => {
338 return Err(self
339 .slots
340 .degenerate("polynomial B-spline surface must not carry WeightsData"));
341 }
342 (true, true) => {}
343 }
344 let grid = self.control_points()?;
345 let value = self.slots.req(slot::WEIGHTS_DATA, "WeightsData")?;
346 let outer = value
347 .as_list()
348 .ok_or_else(|| self.slots.degenerate("WeightsData must be a list of lists"))?;
349
350 if outer.len() != grid.u_count() {
351 return Err(self.slots.degenerate(format!(
352 "WeightsData has {} rows but the control point grid has {}",
353 outer.len(),
354 grid.u_count()
355 )));
356 }
357
358 let mut weights = Vec::with_capacity(outer.len());
359 for (u_index, row_value) in outer.iter().enumerate() {
360 let inner = row_value.as_list().ok_or_else(|| {
361 self.slots
362 .degenerate(format!("WeightsData row {u_index} is not a list"))
363 })?;
364 if inner.len() != grid.v_count() {
365 return Err(self.slots.degenerate(format!(
366 "WeightsData row {u_index} has {} entries but the grid has {}",
367 inner.len(),
368 grid.v_count()
369 )));
370 }
371 let mut row = Vec::with_capacity(inner.len());
372 for (v_index, w) in inner.iter().enumerate() {
373 let weight = w.unwrap_typed().as_f64().ok_or_else(|| {
374 self.slots
375 .degenerate(format!("WeightsData[{u_index}][{v_index}] is not a number"))
376 })?;
377 if !weight.is_finite() {
378 return Err(self.slots.degenerate(format!(
379 "weight {weight} at control point [{u_index}][{v_index}] must be finite"
380 )));
381 }
382 if weight <= 0.0 {
383 return Err(self.slots.degenerate(format!(
384 "weight {weight} at control point [{u_index}][{v_index}] must be positive"
385 )));
386 }
387 row.push(weight);
388 }
389 weights.push(row);
390 }
391 Ok(Some(weights))
392 }
393
394 fn degree(
395 &self,
396 index: usize,
397 name: &'static str,
398 control_count: usize,
399 ) -> GeometryResult<usize> {
400 let raw = self.slots.req_i64(index, name)?;
401 if raw < 1 {
402 return Err(self
403 .slots
404 .degenerate(format!("{name} must be at least 1, found {raw}")));
405 }
406 let degree = usize::try_from(raw).map_err(|_| {
407 self.slots
408 .degenerate(format!("{name} exceeds platform limits"))
409 })?;
410 if degree >= control_count {
411 return Err(self.slots.degenerate(format!(
412 "{name} {degree} must be smaller than the {control_count} control points"
413 )));
414 }
415 Ok(degree)
416 }
417
418 fn knot_vector(
420 &self,
421 knots_index: usize,
422 knots_name: &'static str,
423 mult_index: usize,
424 mult_name: &'static str,
425 expected: usize,
426 ) -> GeometryResult<KnotVector> {
427 let values = self.slots.req_f64_list(knots_index, knots_name)?;
428 let raw = self.slots.req(mult_index, mult_name)?;
429 let items = raw
430 .as_list()
431 .ok_or_else(|| self.slots.degenerate(format!("{mult_name} must be a list")))?;
432
433 let mut multiplicities = Vec::with_capacity(items.len());
434 for item in items {
435 match item.unwrap_typed() {
436 Value::Integer(i) if *i >= 1 => {
437 multiplicities.push(usize::try_from(*i).map_err(|_| {
438 self.slots
439 .degenerate(format!("{mult_name} exceeds platform limits"))
440 })?);
441 }
442 other => {
443 return Err(self.slots.degenerate(format!(
444 "{mult_name} entry must be a positive integer, found {other:?}"
445 )));
446 }
447 }
448 }
449
450 if values.len() != multiplicities.len() {
451 return Err(self.slots.degenerate(format!(
452 "{knots_name} has {} entries but {mult_name} has {}; they are parallel lists",
453 values.len(),
454 multiplicities.len()
455 )));
456 }
457 for (index, value) in values.iter().enumerate() {
458 if !value.is_finite() {
459 return Err(self.slots.degenerate(format!(
460 "{knots_name}[{index}] must be finite, found {value}"
461 )));
462 }
463 }
464 for pair in values.windows(2) {
465 if pair[1] <= pair[0] {
466 return Err(self.slots.degenerate(format!(
467 "{knots_name} must be strictly increasing; found {} after {}",
468 pair[1], pair[0]
469 )));
470 }
471 }
472 let total = multiplicities.iter().try_fold(0usize, |total, &value| {
473 total.checked_add(value).ok_or_else(|| {
474 self.slots
475 .degenerate(format!("{mult_name} total overflows usize"))
476 })
477 })?;
478 if total != expected {
479 return Err(self.slots.degenerate(format!(
480 "{mult_name} sums to {total} but must equal control points + degree + 1 = {expected}"
481 )));
482 }
483 Ok(KnotVector {
484 values,
485 multiplicities,
486 })
487 }
488}
489
490pub type SurfaceKnotVector = KnotVector;
496
497#[cfg(test)]
498mod tests {
499 use super::*;
500
501 fn grid(u_count: usize, v_count: usize) -> Value {
504 Value::List(
505 (0..u_count)
506 .map(|u| {
507 Value::List(
508 (0..v_count)
509 .map(|v| Value::Ref(EntityId((u * 10 + v) as u64)))
510 .collect(),
511 )
512 })
513 .collect(),
514 )
515 }
516
517 fn integers(values: &[i64]) -> Value {
518 Value::List(values.iter().map(|i| Value::Integer(*i)).collect())
519 }
520
521 fn reals(values: &[f64]) -> Value {
522 Value::List(values.iter().map(|r| Value::Real(*r)).collect())
523 }
524
525 fn bilinear() -> Entity {
527 Entity::new(
528 "IFCBSPLINESURFACEWITHKNOTS",
529 vec![
530 Value::Integer(1),
531 Value::Integer(1),
532 grid(2, 2),
533 Value::Enum("UNSPECIFIED".into()),
534 Value::Bool(false),
535 Value::Bool(false),
536 Value::Bool(false),
537 integers(&[2, 2]),
538 integers(&[2, 2]),
539 reals(&[0.0, 1.0]),
540 reals(&[0.0, 1.0]),
541 Value::Enum("UNSPECIFIED".into()),
542 ],
543 )
544 }
545
546 #[test]
547 fn inherited_surface_slots_precede_the_knot_and_weight_slots() {
548 let e = bilinear();
549 let view = BSplineSurface::new(EntityId(1), &e);
550 assert_eq!(view.u_degree().unwrap(), 1);
551 assert_eq!(view.v_degree().unwrap(), 1);
552 assert!(view.has_knots());
553 assert!(!view.is_rational());
554 assert_eq!(view.knot_spec(), KnotType::Unspecified);
555 }
556
557 #[test]
561 fn the_outer_control_point_list_runs_along_u_and_the_inner_along_v() {
562 let e = Entity::new(
563 "IFCBSPLINESURFACE",
564 vec![
565 Value::Integer(1),
566 Value::Integer(1),
567 grid(3, 5),
568 Value::Enum("UNSPECIFIED".into()),
569 Value::Bool(false),
570 Value::Bool(false),
571 Value::Bool(false),
572 ],
573 );
574 let points = BSplineSurface::new(EntityId(1), &e)
575 .control_points()
576 .unwrap();
577 assert_eq!(points.u_count(), 3, "outer list length is the u count");
578 assert_eq!(points.v_count(), 5, "inner list length is the v count");
579 assert_eq!(points.get(2, 4), Some(EntityId(24)));
581 assert_eq!(points.rows().len(), 3);
582 }
583
584 #[test]
586 fn a_ragged_control_point_grid_is_rejected() {
587 let mut e = bilinear();
588 e.attributes[slot::CONTROL_POINTS] = Value::List(vec![
589 Value::List(vec![Value::Ref(EntityId(1)), Value::Ref(EntityId(2))]),
590 Value::List(vec![Value::Ref(EntityId(3))]),
591 ]);
592 let err = BSplineSurface::new(EntityId(7), &e)
593 .control_points()
594 .unwrap_err();
595 assert!(err.to_string().contains("rectangular"), "got: {err}");
596 assert!(err.to_string().contains("#7"), "got: {err}");
597 }
598
599 #[test]
600 fn both_knot_vectors_are_checked_against_their_own_control_point_count() {
601 let e = bilinear();
602 let view = BSplineSurface::new(EntityId(1), &e);
603 let u = view.u_knots().unwrap().unwrap();
604 let v = view.v_knots().unwrap().unwrap();
605 assert_eq!(u.expanded(), Some(vec![0.0, 0.0, 1.0, 1.0]));
606 assert_eq!(v.expanded(), Some(vec![0.0, 0.0, 1.0, 1.0]));
607 assert!(u.is_clamped(1));
608 }
609
610 #[test]
613 fn a_wrong_v_multiplicity_sum_is_caught_even_when_u_is_right() {
614 let mut e = bilinear();
615 e.attributes[slot::V_MULTIPLICITIES] = integers(&[2, 3]);
616 let view = BSplineSurface::new(EntityId(1), &e);
617 assert!(view.u_knots().is_ok(), "u is untouched and must still pass");
618 let err = view.v_knots().unwrap_err();
619 assert!(err.to_string().contains("VMultiplicities"), "got: {err}");
620 }
621
622 #[test]
623 fn parallel_knot_lists_of_different_lengths_are_rejected() {
624 let mut e = bilinear();
625 e.attributes[slot::U_KNOTS] = reals(&[0.0, 0.5, 1.0]);
626 let err = BSplineSurface::new(EntityId(1), &e).u_knots().unwrap_err();
627 assert!(err.to_string().contains("parallel"), "got: {err}");
628 }
629
630 #[test]
631 fn non_increasing_knot_values_are_rejected() {
632 let mut e = bilinear();
633 e.attributes[slot::U_KNOTS] = reals(&[1.0, 0.0]);
634 let err = BSplineSurface::new(EntityId(1), &e).u_knots().unwrap_err();
635 assert!(err.to_string().contains("increasing"), "got: {err}");
636 }
637
638 #[test]
639 fn a_surface_without_knots_reports_none_rather_than_failing() {
640 let e = Entity::new(
641 "IFCBSPLINESURFACE",
642 vec![
643 Value::Integer(1),
644 Value::Integer(1),
645 grid(2, 2),
646 Value::Enum("UNSPECIFIED".into()),
647 Value::Bool(false),
648 Value::Bool(false),
649 Value::Bool(false),
650 ],
651 );
652 let view = BSplineSurface::new(EntityId(1), &e);
653 assert_eq!(view.u_knots().unwrap(), None);
654 assert_eq!(view.v_knots().unwrap(), None);
655 assert_eq!(view.weights().unwrap(), None);
656 }
657
658 #[test]
659 fn rational_weights_form_a_grid_of_the_same_shape_as_the_control_points() {
660 let mut attributes = bilinear().attributes;
661 attributes.push(Value::List(vec![reals(&[1.0, 0.5]), reals(&[0.5, 1.0])]));
662 let e = Entity::new("IFCRATIONALBSPLINESURFACEWITHKNOTS", attributes);
663 let view = BSplineSurface::new(EntityId(1), &e);
664 assert!(view.is_rational());
665 assert_eq!(
666 view.weights().unwrap().unwrap(),
667 vec![vec![1.0, 0.5], vec![0.5, 1.0]]
668 );
669 }
670
671 #[test]
672 fn a_weight_grid_of_the_wrong_shape_is_rejected() {
673 let mut attributes = bilinear().attributes;
674 attributes.push(Value::List(vec![reals(&[1.0, 1.0])]));
675 let e = Entity::new("IFCRATIONALBSPLINESURFACEWITHKNOTS", attributes);
676 let err = BSplineSurface::new(EntityId(1), &e).weights().unwrap_err();
677 assert!(err.to_string().contains("rows"), "got: {err}");
678 }
679
680 #[test]
681 fn a_non_positive_weight_anywhere_in_the_grid_is_degenerate() {
682 for bad in [0.0, -1.0] {
683 let mut attributes = bilinear().attributes;
684 attributes.push(Value::List(vec![reals(&[1.0, 1.0]), reals(&[1.0, bad])]));
685 let e = Entity::new("IFCRATIONALBSPLINESURFACEWITHKNOTS", attributes);
686 let err = BSplineSurface::new(EntityId(1), &e).weights().unwrap_err();
687 assert!(err.to_string().contains("positive"), "weight {bad}: {err}");
688 assert!(err.to_string().contains("[1][1]"), "weight {bad}: {err}");
689 }
690 }
691
692 #[test]
693 fn multiplicity_overflow_is_a_typed_error_not_a_panic() {
694 let mut e = bilinear();
695 e.attributes[slot::U_MULTIPLICITIES] = integers(&[i64::MAX, i64::MAX, i64::MAX]);
696 e.attributes[slot::U_KNOTS] = reals(&[0.0, 0.5, 1.0]);
697 let err = BSplineSurface::new(EntityId(8), &e).u_knots().unwrap_err();
698 assert!(err.to_string().contains("overflow"), "got: {err}");
699 }
700
701 #[test]
702 fn each_degree_must_not_exceed_its_control_point_upper_index() {
703 let mut e = bilinear();
704 e.attributes[slot::U_DEGREE] = Value::Integer(2);
705 assert!(BSplineSurface::new(EntityId(9), &e)
706 .u_degree()
707 .unwrap_err()
708 .to_string()
709 .contains("control points"));
710
711 let mut e = bilinear();
712 e.attributes[slot::V_DEGREE] = Value::Integer(2);
713 assert!(BSplineSurface::new(EntityId(10), &e)
714 .v_degree()
715 .unwrap_err()
716 .to_string()
717 .contains("control points"));
718 }
719
720 #[test]
721 fn rational_subtype_requires_weights_and_polynomial_rejects_them() {
722 let attributes = bilinear().attributes;
723 let rational = Entity::new("IFCRATIONALBSPLINESURFACEWITHKNOTS", attributes.clone());
724 assert!(BSplineSurface::new(EntityId(11), &rational)
725 .weights()
726 .unwrap_err()
727 .to_string()
728 .contains("missing WeightsData"));
729
730 let mut polynomial_attributes = attributes;
731 polynomial_attributes.push(Value::List(vec![reals(&[1.0, 1.0]), reals(&[1.0, 1.0])]));
732 let polynomial = Entity::new("IFCBSPLINESURFACEWITHKNOTS", polynomial_attributes);
733 assert!(BSplineSurface::new(EntityId(12), &polynomial)
734 .weights()
735 .unwrap_err()
736 .to_string()
737 .contains("must not carry WeightsData"));
738 }
739
740 #[test]
741 fn degree_zero_in_either_direction_is_rejected() {
742 let mut e = bilinear();
743 e.attributes[slot::U_DEGREE] = Value::Integer(0);
744 assert!(BSplineSurface::new(EntityId(1), &e).u_degree().is_err());
745
746 let mut e = bilinear();
747 e.attributes[slot::V_DEGREE] = Value::Integer(0);
748 assert!(BSplineSurface::new(EntityId(1), &e).v_degree().is_err());
749 }
750
751 #[test]
753 fn surface_form_tokens_parse_without_replacing_the_control_points() {
754 assert_eq!(
755 BSplineSurfaceForm::from_token("SURF_OF_LINEAR_EXTRUSION"),
756 Some(BSplineSurfaceForm::SurfOfLinearExtrusion)
757 );
758 assert_eq!(
759 BSplineSurfaceForm::from_token("CYLINDRICAL_SURF"),
760 Some(BSplineSurfaceForm::CylindricalSurf)
761 );
762 assert_eq!(BSplineSurfaceForm::from_token("BLOB"), None);
763 }
764
765 #[test]
766 fn closure_flags_are_read_independently_for_u_and_v() {
767 let mut e = bilinear();
768 e.attributes[slot::U_CLOSED] = Value::Bool(true);
769 e.attributes[slot::V_CLOSED] = Value::LogicalUnknown;
770 let view = BSplineSurface::new(EntityId(1), &e);
771 assert_eq!(view.u_closed(), Some(true));
772 assert_eq!(view.v_closed(), None, ".U. must not become false");
773 }
774}