#[cfg(feature = "schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
pub(crate) mod assembly_graph;
use cadmpeg_core::text::NonBlankString;
use crate::ids::{BodyId, OccurrenceId, ProductDefinitionId};
use crate::scalar::FiniteReal;
use crate::transform::Transform;
crate::ids::id_type!(
JointId, compose
);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum ProductDefinitionKind {
Part,
Group,
LinkGroup,
Object,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ProductDefinition {
pub id: ProductDefinitionId,
pub kind: ProductDefinitionKind,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_source_name"
)]
pub source_name: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_label"
)]
pub label: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_description"
)]
pub description: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_part_number"
)]
pub part_number: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
#[serde(deserialize_with = "cadmpeg_core::distinct_keys::btree_map")]
pub bom_properties: BTreeMap<NonBlankString, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub bodies: Vec<BodyId>,
#[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 = "scope", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
pub enum PrototypeReference {
Local {
definition: ProductDefinitionId,
},
External {
document: ExternalDocument,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_object"
)]
object: Option<String>,
},
Unresolved {},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "resolution", rename_all = "snake_case", deny_unknown_fields)]
pub enum ExternalDocument {
Path {
path: NonBlankString,
},
DocumentId {
document_id: NonBlankString,
},
Missing {},
}
impl ExternalDocument {
pub fn path(path: impl Into<String>) -> Self {
match NonBlankString::new(path) {
Some(path) => Self::Path { path },
None => Self::Missing {},
}
}
pub fn document_id(document_id: impl Into<String>) -> Self {
match NonBlankString::new(document_id) {
Some(document_id) => Self::DocumentId { document_id },
None => Self::Missing {},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(
tag = "policy",
content = "native_policy",
rename_all = "snake_case",
deny_unknown_fields
)]
pub enum CopyOnChangePolicy {
Disabled,
Enabled,
Owned,
Tracking,
Native(String),
}
#[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 OccurrenceParent {
Root {},
Occurrence {
occurrence: OccurrenceId,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct Occurrence {
pub id: OccurrenceId,
pub prototype: PrototypeReference,
pub parent: OccurrenceParent,
pub ordinal: u32,
pub transform: Transform,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_linked_prototype"
)]
pub linked_prototype: Option<Transform>,
#[serde(deserialize_with = "deserialize_occurrence_scale")]
pub scale: [FiniteReal; 3],
#[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_visible"
)]
pub visible: Option<bool>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_link"
)]
pub link: Option<LinkState>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
}
#[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 LinkMember {
LinkedSubelement {
subelement: String,
},
ElementComponent {
component: ProductDefinitionId,
},
ClaimChild {
claim: bool,
},
CopyOnChange {
state: CopyOnChange,
},
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(with = "LinkStateWire"))]
#[serde(try_from = "LinkStateWire", into = "LinkStateWire")]
pub struct LinkState {
linked_subelements: Vec<String>,
element_component: Option<ProductDefinitionId>,
claim_child: Option<bool>,
copy_on_change: Option<CopyOnChange>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[cfg_attr(feature = "schema", schemars(rename = "LinkState"))]
#[serde(deny_unknown_fields)]
struct LinkStateWire {
members: Vec<LinkMember>,
}
impl TryFrom<LinkStateWire> for LinkState {
type Error = &'static str;
fn try_from(wire: LinkStateWire) -> Result<Self, Self::Error> {
if wire.members.is_empty() {
return Err("link state states at least one member");
}
let mut state = Self {
linked_subelements: Vec::new(),
element_component: None,
claim_child: None,
copy_on_change: None,
};
for member in wire.members {
match member {
LinkMember::LinkedSubelement { subelement } => {
state.linked_subelements.push(subelement);
}
LinkMember::ElementComponent { component } => {
if state.element_component.replace(component).is_some() {
return Err("link state states element_component once");
}
}
LinkMember::ClaimChild { claim } => {
if state.claim_child.replace(claim).is_some() {
return Err("link state states claim_child once");
}
}
LinkMember::CopyOnChange {
state: copy_on_change,
} => {
if state.copy_on_change.replace(copy_on_change).is_some() {
return Err("link state states copy_on_change once");
}
}
}
}
Ok(state)
}
}
impl LinkState {
pub fn new(
linked_subelements: Vec<String>,
element_component: Option<ProductDefinitionId>,
claim_child: Option<bool>,
copy_on_change: Option<CopyOnChange>,
) -> Option<Self> {
if linked_subelements.is_empty()
&& element_component.is_none()
&& claim_child.is_none()
&& copy_on_change.is_none()
{
return None;
}
Some(Self {
linked_subelements,
element_component,
claim_child,
copy_on_change,
})
}
pub fn linked_subelements(&self) -> &[String] {
&self.linked_subelements
}
pub fn element_component(&self) -> Option<&ProductDefinitionId> {
self.element_component.as_ref()
}
pub const fn claim_child(&self) -> Option<bool> {
self.claim_child
}
pub fn copy_on_change(&self) -> Option<&CopyOnChange> {
self.copy_on_change.as_ref()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct CopyOnChange {
pub policy: CopyOnChangePolicy,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_source"
)]
pub source: Option<PrototypeReference>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_group"
)]
pub group: Option<PrototypeReference>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_touched"
)]
pub touched: Option<bool>,
}
crate::units::named_field!(deserialize_occurrence_scale, [FiniteReal; 3], "scale");
impl Occurrence {
pub fn effective_transform(&self) -> Result<Transform, crate::transform::TransformError> {
self.linked_prototype
.map_or(Ok(self.transform), |prototype| {
self.transform.compose(prototype)
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssemblyGraphError {
DuplicateOccurrence(OccurrenceId),
MissingParent {
occurrence: OccurrenceId,
parent: OccurrenceId,
},
ParentCycle(OccurrenceId),
Transform {
occurrence: OccurrenceId,
source: crate::transform::TransformError,
},
}
impl std::fmt::Display for AssemblyGraphError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DuplicateOccurrence(id) => write!(formatter, "duplicate occurrence {id}"),
Self::MissingParent { occurrence, parent } => {
write!(
formatter,
"occurrence {occurrence} has missing parent {parent}"
)
}
Self::ParentCycle(id) => write!(formatter, "occurrence parent cycle at {id}"),
Self::Transform { occurrence, source } => {
write!(formatter, "occurrence {occurrence}: {source}")
}
}
}
}
impl std::error::Error for AssemblyGraphError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Transform { source, .. } => Some(source),
_ => None,
}
}
}
pub struct AssemblyGraph<'a> {
occurrences: assembly_graph::Facts<'a>,
}
#[cfg(test)]
mod tests {
mod joints;
mod occurrences;
use super::{
AssemblyGraph, AssemblyGraphError, CopyOnChange, CopyOnChangePolicy, ExternalDocument,
JointLimits, JointOperand, LinkState, Occurrence, OccurrenceParent, PrototypeReference,
};
use crate::ids::{OccurrenceId, ProductDefinitionId};
use crate::transform::Transform;
#[test]
fn a_blank_selection_id_has_no_wire_spelling() {
assert!(serde_json::from_value::<crate::features::SelectionMember>(
serde_json::json!({"kind": "face", "id": " "})
)
.is_err());
assert!(serde_json::from_value::<crate::features::SelectionMember>(
serde_json::json!({"kind": "face", "id": " a "})
)
.is_ok());
let topology = |id: &str| {
serde_json::json!({
"id": "test:model:feature-result#1",
"output_of": "test:test:feature#1",
"members": [{"kind": "face", "id": id}],
})
};
assert!(
serde_json::from_value::<crate::features::FeatureResultTopology>(topology(" "))
.is_err()
);
assert!(
serde_json::from_value::<crate::features::FeatureResultTopology>(topology(" a "))
.is_ok()
);
}
#[test]
fn an_external_document_states_its_resolution_and_carries_one_identity() {
let path = ExternalDocument::path("parts/widget.FCStd");
let path_wire = serde_json::to_value(&path).unwrap();
assert_eq!(
path_wire,
serde_json::json!({
"resolution": "path",
"path": "parts/widget.FCStd"
})
);
assert_eq!(
serde_json::from_value::<ExternalDocument>(path_wire.clone()).unwrap(),
path
);
let document_id = ExternalDocument::document_id("document-7");
let document_id_wire = serde_json::to_value(&document_id).unwrap();
assert_eq!(
document_id_wire,
serde_json::json!({"resolution": "document_id", "document_id": "document-7"})
);
assert_eq!(
serde_json::from_value::<ExternalDocument>(document_id_wire).unwrap(),
document_id
);
let missing = ExternalDocument::Missing {};
assert_eq!(ExternalDocument::path(""), missing);
assert_eq!(ExternalDocument::document_id(""), missing);
let missing_wire = serde_json::to_value(&missing).unwrap();
assert_eq!(missing_wire, serde_json::json!({"resolution": "missing"}));
assert_eq!(
serde_json::from_value::<ExternalDocument>(missing_wire).unwrap(),
missing
);
for (invalid, named) in [
(
serde_json::json!({"resolution": "path", "path": ""}),
"must not be blank",
),
(
serde_json::json!({"resolution": "path", "path": " "}),
"must not be blank",
),
(
serde_json::json!({
"resolution": "path",
"path": "parts/widget.FCStd",
"document_id": "document-7"
}),
"document_id",
),
(
serde_json::json!({"resolution": "missing", "path": ""}),
"path",
),
(serde_json::json!({"resolution": "path"}), "path"),
] {
let error = serde_json::from_value::<ExternalDocument>(invalid)
.unwrap_err()
.to_string();
assert!(error.contains(named), "{error}");
}
}
#[test]
fn a_joint_operand_states_the_container_that_owns_its_object() {
let root = JointOperand::root("Body1", Vec::new());
let root_wire = serde_json::to_value(&root).unwrap();
assert_eq!(
root_wire,
serde_json::json!({"container": {"container": "root"}, "object": "Body1"})
);
assert_eq!(
serde_json::from_value::<JointOperand>(root_wire.clone()).unwrap(),
root
);
let occurrence_id = OccurrenceId::mint("test:model:occurrence#0").unwrap();
let occurrence = JointOperand::occurrence(occurrence_id.clone(), "Body1", Vec::new());
let occurrence_wire = serde_json::to_value(&occurrence).unwrap();
assert_eq!(occurrence_wire["container"]["container"], "occurrence");
assert_eq!(
serde_json::from_value::<JointOperand>(occurrence_wire.clone()).unwrap(),
occurrence
);
let external = JointOperand::external(
ExternalDocument::path("parts/widget.FCStd"),
"Body1",
Vec::new(),
);
let external_wire = serde_json::to_value(&external).unwrap();
assert_eq!(external_wire["container"]["container"], "external");
assert_eq!(
serde_json::from_value::<JointOperand>(external_wire).unwrap(),
external
);
let mut both = occurrence_wire;
both["container"]["external_document"] = serde_json::json!({"resolution": "missing"});
let error = serde_json::from_value::<JointOperand>(both)
.unwrap_err()
.to_string();
assert!(error.contains("external_document"), "{error}");
let mut stray = root_wire.clone();
stray["container"]["occurrence"] = serde_json::json!(occurrence_id.as_str());
let error = serde_json::from_value::<JointOperand>(stray)
.unwrap_err()
.to_string();
assert!(error.contains("occurrence"), "{error}");
let mut without_object = root_wire;
without_object.as_object_mut().unwrap().remove("object");
let error = serde_json::from_value::<JointOperand>(without_object)
.unwrap_err()
.to_string();
assert!(error.contains("object"), "{error}");
}
fn translation(x: f64) -> Transform {
Transform::affine([
[1.0, 0.0, 0.0, x],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
])
.expect("translation is affine")
}
fn occurrence(id: &str, parent: OccurrenceParent, x: f64) -> Occurrence {
Occurrence {
id: OccurrenceId::mint(id).expect("valid identity"),
prototype: PrototypeReference::Unresolved {},
parent,
ordinal: 0,
transform: translation(x),
linked_prototype: None,
scale: [crate::scalar::FiniteReal::ONE; 3],
name: None,
visible: None,
link: None,
native_ref: None,
}
}
#[test]
fn resolves_parent_chains_and_conditional_prototype_placement() {
let root = occurrence("test:model:entity#root", OccurrenceParent::Root {}, 1.0);
let mut child = occurrence(
"test:model:entity#child",
OccurrenceParent::Occurrence {
occurrence: root.id.clone(),
},
2.0,
);
child.linked_prototype = Some(translation(10.0));
let occurrences = [child, root];
let mut graph = AssemblyGraph::new(&occurrences).expect("valid graph");
assert_eq!(
crate::index::public_result(super::assembly_graph::resolve_occurrence(
graph
.occurrence(
&OccurrenceId::mint("test:model:entity#child").expect("valid identity")
)
.unwrap(),
&mut graph.occurrences,
&crate::index::PublicStorage
))
.expect("resolved child")
.rows()[0][3],
13.0
);
}
#[test]
fn an_absent_linked_prototype_key_is_the_only_spelling_of_absence() {
let plain = occurrence(
"test:model:occurrence#plain",
OccurrenceParent::Root {},
1.0,
);
let plain_wire = serde_json::to_value(&plain).expect("plain occurrence wire");
assert!(plain_wire.get("linked_prototype").is_none());
assert_eq!(
serde_json::from_value::<Occurrence>(plain_wire).unwrap(),
plain
);
let mut linked = plain;
linked.linked_prototype = Some(translation(10.0));
let linked_wire = serde_json::to_value(&linked).expect("linked occurrence wire");
assert_eq!(
linked_wire.get("linked_prototype"),
Some(&serde_json::to_value(translation(10.0)).unwrap())
);
assert_eq!(
serde_json::from_value::<Occurrence>(linked_wire).unwrap(),
linked
);
let mut identity = serde_json::to_value(&linked).unwrap();
identity["linked_prototype"] = serde_json::to_value(Transform::identity()).unwrap();
assert_eq!(
serde_json::from_value::<Occurrence>(identity)
.unwrap()
.linked_prototype,
Some(Transform::identity())
);
}
#[test]
fn joint_limits_admit_only_finite_ordered_bounds() {
assert!(JointLimits::new(None, None).is_none());
assert!(JointLimits::new(Some(2.0), Some(1.0)).is_none());
for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
assert!(JointLimits::new(Some(value), None).is_none());
assert!(JointLimits::new(None, Some(value)).is_none());
}
for (minimum, maximum) in [
(Some(-2.0), None),
(None, Some(-1.0)),
(Some(0.0), Some(0.0)),
] {
let limits = JointLimits::new(minimum, maximum).unwrap();
assert_eq!(
serde_json::from_value::<JointLimits>(serde_json::to_value(&limits).unwrap())
.unwrap(),
limits
);
}
}
#[test]
fn joint_limits_from_parts_checks_only_presence_and_order() {
let lower = crate::scalar::FiniteReal::new(-2.0).unwrap();
let upper = crate::scalar::FiniteReal::new(1.0).unwrap();
assert!(JointLimits::from_parts(None, None).is_none());
assert!(JointLimits::from_parts(Some(upper), Some(lower)).is_none());
assert_eq!(
JointLimits::from_parts(Some(lower), Some(upper)),
Some(JointLimits::Range(
super::JointLimitRange::new(lower, upper).unwrap()
))
);
}
#[test]
fn a_joint_limit_range_owns_the_order_of_its_bounds() {
use super::JointLimitRange;
use crate::scalar::FiniteReal;
let bound = |value: f64| FiniteReal::new(value).unwrap();
assert!(JointLimitRange::new(bound(2.0), bound(1.0)).is_none());
let equal = JointLimitRange::new(bound(1.0), bound(1.0)).unwrap();
assert_eq!([equal.minimum(), equal.maximum()], [bound(1.0), bound(1.0)]);
let range = JointLimitRange::new(bound(-2.0), bound(3.0)).unwrap();
assert_eq!(
[range.minimum(), range.maximum()],
[bound(-2.0), bound(3.0)]
);
let wire = serde_json::to_value(JointLimits::Range(range)).unwrap();
assert_eq!(
wire,
serde_json::json!({"bounds": "range", "minimum": -2.0, "maximum": 3.0})
);
assert_eq!(
serde_json::from_value::<JointLimits>(wire).unwrap(),
JointLimits::Range(range)
);
let error = serde_json::from_value::<JointLimits>(
serde_json::json!({"bounds": "range", "minimum": 2.0, "maximum": 1.0}),
)
.unwrap_err()
.to_string();
assert!(
error.contains("joint limit range must be ordered"),
"{error}"
);
}
#[test]
fn empty_link_state_is_absent() {
assert!(LinkState::new(Vec::new(), None, None, None).is_none());
assert!(LinkState::new(Vec::new(), None, Some(false), None).is_some());
let plain = occurrence(
"test:model:occurrence#empty-link",
OccurrenceParent::Root {},
0.0,
);
let wire = serde_json::to_value(&plain).unwrap();
assert!(serde_json::from_value::<Occurrence>(wire)
.unwrap()
.link
.is_none());
}
#[test]
fn the_link_state_is_one_nested_object_with_its_own_copy_on_change() {
let mut linked = occurrence("test:model:occurrence#link", OccurrenceParent::Root {}, 1.0);
linked.link = LinkState::new(
vec!["Face1".into()],
Some(ProductDefinitionId::mint("test:model:product#element").expect("valid identity")),
Some(true),
Some(CopyOnChange {
policy: CopyOnChangePolicy::Owned,
source: Some(PrototypeReference::Local {
definition: ProductDefinitionId::mint("test:model:product#source")
.expect("valid identity"),
}),
group: Some(PrototypeReference::Local {
definition: ProductDefinitionId::mint("test:model:product#group")
.expect("valid identity"),
}),
touched: Some(true),
}),
);
let wire = serde_json::to_value(&linked).expect("App::Link occurrence wire");
assert_eq!(
wire["link"]["members"],
serde_json::json!([
{"kind": "linked_subelement", "subelement": "Face1"},
{"kind": "element_component", "component": "test:model:product#element"},
{"kind": "claim_child", "claim": true},
{"kind": "copy_on_change", "state": {
"policy": {"policy": "owned"},
"source": {"scope": "local", "definition": "test:model:product#source"},
"group": {"scope": "local", "definition": "test:model:product#group"},
"touched": true
}}
])
);
assert_eq!(
serde_json::from_value::<Occurrence>(wire.clone()).unwrap(),
linked
);
let mut without_policy = wire.clone();
without_policy["link"]["members"][3]["state"]
.as_object_mut()
.expect("a copy-on-change object")
.remove("policy");
let error = serde_json::from_value::<Occurrence>(without_policy)
.unwrap_err()
.to_string();
assert!(error.contains("policy"), "{error}");
let mut bogus = wire;
bogus["link"]["members"][3]["state"]["zz_bogus"] = serde_json::json!(1);
let error = serde_json::from_value::<Occurrence>(bogus)
.unwrap_err()
.to_string();
assert!(error.contains("zz_bogus"), "{error}");
let mut empty = serde_json::to_value(occurrence(
"test:model:occurrence#empty-link",
OccurrenceParent::Root {},
1.0,
))
.unwrap();
empty["link"] = serde_json::json!({"members": []});
assert!(serde_json::from_value::<Occurrence>(empty).is_err());
}
#[test]
fn rejects_duplicate_missing_and_cyclic_parent_links() {
let duplicate = occurrence("test:model:entity#same", OccurrenceParent::Root {}, 0.0);
assert!(matches!(
AssemblyGraph::new(&[duplicate.clone(), duplicate]),
Err(AssemblyGraphError::DuplicateOccurrence(_))
));
let missing = occurrence(
"test:model:entity#child",
OccurrenceParent::Occurrence {
occurrence: OccurrenceId::mint("test:model:entity#missing")
.expect("valid identity"),
},
0.0,
);
assert!(matches!(
AssemblyGraph::new(&[missing]),
Err(AssemblyGraphError::MissingParent { .. })
));
let first = occurrence(
"test:model:entity#first",
OccurrenceParent::Occurrence {
occurrence: OccurrenceId::mint("test:model:entity#second").expect("valid identity"),
},
0.0,
);
let second = occurrence(
"test:model:entity#second",
OccurrenceParent::Occurrence {
occurrence: OccurrenceId::mint("test:model:entity#first").expect("valid identity"),
},
0.0,
);
assert!(matches!(
AssemblyGraph::new(&[first, second]),
Err(AssemblyGraphError::ParentCycle(_))
));
}
}
impl<'a> AssemblyGraph<'a> {
pub fn new(occurrences: &'a [Occurrence]) -> Result<Self, AssemblyGraphError> {
crate::index::public_result(assembly_graph::build(
occurrences,
&crate::index::PublicStorage,
))
.map(|occurrences| Self { occurrences })
.map_err(|error| match error {
assembly_graph::GraphError::Duplicate(id) => {
AssemblyGraphError::DuplicateOccurrence(id.clone())
}
assembly_graph::GraphError::Missing { occurrence, parent } => {
AssemblyGraphError::MissingParent {
occurrence: occurrence.clone(),
parent: parent.clone(),
}
}
assembly_graph::GraphError::Cycle(id) => AssemblyGraphError::ParentCycle(id.clone()),
assembly_graph::GraphError::Transform { occurrence, source } => {
AssemblyGraphError::Transform {
occurrence: occurrence.clone(),
source,
}
}
})
}
pub fn occurrence(&self, id: &OccurrenceId) -> Option<&'a Occurrence> {
self.occurrences.occurrence(id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "container", rename_all = "snake_case", deny_unknown_fields)]
pub enum OperandContainer {
Root {},
Occurrence {
occurrence: OccurrenceId,
},
External {
external_document: ExternalDocument,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct JointOperand {
pub container: OperandContainer,
pub object: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub subelements: Vec<String>,
}
impl JointOperand {
pub fn root(object: impl Into<String>, subelements: Vec<String>) -> Self {
Self {
container: OperandContainer::Root {},
object: object.into(),
subelements,
}
}
pub fn occurrence(
occurrence: OccurrenceId,
object: impl Into<String>,
subelements: Vec<String>,
) -> Self {
Self {
container: OperandContainer::Occurrence { occurrence },
object: object.into(),
subelements,
}
}
pub fn external(
document: ExternalDocument,
object: impl Into<String>,
subelements: Vec<String>,
) -> Self {
Self {
container: OperandContainer::External {
external_document: document,
},
object: object.into(),
subelements,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "JointLimitsWire", into = "JointLimitsWire")]
pub enum JointLimits {
Minimum {
minimum: FiniteReal,
},
Maximum {
maximum: FiniteReal,
},
Range(JointLimitRange),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct JointLimitRange {
minimum: FiniteReal,
maximum: FiniteReal,
}
impl JointLimitRange {
#[must_use]
pub fn new(minimum: FiniteReal, maximum: FiniteReal) -> Option<Self> {
(minimum.get() <= maximum.get()).then_some(Self { minimum, maximum })
}
#[must_use]
pub const fn minimum(self) -> FiniteReal {
self.minimum
}
#[must_use]
pub const fn maximum(self) -> FiniteReal {
self.maximum
}
}
impl JointLimits {
pub fn new(minimum: Option<f64>, maximum: Option<f64>) -> Option<Self> {
let minimum = match minimum {
None => None,
Some(value) => Some(FiniteReal::new(value)?),
};
let maximum = match maximum {
None => None,
Some(value) => Some(FiniteReal::new(value)?),
};
Self::from_parts(minimum, maximum)
}
pub fn from_parts(minimum: Option<FiniteReal>, maximum: Option<FiniteReal>) -> Option<Self> {
match (minimum, maximum) {
(None, None) => None,
(Some(minimum), None) => Some(Self::Minimum { minimum }),
(None, Some(maximum)) => Some(Self::Maximum { maximum }),
(Some(minimum), Some(maximum)) => {
JointLimitRange::new(minimum, maximum).map(Self::Range)
}
}
}
}
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "bounds", rename_all = "snake_case")]
#[serde(deny_unknown_fields)]
enum JointLimitsWire {
Minimum {
minimum: FiniteReal,
},
Maximum {
maximum: FiniteReal,
},
Range {
minimum: FiniteReal,
maximum: FiniteReal,
},
}
impl From<JointLimits> for JointLimitsWire {
fn from(limits: JointLimits) -> Self {
match limits {
JointLimits::Minimum { minimum } => Self::Minimum { minimum },
JointLimits::Maximum { maximum } => Self::Maximum { maximum },
JointLimits::Range(range) => Self::Range {
minimum: range.minimum(),
maximum: range.maximum(),
},
}
}
}
impl TryFrom<JointLimitsWire> for JointLimits {
type Error = &'static str;
fn try_from(wire: JointLimitsWire) -> Result<Self, Self::Error> {
Ok(match wire {
JointLimitsWire::Minimum { minimum } => Self::Minimum { minimum },
JointLimitsWire::Maximum { maximum } => Self::Maximum { maximum },
JointLimitsWire::Range { minimum, maximum } => Self::Range(
JointLimitRange::new(minimum, maximum)
.ok_or("joint limit range must be ordered")?,
),
})
}
}
#[cfg(feature = "schema")]
impl JsonSchema for JointLimits {
fn schema_name() -> std::borrow::Cow<'static, str> {
"JointLimits".into()
}
fn schema_id() -> std::borrow::Cow<'static, str> {
concat!(module_path!(), "::JointLimits").into()
}
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
JointLimitsWire::json_schema(generator)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct JointConnector {
pub operand: JointOperand,
pub frame: Transform,
pub detached: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "arity", rename_all = "snake_case", deny_unknown_fields)]
pub enum JointOperands {
Grounded {
connector: JointConnector,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_offset_frame"
)]
offset_frame: Option<Transform>,
},
Pair {
kind: PairedJointKind,
connectors: Box<[JointConnector; 2]>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_offset_frames"
)]
offset_frames: Option<[Transform; 2]>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum PairedJointKind {
Fixed {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angle"
)]
angle: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_translation_offset"
)]
translation_offset: Option<[FiniteReal; 3]>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angular_limits"
)]
angular_limits: Option<JointLimits>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_linear_limits"
)]
linear_limits: Option<JointLimits>,
},
Revolute {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angle"
)]
angle: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angular_limits"
)]
angular_limits: Option<JointLimits>,
},
Slider {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance"
)]
distance: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_translation_offset"
)]
translation_offset: Option<[FiniteReal; 3]>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_linear_limits"
)]
linear_limits: Option<JointLimits>,
},
Cylindrical {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angle"
)]
angle: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance"
)]
distance: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angular_limits"
)]
angular_limits: Option<JointLimits>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_linear_limits"
)]
linear_limits: Option<JointLimits>,
},
Ball {},
Distance {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance"
)]
distance: Option<FiniteReal>,
},
Parallel {},
Perpendicular {},
Angle {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angle"
)]
angle: Option<FiniteReal>,
},
RackPinion {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance"
)]
distance: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance2"
)]
distance2: Option<FiniteReal>,
},
Screw {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance"
)]
distance: Option<FiniteReal>,
},
Gears {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance"
)]
distance: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance2"
)]
distance2: Option<FiniteReal>,
},
Belt {
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance"
)]
distance: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance2"
)]
distance2: Option<FiniteReal>,
},
Native {
name: String,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angle"
)]
angle: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_translation_offset"
)]
translation_offset: Option<[FiniteReal; 3]>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance"
)]
distance: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_distance2"
)]
distance2: Option<FiniteReal>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_angular_limits"
)]
angular_limits: Option<JointLimits>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_linear_limits"
)]
linear_limits: Option<JointLimits>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct AssemblyJoint {
pub id: JointId,
operands: JointOperands,
pub suppressed: bool,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "deserialize_native_ref"
)]
pub native_ref: Option<String>,
}
impl AssemblyJoint {
pub fn grounded(
id: JointId,
connector: JointConnector,
offset_frame: Option<Transform>,
) -> Self {
Self::with_operands(
id,
JointOperands::Grounded {
connector,
offset_frame,
},
)
}
pub fn paired(
id: JointId,
kind: PairedJointKind,
connectors: [JointConnector; 2],
offset_frames: Option<[Transform; 2]>,
) -> Self {
Self::with_operands(
id,
JointOperands::Pair {
kind,
connectors: Box::new(connectors),
offset_frames,
},
)
}
fn with_operands(id: JointId, operands: JointOperands) -> Self {
Self {
id,
operands,
suppressed: false,
native_ref: None,
}
}
pub fn connectors(&self) -> impl Iterator<Item = &JointConnector> {
let slice: &[JointConnector] = match &self.operands {
JointOperands::Grounded { connector, .. } => std::slice::from_ref(connector),
JointOperands::Pair { connectors, .. } => connectors.as_slice(),
};
slice.iter()
}
}
#[cfg(test)]
mod nonblank_literal_tests {
#[test]
fn a_formatted_literal_keeps_its_non_blank_prefix() {
assert_eq!(
cadmpeg_core::nonblank_literal!("sldprt:marker-relation:{}", 34).as_str(),
"sldprt:marker-relation:34"
);
assert_eq!(cadmpeg_core::nonblank_literal!("d6").as_str(), "d6");
}
}
cadmpeg_core::named_optional_field!(deserialize_source_name, String, "source_name");
cadmpeg_core::named_optional_field!(deserialize_label, String, "label");
cadmpeg_core::named_optional_field!(deserialize_description, String, "description");
cadmpeg_core::named_optional_field!(deserialize_part_number, String, "part_number");
cadmpeg_core::named_optional_field!(deserialize_native_ref, String, "native_ref");
cadmpeg_core::named_optional_field!(deserialize_object, String, "object");
cadmpeg_core::named_optional_field!(deserialize_linked_prototype, Transform, "linked_prototype");
cadmpeg_core::named_optional_field!(deserialize_name, String, "name");
cadmpeg_core::named_optional_field!(deserialize_visible, bool, "visible");
cadmpeg_core::named_optional_field!(deserialize_link, LinkState, "link");
cadmpeg_core::named_optional_field!(deserialize_source, PrototypeReference, "source");
cadmpeg_core::named_optional_field!(deserialize_group, PrototypeReference, "group");
cadmpeg_core::named_optional_field!(deserialize_touched, bool, "touched");
cadmpeg_core::named_optional_field!(deserialize_offset_frame, Transform, "offset_frame");
cadmpeg_core::named_optional_field!(deserialize_offset_frames, [Transform; 2], "offset_frames");
cadmpeg_core::named_optional_field!(deserialize_angle, FiniteReal, "angle");
cadmpeg_core::named_optional_field!(
deserialize_translation_offset,
[FiniteReal; 3],
"translation_offset"
);
cadmpeg_core::named_optional_field!(deserialize_angular_limits, JointLimits, "angular_limits");
cadmpeg_core::named_optional_field!(deserialize_linear_limits, JointLimits, "linear_limits");
cadmpeg_core::named_optional_field!(deserialize_distance, FiniteReal, "distance");
cadmpeg_core::named_optional_field!(deserialize_distance2, FiniteReal, "distance2");
mod identity_rewrite;
mod serialization;