Skip to main content

cadmpeg_ir/
document.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Versioned document structure and canonical arena ordering.
3
4use std::collections::BTreeMap;
5
6use schemars::JsonSchema;
7use serde::{Deserialize, Deserializer, Serialize};
8
9use crate::appearance::{Appearance, AppearanceBinding};
10use crate::attributes::SourceAttribute;
11use crate::drawings::Drawing;
12use crate::features::{DesignConfiguration, DesignParameter, Feature, FeatureInputTopology};
13use crate::geometry::{Curve, Pcurve, ProceduralCurve, ProceduralSurface, Surface};
14use crate::native::Native;
15use crate::presentation::{PresentationDocument, ViewPresentation};
16use crate::products::{AssemblyJoint, Component, Occurrence};
17use crate::semantic_annotations::SemanticAnnotation;
18use crate::sketches::{
19    Sketch, SketchConstraint, SketchEntity, SpatialSketch, SpatialSketchConstraint,
20    SpatialSketchEntity,
21};
22use crate::spreadsheets::Spreadsheet;
23use crate::subd::SubdSurface;
24use crate::tessellation::Tessellation;
25use crate::topology::{Body, Coedge, Edge, Face, Loop, Point, Region, Shell, Vertex};
26use crate::units::{Tolerances, Units};
27use crate::unknown::{NativeUnknownRecord, UnknownRecord};
28
29macro_rules! arena_registry {
30    ($macro:ident) => {
31        $macro! {
32            bodies: Body, "Body arena.", [] => |e| e.id.0.clone();
33            regions: Region, "Region arena.", [] => |e| e.id.0.clone();
34            shells: Shell, "Shell arena.", [] => |e| e.id.0.clone();
35            faces: Face, "Face arena.", [] => |e| e.id.0.clone();
36            loops: Loop, "Loop arena.", [] => |e| e.id.0.clone();
37            coedges: Coedge, "Coedge arena.", [] => |e| e.id.0.clone();
38            edges: Edge, "Edge arena.", [] => |e| e.id.0.clone();
39            vertices: Vertex, "Vertex arena.", [] => |e| e.id.0.clone();
40            points: Point, "Point arena.", [] => |e| e.id.0.clone();
41            surfaces: Surface, "Surface arena.", [] => |e| e.id.0.clone();
42            curves: Curve, "Curve arena.", [] => |e| e.id.0.clone();
43            subds: SubdSurface, "Subdivision surface arena.", [] => |e| e.id.0.clone();
44            pcurves: Pcurve, "Pcurve arena.", [] => |e| e.id.0.clone();
45            procedural_surfaces: ProceduralSurface, "Procedural surface arena.", [] => |e| e.id.0.clone();
46            procedural_curves: ProceduralCurve, "Procedural curve arena.", [] => |e| e.id.0.clone();
47            features: Feature, "Feature arena.", [] => |e| e.id.0.clone();
48            feature_input_topologies: FeatureInputTopology, "Feature input-topology arena.", [serde(default, skip_serializing_if = "Vec::is_empty")] => |e| e.id.0.clone();
49            configurations: DesignConfiguration, "Design configuration arena.", [serde(default)] => |e| e.id.0.clone();
50            parameters: DesignParameter, "Design parameter arena.", [serde(default)] => |e| e.id.0.clone();
51            sketches: Sketch, "Planar sketch arena.", [serde(default)] => |e| e.id.0.clone();
52            sketch_entities: SketchEntity, "Solved sketch entity arena.", [serde(default)] => |e| e.id.0.clone();
53            sketch_constraints: SketchConstraint, "Sketch constraint arena.", [serde(default)] => |e| e.id.0.clone();
54            spatial_sketches: SpatialSketch, "Spatial sketch arena.", [serde(default)] => |e| e.id.0.clone();
55            spatial_sketch_entities: SpatialSketchEntity, "Solved spatial sketch entity arena.", [serde(default)] => |e| e.id.0.clone();
56            spatial_sketch_constraints: SpatialSketchConstraint, "Spatial sketch constraint arena.", [serde(default, skip_serializing_if = "Vec::is_empty")] => |e| e.id.0.clone();
57            spreadsheets: Spreadsheet, "Spreadsheet arena.", [serde(default)] => |e| e.id.0.clone();
58            components: Component, "Product component arena.", [serde(default)] => |e| e.id.0.clone();
59            occurrences: Occurrence, "Product occurrence arena.", [serde(default)] => |e| e.id.0.clone();
60            assembly_joints: AssemblyJoint, "Assembly joint arena.", [serde(default)] => |e| e.id.0.clone();
61            drawings: Drawing, "Drawing page, resource, view, and annotation arena.", [serde(default)] => |e| e.id.0.clone();
62            semantic_annotations: SemanticAnnotation, "Semantic dimension, note, symbol, and callout arena.", [serde(default)] => |e| e.id.0.clone();
63            presentation_documents: PresentationDocument, "Document presentation arena.", [serde(default)] => |e| e.id.0.clone();
64            view_presentations: ViewPresentation, "View-provider presentation arena.", [serde(default)] => |e| e.id.0.clone();
65            tessellations: Tessellation, "Tessellation arena.", [] => |e| e.id.clone();
66            appearances: Appearance, "Appearance arena.", [] => |e| e.id.0.clone();
67            appearance_bindings: AppearanceBinding, "Appearance binding arena.", [] => |e| e.id.clone();
68            attributes: SourceAttribute, "Attribute arena.", [] => |e| e.id.0.clone();
69            products: crate::product::Product, "Product prototype arena.", [serde(default)] => |e| e.id.0.clone();
70            product_occurrences: crate::product::ProductOccurrence, "Placed product occurrence arena.", [serde(default)] => |e| e.id.0.clone();
71            pmi: crate::pmi::PmiAnnotation, "Product-manufacturing information arena.", [serde(default)] => |e| e.id.0.clone();
72            presentation_layers: crate::presentation::PresentationLayer, "Presentation layer arena.", [serde(default)] => |e| e.id.0.clone();
73        }
74    };
75}
76pub(crate) use arena_registry;
77
78macro_rules! declare_model {
79    ($($field:ident: $ty:ty, $doc:literal, [$($attribute:meta),*] => $key:expr;)*) => {
80        /// Format-neutral entity arenas connected by typed IDs.
81        #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
82        pub struct Model {
83            $(
84                $(#[$attribute])*
85                #[doc = $doc]
86                pub $field: Vec<$ty>,
87            )*
88        }
89
90        impl Model {
91            /// Arena field names in canonical order.
92            pub fn arena_names() -> &'static [&'static str] {
93                &[$(stringify!($field)),*]
94            }
95
96            /// Whether any arena holds an entity whose identity equals `id`.
97            ///
98            /// Entity IDs are globally unique across arenas, so a single hit is
99            /// authoritative. Used by transfer accounting to confirm that a
100            /// `Typed` disposition names entities actually emitted into the IR.
101            pub fn contains_id(&self, id: &str) -> bool {
102                $({
103                    let key = ($key) as fn(&$ty) -> String;
104                    if self.$field.iter().any(|e| key(e) == id) { return true; }
105                })*
106                false
107            }
108
109            /// Every entity identity across all arenas, in canonical arena
110            /// order. Derived from the same `arena_registry!` declaration as
111            /// [`contains_id`](Self::contains_id), so a new arena is covered
112            /// without editing per-arena call sites.
113            pub fn entity_ids(&self) -> Vec<String> {
114                let mut ids = Vec::new();
115                $( ids.extend(self.$field.iter().map($key)); )*
116                ids
117            }
118
119            /// Sort each arena lexicographically by its entity identity.
120            pub fn finalize(&mut self) {
121                $(self.$field.sort_by_key($key);)*
122            }
123        }
124    };
125}
126
127/// The IR schema version this build produces and accepts.
128pub const IR_VERSION: &str = "4";
129
130arena_registry!(declare_model);
131
132fn deserialize_ir_version<'de, D>(deserializer: D) -> Result<String, D::Error>
133where
134    D: Deserializer<'de>,
135{
136    let version = String::deserialize(deserializer)?;
137    if version != IR_VERSION {
138        return Err(serde::de::Error::custom(format!(
139            "unsupported ir_version {version:?}; expected {IR_VERSION}"
140        )));
141    }
142    Ok(version)
143}
144
145fn ir_version_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
146    schemars::json_schema!({
147        "type": "string",
148        "const": IR_VERSION
149    })
150}
151
152/// A versioned CAD document.
153///
154/// `model` holds the format-neutral graph. `native` retains typed
155/// format-specific product data without changing that graph's semantics.
156/// Entity IDs must be globally unique across all document arenas.
157#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
158pub struct CadIr {
159    /// IR schema version.
160    #[serde(deserialize_with = "deserialize_ir_version")]
161    #[schemars(schema_with = "ir_version_schema")]
162    pub ir_version: String,
163    /// Source-container metadata.
164    #[serde(default, skip_serializing_if = "Option::is_none")]
165    pub source: Option<SourceMeta>,
166    /// Canonical unit declaration.
167    pub units: Units,
168    /// Document-wide tolerances.
169    pub tolerances: Tolerances,
170    /// Format-neutral model.
171    pub model: Model,
172    /// Independently versioned native namespaces.
173    #[serde(default)]
174    pub native: Native,
175}
176
177impl CadIr {
178    /// Deserialize the reserved `unknowns` arena for `format`.
179    pub fn native_unknowns(
180        &self,
181        format: &str,
182    ) -> Result<Vec<NativeUnknownRecord>, crate::native::NativeConvertError> {
183        self.native.namespace(format).map_or_else(
184            || Ok(Vec::new()),
185            |namespace| namespace.arena_as("unknowns"),
186        )
187    }
188
189    /// Deserialize every reserved native `unknowns` arena.
190    pub fn all_native_unknowns(
191        &self,
192    ) -> Result<Vec<NativeUnknownRecord>, crate::native::NativeConvertError> {
193        self.native
194            .0
195            .values()
196            .filter(|namespace| namespace.arenas.contains_key("unknowns"))
197            .try_fold(Vec::new(), |mut records, namespace| {
198                records.extend(namespace.arena_as::<NativeUnknownRecord>("unknowns")?);
199                Ok(records)
200            })
201    }
202
203    /// Replace the reserved `unknowns` arena for `format`.
204    pub fn set_native_unknowns(
205        &mut self,
206        format: &str,
207        records: &[NativeUnknownRecord],
208    ) -> Result<(), crate::native::NativeConvertError> {
209        let namespace = self.native.namespace_mut(format);
210        if namespace.version == 0 {
211            namespace.version = 1;
212        }
213        namespace.set_arena("unknowns", records)
214    }
215
216    /// Replace the reserved `unknowns` arena for `format`, consuming the records.
217    ///
218    /// Codecs retaining large source populations should use this form to avoid
219    /// keeping typed and generic native copies alive at the same time.
220    pub fn set_native_unknowns_owned(&mut self, format: &str, records: Vec<UnknownRecord>) {
221        let namespace = self.native.namespace_mut(format);
222        if namespace.version == 0 {
223            namespace.version = 1;
224        }
225        let mut converted = records
226            .into_iter()
227            .map(UnknownRecord::into_native_record)
228            .collect::<Vec<_>>();
229        converted.sort_by(|left, right| left.id.cmp(&right.id));
230        namespace.arenas.insert("unknowns".into(), converted);
231    }
232
233    /// Append one record to the reserved `unknowns` arena for `format`.
234    pub fn push_native_unknown(
235        &mut self,
236        format: &str,
237        record: NativeUnknownRecord,
238    ) -> Result<(), crate::native::NativeConvertError> {
239        let mut records = self.native_unknowns(format)?;
240        records.retain(|existing| existing.id != record.id);
241        records.push(record);
242        self.set_native_unknowns(format, &records)
243    }
244
245    /// Construct an empty current-version document with default tolerances.
246    pub fn empty(units: Units) -> Self {
247        Self {
248            ir_version: IR_VERSION.to_owned(),
249            source: None,
250            units,
251            tolerances: Tolerances::default(),
252            model: Model::default(),
253            native: Native::default(),
254        }
255    }
256
257    /// Serialize the document as pretty JSON.
258    ///
259    /// Call [`CadIr::finalize`] first when canonical arena order is required.
260    pub fn to_canonical_json(&self) -> Result<String, serde_json::Error> {
261        serde_json::to_string_pretty(self)
262    }
263
264    /// Parse JSON and reject any unsupported `ir_version`.
265    pub fn from_json(s: &str) -> Result<Self, serde_json::Error> {
266        let value: serde_json::Value = serde_json::from_str(s)?;
267        let version = value.get("ir_version").and_then(serde_json::Value::as_str);
268        if version != Some(IR_VERSION) {
269            return Err(<serde_json::Error as serde::de::Error>::custom(format!(
270                "unsupported ir_version {version:?}; expected {IR_VERSION}"
271            )));
272        }
273        serde_json::from_value(value)
274    }
275
276    /// Sort model, native, and unknown-record arenas by identity.
277    pub fn finalize(&mut self) {
278        self.model.finalize();
279        self.native.finalize();
280    }
281}
282
283/// Source-container metadata preserved for reporting.
284#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
285pub struct SourceMeta {
286    /// Source format id.
287    pub format: String,
288    /// Format-specific attributes.
289    #[serde(default)]
290    pub attributes: BTreeMap<String, String>,
291}