use crate::features::FinitePoint3;
use crate::ids::SubdId;
use crate::math::{Point3, Vector3};
use crate::provenance::SourceObjectAssociation;
use crate::scalar::{FiniteReal, NonNegativeReal, PositiveReal};
use crate::units::UnitVector3;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SubdSurface {
pub id: SubdId,
pub scheme: SubdScheme,
pub cage: SubdCage,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_source_object"
)]
pub source_object: Option<SourceObjectAssociation>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SubdError {
#[non_exhaustive]
Admission(String),
EditRefused(String),
}
impl std::fmt::Display for SubdError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Admission(message) | Self::EditRefused(message) => formatter.write_str(message),
}
}
}
impl std::error::Error for SubdError {}
const EPS_SUBD_SYMMETRY_FRAME: f64 = 1.0e-9;
fn require_finite_point(field: &str, point: Point3) -> Result<FinitePoint3, SubdError> {
FinitePoint3::new(point).ok_or_else(|| SubdError::Admission(format!("{field} must be finite")))
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SubdCageWire")]
pub struct SubdCage {
vertices: Vec<SubdVertex>,
edges: Vec<SubdEdge>,
faces: Vec<SubdFace>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
symmetries: Vec<SubdSymmetry>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SubdCageWire {
vertices: Vec<SubdVertex>,
edges: Vec<SubdEdge>,
faces: Vec<SubdFace>,
#[serde(default)]
symmetries: Vec<SubdSymmetry>,
}
impl TryFrom<SubdCageWire> for SubdCage {
type Error = SubdError;
fn try_from(wire: SubdCageWire) -> Result<Self, Self::Error> {
let cage = Self {
vertices: wire.vertices,
edges: wire.edges,
faces: wire.faces,
symmetries: wire.symmetries,
};
cage.validate()?;
Ok(cage)
}
}
impl SubdCage {
pub fn new(
vertices: Vec<SubdVertex>,
edges: Vec<SubdEdge>,
faces: Vec<SubdFace>,
symmetries: Vec<SubdSymmetry>,
) -> Result<Self, SubdError> {
let cage = Self {
vertices,
edges,
faces,
symmetries,
};
cage.validate()?;
Ok(cage)
}
pub fn edit_vertices(
&mut self,
edit: impl FnOnce(&mut [SubdVertex]) -> Result<(), SubdError>,
) -> Result<(), SubdError> {
let mut vertices = self.vertices.clone();
edit(&mut vertices)?;
self.validate_vertices(&vertices)?;
self.vertices = vertices;
Ok(())
}
fn validate(&self) -> Result<(), SubdError> {
for (index, edge) in self.edges.iter().enumerate() {
for vertex in edge.vertices {
if cadmpeg_core::decode::index_from_u32(vertex) >= self.vertices.len() {
return Err(SubdError::Admission(format!(
"edges[{index}].vertices contains an out-of-range index"
)));
}
}
}
for (index, face) in self.faces.iter().enumerate() {
let mut first = None;
let mut previous = None;
let mut closed = true;
for use_ in &face.edges {
let Some(edge) = self
.edges
.get(cadmpeg_core::decode::index_from_u32(use_.edge))
else {
return Err(SubdError::Admission(format!(
"faces[{index}].edges references a missing edge"
)));
};
let endpoints = if use_.reversed {
[edge.vertices[1], edge.vertices[0]]
} else {
edge.vertices
};
if closed {
if let Some(previous) = previous {
closed = previous == endpoints[0];
}
}
if first.is_none() {
first = Some(endpoints[0]);
}
previous = Some(endpoints[1]);
}
if closed {
if let (Some(first), Some(previous)) = (first, previous) {
closed = previous == first;
}
}
if !closed {
return Err(SubdError::Admission(format!(
"faces[{index}].edges is not a directed closed ring"
)));
}
}
self.validate_vertices(&self.vertices)?;
for symmetry in &self.symmetries {
for (field, pairs, count) in [
("face_pairs", &symmetry.face_pairs, self.faces.len()),
("edge_pairs", &symmetry.edge_pairs, self.edges.len()),
("vertex_pairs", &symmetry.vertex_pairs, self.vertices.len()),
] {
for pair in pairs {
for index in pair {
if cadmpeg_core::decode::index_from_u32(*index) >= count {
return Err(SubdError::Admission(format!(
"symmetries.{field} contains an out-of-range index"
)));
}
}
}
}
}
Ok(())
}
fn validate_vertices(&self, vertices: &[SubdVertex]) -> Result<(), SubdError> {
let mut grip_indices = std::collections::BTreeSet::new();
for (index, vertex) in vertices.iter().enumerate() {
let Some(layout) = &vertex.secondary_grips else {
continue;
};
for wedge in &layout.wedges {
let SubdGripWedge::Slot {
edge,
sector_face,
spokes,
sectors,
} = wedge
else {
continue;
};
if let Some(edge) = edge {
let Some(edge) = self.edges.get(cadmpeg_core::decode::index_from_u32(*edge))
else {
return Err(SubdError::Admission(format!(
"vertices[{index}].secondary_grips edge is out of range"
)));
};
let mut incident = false;
for owner in edge.vertices {
if cadmpeg_core::decode::index_from_u32(owner) == index {
incident = true;
break;
}
}
if !incident {
return Err(SubdError::Admission(format!(
"vertices[{index}].secondary_grips edge is not incident to its owner"
)));
}
}
if let Some(face) = sector_face {
let Some(face) = self.faces.get(cadmpeg_core::decode::index_from_u32(*face))
else {
return Err(SubdError::Admission(format!(
"vertices[{index}].secondary_grips sector_face is out of range"
)));
};
let mut incident = false;
for use_ in &face.edges {
for owner in
self.edges[cadmpeg_core::decode::index_from_u32(use_.edge)].vertices
{
if cadmpeg_core::decode::index_from_u32(owner) == index {
incident = true;
break;
}
}
if incident {
break;
}
}
if !incident {
return Err(SubdError::Admission(format!("vertices[{index}].secondary_grips sector_face is not incident to its owner")));
}
}
for grip in spokes.iter().chain(sectors) {
let Some(grip) = grip else {
continue;
};
if !grip_indices.insert(grip.source_index) {
return Err(SubdError::Admission(format!(
"vertices[{index}].secondary_grips repeats a source_index"
)));
}
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SubdPlaneFrameWire")]
pub struct SubdPlaneFrame {
origin: FinitePoint3,
first_axis: UnitVector3,
second_axis: UnitVector3,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SubdPlaneFrameWire {
origin: Point3,
first_axis: Vector3,
second_axis: Vector3,
}
impl TryFrom<SubdPlaneFrameWire> for SubdPlaneFrame {
type Error = SubdError;
fn try_from(wire: SubdPlaneFrameWire) -> Result<Self, Self::Error> {
Self::new(wire.origin, wire.first_axis, wire.second_axis)
}
}
impl SubdPlaneFrame {
pub fn new(
origin: Point3,
first_axis: Vector3,
second_axis: Vector3,
) -> Result<Self, SubdError> {
let origin = require_finite_point("origin", origin)?;
let first_axis = UnitVector3::new(first_axis).ok_or_else(|| {
SubdError::Admission("first_axis must be finite and unit length".into())
})?;
let second_axis = UnitVector3::new(second_axis).ok_or_else(|| {
SubdError::Admission("second_axis must be finite and unit length".into())
})?;
if first_axis.as_raw().dot(*second_axis.as_raw()).abs() > EPS_SUBD_SYMMETRY_FRAME {
return Err(SubdError::Admission(
"first_axis and second_axis must be orthogonal".into(),
));
}
Ok(Self {
origin,
first_axis,
second_axis,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum SubdSymmetryKind {
Correspondence {},
Radial(SubdRadialSymmetry),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SubdRadialSymmetryWire")]
pub struct SubdRadialSymmetry {
segments: std::num::NonZeroU32,
sweep: crate::scalar::FiniteReal,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
radial_maps: Vec<SubdRadialSymmetryMap>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SubdRadialSymmetryWire {
segments: std::num::NonZeroU32,
sweep: f64,
#[serde(default)]
radial_maps: Vec<SubdRadialSymmetryMap>,
}
impl TryFrom<SubdRadialSymmetryWire> for SubdRadialSymmetry {
type Error = SubdError;
fn try_from(wire: SubdRadialSymmetryWire) -> Result<Self, Self::Error> {
Self::new(wire.segments, wire.sweep, wire.radial_maps)
}
}
impl SubdRadialSymmetry {
fn new(
segments: std::num::NonZeroU32,
sweep: f64,
radial_maps: Vec<SubdRadialSymmetryMap>,
) -> Result<Self, SubdError> {
let Some(sweep) = FiniteReal::new(sweep) else {
return Err(SubdError::Admission(
"kind.radial.sweep must be finite".to_owned(),
));
};
Self::from_parts(segments, sweep, radial_maps)
}
fn from_parts(
segments: std::num::NonZeroU32,
sweep: FiniteReal,
radial_maps: Vec<SubdRadialSymmetryMap>,
) -> Result<Self, SubdError> {
let mut selectors = std::collections::BTreeSet::new();
for map in &radial_maps {
if !selectors.insert(map.selector) {
return Err(SubdError::Admission(
"radial_maps repeats a selector".to_owned(),
));
}
let mut sources = std::collections::BTreeSet::new();
for [source, _] in &map.pairs {
if !sources.insert(*source) {
return Err(SubdError::Admission(
"radial_maps.pairs repeats a source".to_owned(),
));
}
}
}
Ok(Self {
segments,
sweep,
radial_maps,
})
}
}
impl SubdSymmetryKind {
pub fn radial(
segments: std::num::NonZeroU32,
sweep: f64,
radial_maps: Vec<SubdRadialSymmetryMap>,
) -> Result<Self, SubdError> {
SubdRadialSymmetry::new(segments, sweep, radial_maps).map(Self::Radial)
}
pub fn radial_from_parts(
segments: std::num::NonZeroU32,
sweep: FiniteReal,
radial_maps: Vec<SubdRadialSymmetryMap>,
) -> Result<Self, SubdError> {
SubdRadialSymmetry::from_parts(segments, sweep, radial_maps).map(Self::Radial)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SubdRadialMapSelector {
Ef,
Er,
Ff,
Fr,
Vf,
Vr,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SubdRadialSymmetryMap {
pub selector: SubdRadialMapSelector,
pub pairs: Vec<[u64; 2]>,
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SubdSymmetryWire", into = "SubdSymmetryWire")]
pub struct SubdSymmetry {
kind: SubdSymmetryKind,
pub plane: SubdPlaneFrame,
face_pairs: Vec<[u32; 2]>,
edge_pairs: Vec<[u32; 2]>,
vertex_pairs: Vec<[u32; 2]>,
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(rename = "SubdSymmetry"))]
#[serde(deny_unknown_fields)]
struct SubdSymmetryWire {
kind: SubdSymmetryKind,
plane: SubdPlaneFrame,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
face_pairs: Vec<[u32; 2]>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
edge_pairs: Vec<[u32; 2]>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
vertex_pairs: Vec<[u32; 2]>,
}
impl SubdSymmetry {
pub fn new(
kind: SubdSymmetryKind,
plane: SubdPlaneFrame,
face_pairs: Vec<[u32; 2]>,
edge_pairs: Vec<[u32; 2]>,
vertex_pairs: Vec<[u32; 2]>,
) -> Result<Self, SubdError> {
for (field, pairs) in [
("face_pairs", &face_pairs),
("edge_pairs", &edge_pairs),
("vertex_pairs", &vertex_pairs),
] {
let mut sources = std::collections::BTreeSet::new();
let mut targets = std::collections::BTreeSet::new();
for [source, target] in pairs {
if !sources.insert(*source) || !targets.insert(*target) {
return Err(SubdError::Admission(format!(
"{field} repeats a source or target"
)));
}
}
}
Ok(Self {
kind,
plane,
face_pairs,
edge_pairs,
vertex_pairs,
})
}
}
impl TryFrom<SubdSymmetryWire> for SubdSymmetry {
type Error = SubdError;
fn try_from(wire: SubdSymmetryWire) -> Result<Self, Self::Error> {
Self::new(
wire.kind,
wire.plane,
wire.face_pairs,
wire.edge_pairs,
wire.vertex_pairs,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SubdScheme {
CatmullClark,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SubdVertexWire")]
pub struct SubdVertex {
point: FinitePoint3,
pub tag: SubdVertexTag,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_secondary_grips"
)]
secondary_grips: Option<SubdVertexGripLayout>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SubdVertexWire {
point: Point3,
tag: SubdVertexTag,
#[serde(default, deserialize_with = "deserialize_secondary_grips")]
secondary_grips: Option<SubdVertexGripLayout>,
}
impl TryFrom<SubdVertexWire> for SubdVertex {
type Error = SubdError;
fn try_from(wire: SubdVertexWire) -> Result<Self, Self::Error> {
Self::new(wire.point, wire.tag, wire.secondary_grips)
}
}
impl SubdVertex {
pub fn new(
point: Point3,
tag: SubdVertexTag,
secondary_grips: Option<SubdVertexGripLayout>,
) -> Result<Self, SubdError> {
let point = require_finite_point("point", point)?;
Ok(Self::from_parts(point, tag, secondary_grips))
}
#[must_use]
pub fn from_parts(
point: FinitePoint3,
tag: SubdVertexTag,
secondary_grips: Option<SubdVertexGripLayout>,
) -> Self {
Self {
point,
tag,
secondary_grips,
}
}
pub const fn point(&self) -> FinitePoint3 {
self.point
}
pub fn set_point(&mut self, point: FinitePoint3) {
self.point = point;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SubdGripDirection {
North,
East,
South,
West,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SubdVertexGripLayoutWire")]
pub struct SubdVertexGripLayout {
direction: SubdGripDirection,
wedges: Vec<SubdGripWedge>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SubdVertexGripLayoutWire {
direction: SubdGripDirection,
wedges: Vec<SubdGripWedge>,
}
impl TryFrom<SubdVertexGripLayoutWire> for SubdVertexGripLayout {
type Error = SubdError;
fn try_from(wire: SubdVertexGripLayoutWire) -> Result<Self, Self::Error> {
Self::new(wire.direction, wire.wedges)
}
}
impl SubdVertexGripLayout {
pub fn new(
direction: SubdGripDirection,
wedges: Vec<SubdGripWedge>,
) -> Result<Self, SubdError> {
if wedges.is_empty() {
return Err(SubdError::Admission(
"secondary_grips.wedges is empty".to_owned(),
));
}
let spoke_count = |wedge: &SubdGripWedge| match wedge {
SubdGripWedge::Phantom {} => 0,
SubdGripWedge::Slot { spokes, .. } => spokes.len(),
};
for (index, (wedge, next)) in wedges.iter().zip(wedges.iter().cycle().skip(1)).enumerate() {
let sector_count = match wedge {
SubdGripWedge::Phantom {} => 0,
SubdGripWedge::Slot { sectors, .. } => sectors.len(),
};
if spoke_count(wedge).checked_mul(spoke_count(next)) != Some(sector_count) {
return Err(SubdError::Admission(format!(
"secondary_grips.wedges[{index}].sectors has invalid arity"
)));
}
}
Ok(Self { direction, wedges })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum SubdGripWedge {
Phantom {},
Slot {
#[serde(deserialize_with = "cadmpeg_core::absent_key::nullable")]
edge: Option<u32>,
#[serde(deserialize_with = "cadmpeg_core::absent_key::nullable")]
sector_face: Option<u32>,
spokes: Vec<Option<SubdSecondaryGrip>>,
sectors: Vec<Option<SubdSecondaryGrip>>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SubdSecondaryGripWire")]
pub struct SubdSecondaryGrip {
source_index: u32,
point: FinitePoint3,
weight: PositiveReal,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SubdSecondaryGripWire {
source_index: u32,
point: Point3,
weight: f64,
}
impl TryFrom<SubdSecondaryGripWire> for SubdSecondaryGrip {
type Error = SubdError;
fn try_from(wire: SubdSecondaryGripWire) -> Result<Self, Self::Error> {
Self::new(wire.source_index, wire.point, wire.weight)
}
}
impl SubdSecondaryGrip {
pub fn new(source_index: u32, point: Point3, weight: f64) -> Result<Self, SubdError> {
let point = require_finite_point("point", point)?;
let weight = PositiveReal::new(weight)
.ok_or_else(|| SubdError::Admission("weight must be finite and positive".into()))?;
Ok(Self {
source_index,
point,
weight,
})
}
pub fn from_parts(
source_index: u32,
point: Point3,
weight: PositiveReal,
) -> Result<Self, SubdError> {
let point = require_finite_point("point", point)?;
Ok(Self {
source_index,
point,
weight,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SubdVertexTag {
Smooth,
Crease,
Corner,
Dart,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SubdEdgeWire")]
pub struct SubdEdge {
vertices: [u32; 2],
sharpness: [NonNegativeReal; 2],
pub tag: SubdEdgeTag,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_knot_interval"
)]
knot_interval: Option<PositiveReal>,
sector_coefficients: [FiniteReal; 2],
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SubdEdgeWire {
vertices: [u32; 2],
sharpness: [f64; 2],
tag: SubdEdgeTag,
#[serde(default, deserialize_with = "deserialize_knot_interval")]
knot_interval: Option<f64>,
sector_coefficients: [f64; 2],
}
impl TryFrom<SubdEdgeWire> for SubdEdge {
type Error = SubdError;
fn try_from(wire: SubdEdgeWire) -> Result<Self, Self::Error> {
Self::new(
wire.vertices,
wire.sharpness,
wire.tag,
wire.knot_interval,
wire.sector_coefficients,
)
}
}
impl SubdEdge {
pub fn from_controls(
vertices: [u32; 2],
sharpness: [NonNegativeReal; 2],
tag: SubdEdgeTag,
knot_interval: Option<PositiveReal>,
sector_coefficients: [FiniteReal; 2],
) -> Result<Self, SubdError> {
if vertices[0] == vertices[1] {
return Err(SubdError::Admission(
"vertices must name distinct endpoints".to_owned(),
));
}
Ok(Self {
vertices,
sharpness,
tag,
knot_interval,
sector_coefficients,
})
}
pub fn new(
vertices: [u32; 2],
sharpness: [f64; 2],
tag: SubdEdgeTag,
knot_interval: Option<f64>,
sector_coefficients: [f64; 2],
) -> Result<Self, SubdError> {
Self::admit_raw_controls(
vertices,
sharpness,
tag,
|| {
knot_interval
.map(|value| {
PositiveReal::new(value).ok_or_else(|| {
SubdError::Admission(
"knot_interval must be finite and positive".to_owned(),
)
})
})
.transpose()
},
sector_coefficients,
)
}
pub fn from_parts(
vertices: [u32; 2],
sharpness: [f64; 2],
tag: SubdEdgeTag,
knot_interval: Option<PositiveReal>,
sector_coefficients: [f64; 2],
) -> Result<Self, SubdError> {
Self::admit_raw_controls(
vertices,
sharpness,
tag,
|| Ok(knot_interval),
sector_coefficients,
)
}
fn admit_raw_controls(
vertices: [u32; 2],
sharpness: [f64; 2],
tag: SubdEdgeTag,
interval: impl FnOnce() -> Result<Option<PositiveReal>, SubdError>,
sector_coefficients: [f64; 2],
) -> Result<Self, SubdError> {
let error = |message: &str| SubdError::Admission(message.to_owned());
if vertices[0] == vertices[1] {
return Err(error("vertices must name distinct endpoints"));
}
let [start, end] = sharpness.map(NonNegativeReal::new);
let (Some(start), Some(end)) = (start, end) else {
return Err(error("sharpness must be finite and non-negative"));
};
let knot_interval = interval()?;
let [first, second] = sector_coefficients.map(FiniteReal::new);
let (Some(first), Some(second)) = (first, second) else {
return Err(error("sector_coefficients must be finite"));
};
Ok(Self {
vertices,
sharpness: [start, end],
tag,
knot_interval,
sector_coefficients: [first, second],
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SubdEdgeTag {
Smooth,
SmoothX,
Crease,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SubdFaceWire")]
pub struct SubdFace {
edges: Vec<SubdEdgeUse>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SubdFaceWire {
edges: Vec<SubdEdgeUse>,
}
impl TryFrom<SubdFaceWire> for SubdFace {
type Error = SubdError;
fn try_from(wire: SubdFaceWire) -> Result<Self, Self::Error> {
Self::new(wire.edges)
}
}
impl SubdFace {
pub fn new(edges: Vec<SubdEdgeUse>) -> Result<Self, SubdError> {
if edges.len() < 3 {
return Err(SubdError::Admission(
"edges must contain at least three uses".into(),
));
}
Ok(Self { edges })
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SubdEdgeUse {
pub edge: u32,
pub reversed: bool,
}
#[cfg(test)]
mod tests;
cadmpeg_core::named_optional_field!(
deserialize_source_object,
SourceObjectAssociation,
"source_object"
);
cadmpeg_core::named_optional_field!(
deserialize_secondary_grips,
SubdVertexGripLayout,
"secondary_grips"
);
cadmpeg_core::named_optional_field!(deserialize_knot_interval, f64, "knot_interval");
mod identity_rewrite;
mod serialization;