1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum DatumRule {
29 None,
31 Optional,
33 Required,
35}
36
37pub 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
68pub fn characteristic_for_entity(keyword: &str) -> Option<&'static Characteristic> {
70 CHARACTERISTICS.iter().find(|c| c.step_entity == keyword)
71}
72
73pub 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
82fn 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
113pub 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}