Skip to main content

brep_kernel/feature_pipeline/pmi/annotations/
fcf.rs

1//! Feature control frame (`FCF`): a geometric tolerance on a face or edge —
2//! characteristic, tolerance zone (⌀, material condition) and up to three
3//! datum references, validated against the part's datum registry.
4//!
5//! All fourteen ASME Y14.5-2009 characteristics are offered: AP242 defines an
6//! entity for each (concentricity and symmetry included, which Y14.5-2018
7//! retired) and the export target is the schema, not one drawing standard.
8
9use super::{boolean_field, id_field, number_field, options_field, params, reference_field, schema_entry, string_field, PmiTypeDef};
10use crate::feature_pipeline::pmi::resolve::{a3, direction_of, resolve_reference};
11use crate::feature_pipeline::pmi::{format_number, FcfFrame, PmiAnnotation, PmiContext, PmiGeometry, Resolved};
12use crate::feature_pipeline::SelectionProbe;
13use crate::Vec3;
14
15pub const DEF: PmiTypeDef = PmiTypeDef {
16    type_id: "fcf",
17    short_name: "FCF",
18    icon: "\u{2316}",
19    long_name: "\u{2316} Feature control frame",
20    label: "Feature control frame",
21    applicable,
22    schema,
23    resolve,
24};
25
26/// How many datum references a characteristic takes.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DatumRule {
29    /// A form tolerance: none allowed.
30    None,
31    /// Position / profile: none or up to three.
32    Optional,
33    /// Orientation / location / runout: one to three.
34    Required,
35}
36
37/// One geometric characteristic: id, symbol, label, its AP242 entity and
38/// its datum rule.
39pub struct Characteristic {
40    pub id: &'static str,
41    pub symbol: &'static str,
42    pub label: &'static str,
43    pub step_entity: &'static str,
44    pub datums: DatumRule,
45}
46
47pub const CHARACTERISTICS: [Characteristic; 14] = [
48    Characteristic { id: "straightness", symbol: "\u{23E4}", label: "Straightness", step_entity: "STRAIGHTNESS_TOLERANCE", datums: DatumRule::None },
49    Characteristic { id: "flatness", symbol: "\u{23E5}", label: "Flatness", step_entity: "FLATNESS_TOLERANCE", datums: DatumRule::None },
50    Characteristic { id: "circularity", symbol: "\u{25CB}", label: "Circularity", step_entity: "ROUNDNESS_TOLERANCE", datums: DatumRule::None },
51    Characteristic { id: "cylindricity", symbol: "\u{232D}", label: "Cylindricity", step_entity: "CYLINDRICITY_TOLERANCE", datums: DatumRule::None },
52    Characteristic { id: "profileLine", symbol: "\u{2312}", label: "Profile of a line", step_entity: "LINE_PROFILE_TOLERANCE", datums: DatumRule::Optional },
53    Characteristic { id: "profileSurface", symbol: "\u{2313}", label: "Profile of a surface", step_entity: "SURFACE_PROFILE_TOLERANCE", datums: DatumRule::Optional },
54    Characteristic { id: "angularity", symbol: "\u{2220}", label: "Angularity", step_entity: "ANGULARITY_TOLERANCE", datums: DatumRule::Required },
55    Characteristic { id: "perpendicularity", symbol: "\u{27C2}", label: "Perpendicularity", step_entity: "PERPENDICULARITY_TOLERANCE", datums: DatumRule::Required },
56    Characteristic { id: "parallelism", symbol: "\u{2225}", label: "Parallelism", step_entity: "PARALLELISM_TOLERANCE", datums: DatumRule::Required },
57    Characteristic { id: "position", symbol: "\u{2316}", label: "Position", step_entity: "POSITION_TOLERANCE", datums: DatumRule::Optional },
58    Characteristic { id: "concentricity", symbol: "\u{25CE}", label: "Concentricity", step_entity: "CONCENTRICITY_TOLERANCE", datums: DatumRule::Required },
59    Characteristic { id: "symmetry", symbol: "\u{232F}", label: "Symmetry", step_entity: "SYMMETRY_TOLERANCE", datums: DatumRule::Required },
60    Characteristic { id: "circularRunout", symbol: "\u{2197}", label: "Circular runout", step_entity: "CIRCULAR_RUNOUT_TOLERANCE", datums: DatumRule::Required },
61    Characteristic { id: "totalRunout", symbol: "\u{2330}", label: "Total runout", step_entity: "TOTAL_RUNOUT_TOLERANCE", datums: DatumRule::Required },
62];
63
64pub fn characteristic(id: &str) -> Option<&'static Characteristic> {
65    CHARACTERISTICS.iter().find(|c| c.id == id)
66}
67
68/// The characteristic behind an AP242 entity keyword.
69pub fn characteristic_for_entity(keyword: &str) -> Option<&'static Characteristic> {
70    CHARACTERISTICS.iter().find(|c| c.step_entity == keyword)
71}
72
73/// The material-condition modifier glyph (`Ⓜ` / `Ⓛ`), empty for none.
74pub fn modifier_symbol(modifier: &str) -> &'static str {
75    match modifier.trim().to_ascii_uppercase().as_str() {
76        "MMC" => "\u{24C2}",
77        "LMC" => "\u{24C1}",
78        _ => "",
79    }
80}
81
82/// Exactly one face or edge.
83fn applicable(probe: &SelectionProbe) -> bool {
84    probe.faces + probe.edges == 1
85        && probe.vertices == 0
86        && probe.planes == 0
87        && probe.solids == 0
88        && probe.sketches == 0
89}
90
91fn schema() -> serde_json::Value {
92    let ids: Vec<&str> = CHARACTERISTICS.iter().map(|c| c.id).collect();
93    let modifiers = ["none", "MMC", "LMC"];
94    schema_entry(
95        &DEF,
96        params(vec![
97            ("id", id_field()),
98            ("target", reference_field("Target", &["FACE", "EDGE"], false, 1, 1, "The toleranced feature")),
99            ("characteristic", options_field("Characteristic", &ids, "flatness", "The geometric characteristic")),
100            ("zoneValue", number_field("Tolerance", 0.1, 0.01, "The tolerance zone size")),
101            ("zoneDiameter", boolean_field("⌀ zone", false, "A cylindrical (⌀) tolerance zone")),
102            ("materialCondition", options_field("Material condition", &modifiers, "none", "Ⓜ MMC / Ⓛ LMC on the tolerance")),
103            ("datumA", string_field("Datum 1", "", "The primary datum letter")),
104            ("datumAModifier", options_field("Datum 1 modifier", &modifiers, "none", "")),
105            ("datumB", string_field("Datum 2", "", "The secondary datum letter")),
106            ("datumBModifier", options_field("Datum 2 modifier", &modifiers, "none", "")),
107            ("datumC", string_field("Datum 3", "", "The tertiary datum letter")),
108            ("datumCModifier", options_field("Datum 3 modifier", &modifiers, "none", "")),
109        ]),
110    )
111}
112
113/// The datum reference cells `(letter, modifier)` an annotation declares, in
114/// order, empty letters skipped.
115pub fn datum_references(annotation: &PmiAnnotation) -> Vec<(String, String)> {
116    ["A", "B", "C"]
117        .iter()
118        .filter_map(|slot| {
119            let letter = annotation.text(&format!("datum{slot}")).to_ascii_uppercase();
120            if letter.is_empty() {
121                None
122            } else {
123                Some((letter, annotation.text(&format!("datum{slot}Modifier")).to_ascii_uppercase()))
124            }
125        })
126        .collect()
127}
128
129fn resolve(annotation: &PmiAnnotation, context: &PmiContext<'_>) -> Result<Resolved, String> {
130    let targets = annotation.references("target");
131    let Some(target) = targets.first() else {
132        return Err("select the toleranced feature (a face or an edge)".into());
133    };
134    let id = annotation.text("characteristic");
135    let Some(characteristic) = characteristic(id) else {
136        return Err(format!("unknown characteristic '{id}'"));
137    };
138    let zone = annotation.number("zoneValue", context.env, 0.0)?;
139    if !(zone.is_finite() && zone > 0.0) {
140        return Err("the tolerance zone must be positive".into());
141    }
142    let datums = datum_references(annotation);
143    match characteristic.datums {
144        DatumRule::None if !datums.is_empty() => {
145            return Err(format!("{} is a form tolerance and takes no datum reference", characteristic.label))
146        }
147        DatumRule::Required if datums.is_empty() => {
148            return Err(format!("{} needs at least one datum reference", characteristic.label))
149        }
150        _ => {}
151    }
152    for (letter, _) in &datums {
153        if !super::datum::valid_letter(letter) {
154            return Err(format!("'{letter}' is not a datum letter"));
155        }
156        if !context.datums.contains_key(letter) {
157            return Err(format!("datum {letter} is not defined in the part"));
158        }
159    }
160    let geometry = resolve_reference(context.scene, target)?;
161    let anchor = geometry.representative_point();
162    let normal = direction_of(&geometry).unwrap_or(Vec3::new(0.0, 0.0, 1.0));
163    let mut zone_text = String::new();
164    if annotation.flag("zoneDiameter") {
165        zone_text.push('\u{2300}');
166    }
167    zone_text.push_str(&format_number(zone, 3).trim_end_matches('0').trim_end_matches('.').to_string());
168    let condition = modifier_symbol(annotation.text("materialCondition"));
169    if !condition.is_empty() {
170        zone_text.push(' ');
171        zone_text.push_str(condition);
172    }
173    let datum_cells: Vec<String> = datums
174        .iter()
175        .map(|(letter, modifier)| {
176            let symbol = modifier_symbol(modifier);
177            if symbol.is_empty() {
178                letter.clone()
179            } else {
180                format!("{letter} {symbol}")
181            }
182        })
183        .collect();
184    let mut text = format!("{} | {zone_text}", characteristic.symbol);
185    for cell in &datum_cells {
186        text.push_str(" | ");
187        text.push_str(cell);
188    }
189    let default_label = anchor.add(normal.scale(5.0)).add(Vec3::new(3.0, 2.0, 0.0));
190    Ok(Resolved {
191        text,
192        value: Some(zone),
193        unit: "mm",
194        references: targets,
195        geometry: PmiGeometry::Fcf {
196            anchor: a3(anchor),
197            normal: a3(normal),
198            frame: FcfFrame {
199                characteristic: characteristic.id.to_string(),
200                symbol: characteristic.symbol.to_string(),
201                zone: zone_text,
202                datums: datum_cells,
203            },
204        },
205        default_label: a3(default_label),
206    })
207}