1use crate::curve::bspline::{KnotType, KnotVector};
29use crate::error::GeometryResult;
30use crate::resource::point::CartesianPoint;
31use crate::resource::resolve;
32use crate::slots::Slots;
33use ifc_model::{Entity, EntityId, Model, Value};
34
35pub(crate) mod slot {
41 pub const U_DEGREE: usize = 0;
43 pub const V_DEGREE: usize = 1;
45 pub const CONTROL_POINTS: usize = 2;
47 pub const SURFACE_FORM: usize = 3;
49 pub const U_CLOSED: usize = 4;
51 pub const V_CLOSED: usize = 5;
53 pub const SELF_INTERSECT: usize = 6;
55 pub const U_MULTIPLICITIES: usize = 7;
57 pub const V_MULTIPLICITIES: usize = 8;
59 pub const U_KNOTS: usize = 9;
61 pub const V_KNOTS: usize = 10;
63 pub const KNOT_SPEC: usize = 11;
65 pub const WEIGHTS_DATA: usize = 12;
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
76pub enum BSplineSurfaceForm {
77 PlaneSurf,
79 CylindricalSurf,
81 ConicalSurf,
83 SphericalSurf,
85 ToroidalSurf,
87 SurfOfRevolution,
89 RuledSurf,
91 GeneralisedCone,
93 QuadricSurf,
95 SurfOfLinearExtrusion,
97 Unspecified,
99}
100
101impl BSplineSurfaceForm {
102 pub fn from_token(token: &str) -> Option<Self> {
104 match token.to_ascii_uppercase().as_str() {
105 "PLANE_SURF" => Some(Self::PlaneSurf),
106 "CYLINDRICAL_SURF" => Some(Self::CylindricalSurf),
107 "CONICAL_SURF" => Some(Self::ConicalSurf),
108 "SPHERICAL_SURF" => Some(Self::SphericalSurf),
109 "TOROIDAL_SURF" => Some(Self::ToroidalSurf),
110 "SURF_OF_REVOLUTION" => Some(Self::SurfOfRevolution),
111 "RULED_SURF" => Some(Self::RuledSurf),
112 "GENERALISED_CONE" => Some(Self::GeneralisedCone),
113 "QUADRIC_SURF" => Some(Self::QuadricSurf),
114 "SURF_OF_LINEAR_EXTRUSION" => Some(Self::SurfOfLinearExtrusion),
115 "UNSPECIFIED" => Some(Self::Unspecified),
116 _ => None,
117 }
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct ControlPointGrid {
127 rows: Vec<Vec<EntityId>>,
128}
129
130impl ControlPointGrid {
131 pub fn u_count(&self) -> usize {
133 self.rows.len()
134 }
135
136 pub fn v_count(&self) -> usize {
138 self.rows.first().map_or(0, Vec::len)
139 }
140
141 pub fn get(&self, u_index: usize, v_index: usize) -> Option<EntityId> {
143 self.rows.get(u_index)?.get(v_index).copied()
144 }
145
146 pub fn rows(&self) -> &[Vec<EntityId>] {
148 &self.rows
149 }
150}
151
152#[derive(Debug, Clone, Copy)]
154pub struct BSplineSurface<'m> {
155 slots: Slots<'m>,
156}
157
158impl<'m> BSplineSurface<'m> {
159 pub fn new(id: EntityId, entity: &'m Entity) -> Self {
161 Self {
162 slots: Slots::new(id, entity),
163 }
164 }
165
166 pub fn id(&self) -> EntityId {
168 self.slots.id()
169 }
170
171 pub fn u_degree(&self) -> GeometryResult<usize> {
173 self.degree(slot::U_DEGREE, "UDegree", self.control_points()?.u_count())
174 }
175
176 pub fn v_degree(&self) -> GeometryResult<usize> {
178 self.degree(slot::V_DEGREE, "VDegree", self.control_points()?.v_count())
179 }
180
181 pub fn control_points(&self) -> GeometryResult<ControlPointGrid> {
187 let value = self.slots.req(slot::CONTROL_POINTS, "ControlPointsList")?;
188 let outer = value.as_list().ok_or_else(|| {
189 self.slots
190 .degenerate("ControlPointsList must be a list of lists")
191 })?;
192 if outer.len() < 2 {
193 return Err(self.slots.degenerate(format!(
194 "ControlPointsList needs at least 2 rows along u, found {}",
195 outer.len()
196 )));
197 }
198
199 let mut rows: Vec<Vec<EntityId>> = Vec::with_capacity(outer.len());
200 for (u_index, row_value) in outer.iter().enumerate() {
201 let inner = row_value.as_list().ok_or_else(|| {
202 self.slots
203 .degenerate(format!("ControlPointsList row {u_index} is not a list"))
204 })?;
205 let mut row = Vec::with_capacity(inner.len());
206 for (v_index, point) in inner.iter().enumerate() {
207 let id = point.as_ref_id().ok_or_else(|| {
208 self.slots.degenerate(format!(
209 "ControlPointsList[{u_index}][{v_index}] is not an entity reference"
210 ))
211 })?;
212 row.push(id);
213 }
214 rows.push(row);
215 }
216
217 let v_count = rows[0].len();
218 if v_count < 2 {
219 return Err(self.slots.degenerate(format!(
220 "ControlPointsList needs at least 2 columns along v, found {v_count}"
221 )));
222 }
223 for (u_index, row) in rows.iter().enumerate() {
224 if row.len() != v_count {
225 return Err(self.slots.degenerate(format!(
226 "ControlPointsList row {u_index} has {} points but row 0 has {v_count}; \
227 the grid must be rectangular",
228 row.len()
229 )));
230 }
231 }
232 Ok(ControlPointGrid { rows })
233 }
234
235 pub fn control_point_views<'v>(
241 &self,
242 model: &'v Model,
243 ) -> GeometryResult<Vec<Vec<CartesianPoint<'v>>>> {
244 self.control_points()?
245 .rows()
246 .iter()
247 .map(|row| resolve::cartesian_points(model, self.id(), row))
248 .collect()
249 }
250
251 pub fn surface_form(&self) -> BSplineSurfaceForm {
253 self.slots
254 .opt_enum(slot::SURFACE_FORM)
255 .and_then(BSplineSurfaceForm::from_token)
256 .unwrap_or(BSplineSurfaceForm::Unspecified)
257 }
258
259 pub fn u_closed(&self) -> Option<bool> {
261 self.slots.opt_bool(slot::U_CLOSED)
262 }
263
264 pub fn v_closed(&self) -> Option<bool> {
266 self.slots.opt_bool(slot::V_CLOSED)
267 }
268
269 pub fn self_intersect(&self) -> Option<bool> {
271 self.slots.opt_bool(slot::SELF_INTERSECT)
272 }
273
274 pub fn knot_spec(&self) -> KnotType {
276 self.slots
277 .opt_enum(slot::KNOT_SPEC)
278 .and_then(KnotType::from_token)
279 .unwrap_or(KnotType::Unspecified)
280 }
281
282 pub fn has_knots(&self) -> bool {
284 self.slots.opt(slot::U_KNOTS).is_some()
285 }
286
287 pub fn is_rational(&self) -> bool {
289 self.slots
290 .type_name()
291 .eq_ignore_ascii_case("IFCRATIONALBSPLINESURFACEWITHKNOTS")
292 }
293
294 pub fn u_knots(&self) -> GeometryResult<Option<KnotVector>> {
298 if !self.has_knots() {
299 return Ok(None);
300 }
301 let expected = self
302 .control_points()?
303 .u_count()
304 .checked_add(self.u_degree()?)
305 .and_then(|value| value.checked_add(1))
306 .ok_or_else(|| self.slots.degenerate("u knot count overflows usize"))?;
307 self.knot_vector(
308 slot::U_KNOTS,
309 "UKnots",
310 slot::U_MULTIPLICITIES,
311 "UMultiplicities",
312 expected,
313 )
314 .map(Some)
315 }
316
317 pub fn v_knots(&self) -> GeometryResult<Option<KnotVector>> {
321 if !self.has_knots() {
322 return Ok(None);
323 }
324 let expected = self
325 .control_points()?
326 .v_count()
327 .checked_add(self.v_degree()?)
328 .and_then(|value| value.checked_add(1))
329 .ok_or_else(|| self.slots.degenerate("v knot count overflows usize"))?;
330 self.knot_vector(
331 slot::V_KNOTS,
332 "VKnots",
333 slot::V_MULTIPLICITIES,
334 "VMultiplicities",
335 expected,
336 )
337 .map(Some)
338 }
339
340 pub fn weights(&self) -> GeometryResult<Option<Vec<Vec<f64>>>> {
346 let supplied = self.slots.opt(slot::WEIGHTS_DATA).is_some();
347 match (self.is_rational(), supplied) {
348 (false, false) => return Ok(None),
349 (true, false) => {
350 return Err(self
351 .slots
352 .degenerate("rational B-spline surface is missing WeightsData"));
353 }
354 (false, true) => {
355 return Err(self
356 .slots
357 .degenerate("polynomial B-spline surface must not carry WeightsData"));
358 }
359 (true, true) => {}
360 }
361 let grid = self.control_points()?;
362 let value = self.slots.req(slot::WEIGHTS_DATA, "WeightsData")?;
363 let outer = value
364 .as_list()
365 .ok_or_else(|| self.slots.degenerate("WeightsData must be a list of lists"))?;
366
367 if outer.len() != grid.u_count() {
368 return Err(self.slots.degenerate(format!(
369 "WeightsData has {} rows but the control point grid has {}",
370 outer.len(),
371 grid.u_count()
372 )));
373 }
374
375 let mut weights = Vec::with_capacity(outer.len());
376 for (u_index, row_value) in outer.iter().enumerate() {
377 let inner = row_value.as_list().ok_or_else(|| {
378 self.slots
379 .degenerate(format!("WeightsData row {u_index} is not a list"))
380 })?;
381 if inner.len() != grid.v_count() {
382 return Err(self.slots.degenerate(format!(
383 "WeightsData row {u_index} has {} entries but the grid has {}",
384 inner.len(),
385 grid.v_count()
386 )));
387 }
388 let mut row = Vec::with_capacity(inner.len());
389 for (v_index, w) in inner.iter().enumerate() {
390 let weight = w.unwrap_typed().as_f64().ok_or_else(|| {
391 self.slots
392 .degenerate(format!("WeightsData[{u_index}][{v_index}] is not a number"))
393 })?;
394 if !weight.is_finite() {
395 return Err(self.slots.degenerate(format!(
396 "weight {weight} at control point [{u_index}][{v_index}] must be finite"
397 )));
398 }
399 if weight <= 0.0 {
400 return Err(self.slots.degenerate(format!(
401 "weight {weight} at control point [{u_index}][{v_index}] must be positive"
402 )));
403 }
404 row.push(weight);
405 }
406 weights.push(row);
407 }
408 Ok(Some(weights))
409 }
410
411 fn degree(
412 &self,
413 index: usize,
414 name: &'static str,
415 control_count: usize,
416 ) -> GeometryResult<usize> {
417 let raw = self.slots.req_i64(index, name)?;
418 if raw < 1 {
419 return Err(self
420 .slots
421 .degenerate(format!("{name} must be at least 1, found {raw}")));
422 }
423 let degree = usize::try_from(raw).map_err(|_| {
424 self.slots
425 .degenerate(format!("{name} exceeds platform limits"))
426 })?;
427 if degree >= control_count {
428 return Err(self.slots.degenerate(format!(
429 "{name} {degree} must be smaller than the {control_count} control points"
430 )));
431 }
432 Ok(degree)
433 }
434
435 fn knot_vector(
437 &self,
438 knots_index: usize,
439 knots_name: &'static str,
440 mult_index: usize,
441 mult_name: &'static str,
442 expected: usize,
443 ) -> GeometryResult<KnotVector> {
444 let values = self.slots.req_f64_list(knots_index, knots_name)?;
445 let raw = self.slots.req(mult_index, mult_name)?;
446 let items = raw
447 .as_list()
448 .ok_or_else(|| self.slots.degenerate(format!("{mult_name} must be a list")))?;
449
450 let mut multiplicities = Vec::with_capacity(items.len());
451 for item in items {
452 match item.unwrap_typed() {
453 Value::Integer(i) if *i >= 1 => {
454 multiplicities.push(usize::try_from(*i).map_err(|_| {
455 self.slots
456 .degenerate(format!("{mult_name} exceeds platform limits"))
457 })?);
458 }
459 other => {
460 return Err(self.slots.degenerate(format!(
461 "{mult_name} entry must be a positive integer, found {other:?}"
462 )));
463 }
464 }
465 }
466
467 if values.len() != multiplicities.len() {
468 return Err(self.slots.degenerate(format!(
469 "{knots_name} has {} entries but {mult_name} has {}; they are parallel lists",
470 values.len(),
471 multiplicities.len()
472 )));
473 }
474 for (index, value) in values.iter().enumerate() {
475 if !value.is_finite() {
476 return Err(self.slots.degenerate(format!(
477 "{knots_name}[{index}] must be finite, found {value}"
478 )));
479 }
480 }
481 for pair in values.windows(2) {
482 if pair[1] <= pair[0] {
483 return Err(self.slots.degenerate(format!(
484 "{knots_name} must be strictly increasing; found {} after {}",
485 pair[1], pair[0]
486 )));
487 }
488 }
489 let total = multiplicities.iter().try_fold(0usize, |total, &value| {
490 total.checked_add(value).ok_or_else(|| {
491 self.slots
492 .degenerate(format!("{mult_name} total overflows usize"))
493 })
494 })?;
495 if total != expected {
496 return Err(self.slots.degenerate(format!(
497 "{mult_name} sums to {total} but must equal control points + degree + 1 = {expected}"
498 )));
499 }
500 Ok(KnotVector {
501 values,
502 multiplicities,
503 })
504 }
505}
506
507pub type SurfaceKnotVector = KnotVector;
513
514#[cfg(test)]
515mod tests;