Skip to main content

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/// The modifier word for the maximum material condition, Ⓜ: on a
59/// tolerance's [`GeometricTolerance::modifiers`] or as a
60/// [`DatumModifier::Simple`] on a datum reference.
61pub const MAXIMUM_MATERIAL: &str = "maximum_material_requirement";
62
63/// The modifier word for the least material condition, Ⓛ.
64pub const LEAST_MATERIAL: &str = "least_material_requirement";
65
66/// A geometric tolerance: flatness, position, profile, and their kin.
67#[derive(Debug, Clone, Default)]
68pub struct GeometricTolerance {
69    /// The kind, as a lower-case word: `flatness`, `position`,
70    /// `surface_profile`, `line_profile`, `circular_runout`,
71    /// `total_runout`, `perpendicularity`, …: the AP242 entity name without
72    /// its `_tolerance`.
73    pub kind: String,
74    /// The annotation's own name.
75    pub name: String,
76    /// The tolerance zone's magnitude.
77    pub magnitude: f64,
78    /// Zone and material-condition modifiers, as lower-case words:
79    /// `maximum_material_requirement`, `unequally_disposed`, …: the
80    /// exchange vocabulary for Ⓜ, Ⓤ and their kin.
81    pub modifiers: Vec<String>,
82    /// The datums the tolerance references, in precedence order: the
83    /// datum reference frame, primary first. A composite reference (two
84    /// datums acting as one, ISO's `A-B`) is a single entry with its labels
85    /// hyphen-joined.
86    pub datums: Vec<DatumReference>,
87    /// Whether the datums are stated as an AP242 datum system: one
88    /// compartment per reference, at most three, each carrying its own
89    /// modifiers. A reference with modifiers is written that way whether
90    /// or not this is set, the bare datum list having nowhere to put them;
91    /// the reader sets it when the file stated a datum system.
92    pub datum_system: bool,
93    /// The topology the tolerance controls.
94    pub items: Vec<TShapeId>,
95}
96
97/// One entry of a datum reference frame: a datum, or a common datum, and
98/// the modifiers that apply to it there.
99#[derive(Debug, Clone, PartialEq, Default)]
100pub struct DatumReference {
101    /// The datum's letter, or a common datum's letters hyphen-joined: `A`,
102    /// `A-B`.
103    pub label: String,
104    /// How the datum is simulated in this frame: at its maximum or least
105    /// material boundary, free state, projected, …
106    pub modifiers: Vec<DatumModifier>,
107}
108
109impl DatumReference {
110    /// A reference to `label` with no modifiers.
111    #[must_use]
112    pub fn new(label: impl Into<String>) -> Self {
113        Self {
114            label: label.into(),
115            modifiers: Vec::new(),
116        }
117    }
118
119    /// The same reference with one more modifier.
120    #[must_use]
121    pub fn with(mut self, modifier: DatumModifier) -> Self {
122        self.modifiers.push(modifier);
123        self
124    }
125}
126
127impl From<&str> for DatumReference {
128    fn from(label: &str) -> Self {
129        Self::new(label)
130    }
131}
132
133impl From<String> for DatumReference {
134    fn from(label: String) -> Self {
135        Self::new(label)
136    }
137}
138
139impl PartialEq<str> for DatumReference {
140    /// Equal to a bare label: the same label and no modifiers.
141    fn eq(&self, label: &str) -> bool {
142        self.modifiers.is_empty() && self.label == label
143    }
144}
145
146impl PartialEq<&str> for DatumReference {
147    fn eq(&self, label: &&str) -> bool {
148        *self == **label
149    }
150}
151
152/// A modifier on one datum reference of a frame.
153#[derive(Debug, Clone, PartialEq)]
154pub enum DatumModifier {
155    /// A modifier word, lower-case, from AP242's
156    /// `simple_datum_reference_modifier`: [`MAXIMUM_MATERIAL`],
157    /// [`LEAST_MATERIAL`], `free_state`, `basic`, `translation`, …
158    Simple(String),
159    /// A modifier that states a size: AP242's
160    /// `datum_reference_modifier_with_value`.
161    WithValue {
162        /// What the value sizes.
163        kind: DatumModifierKind,
164        /// The size, in the document's length unit.
165        value: f64,
166    },
167}
168
169impl DatumModifier {
170    /// The maximum material condition, Ⓜ.
171    #[must_use]
172    pub fn maximum_material() -> Self {
173        Self::Simple(MAXIMUM_MATERIAL.to_owned())
174    }
175
176    /// The least material condition, Ⓛ.
177    #[must_use]
178    pub fn least_material() -> Self {
179        Self::Simple(LEAST_MATERIAL.to_owned())
180    }
181}
182
183/// What a [`DatumModifier::WithValue`] sizes.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
185pub enum DatumModifierKind {
186    /// The diameter of a circular or cylindrical datum simulator.
187    CircularOrCylindrical,
188    /// A distance the modifier states.
189    Distance,
190    /// The length the datum is projected over.
191    Projected,
192    /// The diameter of a spherical datum simulator.
193    Spherical,
194}
195
196impl DatumModifierKind {
197    /// The kind as its lower-case exchange word: `circular_or_cylindrical`,
198    /// `distance`, `projected`, `spherical`.
199    #[must_use]
200    pub fn word(self) -> &'static str {
201        match self {
202            Self::CircularOrCylindrical => "circular_or_cylindrical",
203            Self::Distance => "distance",
204            Self::Projected => "projected",
205            Self::Spherical => "spherical",
206        }
207    }
208
209    /// The kind its exchange word names, in either case.
210    #[must_use]
211    pub fn from_word(word: &str) -> Option<Self> {
212        [
213            Self::CircularOrCylindrical,
214            Self::Distance,
215            Self::Projected,
216            Self::Spherical,
217        ]
218        .into_iter()
219        .find(|k| k.word().eq_ignore_ascii_case(word))
220    }
221}
222
223/// A datum: a letter naming a feature other annotations reference.
224#[derive(Debug, Clone)]
225pub struct Datum {
226    /// The letter: `A`, `B`, …
227    pub label: String,
228    /// The feature's topology.
229    pub items: Vec<TShapeId>,
230}
231
232/// A document's PMI, in file order: the semantic annotations, the targets a
233/// datum is actually contacted at, and the drawn presentation.
234#[derive(Debug, Clone, Default)]
235pub struct Pmi {
236    /// Dimensional characteristics.
237    pub dimensions: Vec<Dimension>,
238    /// Geometric tolerances.
239    pub tolerances: Vec<GeometricTolerance>,
240    /// Datums.
241    pub datums: Vec<Datum>,
242    /// The targets datums are established at.
243    pub targets: Vec<DatumTarget>,
244    /// The drawn annotations.
245    pub callouts: Vec<Callout>,
246}
247
248impl Pmi {
249    /// No annotations.
250    #[must_use]
251    pub fn new() -> Self {
252        Self::default()
253    }
254
255    /// Whether anything is annotated.
256    #[must_use]
257    pub fn is_empty(&self) -> bool {
258        self.dimensions.is_empty()
259            && self.tolerances.is_empty()
260            && self.datums.is_empty()
261            && self.targets.is_empty()
262            && self.callouts.is_empty()
263    }
264
265    /// The targets belonging to one datum letter, in the order the file gave.
266    pub fn targets_of<'a>(&'a self, datum: &'a str) -> impl Iterator<Item = &'a DatumTarget> + 'a {
267        self.targets.iter().filter(move |t| t.datum == datum)
268    }
269}
270
271/// Where a datum is actually contacted: the target a fixture touches it at.
272///
273/// A datum plane on a casting is not contacted over its whole face (it rests
274/// on three pads), and the drawing says so with targets: `A1`, `A2`, `A3`,
275/// each a point, a line or an area of stated size. The distinction matters to
276/// anything that inspects the part, because the datum it should establish is
277/// the one the targets define and not the nominal surface.
278#[derive(Debug, Clone, Copy, PartialEq)]
279pub enum DatumTargetKind {
280    /// A point contact.
281    Point,
282    /// A line contact, of the stated length.
283    Line {
284        /// How long the contact runs.
285        length: f64,
286    },
287    /// A rectangular area.
288    Rectangle {
289        /// Along the target's own reference direction.
290        length: f64,
291        /// Across it.
292        width: f64,
293    },
294    /// A circular area.
295    Circle {
296        /// The contact patch's diameter.
297        diameter: f64,
298    },
299}
300
301/// One datum target: which datum, which target of it, and where.
302#[derive(Debug, Clone)]
303pub struct DatumTarget {
304    /// The datum's letter: `A` for `A1`.
305    pub datum: String,
306    /// The target's own number: `1` for `A1`.
307    pub index: u32,
308    /// Point, line, or area, with its size.
309    pub kind: DatumTargetKind,
310    /// Where the target sits, in the part's own coordinates.
311    pub at: ogeom_math::Point,
312    /// The target's own frame: its normal, and the direction its length runs
313    /// in. Absent where the file placed the target by point alone, which a
314    /// point target does not need.
315    pub frame: Option<ogeom_math::Frame>,
316    /// The topology the target is placed on.
317    pub items: Vec<TShapeId>,
318}
319
320impl DatumTarget {
321    /// The identifier a drawing shows: the letter and the number, `A1`.
322    #[must_use]
323    pub fn identifier(&self) -> String {
324        format!("{}{}", self.datum, self.index)
325    }
326}
327
328/// One drawn annotation: the geometry a viewer puts on the screen.
329///
330/// *Presentation* PMI, as against the semantic kind above. The two are
331/// separate on purpose and in the file: the semantic annotation says a
332/// tolerance is `0.1` and controls this face, and the presentation says where
333/// its frame and leader are drawn. Neither derives from the other (a drawing
334/// places its callouts where a draughtsman put them), so a document that
335/// wants both carries both, and [`Callout::annotates`] is the link between.
336#[derive(Debug, Clone)]
337pub struct Callout {
338    /// The name the file gave it, which is how a drawing names its own
339    /// annotations: `Flatness.1`, `Linear Size.3`.
340    pub name: String,
341    /// The plane the annotation is drawn in, where the file stated one.
342    pub plane: Option<ogeom_math::Frame>,
343    /// The drawn geometry: polylines, in the part's own coordinates.
344    pub polylines: Vec<Vec<ogeom_math::Point>>,
345    /// Which semantic annotation this draws, where the file said.
346    pub annotates: Option<Annotated>,
347    /// The topology a note describes (a bore its thread callout names),
348    /// where it draws no semantic annotation to say so through.
349    pub items: Vec<ogeom_topo::TShapeId>,
350    /// The words the callout carries as text, beside the strokes it draws.
351    pub text: Option<String>,
352}
353
354/// Which semantic annotation a callout draws.
355#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
356pub enum Annotated {
357    /// The dimension at this index of [`Pmi::dimensions`].
358    Dimension(usize),
359    /// The tolerance at this index of [`Pmi::tolerances`].
360    Tolerance(usize),
361    /// The datum at this index of [`Pmi::datums`].
362    Datum(usize),
363}