use super::selection_overlap::face_selections_overlap;
use super::{EdgeSelection, FaceSelection};
use crate::scalar::{
FiniteReal, Fraction, InteriorAngle, Length, NonNegativeLength, PositiveLength,
};
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[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 RadiusForm {
Constant,
Chordal,
Asymmetric,
Variable,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum RadiusSpec {
Unresolved {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_form"
)]
form: Option<RadiusForm>,
},
Constant {
radius: PositiveLength,
},
Chordal {
chord_length: PositiveLength,
},
Asymmetric {
offset_one: PositiveLength,
offset_two: PositiveLength,
},
Variable {
points: VariableRadii,
},
}
impl RadiusSpec {
pub const fn is_unresolved(&self) -> bool {
matches!(self, Self::Unresolved { .. })
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct FilletGroup {
pub edges: EdgeSelection,
pub radius: RadiusSpec,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_tangency_weight"
)]
pub tangency_weight: Option<FiniteReal>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
pub struct FullRoundFilletGroup {
#[serde(rename = "center_faces")]
center: FaceSelection,
#[serde(rename = "side_one_faces")]
side_one: FullRoundSideSelection,
#[serde(rename = "side_two_faces")]
side_two: FullRoundSideSelection,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct FullRoundFilletGroupWire {
#[serde(rename = "center_faces")]
center: FaceSelection,
#[serde(rename = "side_one_faces")]
side_one: FullRoundSideSelection,
#[serde(rename = "side_two_faces")]
side_two: FullRoundSideSelection,
}
impl FullRoundFilletGroup {
pub fn new(
center_faces: FaceSelection,
side_one_faces: FullRoundSideSelection,
side_two_faces: FullRoundSideSelection,
) -> Result<Self, &'static str> {
fn explicit(side: &FullRoundSideSelection) -> Option<&FaceSelection> {
match side {
FullRoundSideSelection::Explicit(faces) => Some(faces),
FullRoundSideSelection::Automatic | FullRoundSideSelection::Unresolved => None,
}
}
let first = explicit(&side_one_faces);
let second = explicit(&side_two_faces);
for (first, second) in [
Some(¢er_faces).zip(first),
Some(¢er_faces).zip(second),
first.zip(second),
]
.into_iter()
.flatten()
{
if face_selections_overlap(first, second) {
return Err(
"center_faces, side_one_faces and side_two_faces must be pairwise disjoint",
);
}
}
Ok(Self {
center: center_faces,
side_one: side_one_faces,
side_two: side_two_faces,
})
}
pub fn center_faces(&self) -> &FaceSelection {
&self.center
}
pub fn side_one_faces(&self) -> &FullRoundSideSelection {
&self.side_one
}
pub fn side_two_faces(&self) -> &FullRoundSideSelection {
&self.side_two
}
}
impl<'de> Deserialize<'de> for FullRoundFilletGroup {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let wire = FullRoundFilletGroupWire::deserialize(deserializer)?;
Self::new(wire.center, wire.side_one, wire.side_two).map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum FullRoundSideSelection {
Automatic,
Explicit(FaceSelection),
Unresolved,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ChamferGroup {
pub edges: EdgeSelection,
pub spec: ChamferSpec,
}
const INVALID_VARIABLE_RADII: &str = "variable radius points require at least two ordered parameters in [0, 1] and nonnegative radii with one positive radius";
#[derive(Debug, Clone, PartialEq)]
pub enum VariableRadiiMapError<E> {
Radius(E),
Admission(&'static str),
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(into = "Vec<VariableRadius>"))]
#[serde(try_from = "Vec<VariableRadius>")]
pub struct VariableRadii(Vec<VariableRadius<Fraction, NonNegativeLength>>);
impl VariableRadii {
pub fn new(points: Vec<VariableRadius>) -> Result<Self, &'static str> {
let points = points
.into_iter()
.map(|point| {
let parameter = Fraction::new(point.parameter).ok_or(INVALID_VARIABLE_RADII)?;
let radius = NonNegativeLength::try_from(point.radius)
.map_err(|_| INVALID_VARIABLE_RADII)?;
Ok(VariableRadius { parameter, radius })
})
.collect::<Result<Vec<_>, &'static str>>()?;
Self::from_parts(points)
}
pub fn from_parts(
points: Vec<VariableRadius<Fraction, NonNegativeLength>>,
) -> Result<Self, &'static str> {
if points.len() < 2
|| !points.iter().any(|point| point.radius.get() > 0.0)
|| points
.windows(2)
.any(|pair| pair[0].parameter >= pair[1].parameter)
{
return Err(INVALID_VARIABLE_RADII);
}
Ok(Self(points))
}
pub fn as_slice(&self) -> &[VariableRadius<Fraction, NonNegativeLength>] {
&self.0
}
pub fn try_map_radii<E>(
mut self,
mut map: impl FnMut(NonNegativeLength) -> Result<NonNegativeLength, E>,
) -> Result<Self, VariableRadiiMapError<E>> {
let mut positive = false;
for point in &mut self.0 {
point.radius = map(point.radius).map_err(VariableRadiiMapError::Radius)?;
positive |= point.radius.get() > 0.0;
}
if positive {
Ok(self)
} else {
Err(VariableRadiiMapError::Admission(INVALID_VARIABLE_RADII))
}
}
}
impl TryFrom<Vec<VariableRadius>> for VariableRadii {
type Error = &'static str;
fn try_from(points: Vec<VariableRadius>) -> Result<Self, Self::Error> {
Self::new(points)
}
}
impl Serialize for VariableRadii {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.0.serialize(serializer)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct VariableRadius<P = f64, L = Length> {
pub parameter: P,
pub radius: L,
}
impl VariableRadius<Fraction, NonNegativeLength> {
#[must_use]
pub fn to_raw(&self) -> VariableRadius {
VariableRadius {
parameter: self.parameter.get(),
radius: Length::from(self.radius),
}
}
}
#[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 ChamferForm {
Distance,
TwoDistances,
DistanceAngle,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum ChamferSpec {
Unresolved {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_chamfer_spec_form"
)]
form: Option<ChamferForm>,
},
Distance {
distance: PositiveLength,
},
TwoDistances {
first: PositiveLength,
second: PositiveLength,
},
DistanceAngle {
distance: PositiveLength,
angle: InteriorAngle,
},
}
impl ChamferSpec {
pub const fn is_unresolved(self) -> bool {
matches!(self, Self::Unresolved { .. })
}
}
cadmpeg_core::named_optional_field!(deserialize_form, RadiusForm, "form");
cadmpeg_core::named_optional_field!(deserialize_tangency_weight, FiniteReal, "tangency_weight");
cadmpeg_core::named_optional_field!(deserialize_chamfer_spec_form, ChamferForm, "form");
mod decode_clone;
#[cfg(test)]
mod tests;
mod identity_rewrite;