use super::{
BodySelection, FaceSelection, FeatureDirection3, FeatureId, FinitePoint3, PathRef,
SelectionMembers,
};
use crate::ids::OccurrenceId;
use crate::scalar::{Angle, Length, PositiveAngle, PositiveLength, 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(
tag = "kind",
content = "value",
rename_all = "snake_case",
deny_unknown_fields
)]
pub enum PatternSeed {
Feature(FeatureId),
Faces(FaceSelection),
Bodies(BodySelection),
Occurrences(
#[serde(deserialize_with = "deserialize_local_occurrences")] SelectionMembers<OccurrenceId>,
),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub enum NoNestedComposite {}
pub type StagePatternKind = PatternKind<NoNestedComposite>;
pub trait CompositeStages: Sized {
fn stages(&self) -> &[PatternStage];
fn rebuild(stages: Vec<PatternStage>) -> Result<Self, &'static str>;
fn try_map_stage_lengths<E>(
&self,
edit: &mut impl FnMut(PatternLengthField<'_>) -> Result<(), E>,
) -> Result<Self, PatternLengthEditError<E>>;
fn try_map_stage_lengths_owned<E>(
self,
edit: &mut impl FnMut(PatternLengthField<'_>) -> Result<(), E>,
) -> Result<Self, PatternLengthEditError<E>>;
}
impl CompositeStages for CompositePattern {
fn stages(&self) -> &[PatternStage] {
&self.0
}
fn rebuild(stages: Vec<PatternStage>) -> Result<Self, &'static str> {
Self::new(stages)
}
fn try_map_stage_lengths<E>(
&self,
edit: &mut impl FnMut(PatternLengthField<'_>) -> Result<(), E>,
) -> Result<Self, PatternLengthEditError<E>> {
self.clone().try_map_stage_lengths_owned(edit)
}
fn try_map_stage_lengths_owned<E>(
mut self,
edit: &mut impl FnMut(PatternLengthField<'_>) -> Result<(), E>,
) -> Result<Self, PatternLengthEditError<E>> {
for stage in &mut self.0 {
let pattern = std::mem::replace(stage.pattern.as_mut(), StagePatternKind::UNRESOLVED);
*stage.pattern = pattern.try_map_lengths_owned(edit)?;
}
Ok(self)
}
}
impl CompositeStages for NoNestedComposite {
fn stages(&self) -> &[PatternStage] {
match *self {}
}
fn rebuild(_stages: Vec<PatternStage>) -> Result<Self, &'static str> {
Err("a composite stage applies no nested sequence of stages")
}
fn try_map_stage_lengths<E>(
&self,
_edit: &mut impl FnMut(PatternLengthField<'_>) -> Result<(), E>,
) -> Result<Self, PatternLengthEditError<E>> {
match *self {}
}
fn try_map_stage_lengths_owned<E>(
self,
_edit: &mut impl FnMut(PatternLengthField<'_>) -> Result<(), E>,
) -> Result<Self, PatternLengthEditError<E>> {
match self {}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct PatternKind<C = CompositePattern>(PatternTransform<C>);
pub enum PatternLengthField<'a> {
Length(&'a mut Length),
PositiveLength(&'a mut PositiveLength),
Point(&'a mut FinitePoint3),
}
#[derive(Debug, Clone, PartialEq)]
pub enum PatternLengthEditError<E> {
Field(E),
Offsets(&'static str),
}
impl<C: CompositeStages + Clone> PatternKind<C> {
pub fn try_map_lengths<E>(
&self,
edit: &mut impl FnMut(PatternLengthField<'_>) -> Result<(), E>,
) -> Result<Self, PatternLengthEditError<E>> {
self.clone().try_map_lengths_owned(edit)
}
}
impl<C: CompositeStages> PatternKind<C> {
pub fn try_map_lengths_owned<E>(
self,
edit: &mut impl FnMut(PatternLengthField<'_>) -> Result<(), E>,
) -> Result<Self, PatternLengthEditError<E>> {
let mut transform = self.0;
match &mut transform {
PatternTransform::Linear {
spacing, second, ..
} => {
edit(PatternLengthField::PositiveLength(spacing))
.map_err(PatternLengthEditError::Field)?;
if let Some(second) = second {
edit(PatternLengthField::PositiveLength(&mut second.spacing))
.map_err(PatternLengthEditError::Field)?;
}
}
PatternTransform::LinearOffsets { offsets, .. } => {
for offset in offsets.iter_mut() {
edit(PatternLengthField::Length(offset))
.map_err(PatternLengthEditError::Field)?;
}
if !valid_increasing_locations(offsets.iter().map(|offset| offset.get())) {
return Err(PatternLengthEditError::Offsets(
"pattern offsets must start at zero and strictly increase",
));
}
}
PatternTransform::CurveDriven { spacing, .. } => {
edit(PatternLengthField::PositiveLength(spacing))
.map_err(PatternLengthEditError::Field)?;
}
PatternTransform::Circular { axis_origin, .. }
| PatternTransform::CircularAngles { axis_origin, .. } => {
edit(PatternLengthField::Point(axis_origin))
.map_err(PatternLengthEditError::Field)?;
}
PatternTransform::Mirror { plane_origin, .. } => {
edit(PatternLengthField::Point(plane_origin))
.map_err(PatternLengthEditError::Field)?;
}
PatternTransform::Composite { .. } => {
return match transform {
PatternTransform::Composite { stages } => {
Ok(Self(PatternTransform::Composite {
stages: stages.try_map_stage_lengths_owned(edit)?,
}))
}
other => Ok(Self(other)),
};
}
PatternTransform::Scale { center, .. } => {
if let PatternScaleCenter::Point(point) = center {
edit(PatternLengthField::Point(point))
.map_err(PatternLengthEditError::Field)?;
}
}
PatternTransform::Unresolved { .. } | PatternTransform::MirrorReference { .. } => {}
}
Ok(Self(transform))
}
}
impl<C> PatternKind<C> {
pub const UNRESOLVED: Self = Self(PatternTransform::Unresolved { form: None });
pub const UNRESOLVED_LINEAR: Self = Self(PatternTransform::Unresolved {
form: Some(PatternForm::Linear),
});
pub const UNRESOLVED_CIRCULAR: Self = Self(PatternTransform::Unresolved {
form: Some(PatternForm::Circular),
});
pub const UNRESOLVED_CURVE_DRIVEN: Self = Self(PatternTransform::Unresolved {
form: Some(PatternForm::CurveDriven),
});
pub const UNRESOLVED_MIRROR: Self = Self(PatternTransform::Unresolved {
form: Some(PatternForm::Mirror),
});
pub const UNRESOLVED_SCALE: Self = Self(PatternTransform::Unresolved {
form: Some(PatternForm::Scale),
});
pub const UNRESOLVED_COMPOSITE: Self = Self(PatternTransform::Unresolved {
form: Some(PatternForm::Composite),
});
pub fn new(transform: PatternTransform<C>) -> Result<Self, &'static str> {
let require =
|condition: bool, message: &'static str| condition.then_some(()).ok_or(message);
match &transform {
PatternTransform::Unresolved { .. } => {}
PatternTransform::Linear { count, second, .. } => {
require(*count > 0, "pattern count must be positive")?;
if let Some(second) = second {
require(second.count > 0, "pattern second.count must be positive")?;
}
}
PatternTransform::LinearOffsets { offsets, .. } => {
require(
valid_increasing_locations(offsets.iter().map(|offset| offset.get())),
"pattern offsets must start at zero and strictly increase",
)?;
}
PatternTransform::Circular { count, .. } => {
require(*count > 0, "pattern count must be positive")?;
}
PatternTransform::CircularAngles { angles, .. } => {
require(
valid_increasing_locations(angles.iter().map(|angle| angle.get())),
"pattern angles must start at zero and strictly increase",
)?;
}
PatternTransform::CurveDriven { count, .. } => {
require(*count > 0, "pattern count must be positive")?;
}
PatternTransform::Mirror { .. } => {}
PatternTransform::MirrorReference {
plane: FaceSelection::Native(reference),
} => {
require(
!reference.is_empty(),
"pattern plane native reference must be nonempty",
)?;
}
PatternTransform::MirrorReference { .. } => {}
PatternTransform::Scale { count, .. } => {
require(*count >= 2, "scale pattern count must be at least two")?;
}
PatternTransform::Composite { .. } => {}
}
Ok(Self(transform))
}
pub fn definition(&self) -> &PatternTransform<C> {
&self.0
}
pub fn curve_path_mut(&mut self) -> Option<&mut Option<PathRef>> {
match &mut self.0 {
PatternTransform::CurveDriven { path, .. } => Some(path),
_ => None,
}
}
pub const fn is_unresolved(&self) -> bool {
matches!(self.0, PatternTransform::Unresolved { .. })
}
}
impl PatternTransform<NoNestedComposite> {
#[must_use]
pub fn widen<C>(self) -> PatternTransform<C> {
match self {
Self::Unresolved { form } => PatternTransform::Unresolved { form },
Self::Linear {
direction,
spacing,
count,
second,
} => PatternTransform::Linear {
direction,
spacing,
count,
second,
},
Self::LinearOffsets { direction, offsets } => {
PatternTransform::LinearOffsets { direction, offsets }
}
Self::Circular {
axis_origin,
axis_dir,
angle,
count,
} => PatternTransform::Circular {
axis_origin,
axis_dir,
angle,
count,
},
Self::CircularAngles {
axis_origin,
axis_dir,
angles,
} => PatternTransform::CircularAngles {
axis_origin,
axis_dir,
angles,
},
Self::CurveDriven {
path,
spacing,
count,
} => PatternTransform::CurveDriven {
path,
spacing,
count,
},
Self::Mirror {
plane_origin,
plane_normal,
} => PatternTransform::Mirror {
plane_origin,
plane_normal,
},
Self::MirrorReference { plane } => PatternTransform::MirrorReference { plane },
Self::Scale {
center,
final_factor,
count,
} => PatternTransform::Scale {
center,
final_factor,
count,
},
Self::Composite { stages } => match stages {},
}
}
}
impl StagePatternKind {
#[must_use]
pub fn widen<C>(self) -> PatternKind<C> {
PatternKind(self.0.widen())
}
}
#[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 PatternForm {
Linear,
Circular,
CurveDriven,
Mirror,
Scale,
Composite,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum PatternTransform<C = CompositePattern> {
Unresolved {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_form"
)]
form: Option<PatternForm>,
},
Linear {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_direction"
)]
direction: Option<FeatureDirection3>,
spacing: PositiveLength,
count: u32,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_second"
)]
second: Option<LinearPatternDirection>,
},
LinearOffsets {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_direction"
)]
direction: Option<FeatureDirection3>,
offsets: Vec<Length>,
},
Circular {
axis_origin: FinitePoint3,
axis_dir: FeatureDirection3,
angle: PositiveAngle,
count: u32,
},
CircularAngles {
axis_origin: FinitePoint3,
axis_dir: UnitVector3,
angles: Vec<Angle>,
},
CurveDriven {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_path"
)]
path: Option<PathRef>,
spacing: PositiveLength,
count: u32,
},
Mirror {
plane_origin: FinitePoint3,
plane_normal: FeatureDirection3,
},
MirrorReference {
plane: FaceSelection,
},
Scale {
center: PatternScaleCenter,
final_factor: PositiveReal,
count: u32,
},
Composite {
stages: C,
},
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "Vec<PatternStage>", into = "Vec<PatternStage>")]
pub struct CompositePattern(Vec<PatternStage>);
impl CompositePattern {
pub fn new(stages: Vec<PatternStage>) -> Result<Self, &'static str> {
if stages.is_empty() {
return Err("pattern stages must be nonempty");
}
if !composite_composition_is_valid(&stages) {
return Err("pattern stage counts must not overflow and must divide aligned slices");
}
Ok(Self(stages))
}
}
fn stage_combination(index: usize, stage: &PatternStage) -> PatternStageCombination {
if index == 0 {
PatternStageCombination::Initialize
} else if matches!(stage.pattern.definition(), PatternTransform::Scale { .. }) {
PatternStageCombination::AlignedSlices
} else {
PatternStageCombination::CartesianProduct
}
}
impl std::ops::Deref for CompositePattern {
type Target = [PatternStage];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TryFrom<Vec<PatternStage>> for CompositePattern {
type Error = &'static str;
fn try_from(stages: Vec<PatternStage>) -> Result<Self, Self::Error> {
Self::new(stages)
}
}
impl From<CompositePattern> for Vec<PatternStage> {
fn from(pattern: CompositePattern) -> Self {
pattern.0
}
}
impl<'a> IntoIterator for &'a CompositePattern {
type Item = &'a PatternStage;
type IntoIter = std::slice::Iter<'a, PatternStage>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<C: Serialize> Serialize for PatternKind<C> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de, C: Deserialize<'de>> Deserialize<'de> for PatternKind<C> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
Self::new(PatternTransform::<C>::deserialize(deserializer)?)
.map_err(serde::de::Error::custom)
}
}
#[cfg(feature = "schema")]
impl<C: JsonSchema> JsonSchema for PatternKind<C> {
fn schema_name() -> std::borrow::Cow<'static, str> {
"PatternKind".into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
PatternTransform::<C>::json_schema(generator)
}
}
fn composite_composition_is_valid(stages: &[crate::features::patterns::PatternStage]) -> bool {
let mut occurrences = None;
stages.iter().enumerate().all(|(index, stage)| {
let Some(stage_count) = pattern_occurrence_count(stage.pattern.definition()) else {
return true;
};
if index == 0 {
occurrences = Some(stage_count);
return true;
}
match stage_combination(index, stage) {
PatternStageCombination::CartesianProduct => {
if let Some(count) = occurrences {
occurrences = count.checked_mul(stage_count);
occurrences.is_some()
} else {
true
}
}
PatternStageCombination::AlignedSlices => {
occurrences.is_none_or(|count| count % stage_count == 0)
}
PatternStageCombination::Initialize => false,
}
})
}
fn pattern_occurrence_count<C>(pattern: &PatternTransform<C>) -> Option<usize> {
match pattern {
PatternTransform::Linear { count, .. }
| PatternTransform::Circular { count, .. }
| PatternTransform::CurveDriven { count, .. }
| PatternTransform::Scale { count, .. } => usize::try_from(*count).ok(),
PatternTransform::LinearOffsets { offsets, .. } => Some(offsets.len()),
PatternTransform::CircularAngles { angles, .. } => Some(angles.len()),
PatternTransform::Mirror { .. } | PatternTransform::MirrorReference { .. } => Some(2),
PatternTransform::Unresolved { .. } | PatternTransform::Composite { .. } => None,
}
}
fn valid_increasing_locations(locations: impl Iterator<Item = f64>) -> bool {
let mut locations = locations;
let Some(first) = locations.next() else {
return false;
};
first == 0.0
&& locations
.try_fold(first, |previous, location| {
(location.is_finite() && location > previous).then_some(location)
})
.is_some()
}
#[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 PatternScaleCenter {
FirstSeedCentroid,
Point(FinitePoint3),
Native(String),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct PatternStage {
pub pattern: Box<StagePatternKind>,
}
#[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 PatternStageCombination {
Initialize,
CartesianProduct,
AlignedSlices,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct LinearPatternDirection {
pub direction: FeatureDirection3,
pub spacing: PositiveLength,
pub count: u32,
}
cadmpeg_core::named_optional_field!(deserialize_form, PatternForm, "form");
cadmpeg_core::named_optional_field!(deserialize_direction, FeatureDirection3, "direction");
cadmpeg_core::named_optional_field!(deserialize_second, LinearPatternDirection, "second");
cadmpeg_core::named_optional_field!(deserialize_path, PathRef, "path");
selection_field_deserializer!(deserialize_local_occurrences, "occurrences");
mod decode_clone;
#[cfg(test)]
mod tests;
mod identity_rewrite;
mod serialization;