mod admission;
use admission::{RadiusAdmission, StandardAdmission};
use super::selection_overlap::{face_selections_overlap, standard_result, OverlapAdmission};
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 fn try_clone_for_decode(
&self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
operation: &'static str,
) -> Result<Self, cadmpeg_core::CodecError> {
super::decode_clone::CloneForDecode::try_clone_for_decode(self, ctx, operation)
}
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,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
) -> Result<Result<Self, &'static str>, cadmpeg_core::decode::ResourceLimit> {
Self::build(ctx, center_faces, side_one_faces, side_two_faces)
}
fn build<S: OverlapAdmission>(
admission: &S,
center_faces: FaceSelection,
side_one_faces: FullRoundSideSelection,
side_two_faces: FullRoundSideSelection,
) -> Result<Result<Self, &'static str>, S::Error> {
fn explicit(side: &FullRoundSideSelection) -> Option<&FaceSelection> {
match side {
FullRoundSideSelection::Explicit(faces) => Some(faces),
FullRoundSideSelection::Automatic | FullRoundSideSelection::Unresolved => None,
}
}
admission.work(0)?;
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(admission, first, second)? {
return Ok(Err(
"center_faces, side_one_faces and side_two_faces must be pairwise disjoint",
));
}
}
Ok(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)?;
standard_result(Self::build(
&super::selection_overlap::StandardAdmission,
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>,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
) -> Result<Result<Self, &'static str>, cadmpeg_core::CodecError> {
admission::finish(Self::build(ctx, points))
}
fn build<A: RadiusAdmission>(
admission: &A,
points: Vec<VariableRadius>,
) -> Result<Self, A::Error> {
let points = admission.collect(points, |point| {
let parameter = Fraction::new(point.parameter).ok_or_else(|| admission.invalid())?;
let radius =
NonNegativeLength::try_from(point.radius).map_err(|_| admission.invalid())?;
Ok(VariableRadius { parameter, radius })
})?;
Self::build_parts(admission, points)
}
pub fn from_parts(
points: Vec<VariableRadius<Fraction, NonNegativeLength>>,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
) -> Result<Result<Self, &'static str>, cadmpeg_core::CodecError> {
admission::finish(Self::build_parts(ctx, points))
}
fn build_parts<A: RadiusAdmission>(
admission: &A,
points: Vec<VariableRadius<Fraction, NonNegativeLength>>,
) -> Result<Self, A::Error> {
if points.len() < 2 {
return Err(admission.invalid());
}
let mut positive = false;
for point in &points {
admission.work("IR variable radius positivity")?;
if point.radius.get() > 0.0 {
positive = true;
break;
}
}
if !positive {
return Err(admission.invalid());
}
for pair in points.windows(2) {
admission.work("IR variable radius parameter comparison")?;
if pair[0].parameter >= pair[1].parameter {
return Err(admission.invalid());
}
}
Ok(Self(points))
}
pub fn as_slice(&self) -> &[VariableRadius<Fraction, NonNegativeLength>] {
&self.0
}
pub fn try_map_radii<E>(
mut self,
ctx: &cadmpeg_core::decode::DecodeContext<'_>,
mut map: impl FnMut(NonNegativeLength) -> Result<NonNegativeLength, E>,
) -> Result<Result<Self, VariableRadiiMapError<E>>, cadmpeg_core::CodecError> {
let mut positive = false;
for point in &mut self.0 {
ctx.charge_work(1, "IR variable radii scaling work")?;
point.radius = match map(point.radius) {
Ok(radius) => radius,
Err(error) => return Ok(Err(VariableRadiiMapError::Radius(error))),
};
positive |= point.radius.get() > 0.0;
}
Ok(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::build(&StandardAdmission, 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;