use crate::curve::bspline::{KnotType, KnotVector};
use crate::error::GeometryResult;
use crate::resource::point::CartesianPoint;
use crate::resource::resolve;
use crate::slots::Slots;
use ifc_model::{Entity, EntityId, Model, Value};
pub(crate) mod slot {
pub const U_DEGREE: usize = 0;
pub const V_DEGREE: usize = 1;
pub const CONTROL_POINTS: usize = 2;
pub const SURFACE_FORM: usize = 3;
pub const U_CLOSED: usize = 4;
pub const V_CLOSED: usize = 5;
pub const SELF_INTERSECT: usize = 6;
pub const U_MULTIPLICITIES: usize = 7;
pub const V_MULTIPLICITIES: usize = 8;
pub const U_KNOTS: usize = 9;
pub const V_KNOTS: usize = 10;
pub const KNOT_SPEC: usize = 11;
pub const WEIGHTS_DATA: usize = 12;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BSplineSurfaceForm {
PlaneSurf,
CylindricalSurf,
ConicalSurf,
SphericalSurf,
ToroidalSurf,
SurfOfRevolution,
RuledSurf,
GeneralisedCone,
QuadricSurf,
SurfOfLinearExtrusion,
Unspecified,
}
impl BSplineSurfaceForm {
pub fn from_token(token: &str) -> Option<Self> {
match token.to_ascii_uppercase().as_str() {
"PLANE_SURF" => Some(Self::PlaneSurf),
"CYLINDRICAL_SURF" => Some(Self::CylindricalSurf),
"CONICAL_SURF" => Some(Self::ConicalSurf),
"SPHERICAL_SURF" => Some(Self::SphericalSurf),
"TOROIDAL_SURF" => Some(Self::ToroidalSurf),
"SURF_OF_REVOLUTION" => Some(Self::SurfOfRevolution),
"RULED_SURF" => Some(Self::RuledSurf),
"GENERALISED_CONE" => Some(Self::GeneralisedCone),
"QUADRIC_SURF" => Some(Self::QuadricSurf),
"SURF_OF_LINEAR_EXTRUSION" => Some(Self::SurfOfLinearExtrusion),
"UNSPECIFIED" => Some(Self::Unspecified),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ControlPointGrid {
rows: Vec<Vec<EntityId>>,
}
impl ControlPointGrid {
pub fn u_count(&self) -> usize {
self.rows.len()
}
pub fn v_count(&self) -> usize {
self.rows.first().map_or(0, Vec::len)
}
pub fn get(&self, u_index: usize, v_index: usize) -> Option<EntityId> {
self.rows.get(u_index)?.get(v_index).copied()
}
pub fn rows(&self) -> &[Vec<EntityId>] {
&self.rows
}
}
#[derive(Debug, Clone, Copy)]
pub struct BSplineSurface<'m> {
slots: Slots<'m>,
}
impl<'m> BSplineSurface<'m> {
pub fn new(id: EntityId, entity: &'m Entity) -> Self {
Self {
slots: Slots::new(id, entity),
}
}
pub fn id(&self) -> EntityId {
self.slots.id()
}
pub fn u_degree(&self) -> GeometryResult<usize> {
self.degree(slot::U_DEGREE, "UDegree", self.control_points()?.u_count())
}
pub fn v_degree(&self) -> GeometryResult<usize> {
self.degree(slot::V_DEGREE, "VDegree", self.control_points()?.v_count())
}
pub fn control_points(&self) -> GeometryResult<ControlPointGrid> {
let value = self.slots.req(slot::CONTROL_POINTS, "ControlPointsList")?;
let outer = value.as_list().ok_or_else(|| {
self.slots
.degenerate("ControlPointsList must be a list of lists")
})?;
if outer.len() < 2 {
return Err(self.slots.degenerate(format!(
"ControlPointsList needs at least 2 rows along u, found {}",
outer.len()
)));
}
let mut rows: Vec<Vec<EntityId>> = Vec::with_capacity(outer.len());
for (u_index, row_value) in outer.iter().enumerate() {
let inner = row_value.as_list().ok_or_else(|| {
self.slots
.degenerate(format!("ControlPointsList row {u_index} is not a list"))
})?;
let mut row = Vec::with_capacity(inner.len());
for (v_index, point) in inner.iter().enumerate() {
let id = point.as_ref_id().ok_or_else(|| {
self.slots.degenerate(format!(
"ControlPointsList[{u_index}][{v_index}] is not an entity reference"
))
})?;
row.push(id);
}
rows.push(row);
}
let v_count = rows[0].len();
if v_count < 2 {
return Err(self.slots.degenerate(format!(
"ControlPointsList needs at least 2 columns along v, found {v_count}"
)));
}
for (u_index, row) in rows.iter().enumerate() {
if row.len() != v_count {
return Err(self.slots.degenerate(format!(
"ControlPointsList row {u_index} has {} points but row 0 has {v_count}; \
the grid must be rectangular",
row.len()
)));
}
}
Ok(ControlPointGrid { rows })
}
pub fn control_point_views<'v>(
&self,
model: &'v Model,
) -> GeometryResult<Vec<Vec<CartesianPoint<'v>>>> {
self.control_points()?
.rows()
.iter()
.map(|row| resolve::cartesian_points(model, self.id(), row))
.collect()
}
pub fn surface_form(&self) -> BSplineSurfaceForm {
self.slots
.opt_enum(slot::SURFACE_FORM)
.and_then(BSplineSurfaceForm::from_token)
.unwrap_or(BSplineSurfaceForm::Unspecified)
}
pub fn u_closed(&self) -> Option<bool> {
self.slots.opt_bool(slot::U_CLOSED)
}
pub fn v_closed(&self) -> Option<bool> {
self.slots.opt_bool(slot::V_CLOSED)
}
pub fn self_intersect(&self) -> Option<bool> {
self.slots.opt_bool(slot::SELF_INTERSECT)
}
pub fn knot_spec(&self) -> KnotType {
self.slots
.opt_enum(slot::KNOT_SPEC)
.and_then(KnotType::from_token)
.unwrap_or(KnotType::Unspecified)
}
pub fn has_knots(&self) -> bool {
self.slots.opt(slot::U_KNOTS).is_some()
}
pub fn is_rational(&self) -> bool {
self.slots
.type_name()
.eq_ignore_ascii_case("IFCRATIONALBSPLINESURFACEWITHKNOTS")
}
pub fn u_knots(&self) -> GeometryResult<Option<KnotVector>> {
if !self.has_knots() {
return Ok(None);
}
let expected = self
.control_points()?
.u_count()
.checked_add(self.u_degree()?)
.and_then(|value| value.checked_add(1))
.ok_or_else(|| self.slots.degenerate("u knot count overflows usize"))?;
self.knot_vector(
slot::U_KNOTS,
"UKnots",
slot::U_MULTIPLICITIES,
"UMultiplicities",
expected,
)
.map(Some)
}
pub fn v_knots(&self) -> GeometryResult<Option<KnotVector>> {
if !self.has_knots() {
return Ok(None);
}
let expected = self
.control_points()?
.v_count()
.checked_add(self.v_degree()?)
.and_then(|value| value.checked_add(1))
.ok_or_else(|| self.slots.degenerate("v knot count overflows usize"))?;
self.knot_vector(
slot::V_KNOTS,
"VKnots",
slot::V_MULTIPLICITIES,
"VMultiplicities",
expected,
)
.map(Some)
}
pub fn weights(&self) -> GeometryResult<Option<Vec<Vec<f64>>>> {
let supplied = self.slots.opt(slot::WEIGHTS_DATA).is_some();
match (self.is_rational(), supplied) {
(false, false) => return Ok(None),
(true, false) => {
return Err(self
.slots
.degenerate("rational B-spline surface is missing WeightsData"));
}
(false, true) => {
return Err(self
.slots
.degenerate("polynomial B-spline surface must not carry WeightsData"));
}
(true, true) => {}
}
let grid = self.control_points()?;
let value = self.slots.req(slot::WEIGHTS_DATA, "WeightsData")?;
let outer = value
.as_list()
.ok_or_else(|| self.slots.degenerate("WeightsData must be a list of lists"))?;
if outer.len() != grid.u_count() {
return Err(self.slots.degenerate(format!(
"WeightsData has {} rows but the control point grid has {}",
outer.len(),
grid.u_count()
)));
}
let mut weights = Vec::with_capacity(outer.len());
for (u_index, row_value) in outer.iter().enumerate() {
let inner = row_value.as_list().ok_or_else(|| {
self.slots
.degenerate(format!("WeightsData row {u_index} is not a list"))
})?;
if inner.len() != grid.v_count() {
return Err(self.slots.degenerate(format!(
"WeightsData row {u_index} has {} entries but the grid has {}",
inner.len(),
grid.v_count()
)));
}
let mut row = Vec::with_capacity(inner.len());
for (v_index, w) in inner.iter().enumerate() {
let weight = w.unwrap_typed().as_f64().ok_or_else(|| {
self.slots
.degenerate(format!("WeightsData[{u_index}][{v_index}] is not a number"))
})?;
if !weight.is_finite() {
return Err(self.slots.degenerate(format!(
"weight {weight} at control point [{u_index}][{v_index}] must be finite"
)));
}
if weight <= 0.0 {
return Err(self.slots.degenerate(format!(
"weight {weight} at control point [{u_index}][{v_index}] must be positive"
)));
}
row.push(weight);
}
weights.push(row);
}
Ok(Some(weights))
}
fn degree(
&self,
index: usize,
name: &'static str,
control_count: usize,
) -> GeometryResult<usize> {
let raw = self.slots.req_i64(index, name)?;
if raw < 1 {
return Err(self
.slots
.degenerate(format!("{name} must be at least 1, found {raw}")));
}
let degree = usize::try_from(raw).map_err(|_| {
self.slots
.degenerate(format!("{name} exceeds platform limits"))
})?;
if degree >= control_count {
return Err(self.slots.degenerate(format!(
"{name} {degree} must be smaller than the {control_count} control points"
)));
}
Ok(degree)
}
fn knot_vector(
&self,
knots_index: usize,
knots_name: &'static str,
mult_index: usize,
mult_name: &'static str,
expected: usize,
) -> GeometryResult<KnotVector> {
let values = self.slots.req_f64_list(knots_index, knots_name)?;
let raw = self.slots.req(mult_index, mult_name)?;
let items = raw
.as_list()
.ok_or_else(|| self.slots.degenerate(format!("{mult_name} must be a list")))?;
let mut multiplicities = Vec::with_capacity(items.len());
for item in items {
match item.unwrap_typed() {
Value::Integer(i) if *i >= 1 => {
multiplicities.push(usize::try_from(*i).map_err(|_| {
self.slots
.degenerate(format!("{mult_name} exceeds platform limits"))
})?);
}
other => {
return Err(self.slots.degenerate(format!(
"{mult_name} entry must be a positive integer, found {other:?}"
)));
}
}
}
if values.len() != multiplicities.len() {
return Err(self.slots.degenerate(format!(
"{knots_name} has {} entries but {mult_name} has {}; they are parallel lists",
values.len(),
multiplicities.len()
)));
}
for (index, value) in values.iter().enumerate() {
if !value.is_finite() {
return Err(self.slots.degenerate(format!(
"{knots_name}[{index}] must be finite, found {value}"
)));
}
}
for pair in values.windows(2) {
if pair[1] <= pair[0] {
return Err(self.slots.degenerate(format!(
"{knots_name} must be strictly increasing; found {} after {}",
pair[1], pair[0]
)));
}
}
let total = multiplicities.iter().try_fold(0usize, |total, &value| {
total.checked_add(value).ok_or_else(|| {
self.slots
.degenerate(format!("{mult_name} total overflows usize"))
})
})?;
if total != expected {
return Err(self.slots.degenerate(format!(
"{mult_name} sums to {total} but must equal control points + degree + 1 = {expected}"
)));
}
Ok(KnotVector {
values,
multiplicities,
})
}
}
pub type SurfaceKnotVector = KnotVector;
#[cfg(test)]
mod tests;