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}