Skip to main content

cadmpeg_ir/
products.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Neutral product structure and occurrence instancing.
3
4use schemars::JsonSchema;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7
8/// Stable product-component identity.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
10#[serde(transparent)]
11pub struct ComponentId(pub String);
12
13/// Stable placed-occurrence identity.
14#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
15#[serde(transparent)]
16pub struct OccurrenceId(pub String);
17
18/// Stable assembly-joint identity.
19#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
20#[serde(transparent)]
21pub struct JointId(pub String);
22
23/// Role of a component definition in the product tree.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
25#[serde(rename_all = "snake_case")]
26pub enum ComponentKind {
27    /// Product part or assembly container.
28    Part,
29    /// Generic ordered object group.
30    Group,
31    /// Container whose children are link instances.
32    LinkGroup,
33    /// Reusable leaf object without container semantics.
34    Object,
35}
36
37/// A reusable product definition or structural container.
38#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
39pub struct Component {
40    /// Globally unique definition identity.
41    pub id: ComponentId,
42    /// Structural role.
43    pub kind: ComponentKind,
44    /// Stable source object name used by product/BOM tooling.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub source_name: Option<String>,
47    /// User-visible component label, when distinct from the source name.
48    #[serde(default, skip_serializing_if = "Option::is_none")]
49    pub label: Option<String>,
50    /// User-maintained BOM description.
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub description: Option<String>,
53    /// User-maintained part or stock number.
54    #[serde(default, skip_serializing_if = "Option::is_none")]
55    pub part_number: Option<String>,
56    /// Additional persisted BOM identity fields by exact property name.
57    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
58    pub bom_properties: BTreeMap<String, String>,
59    /// Direct containing component, absent for a product root.
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub parent: Option<ComponentId>,
62    /// Placement relative to the direct container.
63    #[serde(default = "identity_transform")]
64    pub local_transform: [[f64; 4]; 4],
65    /// Placement composed through all containing components exactly once.
66    #[serde(default = "identity_transform")]
67    pub resolved_transform: [[f64; 4]; 4],
68    /// Direct component definitions in source order.
69    #[serde(default, skip_serializing_if = "Vec::is_empty")]
70    pub components: Vec<ComponentId>,
71    /// Direct placed uses in source order.
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub occurrences: Vec<OccurrenceId>,
74    /// Format-native object supplying this definition.
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub native_ref: Option<String>,
77}
78
79fn identity_transform() -> [[f64; 4]; 4] {
80    [
81        [1.0, 0.0, 0.0, 0.0],
82        [0.0, 1.0, 0.0, 0.0],
83        [0.0, 0.0, 1.0, 0.0],
84        [0.0, 0.0, 0.0, 1.0],
85    ]
86}
87
88/// Local or unresolved external prototype of an occurrence.
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
90#[serde(tag = "scope", rename_all = "snake_case")]
91pub enum ComponentReference {
92    /// Prototype resolves to a definition in this document.
93    Local {
94        /// Resolved component definition.
95        component: ComponentId,
96    },
97    /// Prototype belongs to another document, loaded or not.
98    External {
99        /// Persisted external-document reference and unresolved state.
100        document: ExternalDocumentReference,
101        /// Persisted object identity within that document.
102        #[serde(default, skip_serializing_if = "Option::is_none")]
103        object: Option<String>,
104    },
105    /// The source intentionally carries no resolvable prototype.
106    Unresolved,
107}
108
109/// First-class external document reference without implicit loading.
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
111pub struct ExternalDocumentReference {
112    /// File path when the source explicitly stores a file attribute.
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub path: Option<String>,
115    /// Document identity when the source explicitly stores a document attribute.
116    #[serde(default, skip_serializing_if = "Option::is_none")]
117    pub document_id: Option<String>,
118    /// Deterministic resolution state; decoding never opens external documents.
119    pub resolution: ExternalResolution,
120}
121
122/// Resolution state of an external product reference.
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
124#[serde(rename_all = "snake_case")]
125pub enum ExternalResolution {
126    /// Target document was not loaded by this decode.
127    Unresolved,
128    /// Persisted reference was present but empty or structurally unusable.
129    MissingReference,
130}
131
132/// Copy-on-change ownership behavior of a link.
133#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
134#[serde(tag = "policy", content = "native_policy", rename_all = "snake_case")]
135pub enum CopyOnChangePolicy {
136    /// Link follows its prototype without making an owned copy.
137    Disabled,
138    /// Copy is created when a marked prototype property changes.
139    Enabled,
140    /// Link currently owns a changed copy.
141    Owned,
142    /// Owned copy continues tracking its original source.
143    Tracking,
144    /// Future policy retained without reinterpretation.
145    Native(String),
146}
147
148/// One placed use, including an element of a link array.
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
150pub struct Occurrence {
151    /// Globally unique instance identity.
152    pub id: OccurrenceId,
153    /// Reusable definition used by this instance.
154    pub prototype: ComponentReference,
155    /// Direct containing component, absent for a root occurrence.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    pub parent: Option<ComponentId>,
158    /// Zero-based link-array element index.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    pub array_index: Option<u32>,
161    /// Placement relative to the direct container.
162    pub local_transform: [[f64; 4]; 4],
163    /// Linked prototype placement contribution selected by link-transform policy.
164    #[serde(default = "identity_transform")]
165    pub prototype_transform: [[f64; 4]; 4],
166    /// Placement composed through all containers exactly once.
167    pub resolved_transform: [[f64; 4]; 4],
168    /// Per-axis instance scale.
169    pub scale: [f64; 3],
170    /// Persisted prototype subelement selection.
171    #[serde(default, skip_serializing_if = "Vec::is_empty")]
172    pub linked_subelements: Vec<String>,
173    /// Per-element visibility override.
174    #[serde(default, skip_serializing_if = "Option::is_none")]
175    pub visible: Option<bool>,
176    /// Explicit application object representing this array element.
177    #[serde(default, skip_serializing_if = "Option::is_none")]
178    pub element_component: Option<ComponentId>,
179    /// Whether this link claims its prototype in the source tree.
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    pub claim_child: Option<bool>,
182    /// Copy-on-change ownership policy.
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub copy_on_change: Option<CopyOnChangePolicy>,
185    /// Original component tracked by copy-on-change.
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub copy_on_change_source: Option<ComponentId>,
188    /// Internal component holding owned copies.
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub copy_on_change_group: Option<ComponentId>,
191    /// Whether the tracked source was persisted as changed.
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub copy_on_change_touched: Option<bool>,
194    /// Whether the prototype placement participates in evaluation.
195    #[serde(default, skip_serializing_if = "Option::is_none")]
196    pub link_transform: Option<bool>,
197    /// Format-native object supplying this instance.
198    #[serde(default, skip_serializing_if = "Option::is_none")]
199    pub native_ref: Option<String>,
200}
201
202/// Neutral family of an assembly joint.
203#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
204#[serde(tag = "kind", content = "native_kind", rename_all = "snake_case")]
205pub enum JointKind {
206    /// Rigid connection with no relative degrees of freedom.
207    Fixed,
208    /// Rotation about one axis.
209    Revolute,
210    /// Translation along one axis.
211    Slider,
212    /// Coupled rotation and translation on one axis.
213    Cylindrical,
214    /// Rotation about a common point.
215    Ball,
216    /// Maintains a scalar separation.
217    Distance,
218    /// Maintains parallel connector directions.
219    Parallel,
220    /// Maintains perpendicular connector directions.
221    Perpendicular,
222    /// Maintains an angular separation.
223    Angle,
224    /// Couples rack translation to pinion rotation.
225    RackPinion,
226    /// Couples translation and rotation by screw pitch.
227    Screw,
228    /// Couples two gear rotations.
229    Gears,
230    /// Couples two pulley rotations through a belt.
231    Belt,
232    /// Persisted grounding of a component.
233    Grounded,
234    /// Future application-defined family retained without relabeling.
235    Native(String),
236}
237
238/// One connector operand and its selected native subelements.
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
240pub struct JointOperand {
241    /// Local component when the object resolves within this document.
242    #[serde(default, skip_serializing_if = "Option::is_none")]
243    pub component: Option<ComponentId>,
244    /// External document token when resolution is intentionally deferred.
245    #[serde(default, skip_serializing_if = "Option::is_none")]
246    pub external_document: Option<ExternalDocumentReference>,
247    /// Exact referenced application object identity.
248    #[serde(default, skip_serializing_if = "Option::is_none")]
249    pub object: Option<String>,
250    /// Ordered persistent object/element paths.
251    #[serde(default, skip_serializing_if = "Vec::is_empty")]
252    pub subelements: Vec<String>,
253}
254
255/// Optional enabled interval for a joint degree of freedom.
256#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
257pub struct JointLimits {
258    /// Lower bound when enabled.
259    #[serde(default, skip_serializing_if = "Option::is_none")]
260    pub minimum: Option<f64>,
261    /// Upper bound when enabled.
262    #[serde(default, skip_serializing_if = "Option::is_none")]
263    pub maximum: Option<f64>,
264}
265
266/// Neutral assembly constraint between connector frames.
267#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
268pub struct AssemblyJoint {
269    /// Globally unique joint identity.
270    pub id: JointId,
271    /// Joint kinematic family.
272    pub kind: JointKind,
273    /// Ordered connector or grounded-object operands.
274    pub operands: Vec<JointOperand>,
275    /// Connector-local frames in operand order.
276    pub frames: Vec<[[f64; 4]; 4]>,
277    /// Connector attachment offsets in operand order.
278    #[serde(default, skip_serializing_if = "Vec::is_empty")]
279    pub offset_frames: Vec<[[f64; 4]; 4]>,
280    /// Whether solving this joint is suppressed.
281    pub suppressed: bool,
282    /// Per-connector detach flags.
283    pub detached: [bool; 2],
284    /// Angular offset in radians.
285    #[serde(default, skip_serializing_if = "Option::is_none")]
286    pub angle: Option<f64>,
287    /// Primary linear offset in document length units.
288    #[serde(default, skip_serializing_if = "Option::is_none")]
289    pub distance: Option<f64>,
290    /// Secondary linear offset in document length units.
291    #[serde(default, skip_serializing_if = "Option::is_none")]
292    pub distance2: Option<f64>,
293    /// Enabled angular interval in radians.
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub angular_limits: Option<JointLimits>,
296    /// Enabled linear interval in document length units.
297    #[serde(default, skip_serializing_if = "Option::is_none")]
298    pub linear_limits: Option<JointLimits>,
299    /// Exact persisted scalar state, including future controls.
300    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
301    pub properties: BTreeMap<String, String>,
302    /// Format-native joint record supplying this constraint.
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub native_ref: Option<String>,
305}