use crate::math::{Point2, Point3, Vector3};
use crate::transform::Transform;
use crate::{
features::{FinitePoint3, FiniteVector3, ParameterId},
scalar::{Angle, FiniteReal, Length, PositiveAngle, PositiveLength, PositiveReal},
units::{FinitePoint2, UnitVector2, UnitVector3},
};
use cadmpeg_core::text::NonBlankString;
#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, thiserror::Error)]
pub enum SketchCollectionError {
#[error("{0}")]
Invalid(&'static str),
#[error("resource refusal: {0:?}")]
Resource(cadmpeg_core::decode::ResourceLimit),
#[error("{0}")]
Admission(String),
}
impl From<cadmpeg_core::CodecError> for SketchCollectionError {
fn from(error: cadmpeg_core::CodecError) -> Self {
match error {
cadmpeg_core::CodecError::ResourceLimit(limit) => Self::Resource(limit),
error => Self::Admission(error.to_string()),
}
}
}
fn distinct_sketch_members<'id>(identities: impl Iterator<Item = &'id str>) -> bool {
let mut members: Vec<&str> = Vec::new();
for identity in identities {
match members.binary_search(&identity) {
Ok(_) => return false,
Err(position) => members.insert(position, identity),
}
}
true
}
pub mod scaling;
crate::ids::id_type!(
SketchId, compose, into_string
);
crate::ids::id_type!(
SketchEntityId, compose
);
crate::ids::id_type!(
SpatialSketchId, compose
);
crate::ids::id_type!(
SpatialSketchEntityId, compose
);
crate::ids::id_type!(
SketchConstraintId, compose
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "i32", into = "i32")]
#[repr(i32)]
pub enum SketchFontWeight {
Regular = 400,
Medium = 500,
Bold = 750,
}
impl TryFrom<i32> for SketchFontWeight {
type Error = &'static str;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
400 => Ok(Self::Regular),
500 => Ok(Self::Medium),
750 => Ok(Self::Bold),
_ => Err("sketch text font_weight must be 400, 500, or 750"),
}
}
}
impl From<SketchFontWeight> for i32 {
fn from(value: SketchFontWeight) -> Self {
match value {
SketchFontWeight::Regular => 400,
SketchFontWeight::Medium => 500,
SketchFontWeight::Bold => 750,
}
}
}
#[cfg(feature = "schema")]
impl JsonSchema for SketchFontWeight {
fn schema_name() -> std::borrow::Cow<'static, str> {
"SketchFontWeight".into()
}
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({"type": "integer", "enum": [400, 500, 750]})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", content = "native_value", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SketchTextHorizontalAlignment {
Left,
Center,
Right,
Native(u32),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", content = "native_value", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SketchTextVerticalAlignment {
Top,
Middle,
Bottom,
Native(u32),
}
#[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 SketchAxis {
Horizontal,
Vertical,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Sketch {
pub id: SketchId,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_name"
)]
pub name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_configuration"
)]
pub configuration: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_visible"
)]
pub visible: Option<bool>,
pub placement: SketchPlacement,
#[serde(default, skip_serializing_if = "SketchProfiles::is_empty")]
pub profiles: SketchProfiles,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SketchPlacement {
Unresolved {},
Resolved {
frame: SketchPlaneFrame,
},
}
const EPS_SKETCH_PLANE_ORTHOGONALITY: f64 = 1.0e-9;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SketchPlaneFrameWire")]
pub struct SketchPlaneFrame {
origin: FinitePoint3,
normal: FiniteVector3,
u_axis: FiniteVector3,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SketchPlaneFrameWire {
origin: Point3,
normal: Vector3,
u_axis: Vector3,
}
impl TryFrom<SketchPlaneFrameWire> for SketchPlaneFrame {
type Error = &'static str;
fn try_from(wire: SketchPlaneFrameWire) -> Result<Self, Self::Error> {
let normal =
FiniteVector3::new(wire.normal).ok_or("sketch normal must be finite and nonzero")?;
let unit_normal = normal
.unit_nonzero()
.ok_or("sketch normal must be finite and nonzero")?;
let u_axis =
FiniteVector3::new(wire.u_axis).ok_or("sketch u_axis must be finite and nonzero")?;
let unit_u_axis = u_axis
.unit_nonzero()
.ok_or("sketch u_axis must be finite and nonzero")?;
if unit_normal.dot(unit_u_axis).abs() > EPS_SKETCH_PLANE_ORTHOGONALITY {
return Err("sketch normal and u_axis must be perpendicular");
}
let origin = FinitePoint3::new(wire.origin).ok_or("sketch origin must be finite")?;
Ok(Self {
origin,
normal,
u_axis,
})
}
}
impl SketchPlacement {
pub fn try_resolved(
origin: Point3,
normal: Vector3,
u_axis: Vector3,
) -> Result<Self, &'static str> {
Ok(Self::Resolved {
frame: SketchPlaneFrameWire {
origin,
normal,
u_axis,
}
.try_into()?,
})
}
#[must_use]
pub fn with_origin(self, origin: FinitePoint3) -> Self {
match self {
Self::Unresolved {} => Self::Unresolved {},
Self::Resolved { frame } => Self::Resolved {
frame: SketchPlaneFrame { origin, ..frame },
},
}
}
pub fn resolved(self) -> Option<(FinitePoint3, FiniteVector3, FiniteVector3)> {
match self {
Self::Unresolved {} => None,
Self::Resolved { frame } => Some((frame.origin, frame.normal, frame.u_axis)),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "Vec<Vec<SketchEntityUse>>")]
pub struct SketchProfiles(Vec<Vec<SketchEntityUse>>);
impl TryFrom<Vec<Vec<SketchEntityUse>>> for SketchProfiles {
type Error = &'static str;
fn try_from(profiles: Vec<Vec<SketchEntityUse>>) -> Result<Self, Self::Error> {
if profiles.iter().any(Vec::is_empty) {
return Err("sketch profiles must contain no empty chain");
}
Ok(Self(profiles))
}
}
impl std::ops::Deref for SketchProfiles {
type Target = [Vec<SketchEntityUse>];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> IntoIterator for &'a SketchProfiles {
type Item = &'a Vec<SketchEntityUse>;
type IntoIter = std::slice::Iter<'a, Vec<SketchEntityUse>>;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl IntoIterator for SketchProfiles {
type Item = Vec<SketchEntityUse>;
type IntoIter = std::vec::IntoIter<Vec<SketchEntityUse>>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl SketchProfiles {
#[must_use]
pub fn as_slice(&self) -> &[Vec<SketchEntityUse>] {
&self.0
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn push_single(&mut self, entity: SketchEntityUse) {
self.0.push(vec![entity]);
}
pub fn retain_uses(&mut self, mut keep: impl FnMut(&SketchEntityUse) -> bool) {
let mut decisions = Vec::new();
for profile in &self.0 {
for usage in profile {
decisions.push(keep(usage));
}
}
let mut decisions = decisions.into_iter();
for profile in &mut self.0 {
profile.retain(|_| decisions.next() == Some(true));
}
self.0.retain(|profile| !profile.is_empty());
}
pub fn try_push(&mut self, profile: Vec<SketchEntityUse>) -> Result<(), &'static str> {
if profile.is_empty() {
return Err("sketch profile chain must be nonempty");
}
self.0.push(profile);
Ok(())
}
pub fn edit(
&mut self,
edit: impl FnOnce(&mut Vec<Vec<SketchEntityUse>>),
) -> Result<(), &'static str> {
let mut profiles = self.0.clone();
edit(&mut profiles);
*self = profiles.try_into()?;
Ok(())
}
}
impl Sketch {
pub fn resolved_placement(&self) -> Option<(FinitePoint3, FiniteVector3, FiniteVector3)> {
self.placement.resolved()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SketchEntityUse {
pub entity: SketchEntityId,
#[serde(default)]
pub reversed: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SketchEntity {
id: SketchEntityId,
pub sketch: SketchId,
#[serde(default)]
pub construction: bool,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_geometry_ref"
)]
pub geometry_ref: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub endpoint_refs: Vec<String>,
pub geometry: SketchGeometry,
}
impl SketchEntity {
pub fn new(id: SketchEntityId, sketch: SketchId, geometry: SketchGeometry) -> Self {
Self {
id,
sketch,
construction: false,
native_ref: None,
geometry_ref: None,
endpoint_refs: Vec::new(),
geometry,
}
}
pub fn id(&self) -> &SketchEntityId {
&self.id
}
#[must_use]
pub fn with_construction(mut self, construction: bool) -> Self {
self.construction = construction;
self
}
#[must_use]
pub fn with_native_ref(mut self, native_ref: Option<String>) -> Self {
self.native_ref = native_ref;
self
}
#[must_use]
pub fn with_geometry_ref(mut self, geometry_ref: Option<String>) -> Self {
self.geometry_ref = geometry_ref;
self
}
#[must_use]
pub fn with_endpoint_refs(mut self, endpoint_refs: Vec<String>) -> Self {
self.endpoint_refs = endpoint_refs;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
pub struct ReferenceLineDirection(FinitePoint2);
impl ReferenceLineDirection {
pub fn new(direction: FinitePoint2) -> Option<Self> {
(direction.u.hypot(direction.v) > f64::EPSILON).then_some(Self(direction))
}
pub fn get(self) -> Point2 {
self.0.get()
}
}
impl std::ops::Deref for ReferenceLineDirection {
type Target = Point2;
fn deref(&self) -> &Point2 {
self.0.as_raw()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct EllipseRadii<L> {
pub major_radius: L,
pub minor_radius: L,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OrderedMajorRadius {
major: PositiveLength,
minor: PositiveLength,
}
impl OrderedMajorRadius {
pub fn new(major: PositiveLength, minor: PositiveLength) -> Option<Self> {
(major.get() >= minor.get()).then_some(Self { major, minor })
}
pub const fn major(self) -> PositiveLength {
self.major
}
pub const fn minor(self) -> PositiveLength {
self.minor
}
}
impl Serialize for OrderedMajorRadius {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeStruct;
let mut radii = serializer.serialize_struct("EllipseRadii", 2)?;
radii.serialize_field("major_radius", &self.major)?;
radii.serialize_field("minor_radius", &self.minor)?;
radii.end()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(into = "SketchGeometryDefinition"))]
#[serde(try_from = "SketchGeometryDefinition")]
pub struct SketchGeometry(
SketchGeometryDefinition<
FinitePoint2,
PositiveLength,
FiniteReal,
PositiveReal,
ReferenceLineDirection,
OrderedMajorRadius,
>,
);
impl SketchGeometry {
#[must_use]
pub fn from_admitted_definition(
definition: SketchGeometryDefinition<
FinitePoint2,
PositiveLength,
FiniteReal,
PositiveReal,
ReferenceLineDirection,
OrderedMajorRadius,
>,
) -> Self {
Self(definition)
}
#[must_use]
pub fn native(native_kind: NonBlankString) -> Self {
Self(SketchGeometryDefinition::Native { native_kind })
}
#[must_use]
pub fn nurbs(curve: crate::geometry::pcurve::PcurveNurbs) -> Self {
Self(SketchGeometryDefinition::Nurbs { curve })
}
pub fn from_parts(
definition: SketchGeometryDefinition<
FinitePoint2,
PositiveLength,
FiniteReal,
PositiveReal,
>,
) -> Result<Self, &'static str> {
use SketchGeometryDefinition as Definition;
Ok(Self(match definition {
Definition::Point { position } => Definition::Point { position },
Definition::Line { start, end } => Definition::Line { start, end },
Definition::ReferenceLine { origin, direction } => Definition::ReferenceLine {
origin,
direction: ReferenceLineDirection::new(direction).ok_or(
"sketch reference line requires finite origin and nonzero finite direction",
)?,
},
Definition::Circle { center, radius } => Definition::Circle { center, radius },
Definition::Arc {
center,
radius,
start_angle,
end_angle,
} => Definition::Arc {
center,
radius,
start_angle,
end_angle,
},
Definition::Ellipse {
center,
major_angle,
radii,
bounds,
} => Definition::Ellipse {
center,
major_angle,
radii: OrderedMajorRadius::new(radii.major_radius, radii.minor_radius)
.ok_or("sketch ellipse major_radius must be at least minor_radius")?,
bounds,
},
Definition::Hyperbola {
center,
major_angle,
major_radius,
minor_radius,
bounds,
} => Definition::Hyperbola {
center,
major_angle,
major_radius,
minor_radius,
bounds,
},
Definition::Parabola {
vertex,
axis_angle,
focal_length,
bounds,
} => Definition::Parabola {
vertex,
axis_angle,
focal_length,
bounds,
},
Definition::Nurbs { curve } => Definition::Nurbs { curve },
Definition::Text {
text,
font_family,
font_weight,
height,
width_factor,
placement,
horizontal_alignment,
vertical_alignment,
} => Definition::Text {
text,
font_family,
font_weight,
height,
width_factor,
placement,
horizontal_alignment,
vertical_alignment,
},
Definition::ExternalReference {
document,
object,
subelements,
} => Definition::ExternalReference {
document,
object,
subelements,
},
Definition::Native { native_kind } => Definition::Native { native_kind },
}))
}
#[must_use]
pub fn definition(
&self,
) -> &SketchGeometryDefinition<
FinitePoint2,
PositiveLength,
FiniteReal,
PositiveReal,
ReferenceLineDirection,
OrderedMajorRadius,
> {
&self.0
}
#[must_use]
pub fn into_definition(
self,
) -> SketchGeometryDefinition<
FinitePoint2,
PositiveLength,
FiniteReal,
PositiveReal,
ReferenceLineDirection,
OrderedMajorRadius,
> {
self.0
}
}
impl
SketchGeometryDefinition<
FinitePoint2,
PositiveLength,
FiniteReal,
PositiveReal,
ReferenceLineDirection,
OrderedMajorRadius,
>
{
#[must_use]
pub fn to_raw(&self) -> SketchGeometryDefinition {
use SketchGeometryDefinition as Definition;
match self {
Self::Point { position } => Definition::Point {
position: position.get(),
},
Self::Line { start, end } => Definition::Line {
start: start.get(),
end: end.get(),
},
Self::ReferenceLine { origin, direction } => Definition::ReferenceLine {
origin: origin.get(),
direction: direction.get(),
},
Self::Circle { center, radius } => Definition::Circle {
center: center.get(),
radius: Length::from(*radius),
},
Self::Arc {
center,
radius,
start_angle,
end_angle,
} => Definition::Arc {
center: center.get(),
radius: Length::from(*radius),
start_angle: *start_angle,
end_angle: *end_angle,
},
Self::Ellipse {
center,
major_angle,
radii,
bounds,
} => Definition::Ellipse {
center: center.get(),
major_angle: *major_angle,
radii: EllipseRadii {
major_radius: Length::from(radii.major()),
minor_radius: Length::from(radii.minor()),
},
bounds: *bounds,
},
Self::Hyperbola {
center,
major_angle,
major_radius,
minor_radius,
bounds,
} => Definition::Hyperbola {
center: center.get(),
major_angle: *major_angle,
major_radius: Length::from(*major_radius),
minor_radius: Length::from(*minor_radius),
bounds: bounds.map(FiniteReal::raw_array),
},
Self::Parabola {
vertex,
axis_angle,
focal_length,
bounds,
} => Definition::Parabola {
vertex: vertex.get(),
axis_angle: *axis_angle,
focal_length: Length::from(*focal_length),
bounds: bounds.map(FiniteReal::raw_array),
},
Self::Nurbs { curve } => Definition::Nurbs {
curve: curve.clone(),
},
Self::Text {
text,
font_family,
font_weight,
height,
width_factor,
placement,
horizontal_alignment,
vertical_alignment,
} => Definition::Text {
text: text.clone(),
font_family: font_family.clone(),
font_weight: *font_weight,
height: Length::from(*height),
width_factor: width_factor.map(PositiveReal::get),
placement: placement.map(|placement| TextPlacement {
anchor: placement.anchor.get(),
rotation: placement.rotation,
}),
horizontal_alignment: *horizontal_alignment,
vertical_alignment: *vertical_alignment,
},
Self::ExternalReference {
document,
object,
subelements,
} => Definition::ExternalReference {
document: document.clone(),
object: object.clone(),
subelements: subelements.clone(),
},
Self::Native { native_kind } => Definition::Native {
native_kind: native_kind.clone(),
},
}
}
}
fn admit_sketch_point(point: Point2, message: &'static str) -> Result<FinitePoint2, &'static str> {
FinitePoint2::new(point).ok_or(message)
}
fn admit_sketch_length(
length: Length,
message: &'static str,
) -> Result<PositiveLength, &'static str> {
PositiveLength::try_from(length).map_err(|_| message)
}
fn admit_sketch_bounds(
bounds: Option<[f64; 2]>,
message: &'static str,
) -> Result<Option<[FiniteReal; 2]>, &'static str> {
bounds
.map(|bounds| FiniteReal::array(bounds).ok_or(message))
.transpose()
}
impl TryFrom<SketchGeometryDefinition> for SketchGeometry {
type Error = &'static str;
fn try_from(definition: SketchGeometryDefinition) -> Result<Self, Self::Error> {
use SketchGeometryDefinition as Definition;
const CIRCULAR: &str =
"sketch circular geometry requires finite center and positive finite radius";
Self::from_parts(match definition {
Definition::Point { position } => Definition::Point {
position: admit_sketch_point(position, "sketch point position must be finite")?,
},
Definition::Line { start, end } => {
const LINE: &str = "sketch line endpoints must be finite";
Definition::Line {
start: admit_sketch_point(start, LINE)?,
end: admit_sketch_point(end, LINE)?,
}
}
Definition::ReferenceLine { origin, direction } => {
const REFERENCE: &str =
"sketch reference line requires finite origin and nonzero finite direction";
Definition::ReferenceLine {
origin: admit_sketch_point(origin, REFERENCE)?,
direction: admit_sketch_point(direction, REFERENCE)?,
}
}
Definition::Circle { center, radius } => Definition::Circle {
center: admit_sketch_point(center, CIRCULAR)?,
radius: admit_sketch_length(radius, CIRCULAR)?,
},
Definition::Arc {
center,
radius,
start_angle,
end_angle,
} => Definition::Arc {
center: admit_sketch_point(center, CIRCULAR)?,
radius: admit_sketch_length(radius, CIRCULAR)?,
start_angle,
end_angle,
},
Definition::Ellipse {
center,
major_angle,
radii,
bounds,
} => {
const RADII: &str = "sketch ellipse radii must be positive and finite";
Definition::Ellipse {
center: admit_sketch_point(
center,
"sketch ellipse center and major_angle must be finite",
)?,
major_angle,
radii: EllipseRadii {
major_radius: admit_sketch_length(radii.major_radius, RADII)?,
minor_radius: admit_sketch_length(radii.minor_radius, RADII)?,
},
bounds,
}
}
Definition::Hyperbola {
center,
major_angle,
major_radius,
minor_radius,
bounds,
} => {
const RADII: &str = "sketch hyperbola radii must be positive and finite";
Definition::Hyperbola {
center: admit_sketch_point(
center,
"sketch hyperbola center and major_angle must be finite",
)?,
major_angle,
major_radius: admit_sketch_length(major_radius, RADII)?,
minor_radius: admit_sketch_length(minor_radius, RADII)?,
bounds: admit_sketch_bounds(bounds, "sketch hyperbola bounds must be finite")?,
}
}
Definition::Parabola {
vertex,
axis_angle,
focal_length,
bounds,
} => Definition::Parabola {
vertex: admit_sketch_point(
vertex,
"sketch parabola vertex and axis_angle must be finite",
)?,
axis_angle,
focal_length: admit_sketch_length(
focal_length,
"sketch parabola focal_length must be positive and finite",
)?,
bounds: admit_sketch_bounds(bounds, "sketch parabola bounds must be finite")?,
},
Definition::Nurbs { curve } => Definition::Nurbs { curve },
Definition::Text {
text,
font_family,
font_weight,
height,
width_factor,
placement,
horizontal_alignment,
vertical_alignment,
} => Definition::Text {
text,
font_family,
font_weight,
height: admit_sketch_length(
height,
"sketch text height must be positive and finite",
)?,
width_factor: width_factor
.map(|value| {
PositiveReal::new(value)
.ok_or("sketch text width_factor must be positive and finite")
})
.transpose()?,
placement: placement
.map(|placement| {
Ok::<_, &'static str>(TextPlacement {
anchor: admit_sketch_point(
placement.anchor,
"sketch text anchor and rotation must be finite",
)?,
rotation: placement.rotation,
})
})
.transpose()?,
horizontal_alignment,
vertical_alignment,
},
Definition::ExternalReference {
document,
object,
subelements,
} => Definition::ExternalReference {
document,
object,
subelements,
},
Definition::Native { native_kind } => Definition::Native { native_kind },
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
#[serde(bound(
deserialize = "P: Deserialize<'de>, L: Deserialize<'de>, R: Deserialize<'de>, W: Deserialize<'de>, D: Deserialize<'de>, M: Deserialize<'de>"
))]
pub enum SketchGeometryDefinition<
P = Point2,
L = Length,
R = f64,
W = f64,
D = P,
M = EllipseRadii<L>,
> {
Point {
position: P,
},
Line {
start: P,
end: P,
},
ReferenceLine {
origin: P,
direction: D,
},
Circle {
center: P,
radius: L,
},
Arc {
center: P,
radius: L,
start_angle: Angle,
end_angle: Angle,
},
Ellipse {
center: P,
major_angle: Angle,
#[serde(flatten)]
radii: M,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_bounds"
)]
bounds: Option<[Angle; 2]>,
},
Hyperbola {
center: P,
major_angle: Angle,
major_radius: L,
minor_radius: L,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_sketch_geometry_definition_bounds"
)]
bounds: Option<[R; 2]>,
},
Parabola {
vertex: P,
axis_angle: Angle,
focal_length: L,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_sketch_geometry_definition_bounds"
)]
bounds: Option<[R; 2]>,
},
Nurbs {
curve: crate::geometry::pcurve::PcurveNurbs,
},
Text {
text: NonBlankString,
font_family: NonBlankString,
font_weight: SketchFontWeight,
height: L,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_width_factor"
)]
width_factor: Option<W>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_placement"
)]
placement: Option<TextPlacement<P>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_horizontal_alignment"
)]
horizontal_alignment: Option<SketchTextHorizontalAlignment>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_vertical_alignment"
)]
vertical_alignment: Option<SketchTextVerticalAlignment>,
},
ExternalReference {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_document"
)]
document: Option<String>,
#[serde(deserialize_with = "deserialize_object")]
object: NonBlankString,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
subelements: Vec<String>,
},
Native {
#[serde(deserialize_with = "deserialize_native_kind")]
native_kind: NonBlankString,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct TextPlacement<P = Point2> {
pub anchor: P,
pub rotation: Angle,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SpatialSketch {
pub id: SpatialSketchId,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_name"
)]
pub name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_configuration"
)]
pub configuration: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_visible"
)]
pub visible: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub profiles: Vec<SpatialSketchProfile>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
}
const EPS_SPATIAL_PROFILE_FRAME: f64 = 1.0e-9;
const SPATIAL_PROFILE_AXES_ERROR: &str =
"spatial profile normal and u_axis must be unit and orthogonal";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SpatialSketchProfileWire")]
pub struct SpatialSketchProfile {
origin: FinitePoint3,
normal: UnitVector3,
u_axis: UnitVector3,
boundary: Vec<SpatialSketchEntityUse>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SpatialSketchProfileWire {
origin: Point3,
normal: Vector3,
u_axis: Vector3,
boundary: Vec<SpatialSketchEntityUse>,
}
impl TryFrom<SpatialSketchProfileWire> for SpatialSketchProfile {
type Error = SketchCollectionError;
fn try_from(wire: SpatialSketchProfileWire) -> Result<Self, Self::Error> {
let origin = FinitePoint3::new(wire.origin).ok_or(SketchCollectionError::Invalid(
"spatial profile origin must be finite",
))?;
let normal = UnitVector3::new(wire.normal)
.ok_or(SketchCollectionError::Invalid(SPATIAL_PROFILE_AXES_ERROR))?;
let u_axis = UnitVector3::new(wire.u_axis)
.ok_or(SketchCollectionError::Invalid(SPATIAL_PROFILE_AXES_ERROR))?;
let [n, u] = [normal.as_raw(), u_axis.as_raw()];
if (n.x * u.x + n.y * u.y + n.z * u.z).abs() > EPS_SPATIAL_PROFILE_FRAME {
return Err(SketchCollectionError::Invalid(SPATIAL_PROFILE_AXES_ERROR));
}
let invalid = "spatial profile boundary must be nonempty and contain distinct entities";
if wire.boundary.is_empty() {
return Err(SketchCollectionError::Invalid(invalid));
}
let mut members = std::collections::HashSet::new();
members
.try_reserve(wire.boundary.len())
.map_err(|error| SketchCollectionError::Admission(error.to_string()))?;
for use_ in &wire.boundary {
if !members.insert(&use_.entity) {
return Err(SketchCollectionError::Invalid(invalid));
}
}
Ok(Self {
origin,
normal,
u_axis,
boundary: wire.boundary,
})
}
}
impl SpatialSketchProfile {
pub fn try_new(
origin: Point3,
normal: Vector3,
u_axis: Vector3,
boundary: Vec<SpatialSketchEntityUse>,
) -> Result<Self, &'static str> {
let Some(origin) = FinitePoint3::new(origin) else {
return Err("spatial profile origin must be finite");
};
let Some(normal) = UnitVector3::new(normal) else {
return Err(SPATIAL_PROFILE_AXES_ERROR);
};
let Some(u_axis) = UnitVector3::new(u_axis) else {
return Err(SPATIAL_PROFILE_AXES_ERROR);
};
Self::from_parts(origin, normal, u_axis, boundary)
}
pub fn from_parts(
origin: FinitePoint3,
normal: UnitVector3,
u_axis: UnitVector3,
boundary: Vec<SpatialSketchEntityUse>,
) -> Result<Self, &'static str> {
let [n, u] = [normal.as_raw(), u_axis.as_raw()];
let dot = n.x * u.x + n.y * u.y + n.z * u.z;
if dot.abs() > EPS_SPATIAL_PROFILE_FRAME {
return Err(SPATIAL_PROFILE_AXES_ERROR);
}
if boundary.is_empty() {
return Err("spatial profile boundary must be nonempty and contain distinct entities");
}
if !distinct_sketch_members(boundary.iter().map(|use_| use_.entity.as_str())) {
return Err("spatial profile boundary must be nonempty and contain distinct entities");
}
Ok(Self {
origin,
normal,
u_axis,
boundary,
})
}
#[must_use]
pub fn origin(&self) -> FinitePoint3 {
self.origin
}
#[must_use]
pub fn normal(&self) -> UnitVector3 {
self.normal
}
#[must_use]
pub fn u_axis(&self) -> UnitVector3 {
self.u_axis
}
#[must_use]
pub fn boundary(&self) -> &[SpatialSketchEntityUse] {
&self.boundary
}
pub fn set_origin(&mut self, origin: Point3) -> Result<(), &'static str> {
self.origin = FinitePoint3::new(origin).ok_or("spatial profile origin must be finite")?;
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SpatialSketchEntityUse {
pub entity: SpatialSketchEntityId,
#[serde(default)]
pub reversed: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SpatialSketchEntity {
id: SpatialSketchEntityId,
pub sketch: SpatialSketchId,
#[serde(default)]
pub construction: bool,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_geometry_ref"
)]
pub geometry_ref: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub endpoint_refs: Vec<String>,
pub geometry: SpatialSketchGeometry,
}
impl SpatialSketchEntity {
pub fn new(
id: SpatialSketchEntityId,
sketch: SpatialSketchId,
geometry: SpatialSketchGeometry,
) -> Self {
Self {
id,
sketch,
construction: false,
native_ref: None,
geometry_ref: None,
endpoint_refs: Vec::new(),
geometry,
}
}
pub fn id(&self) -> &SpatialSketchEntityId {
&self.id
}
#[must_use]
pub fn with_construction(mut self, construction: bool) -> Self {
self.construction = construction;
self
}
#[must_use]
pub fn with_native_ref(mut self, native_ref: Option<String>) -> Self {
self.native_ref = native_ref;
self
}
#[must_use]
pub fn with_geometry_ref(mut self, geometry_ref: Option<String>) -> Self {
self.geometry_ref = geometry_ref;
self
}
#[must_use]
pub fn with_endpoint_refs(mut self, endpoint_refs: Vec<String>) -> Self {
self.endpoint_refs = endpoint_refs;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SpatialSketchConstraint {
pub id: SketchConstraintId,
pub sketch: SpatialSketchId,
pub definition: SpatialSketchConstraintDefinition,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SpatialSketchEntityPair {
pub first: SpatialSketchEntityId,
pub second: SpatialSketchEntityId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(
feature = "schema",
schemars(into = "SpatialSketchConstraintDefinitionInput")
)]
#[serde(try_from = "SpatialSketchConstraintDefinitionInput")]
pub struct SpatialSketchConstraintDefinition(
SpatialSketchConstraintDefinitionInput<UnitVector3, PositiveLength>,
);
impl SpatialSketchConstraintDefinition {
#[must_use]
pub fn kind(&self) -> &SpatialSketchConstraintDefinitionInput<UnitVector3, PositiveLength> {
&self.0
}
pub fn edit<R>(
&mut self,
edit: impl FnOnce(&mut SpatialSketchConstraintDefinitionInput) -> R,
) -> Result<R, &'static str> {
let mut kind = self.0.to_raw();
let result = edit(&mut kind);
*self = kind.try_into()?;
Ok(result)
}
}
impl TryFrom<SpatialSketchConstraintDefinitionInput> for SpatialSketchConstraintDefinition {
type Error = &'static str;
fn try_from(kind: SpatialSketchConstraintDefinitionInput) -> Result<Self, Self::Error> {
use SpatialSketchConstraintDefinitionInput as Kind;
const INVALID: &str = "invalid spatial sketch constraint local arity or scalar value";
let kind = kind.map_values(
|direction| UnitVector3::new(direction).ok_or(INVALID),
|distance| PositiveLength::try_from(distance).map_err(|_| INVALID),
)?;
let valid = match &kind {
Kind::Native { operands, .. } => !operands.is_empty(),
Kind::LineLength { .. } => true,
Kind::Coincident { first, second }
| Kind::Tangent { first, second }
| Kind::PointDistance { first, second, .. }
| Kind::ParallelLineDistance { first, second, .. } => first != second,
Kind::Symmetric {
first,
second,
axis,
} => first != second && first != axis && second != axis,
Kind::PointOnSurface { point, surface } => point != surface,
Kind::Midpoint { point, entity } => point != entity,
Kind::PointLineDistance { point, line, .. } => point != line,
Kind::RepeatedLineLength { entities, .. } | Kind::SplineGroup { entities } => {
entities.len() >= 2
&& entities
.iter()
.collect::<std::collections::HashSet<_>>()
.len()
== entities.len()
}
Kind::RepeatedParallelLineDistance { pairs, .. } => {
let mut entities = std::collections::HashSet::new();
pairs.len() >= 2
&& pairs
.iter()
.all(|pair| entities.insert(&pair.first) && entities.insert(&pair.second))
}
Kind::ParallelLineSetDistance { first, second, .. } => {
!first.is_empty()
&& !second.is_empty()
&& (first.len() > 1 || second.len() > 1)
&& first
.iter()
.chain(second)
.collect::<std::collections::HashSet<_>>()
.len()
== first.len() + second.len()
}
Kind::Offset {
sources, results, ..
} => {
!sources.is_empty()
&& !results.is_empty()
&& sources
.iter()
.chain(results)
.collect::<std::collections::HashSet<_>>()
.len()
== sources.len() + results.len()
}
Kind::ParallelToDirection { .. } => true,
};
if !valid {
return Err(INVALID);
}
Ok(Self(kind))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SpatialSketchConstraintDefinitionInput<U = Vector3, L = crate::scalar::Length> {
Native {
#[serde(deserialize_with = "deserialize_native_kind")]
native_kind: NonBlankString,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_state"
)]
native_state: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_parameter"
)]
parameter: Option<crate::features::ParameterId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
operands: Vec<SketchNativeOperand>,
},
Coincident {
first: SpatialSketchEntityId,
second: SpatialSketchEntityId,
},
Symmetric {
first: SpatialSketchEntityId,
second: SpatialSketchEntityId,
axis: SpatialSketchEntityId,
},
PointOnSurface {
point: SpatialSketchEntityId,
surface: SpatialSketchEntityId,
},
Midpoint {
point: SpatialSketchEntityId,
entity: SpatialSketchEntityId,
},
Tangent {
first: SpatialSketchEntityId,
second: SpatialSketchEntityId,
},
PointDistance {
first: SpatialSketchEntityId,
second: SpatialSketchEntityId,
parameter: crate::features::ParameterId,
},
PointLineDistance {
point: SpatialSketchEntityId,
line: SpatialSketchEntityId,
parameter: crate::features::ParameterId,
},
LineLength {
entity: SpatialSketchEntityId,
parameter: crate::features::ParameterId,
},
RepeatedLineLength {
entities: Vec<SpatialSketchEntityId>,
parameter: crate::features::ParameterId,
},
ParallelLineDistance {
first: SpatialSketchEntityId,
second: SpatialSketchEntityId,
parameter: crate::features::ParameterId,
},
RepeatedParallelLineDistance {
pairs: Vec<SpatialSketchEntityPair>,
parameter: crate::features::ParameterId,
},
ParallelLineSetDistance {
first: Vec<SpatialSketchEntityId>,
second: Vec<SpatialSketchEntityId>,
parameter: crate::features::ParameterId,
},
Offset {
sources: Vec<SpatialSketchEntityId>,
results: Vec<SpatialSketchEntityId>,
normal: U,
distance: L,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_spatial_sketch_constraint_definition_input_parameter"
)]
parameter: Option<OffsetParameter>,
},
ParallelToDirection {
entity: SpatialSketchEntityId,
direction: U,
},
SplineGroup {
entities: Vec<SpatialSketchEntityId>,
},
}
impl<U, L> SpatialSketchConstraintDefinitionInput<U, L> {
fn map_values<V, M, E>(
self,
mut direction: impl FnMut(U) -> Result<V, E>,
length: impl FnOnce(L) -> Result<M, E>,
) -> Result<SpatialSketchConstraintDefinitionInput<V, M>, E> {
use SpatialSketchConstraintDefinitionInput as Kind;
Ok(match self {
Self::Native {
native_kind,
native_state,
parameter,
operands,
} => Kind::Native {
native_kind,
native_state,
parameter,
operands,
},
Self::Coincident { first, second } => Kind::Coincident { first, second },
Self::Symmetric {
first,
second,
axis,
} => Kind::Symmetric {
first,
second,
axis,
},
Self::PointOnSurface { point, surface } => Kind::PointOnSurface { point, surface },
Self::Midpoint { point, entity } => Kind::Midpoint { point, entity },
Self::Tangent { first, second } => Kind::Tangent { first, second },
Self::PointDistance {
first,
second,
parameter,
} => Kind::PointDistance {
first,
second,
parameter,
},
Self::PointLineDistance {
point,
line,
parameter,
} => Kind::PointLineDistance {
point,
line,
parameter,
},
Self::LineLength { entity, parameter } => Kind::LineLength { entity, parameter },
Self::RepeatedLineLength {
entities,
parameter,
} => Kind::RepeatedLineLength {
entities,
parameter,
},
Self::ParallelLineDistance {
first,
second,
parameter,
} => Kind::ParallelLineDistance {
first,
second,
parameter,
},
Self::RepeatedParallelLineDistance { pairs, parameter } => {
Kind::RepeatedParallelLineDistance { pairs, parameter }
}
Self::ParallelLineSetDistance {
first,
second,
parameter,
} => Kind::ParallelLineSetDistance {
first,
second,
parameter,
},
Self::Offset {
sources,
results,
normal,
distance,
parameter,
} => Kind::Offset {
sources,
results,
normal: direction(normal)?,
distance: length(distance)?,
parameter,
},
Self::ParallelToDirection {
entity,
direction: value,
} => Kind::ParallelToDirection {
entity,
direction: direction(value)?,
},
Self::SplineGroup { entities } => Kind::SplineGroup { entities },
})
}
}
impl SpatialSketchConstraintDefinitionInput<UnitVector3, PositiveLength> {
#[must_use]
pub fn to_raw(&self) -> SpatialSketchConstraintDefinitionInput {
let Ok(raw) = self.clone().map_values(
|direction| Ok::<_, std::convert::Infallible>(Vector3::from(direction)),
|distance| Ok(crate::scalar::Length::from(distance)),
);
raw
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(transparent)]
pub struct SpatialSketchNurbsCurve(crate::geometry::nurbs::NurbsCurve);
impl TryFrom<crate::geometry::nurbs::NurbsCurve> for SpatialSketchNurbsCurve {
type Error = &'static str;
fn try_from(curve: crate::geometry::nurbs::NurbsCurve) -> Result<Self, Self::Error> {
if curve.degree() == 0 {
return Err("spatial sketch NURBS degree must be at least one");
}
if curve
.weights()
.is_some_and(|weights| weights.iter().any(|weight| weight.get() <= 0.0))
{
return Err("spatial sketch NURBS weights must be positive");
}
Ok(Self(curve))
}
}
impl<'de> Deserialize<'de> for SpatialSketchNurbsCurve {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Self::try_from(crate::geometry::nurbs::NurbsCurve::deserialize(
deserializer,
)?)
.map_err(serde::de::Error::custom)
}
}
impl std::ops::Deref for SpatialSketchNurbsCurve {
type Target = crate::geometry::nurbs::NurbsCurve;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl SpatialSketchNurbsCurve {
pub fn try_map_control_points<E>(
&mut self,
map: impl Fn(usize, FinitePoint3) -> Result<FinitePoint3, E>,
) -> Result<(), E> {
self.0.try_map_control_points(map)
}
}
const EPS_SPATIAL_LINE_LENGTH: f64 = 1.0e-12;
const EPS_SPATIAL_CIRCLE_FRAME: f64 = 1.0e-9;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(into = "SpatialSketchGeometryDefinition"))]
#[serde(try_from = "SpatialSketchGeometryDefinition")]
pub struct SpatialSketchGeometry(
SpatialSketchGeometryDefinition<FinitePoint3, UnitVector3, PositiveLength>,
);
impl SpatialSketchGeometry {
#[must_use]
pub fn definition(
&self,
) -> &SpatialSketchGeometryDefinition<FinitePoint3, UnitVector3, PositiveLength> {
&self.0
}
pub fn try_line_from_parts(
start: FinitePoint3,
end: FinitePoint3,
) -> Result<Self, &'static str> {
let start_point = start.get();
let end_point = end.get();
let distance = (end_point.x - start_point.x)
.hypot(end_point.y - start_point.y)
.hypot(end_point.z - start_point.z);
if distance <= EPS_SPATIAL_LINE_LENGTH {
return Err("spatial sketch line endpoints must be finite and separated");
}
Ok(Self(SpatialSketchGeometryDefinition::Line { start, end }))
}
}
type CircularFrame = (FinitePoint3, PositiveLength, UnitVector3, UnitVector3);
fn admit_spatial_circle(
center: Point3,
radius: Length,
normal: Vector3,
reference_direction: Vector3,
) -> Result<CircularFrame, &'static str> {
let (Some(center), Ok(radius)) = (FinitePoint3::new(center), PositiveLength::try_from(radius))
else {
return Err("spatial circular geometry requires finite center and positive finite radius");
};
let (Some(normal), Some(reference_direction)) = (
UnitVector3::new(normal),
UnitVector3::new(reference_direction),
) else {
return Err("spatial circular normal and reference_direction must be unit and orthogonal");
};
if normal.as_raw().dot(*reference_direction.as_raw()).abs() > EPS_SPATIAL_CIRCLE_FRAME {
return Err("spatial circular normal and reference_direction must be unit and orthogonal");
}
Ok((center, radius, normal, reference_direction))
}
impl TryFrom<SpatialSketchGeometryDefinition> for SpatialSketchGeometry {
type Error = &'static str;
fn try_from(definition: SpatialSketchGeometryDefinition) -> Result<Self, Self::Error> {
use SpatialSketchGeometryDefinition as Definition;
Ok(Self(match definition {
Definition::Point { position } => Definition::Point {
position: FinitePoint3::new(position)
.ok_or("spatial sketch point position must be finite")?,
},
Definition::Line { start, end } => {
let distance = (end.x - start.x)
.hypot(end.y - start.y)
.hypot(end.z - start.z);
let (Some(start), Some(end)) = (FinitePoint3::new(start), FinitePoint3::new(end))
else {
return Err("spatial sketch line endpoints must be finite and separated");
};
if distance <= EPS_SPATIAL_LINE_LENGTH {
return Err("spatial sketch line endpoints must be finite and separated");
}
Definition::Line { start, end }
}
Definition::Circle {
center,
normal,
reference_direction,
radius,
} => {
let (center, radius, normal, reference_direction) =
admit_spatial_circle(center, radius, normal, reference_direction)?;
Definition::Circle {
center,
normal,
reference_direction,
radius,
}
}
Definition::Arc {
center,
normal,
reference_direction,
radius,
start_angle,
end_angle,
} => {
let (center, radius, normal, reference_direction) =
admit_spatial_circle(center, radius, normal, reference_direction)?;
if start_angle == end_angle {
return Err("spatial sketch arc angles must be finite and distinct");
}
Definition::Arc {
center,
normal,
reference_direction,
radius,
start_angle,
end_angle,
}
}
Definition::Nurbs { curve } => Definition::Nurbs { curve },
Definition::NurbsSurface { surface } => Definition::NurbsSurface { surface },
Definition::Native { native_kind } => Definition::Native { native_kind },
}))
}
}
impl SpatialSketchGeometryDefinition<FinitePoint3, UnitVector3, PositiveLength> {
#[must_use]
pub fn to_raw(&self) -> SpatialSketchGeometryDefinition {
use SpatialSketchGeometryDefinition as Definition;
match self {
Self::Point { position } => Definition::Point {
position: position.get(),
},
Self::Line { start, end } => Definition::Line {
start: start.get(),
end: end.get(),
},
Self::Circle {
center,
normal,
reference_direction,
radius,
} => Definition::Circle {
center: center.get(),
normal: Vector3::from(*normal),
reference_direction: Vector3::from(*reference_direction),
radius: Length::from(*radius),
},
Self::Arc {
center,
normal,
reference_direction,
radius,
start_angle,
end_angle,
} => Definition::Arc {
center: center.get(),
normal: Vector3::from(*normal),
reference_direction: Vector3::from(*reference_direction),
radius: Length::from(*radius),
start_angle: *start_angle,
end_angle: *end_angle,
},
Self::Nurbs { curve } => Definition::Nurbs {
curve: curve.clone(),
},
Self::NurbsSurface { surface } => Definition::NurbsSurface {
surface: surface.clone(),
},
Self::Native { native_kind } => Definition::Native {
native_kind: native_kind.clone(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SpatialSketchGeometryDefinition<P = Point3, V = Vector3, L = Length> {
Point {
position: P,
},
Line {
start: P,
end: P,
},
Circle {
center: P,
normal: V,
reference_direction: V,
radius: L,
},
Arc {
center: P,
normal: V,
reference_direction: V,
radius: L,
start_angle: Angle,
end_angle: Angle,
},
Nurbs {
curve: SpatialSketchNurbsCurve,
},
NurbsSurface {
surface: crate::geometry::nurbs::BsplineSurface,
},
Native {
#[serde(deserialize_with = "deserialize_native_kind")]
native_kind: NonBlankString,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SketchConstraint {
pub id: SketchConstraintId,
pub sketch: SketchId,
pub definition: SketchConstraintDefinition,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_name"
)]
pub name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_driving"
)]
pub driving: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_active"
)]
pub active: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_virtual_space"
)]
pub virtual_space: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_visible"
)]
pub visible: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_orientation"
)]
pub orientation: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_label_distance"
)]
pub label_distance: Option<SketchLabelValue>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_label_position"
)]
pub label_position: Option<SketchLabelValue>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_metadata"
)]
pub metadata: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", content = "entity", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SketchLocus {
Entity(SketchEntityId),
Start(SketchEntityId),
End(SketchEntityId),
Center(SketchEntityId),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SketchCoordinateAxis {
U,
V,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct NativeOperandField {
pub name: NonBlankString,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_role"
)]
pub role: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SketchNativeOperand {
pub native_kind: NonBlankString,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_field"
)]
pub field: Option<NativeOperandField>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_object_index"
)]
pub object_index: Option<u32>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SketchOffsetPair {
pub source: SketchEntityId,
pub result: SketchEntityId,
#[serde(default)]
pub source_reversed: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct OffsetParameter {
pub id: ParameterId,
pub negated: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SketchPatternDirection {
direction: UnitVector2,
spacing: Length,
pub distance: Option<SketchPatternDistance>,
pub count_parameter: Option<ParameterId>,
}
const EPS_PATTERN_DIRECTION_ORTHOGONALITY: f64 = 1.0e-9;
impl SketchPatternDirection {
pub fn new(
direction: [f64; 2],
spacing: Length,
distance: Option<SketchPatternDistance>,
count_parameter: Option<ParameterId>,
) -> Option<Self> {
let direction = UnitVector2::new(direction)?;
Some(Self {
direction,
spacing,
distance,
count_parameter,
})
}
#[must_use]
pub fn direction(&self) -> UnitVector2 {
self.direction
}
#[must_use]
pub fn spacing(&self) -> Length {
self.spacing
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SketchPatternDistance {
Spacing {
parameter: ParameterId,
},
Span {
parameter: ParameterId,
},
}
impl SketchPatternDistance {
#[must_use]
pub fn parameter(&self) -> &ParameterId {
match self {
Self::Spacing { parameter } | Self::Span { parameter } => parameter,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SketchPatternInstance {
pub entities: Vec<SketchEntityId>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SketchCircularPatternInstance {
pub angle: crate::scalar::NonZeroAngle,
pub entities: Vec<SketchEntityId>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(
try_from = "SketchRectangularPatternWire",
into = "SketchRectangularPatternWire"
)]
pub struct SketchRectangularPattern {
directions: [SketchPatternDirection; 2],
rows: Vec<Vec<SketchPatternInstance>>,
}
impl SketchRectangularPattern {
pub fn new(
directions: [SketchPatternDirection; 2],
rows: Vec<Vec<SketchPatternInstance>>,
) -> Option<Self> {
let row_count = u32::try_from(rows.len()).ok()?;
let column_count = u32::try_from(rows.first()?.len()).ok()?;
if row_count == 0
|| column_count == 0
|| rows
.iter()
.any(|row| row.len() != cadmpeg_core::decode::index_from_u32(column_count))
{
return None;
}
let entity_arity = rows.first()?.first()?.entities.len();
if entity_arity == 0
|| rows
.iter()
.flatten()
.any(|instance| instance.entities.len() != entity_arity)
{
return None;
}
let [first, second] = [directions[0].direction.get(), directions[1].direction.get()];
let dot = first[0] * second[0] + first[1] * second[1];
let mut entities = std::collections::HashSet::new();
if dot.abs() > EPS_PATTERN_DIRECTION_ORTHOGONALITY
|| rows.iter().flatten().any(|instance| {
instance
.entities
.iter()
.any(|entity| !entities.insert(entity))
})
{
return None;
}
Some(Self { directions, rows })
}
#[must_use]
pub fn directions(&self) -> &[SketchPatternDirection; 2] {
&self.directions
}
#[must_use]
pub fn rows(&self) -> &[Vec<SketchPatternInstance>] {
&self.rows
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(
try_from = "SketchCircularPatternWire",
into = "SketchCircularPatternWire"
)]
pub struct SketchCircularPattern {
center: SketchEntityId,
angle: Angle,
angle_parameter: Option<ParameterId>,
count_parameter: Option<ParameterId>,
seed: Vec<SketchEntityId>,
instances: SeededMembers<SketchCircularPatternInstance>,
}
#[derive(Debug, Clone, PartialEq)]
struct SeededMembers<T> {
members: Vec<T>,
}
impl<T> TryFrom<Vec<T>> for SeededMembers<T> {
type Error = &'static str;
fn try_from(members: Vec<T>) -> Result<Self, Self::Error> {
if members.is_empty() {
return Err("population states no member");
}
let Some(_) = u32::try_from(members.len())
.ok()
.and_then(|population| population.checked_add(1))
else {
return Err("population holds more members than the seeded count can state");
};
Ok(Self { members })
}
}
impl<T> std::ops::Deref for SeededMembers<T> {
type Target = [T];
fn deref(&self) -> &[T] {
&self.members
}
}
impl<T> IntoIterator for SeededMembers<T> {
type Item = T;
type IntoIter = std::vec::IntoIter<T>;
fn into_iter(self) -> Self::IntoIter {
self.members.into_iter()
}
}
impl<'a, T> IntoIterator for &'a SeededMembers<T> {
type Item = &'a T;
type IntoIter = std::slice::Iter<'a, T>;
fn into_iter(self) -> Self::IntoIter {
self.members.iter()
}
}
const CIRCULAR_PATTERN_ARITY: &str =
"circular pattern seed and instances must share one fixed positive entity arity over distinct entities";
impl SketchCircularPattern {
pub fn new(
center: SketchEntityId,
angle: Angle,
angle_parameter: Option<ParameterId>,
count_parameter: Option<ParameterId>,
seed: Vec<SketchEntityId>,
instances: Vec<SketchCircularPatternInstance>,
) -> Option<Self> {
Self::admit(
center,
angle,
angle_parameter,
count_parameter,
seed,
instances,
)
.ok()
}
fn admit(
center: SketchEntityId,
angle: Angle,
angle_parameter: Option<ParameterId>,
count_parameter: Option<ParameterId>,
seed: Vec<SketchEntityId>,
instances: Vec<SketchCircularPatternInstance>,
) -> Result<Self, &'static str> {
let entity_arity = seed.len();
if entity_arity == 0
|| instances
.iter()
.any(|instance| instance.entities.len() != entity_arity)
{
return Err(CIRCULAR_PATTERN_ARITY);
}
let mut entities = std::collections::HashSet::new();
if seed
.iter()
.chain(
instances
.iter()
.flat_map(|instance| instance.entities.iter()),
)
.any(|entity| entity == ¢er || !entities.insert(entity))
{
return Err(CIRCULAR_PATTERN_ARITY);
}
Ok(Self {
center,
angle,
angle_parameter,
count_parameter,
seed,
instances: SeededMembers::try_from(instances)?,
})
}
#[must_use]
pub fn center(&self) -> &SketchEntityId {
&self.center
}
#[must_use]
pub fn angle_parameter(&self) -> Option<&ParameterId> {
self.angle_parameter.as_ref()
}
#[must_use]
pub fn count_parameter(&self) -> Option<&ParameterId> {
self.count_parameter.as_ref()
}
#[must_use]
pub fn instances(&self) -> &[SketchCircularPatternInstance] {
&self.instances
}
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SketchPatternDirectionWire {
direction: [f64; 2],
spacing: Length,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance"
)]
distance: Option<SketchPatternDistance>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_count_parameter"
)]
count_parameter: Option<ParameterId>,
}
impl SketchPatternDirectionWire {
fn from_direction(value: &SketchPatternDirection) -> Self {
Self {
direction: value.direction.get(),
spacing: value.spacing,
distance: value.distance.clone(),
count_parameter: value.count_parameter.clone(),
}
}
fn into_direction(self) -> Result<SketchPatternDirection, &'static str> {
SketchPatternDirection::new(
self.direction,
self.spacing,
self.distance,
self.count_parameter,
)
.ok_or("pattern direction must be finite and unit, with finite spacing")
}
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(rename = "SketchRectangularPattern"))]
#[serde(deny_unknown_fields)]
struct SketchRectangularPatternWire {
directions: [SketchPatternDirectionWire; 2],
rows: Vec<Vec<SketchPatternInstance>>,
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(rename = "SketchCircularPattern"))]
#[serde(deny_unknown_fields)]
struct SketchCircularPatternWire {
center: SketchEntityId,
angle: Angle,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angle_parameter"
)]
angle_parameter: Option<ParameterId>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_count_parameter"
)]
count_parameter: Option<ParameterId>,
seed: Vec<SketchEntityId>,
instances: Vec<SketchCircularPatternInstance>,
}
impl From<SketchRectangularPattern> for SketchRectangularPatternWire {
fn from(pattern: SketchRectangularPattern) -> Self {
Self {
directions: [
SketchPatternDirectionWire::from_direction(&pattern.directions[0]),
SketchPatternDirectionWire::from_direction(&pattern.directions[1]),
],
rows: pattern.rows,
}
}
}
impl TryFrom<SketchRectangularPatternWire> for SketchRectangularPattern {
type Error = &'static str;
fn try_from(wire: SketchRectangularPatternWire) -> Result<Self, Self::Error> {
let [first, second] = wire.directions;
let directions = [first.into_direction()?, second.into_direction()?];
Self::new(directions, wire.rows).ok_or(
"rectangular pattern rows must form one non-empty grid whose instances have one fixed positive entity arity",
)
}
}
impl From<SketchCircularPattern> for SketchCircularPatternWire {
fn from(pattern: SketchCircularPattern) -> Self {
Self {
center: pattern.center,
angle: pattern.angle,
angle_parameter: pattern.angle_parameter,
count_parameter: pattern.count_parameter,
seed: pattern.seed,
instances: pattern.instances.into_iter().collect(),
}
}
}
impl TryFrom<SketchCircularPatternWire> for SketchCircularPattern {
type Error = &'static str;
fn try_from(wire: SketchCircularPatternWire) -> Result<Self, Self::Error> {
Self::admit(
wire.center,
wire.angle,
wire.angle_parameter,
wire.count_parameter,
wire.seed,
wire.instances,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SketchDistanceMeasurement {
Distance {
first: SketchLocus,
second: SketchLocus,
},
Horizontal {
first: SketchLocus,
second: SketchLocus,
},
Vertical {
first: SketchLocus,
second: SketchLocus,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct SketchDistancePair {
pub first: SketchLocus,
pub second: SketchLocus,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(
from = "SketchInternalAlignmentWire",
into = "SketchInternalAlignmentWire"
)]
pub enum SketchInternalAlignment {
EllipseMajorDiameter,
EllipseMinorDiameter,
EllipseFocus1,
EllipseFocus2,
HyperbolaMajor,
HyperbolaMinor,
HyperbolaFocus,
ParabolaFocus,
BsplineControlPoint(u32),
BsplineKnotPoint(u32),
ParabolaFocalAxis,
}
#[derive(Clone, Copy, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "alignment", rename_all = "snake_case", deny_unknown_fields)]
enum SketchInternalAlignmentWire {
EllipseMajorDiameter {},
EllipseMinorDiameter {},
EllipseFocus1 {},
EllipseFocus2 {},
HyperbolaMajor {},
HyperbolaMinor {},
HyperbolaFocus {},
ParabolaFocus {},
BsplineControlPoint {
index: u32,
},
BsplineKnotPoint {
index: u32,
},
ParabolaFocalAxis {},
}
impl From<SketchInternalAlignmentWire> for SketchInternalAlignment {
fn from(wire: SketchInternalAlignmentWire) -> Self {
match wire {
SketchInternalAlignmentWire::EllipseMajorDiameter {} => Self::EllipseMajorDiameter,
SketchInternalAlignmentWire::EllipseMinorDiameter {} => Self::EllipseMinorDiameter,
SketchInternalAlignmentWire::EllipseFocus1 {} => Self::EllipseFocus1,
SketchInternalAlignmentWire::EllipseFocus2 {} => Self::EllipseFocus2,
SketchInternalAlignmentWire::HyperbolaMajor {} => Self::HyperbolaMajor,
SketchInternalAlignmentWire::HyperbolaMinor {} => Self::HyperbolaMinor,
SketchInternalAlignmentWire::HyperbolaFocus {} => Self::HyperbolaFocus,
SketchInternalAlignmentWire::ParabolaFocus {} => Self::ParabolaFocus,
SketchInternalAlignmentWire::BsplineControlPoint { index } => {
Self::BsplineControlPoint(index)
}
SketchInternalAlignmentWire::BsplineKnotPoint { index } => {
Self::BsplineKnotPoint(index)
}
SketchInternalAlignmentWire::ParabolaFocalAxis {} => Self::ParabolaFocalAxis,
}
}
}
impl From<SketchInternalAlignment> for SketchInternalAlignmentWire {
fn from(value: SketchInternalAlignment) -> Self {
match value {
SketchInternalAlignment::EllipseMajorDiameter => Self::EllipseMajorDiameter {},
SketchInternalAlignment::EllipseMinorDiameter => Self::EllipseMinorDiameter {},
SketchInternalAlignment::EllipseFocus1 => Self::EllipseFocus1 {},
SketchInternalAlignment::EllipseFocus2 => Self::EllipseFocus2 {},
SketchInternalAlignment::HyperbolaMajor => Self::HyperbolaMajor {},
SketchInternalAlignment::HyperbolaMinor => Self::HyperbolaMinor {},
SketchInternalAlignment::HyperbolaFocus => Self::HyperbolaFocus {},
SketchInternalAlignment::ParabolaFocus => Self::ParabolaFocus {},
SketchInternalAlignment::BsplineControlPoint(index) => {
Self::BsplineControlPoint { index }
}
SketchInternalAlignment::BsplineKnotPoint(index) => Self::BsplineKnotPoint { index },
SketchInternalAlignment::ParabolaFocalAxis => Self::ParabolaFocalAxis {},
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SketchPolygonWire")]
pub struct SketchPolygon {
entities: Vec<SketchEntityId>,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SketchPolygonWire {
entities: Vec<SketchEntityId>,
}
impl TryFrom<SketchPolygonWire> for SketchPolygon {
type Error = SketchCollectionError;
fn try_from(wire: SketchPolygonWire) -> Result<Self, Self::Error> {
let invalid = "entities requires at least three distinct polygon members";
if wire.entities.len() < 3 {
return Err(SketchCollectionError::Invalid(invalid));
}
let mut members = std::collections::HashSet::new();
members
.try_reserve(wire.entities.len())
.map_err(|error| SketchCollectionError::Admission(error.to_string()))?;
for entity in &wire.entities {
if !members.insert(entity) {
return Err(SketchCollectionError::Invalid(invalid));
}
}
Ok(Self {
entities: wire.entities,
})
}
}
impl SketchPolygon {
pub fn try_new(entities: Vec<SketchEntityId>) -> Result<Self, &'static str> {
if entities.len() < 3 {
return Err("entities requires at least three distinct polygon members");
}
if !distinct_sketch_members(entities.iter().map(SketchEntityId::as_str)) {
return Err("entities requires at least three distinct polygon members");
}
Ok(Self { entities })
}
pub fn entities(&self) -> &[SketchEntityId] {
&self.entities
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SketchSameCoordinateWire")]
pub struct SketchSameCoordinate {
first: SketchLocus,
second: SketchLocus,
axis: SketchCoordinateAxis,
}
#[derive(Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
struct SketchSameCoordinateWire {
first: SketchLocus,
second: SketchLocus,
axis: SketchCoordinateAxis,
}
impl TryFrom<SketchSameCoordinateWire> for SketchSameCoordinate {
type Error = &'static str;
fn try_from(wire: SketchSameCoordinateWire) -> Result<Self, Self::Error> {
Self::try_new(wire.first, wire.second, wire.axis)
}
}
impl SketchSameCoordinate {
pub fn try_new(
first: SketchLocus,
second: SketchLocus,
axis: SketchCoordinateAxis,
) -> Result<Self, &'static str> {
if first == second {
return Err("first and second require distinct loci");
}
Ok(Self {
first,
second,
axis,
})
}
pub fn first(&self) -> &SketchLocus {
&self.first
}
pub fn second(&self) -> &SketchLocus {
&self.second
}
pub fn axis(&self) -> SketchCoordinateAxis {
self.axis
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "f64")]
pub struct SketchLabelValue(f64);
impl TryFrom<f64> for SketchLabelValue {
type Error = &'static str;
fn try_from(value: f64) -> Result<Self, Self::Error> {
if !value.is_finite() {
return Err("sketch constraint label coordinate must be finite");
}
Ok(Self(value))
}
}
const EPS_POLAR_DISTANCE_ZERO: f64 = 1.0e-12;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(try_from = "SketchConstraintDefinitionInput")]
pub struct SketchConstraintDefinition(SketchConstraintDefinitionInput);
impl SketchConstraintDefinition {
#[must_use]
pub fn native_with_operand(
native_kind: NonBlankString,
native_properties: BTreeMap<String, String>,
entities: Vec<SketchEntityId>,
operand: SketchNativeOperand,
) -> Self {
Self(SketchConstraintDefinitionInput::Native {
native_kind,
native_state: None,
native_flags: None,
native_properties,
entities,
parameter: None,
operands: vec![operand],
})
}
#[must_use]
pub fn kind(&self) -> &SketchConstraintDefinitionInput {
&self.0
}
pub fn set_offset_parameter(&mut self, driving: OffsetParameter) -> bool {
let SketchConstraintDefinitionInput::Offset { parameter, .. } = &mut self.0 else {
return false;
};
*parameter = Some(driving);
true
}
#[must_use]
pub fn into_kind(self) -> SketchConstraintDefinitionInput {
self.0
}
pub fn edit<R>(
&mut self,
edit: impl FnOnce(&mut SketchConstraintDefinitionInput) -> R,
) -> Result<R, &'static str> {
let mut kind = self.0.clone();
let result = edit(&mut kind);
*self = kind.try_into()?;
Ok(result)
}
}
impl TryFrom<SketchConstraintDefinitionInput> for SketchConstraintDefinition {
type Error = &'static str;
fn try_from(kind: SketchConstraintDefinitionInput) -> Result<Self, Self::Error> {
use SketchConstraintDefinitionInput as Kind;
let valid = match &kind {
Kind::Coincident { entities } | Kind::SplineGroup { entities } => entities.len() >= 2,
Kind::CoincidentLoci { loci } => loci.len() >= 2,
Kind::Distance { entities, .. } => !entities.is_empty(),
Kind::TextFrame { text, frame } => {
!frame.is_empty() && frame.iter().all(|entity| entity != text)
}
Kind::TextPath {
text,
path,
glyph_transforms,
} => text != path && !glyph_transforms.is_empty(),
Kind::DistanceLociValue { distance, .. } => distance.get() >= 0.0,
Kind::PolarDistance {
distance, angle, ..
} => {
distance.get() >= 0.0
&& if distance.get() <= EPS_POLAR_DISTANCE_ZERO {
angle.is_none()
} else {
angle.is_some()
}
}
Kind::AngleDifference { value, .. } => {
(0.0..=std::f64::consts::PI).contains(&value.get())
}
Kind::ScalarEquality { first, second } => first != second,
Kind::RepeatedDistance { measurements, .. } => {
let mut entities = std::collections::HashSet::new();
!measurements.is_empty()
&& measurements.iter().all(|measurement| {
let (first, second) = match measurement {
SketchDistanceMeasurement::Distance { first, second }
| SketchDistanceMeasurement::Horizontal { first, second }
| SketchDistanceMeasurement::Vertical { first, second } => {
(first, second)
}
};
let entity = |locus: &SketchLocus| match locus {
SketchLocus::Entity(id)
| SketchLocus::Start(id)
| SketchLocus::End(id)
| SketchLocus::Center(id) => id.clone(),
};
entities.insert(entity(first)) && entities.insert(entity(second))
})
}
Kind::RepeatedLength { entities, .. }
| Kind::RepeatedRadius { entities, .. }
| Kind::RepeatedDiameter { entities, .. } => {
entities.len() >= 2
&& entities
.iter()
.collect::<std::collections::HashSet<_>>()
.len()
== entities.len()
}
Kind::ParallelLineSetDistance { first, second, .. } => {
!first.is_empty()
&& !second.is_empty()
&& (first.len() > 1 || second.len() > 1)
&& first
.iter()
.chain(second)
.collect::<std::collections::HashSet<_>>()
.len()
== first.len() + second.len()
}
Kind::Offset {
pairs, distance, ..
} => {
let mut sources = std::collections::HashSet::new();
let mut results = std::collections::HashSet::new();
!pairs.is_empty()
&& distance.get() > 0.0
&& pairs.iter().all(|pair| {
pair.source != pair.result
&& sources.insert(&pair.source)
&& results.insert(&pair.result)
})
}
Kind::ProjectedCopy { source, result } => source != result,
Kind::Group { elements } | Kind::Text { elements, .. } => !elements.is_empty(),
Kind::Native {
entities, operands, ..
} => !entities.is_empty() || !operands.is_empty(),
Kind::Disabled {} => true,
Kind::Polygon { .. } => true,
Kind::RectangularPattern { .. } => true,
Kind::CircularPattern { .. } => true,
Kind::SameCoordinate { .. } => true,
Kind::PointOnObject { .. } => true,
Kind::Midpoint { .. } => true,
Kind::PointCoordinateValues { .. } => true,
Kind::MidpointCoordinate { .. } => true,
Kind::AtIntersection { .. } => true,
Kind::Concentric { .. } => true,
Kind::Coradial { .. } => true,
Kind::Collinear { .. } => true,
Kind::Symmetric { .. } => true,
Kind::PointSymmetric { .. } => true,
Kind::Horizontal { .. } => true,
Kind::Vertical { .. } => true,
Kind::Parallel { .. } => true,
Kind::Perpendicular { .. } => true,
Kind::Tangent { .. } => true,
Kind::TangentLoci { .. } => true,
Kind::Curvature { .. } => true,
Kind::Equal { .. } => true,
Kind::Fixed { .. } => true,
Kind::ArcAngle { .. } => true,
Kind::EllipseAngle { .. } => true,
Kind::DistanceLoci { .. } => true,
Kind::EqualDistance { .. } => true,
Kind::HorizontalDistance { .. } => true,
Kind::VerticalDistance { .. } => true,
Kind::Angle { .. } => true,
Kind::AngleToAxis { .. } => true,
Kind::Radius { .. } => true,
Kind::Diameter { .. } => true,
Kind::SnellsLaw { .. } => true,
Kind::Weight { .. } => true,
Kind::InternalAlignment { .. } => true,
};
if !valid {
return Err("invalid sketch constraint local arity or scalar value");
}
Ok(Self(kind))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum SketchConstraintDefinitionInput {
Disabled {},
Coincident {
entities: Vec<SketchEntityId>,
},
Polygon {
polygon: SketchPolygon,
},
SplineGroup {
entities: Vec<SketchEntityId>,
},
RectangularPattern {
pattern: SketchRectangularPattern,
},
CircularPattern {
pattern: SketchCircularPattern,
},
TextFrame {
text: SketchEntityId,
frame: Vec<SketchEntityId>,
},
TextPath {
text: SketchEntityId,
path: SketchEntityId,
glyph_transforms: Vec<Transform>,
},
CoincidentLoci {
loci: Vec<SketchLocus>,
},
SameCoordinate {
relation: SketchSameCoordinate,
},
PointOnObject {
point: SketchLocus,
entity: SketchEntityId,
},
Midpoint {
point: SketchLocus,
entity: SketchEntityId,
},
PointCoordinateValues {
point: SketchLocus,
values: [Length; 2],
},
MidpointCoordinate {
first: SketchLocus,
second: SketchLocus,
axis: SketchCoordinateAxis,
value: Length,
},
Offset {
pairs: Vec<SketchOffsetPair>,
distance: Length,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_spatial_sketch_constraint_definition_input_parameter"
)]
parameter: Option<OffsetParameter>,
},
ProjectedCopy {
source: SketchEntityId,
result: SketchEntityId,
},
AtIntersection {
point: SketchLocus,
first: SketchEntityId,
second: SketchEntityId,
},
Concentric {
first: SketchEntityId,
second: SketchEntityId,
},
Coradial {
first: SketchEntityId,
second: SketchEntityId,
},
Collinear {
first: SketchEntityId,
second: SketchEntityId,
},
Symmetric {
first: SketchLocus,
second: SketchLocus,
axis: SketchEntityId,
},
PointSymmetric {
first: SketchLocus,
second: SketchLocus,
center: SketchLocus,
},
Horizontal {
entity: SketchEntityId,
},
Vertical {
entity: SketchEntityId,
},
Parallel {
first: SketchEntityId,
second: SketchEntityId,
},
Perpendicular {
first: SketchEntityId,
second: SketchEntityId,
},
Tangent {
first: SketchEntityId,
second: SketchEntityId,
},
TangentLoci {
first: SketchLocus,
second: SketchLocus,
},
Curvature {
first: SketchEntityId,
second: SketchEntityId,
},
Equal {
first: SketchEntityId,
second: SketchEntityId,
},
Fixed {
entity: SketchEntityId,
},
ArcAngle {
entity: SketchEntityId,
angle: PositiveAngle,
},
EllipseAngle {
entity: SketchEntityId,
angle: PositiveAngle,
},
Distance {
entities: Vec<SketchEntityId>,
parameter: ParameterId,
},
DistanceLoci {
first: SketchLocus,
second: SketchLocus,
parameter: ParameterId,
},
DistanceLociValue {
first: SketchLocus,
second: SketchLocus,
distance: Length,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_sketch_constraint_definition_input_parameter"
)]
parameter: Option<ParameterId>,
},
PolarDistance {
first: SketchLocus,
second: SketchLocus,
distance: Length,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angle"
)]
angle: Option<Angle>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance_parameter"
)]
distance_parameter: Option<ParameterId>,
},
AngleDifference {
first: u32,
second: u32,
difference: u32,
value: Angle,
},
ScalarEquality {
first: u32,
second: u32,
},
EqualDistance {
first: SketchDistancePair,
second: SketchDistancePair,
},
HorizontalDistance {
first: SketchLocus,
second: SketchLocus,
parameter: ParameterId,
},
VerticalDistance {
first: SketchLocus,
second: SketchLocus,
parameter: ParameterId,
},
RepeatedDistance {
measurements: Vec<SketchDistanceMeasurement>,
parameter: ParameterId,
},
RepeatedLength {
entities: Vec<SketchEntityId>,
parameter: ParameterId,
},
ParallelLineSetDistance {
first: Vec<SketchEntityId>,
second: Vec<SketchEntityId>,
parameter: ParameterId,
},
Angle {
first: SketchEntityId,
second: SketchEntityId,
parameter: ParameterId,
},
AngleToAxis {
entity: SketchEntityId,
axis: SketchAxis,
parameter: ParameterId,
},
Radius {
entity: SketchEntityId,
parameter: ParameterId,
},
RepeatedRadius {
entities: Vec<SketchEntityId>,
parameter: ParameterId,
},
Diameter {
entity: SketchEntityId,
parameter: ParameterId,
},
RepeatedDiameter {
entities: Vec<SketchEntityId>,
parameter: ParameterId,
},
SnellsLaw {
incident: SketchLocus,
refracted: SketchLocus,
interface: SketchEntityId,
parameter: ParameterId,
},
Weight {
entity: SketchEntityId,
parameter: ParameterId,
},
InternalAlignment {
helper: SketchEntityId,
parent: SketchEntityId,
alignment: SketchInternalAlignment,
},
Group {
elements: Vec<SketchLocus>,
},
Text {
elements: Vec<SketchLocus>,
text: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_font"
)]
font: Option<String>,
is_text_height: bool,
},
Native {
#[serde(deserialize_with = "deserialize_native_kind")]
native_kind: NonBlankString,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_state"
)]
native_state: Option<u64>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_flags"
)]
native_flags: Option<u64>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
native_properties: BTreeMap<String, String>,
entities: Vec<SketchEntityId>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_sketch_constraint_definition_input_parameter"
)]
parameter: Option<ParameterId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
operands: Vec<SketchNativeOperand>,
},
}
impl SketchConstraintDefinitionInput {
#[must_use]
pub fn entity_kind_restriction(
&self,
) -> Option<(&SketchEntityId, SketchEntityKindRestriction)> {
match self {
Self::Midpoint { entity, .. } => {
Some((entity, SketchEntityKindRestriction::BoundedCurve))
}
Self::ArcAngle { entity, .. } => {
Some((entity, SketchEntityKindRestriction::CircularArc))
}
Self::EllipseAngle { entity, .. } => {
Some((entity, SketchEntityKindRestriction::BoundedEllipse))
}
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SketchEntityKindRestriction {
BoundedCurve,
CircularArc,
BoundedEllipse,
}
impl SketchEntityKindRestriction {
#[must_use]
pub fn admits<P, L, R, W, D, M>(
self,
geometry: &SketchGeometryDefinition<P, L, R, W, D, M>,
) -> bool {
if matches!(
geometry,
SketchGeometryDefinition::ExternalReference { .. }
| SketchGeometryDefinition::Native { .. }
) {
return true;
}
match self {
Self::BoundedCurve => matches!(
geometry,
SketchGeometryDefinition::Line { .. }
| SketchGeometryDefinition::Arc { .. }
| SketchGeometryDefinition::Ellipse {
bounds: Some(_),
..
}
| SketchGeometryDefinition::Hyperbola {
bounds: Some(_),
..
}
| SketchGeometryDefinition::Parabola {
bounds: Some(_),
..
}
| SketchGeometryDefinition::Nurbs { .. }
),
Self::CircularArc => matches!(geometry, SketchGeometryDefinition::Arc { .. }),
Self::BoundedEllipse => matches!(
geometry,
SketchGeometryDefinition::Ellipse {
bounds: Some(_),
..
}
),
}
}
}
crate::units::named_field!(
deserialize_object,
cadmpeg_core::text::NonBlankString,
"object"
);
crate::units::named_field!(
deserialize_native_kind,
cadmpeg_core::text::NonBlankString,
"native_kind"
);
#[cfg(test)]
mod tests;
cadmpeg_core::named_optional_field!(deserialize_name, String, "name");
cadmpeg_core::named_optional_field!(deserialize_configuration, String, "configuration");
cadmpeg_core::named_optional_field!(deserialize_visible, bool, "visible");
cadmpeg_core::named_optional_field!(deserialize_native_ref, String, "native_ref");
cadmpeg_core::named_optional_field!(deserialize_geometry_ref, String, "geometry_ref");
cadmpeg_core::named_optional_field!(deserialize_bounds, [Angle; 2], "bounds");
cadmpeg_core::named_optional_field!(
deserialize_sketch_geometry_definition_bounds<R>,
[R; 2],
"bounds"
);
cadmpeg_core::named_optional_field!(deserialize_width_factor<W>, W, "width_factor");
cadmpeg_core::named_optional_field!(deserialize_placement<P>, TextPlacement<P>, "placement");
cadmpeg_core::named_optional_field!(
deserialize_horizontal_alignment,
SketchTextHorizontalAlignment,
"horizontal_alignment"
);
cadmpeg_core::named_optional_field!(
deserialize_vertical_alignment,
SketchTextVerticalAlignment,
"vertical_alignment"
);
cadmpeg_core::named_optional_field!(deserialize_document, String, "document");
cadmpeg_core::named_optional_field!(deserialize_native_state, u64, "native_state");
cadmpeg_core::named_optional_field!(
deserialize_parameter,
crate::features::ParameterId,
"parameter"
);
cadmpeg_core::named_optional_field!(
deserialize_spatial_sketch_constraint_definition_input_parameter,
OffsetParameter,
"parameter"
);
cadmpeg_core::named_optional_field!(deserialize_driving, bool, "driving");
cadmpeg_core::named_optional_field!(deserialize_active, bool, "active");
cadmpeg_core::named_optional_field!(deserialize_virtual_space, bool, "virtual_space");
cadmpeg_core::named_optional_field!(deserialize_orientation, u32, "orientation");
cadmpeg_core::named_optional_field!(
deserialize_label_distance,
SketchLabelValue,
"label_distance"
);
cadmpeg_core::named_optional_field!(
deserialize_label_position,
SketchLabelValue,
"label_position"
);
cadmpeg_core::named_optional_field!(deserialize_metadata, String, "metadata");
cadmpeg_core::named_optional_field!(deserialize_role, u32, "role");
cadmpeg_core::named_optional_field!(deserialize_field, NativeOperandField, "field");
cadmpeg_core::named_optional_field!(deserialize_object_index, u32, "object_index");
cadmpeg_core::named_optional_field!(deserialize_distance, SketchPatternDistance, "distance");
cadmpeg_core::named_optional_field!(deserialize_count_parameter, ParameterId, "count_parameter");
cadmpeg_core::named_optional_field!(deserialize_angle_parameter, ParameterId, "angle_parameter");
cadmpeg_core::named_optional_field!(
deserialize_sketch_constraint_definition_input_parameter,
ParameterId,
"parameter"
);
cadmpeg_core::named_optional_field!(deserialize_angle, Angle, "angle");
cadmpeg_core::named_optional_field!(
deserialize_distance_parameter,
ParameterId,
"distance_parameter"
);
cadmpeg_core::named_optional_field!(deserialize_font, String, "font");
cadmpeg_core::named_optional_field!(deserialize_native_flags, u64, "native_flags");
mod identity_rewrite;