ogeom_doc/pmi.rs
1//! PMI: dimensions, geometric tolerances, datums, their targets, and the
2//! drawing of them.
3//!
4//! The machine-readable annotations AP242 calls *semantic* PMI: the values,
5//! not the leader lines. A dimension carries what it measures and its
6//! plus/minus bounds; a geometric tolerance carries its kind, magnitude and
7//! the datums it references; a datum is a letter on a feature, and its
8//! *targets* are the pads a fixture actually contacts it at. Everything
9//! anchors to topology nodes, the same way colours and names do, so an
10//! annotation survives every placement of the shape it describes.
11//!
12//! And the *presentation* kind, which is the leader lines: where each
13//! annotation is drawn, in which plane, as which polylines. It is kept
14//! separate because it is separate: a drawing places its callouts where a
15//! draughtsman put them, and neither half derives from the other. What ties
16//! them is [`Callout::annotates`], and a document may carry either alone.
17
18use ogeom_topo::TShapeId;
19
20/// What a dimension's value measures.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum MeasureKind {
23 /// A length, in the document's length unit.
24 Length,
25 /// An angle, in radians.
26 Angle,
27}
28
29/// A dimensional characteristic: a size or a location, with its values.
30#[derive(Debug, Clone)]
31pub struct Dimension {
32 /// What the file calls it: `diameter`, `linear distance`, …
33 pub name: String,
34 /// The stated values: one for a plain dimension, several when the file
35 /// states a value with explicit bounds.
36 pub values: Vec<f64>,
37 /// Length or angle.
38 pub kind: MeasureKind,
39 /// The upper allowance, when a plus/minus tolerance applies.
40 pub plus: Option<f64>,
41 /// The lower allowance (typically negative) when one applies.
42 pub minus: Option<f64>,
43 /// The topology the dimension measures, one group per feature: a size
44 /// has one group, a location has one per end.
45 pub features: Vec<Vec<TShapeId>>,
46 /// Whether the dimension runs *between* two features (a location)
47 /// rather than sizing one.
48 pub location: bool,
49}
50
51impl Dimension {
52 /// Every measured node, features flattened.
53 pub fn items(&self) -> impl Iterator<Item = TShapeId> + '_ {
54 self.features.iter().flatten().copied()
55 }
56}
57
58/// A geometric tolerance: flatness, position, profile, and their kin.
59#[derive(Debug, Clone)]
60pub struct GeometricTolerance {
61 /// The kind, as a lower-case word: `flatness`, `position`,
62 /// `surface_profile`, `perpendicularity`, …
63 pub kind: String,
64 /// The annotation's own name.
65 pub name: String,
66 /// The tolerance zone's magnitude.
67 pub magnitude: f64,
68 /// Zone and material-condition modifiers, as lower-case words:
69 /// `maximum_material_requirement`, `unequally_disposed`, …: the
70 /// exchange vocabulary for Ⓜ, Ⓤ and their kin.
71 pub modifiers: Vec<String>,
72 /// The datum letters the tolerance references, in precedence order. A
73 /// composite reference (two datums acting as one, ISO's `A-B`) is a
74 /// single entry with its labels hyphen-joined.
75 pub datums: Vec<String>,
76 /// The topology the tolerance controls.
77 pub items: Vec<TShapeId>,
78}
79
80/// A datum: a letter naming a feature other annotations reference.
81#[derive(Debug, Clone)]
82pub struct Datum {
83 /// The letter: `A`, `B`, …
84 pub label: String,
85 /// The feature's topology.
86 pub items: Vec<TShapeId>,
87}
88
89/// A document's PMI, in file order: the semantic annotations, the targets a
90/// datum is actually contacted at, and the drawn presentation.
91#[derive(Debug, Clone, Default)]
92pub struct Pmi {
93 /// Dimensional characteristics.
94 pub dimensions: Vec<Dimension>,
95 /// Geometric tolerances.
96 pub tolerances: Vec<GeometricTolerance>,
97 /// Datums.
98 pub datums: Vec<Datum>,
99 /// The targets datums are established at.
100 pub targets: Vec<DatumTarget>,
101 /// The drawn annotations.
102 pub callouts: Vec<Callout>,
103}
104
105impl Pmi {
106 /// No annotations.
107 #[must_use]
108 pub fn new() -> Self {
109 Self::default()
110 }
111
112 /// Whether anything is annotated.
113 #[must_use]
114 pub fn is_empty(&self) -> bool {
115 self.dimensions.is_empty()
116 && self.tolerances.is_empty()
117 && self.datums.is_empty()
118 && self.targets.is_empty()
119 && self.callouts.is_empty()
120 }
121
122 /// The targets belonging to one datum letter, in the order the file gave.
123 pub fn targets_of<'a>(&'a self, datum: &'a str) -> impl Iterator<Item = &'a DatumTarget> + 'a {
124 self.targets.iter().filter(move |t| t.datum == datum)
125 }
126}
127
128/// Where a datum is actually contacted: the target a fixture touches it at.
129///
130/// A datum plane on a casting is not contacted over its whole face (it rests
131/// on three pads), and the drawing says so with targets: `A1`, `A2`, `A3`,
132/// each a point, a line or an area of stated size. The distinction matters to
133/// anything that inspects the part, because the datum it should establish is
134/// the one the targets define and not the nominal surface.
135#[derive(Debug, Clone, Copy, PartialEq)]
136pub enum DatumTargetKind {
137 /// A point contact.
138 Point,
139 /// A line contact, of the stated length.
140 Line {
141 /// How long the contact runs.
142 length: f64,
143 },
144 /// A rectangular area.
145 Rectangle {
146 /// Along the target's own reference direction.
147 length: f64,
148 /// Across it.
149 width: f64,
150 },
151 /// A circular area.
152 Circle {
153 /// The contact patch's diameter.
154 diameter: f64,
155 },
156}
157
158/// One datum target: which datum, which target of it, and where.
159#[derive(Debug, Clone)]
160pub struct DatumTarget {
161 /// The datum's letter: `A` for `A1`.
162 pub datum: String,
163 /// The target's own number: `1` for `A1`.
164 pub index: u32,
165 /// Point, line, or area, with its size.
166 pub kind: DatumTargetKind,
167 /// Where the target sits, in the part's own coordinates.
168 pub at: ogeom_math::Point,
169 /// The target's own frame: its normal, and the direction its length runs
170 /// in. Absent where the file placed the target by point alone, which a
171 /// point target does not need.
172 pub frame: Option<ogeom_math::Frame>,
173 /// The topology the target is placed on.
174 pub items: Vec<TShapeId>,
175}
176
177impl DatumTarget {
178 /// The identifier a drawing shows: the letter and the number, `A1`.
179 #[must_use]
180 pub fn identifier(&self) -> String {
181 format!("{}{}", self.datum, self.index)
182 }
183}
184
185/// One drawn annotation: the geometry a viewer puts on the screen.
186///
187/// *Presentation* PMI, as against the semantic kind above. The two are
188/// separate on purpose and in the file: the semantic annotation says a
189/// tolerance is `0.1` and controls this face, and the presentation says where
190/// its frame and leader are drawn. Neither derives from the other (a drawing
191/// places its callouts where a draughtsman put them), so a document that
192/// wants both carries both, and [`Callout::annotates`] is the link between.
193#[derive(Debug, Clone)]
194pub struct Callout {
195 /// The name the file gave it, which is how a drawing names its own
196 /// annotations: `Flatness.1`, `Linear Size.3`.
197 pub name: String,
198 /// The plane the annotation is drawn in, where the file stated one.
199 pub plane: Option<ogeom_math::Frame>,
200 /// The drawn geometry: polylines, in the part's own coordinates.
201 pub polylines: Vec<Vec<ogeom_math::Point>>,
202 /// Which semantic annotation this draws, where the file said.
203 pub annotates: Option<Annotated>,
204}
205
206/// Which semantic annotation a callout draws.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
208pub enum Annotated {
209 /// The dimension at this index of [`Pmi::dimensions`].
210 Dimension(usize),
211 /// The tolerance at this index of [`Pmi::tolerances`].
212 Tolerance(usize),
213 /// The datum at this index of [`Pmi::datums`].
214 Datum(usize),
215}