Skip to main content

brep_kernel/feature_pipeline/pmi/annotations/
mod.rs

1//! The PMI annotation type table — twelve types, each a module exposing its
2//! [`PmiTypeDef`] (`DEF`), its `schema()` and its resolver. The table is the
3//! third applicability family beside the feature catalogue and the assembly
4//! constraint types: an annotation predicate accepts plain AND component
5//! geometry.
6
7pub mod balloon;
8pub mod angle;
9pub mod centerline;
10pub mod datum;
11pub mod explode;
12pub mod fcf;
13pub mod hide;
14pub mod hole_callout;
15pub mod leader;
16pub mod linear;
17pub mod note;
18pub mod radial;
19
20use super::{PmiAnnotation, PmiContext, Resolved};
21use crate::feature_pipeline::SelectionProbe;
22use serde_json::{Map, Value};
23
24/// One annotation type.
25pub struct PmiTypeDef {
26    /// Canonical `type` string (the persisted key).
27    pub type_id: &'static str,
28    /// Id-mint prefix (`DIM` → `DIM3`).
29    pub short_name: &'static str,
30    /// The type's ONE icon — a catalogued glyph character (the app's
31    /// `assets/glyphs` set). Every surface that stands for the type by picture
32    /// (tree row, viewport chip, context offer) reads this.
33    pub icon: &'static str,
34    /// `"{icon} {label}"`.
35    pub long_name: &'static str,
36    /// Plain label for messages and the tree.
37    pub label: &'static str,
38    /// Does the current selection make creating this annotation meaningful?
39    pub applicable: fn(&SelectionProbe) -> bool,
40    /// The annotation's schema (`{type, shortName, longName, label, icon,
41    /// inputParamsSchema}`), the shape the dialog engine consumes.
42    pub schema: fn() -> Value,
43    /// Resolve the annotation against the scene.
44    pub resolve: fn(&PmiAnnotation, &PmiContext<'_>) -> Result<Resolved, String>,
45}
46
47/// Panel / `+` dropdown order.
48pub const PMI_TYPES: [PmiTypeDef; 12] = [
49    linear::DEF,
50    radial::DEF,
51    angle::DEF,
52    leader::DEF,
53    balloon::DEF,
54    note::DEF,
55    hole_callout::DEF,
56    centerline::DEF,
57    datum::DEF,
58    fcf::DEF,
59    explode::DEF,
60    hide::DEF,
61];
62
63/// Look up a type by its canonical `type` string.
64pub fn pmi_type(type_id: &str) -> Option<&'static PmiTypeDef> {
65    PMI_TYPES.iter().find(|def| def.type_id == type_id)
66}
67
68/// The twelve annotation schemas, table order.
69pub fn pmi_schema_catalogue() -> Value {
70    Value::Array(PMI_TYPES.iter().map(|def| (def.schema)()).collect())
71}
72
73/// The solids a list of SOLID / COMPONENT references names, `(scene name,
74/// handle)`, each once: a solid by its name, a component (`ACOMP1`, or a
75/// nested `ACOMP1:ACOMP2`) by every solid it owns. What an explode step
76/// moves and a hide step hides.
77pub(super) fn target_solids(targets: &[String], context: &PmiContext<'_>) -> Result<Vec<(String, u32)>, String> {
78    if targets.is_empty() {
79        return Err("select at least one solid or component".into());
80    }
81    let mut solids: Vec<(String, u32)> = Vec::new();
82    for target in targets {
83        if let Some(handle) = context.scene.resolve_solid(target) {
84            solids.push((target.clone(), handle));
85        } else if crate::is_component_reference(crate::split_component_namespace(target).1) {
86            let members = context.scene.occurrence_solids(target);
87            if members.is_empty() {
88                return Err(format!("component '{target}' has no solids"));
89            }
90            solids.extend(members);
91        } else {
92            return Err(format!("'{target}' is not a solid or component"));
93        }
94    }
95    let mut seen = std::collections::HashSet::new();
96    solids.retain(|(name, _)| seen.insert(name.clone()));
97    Ok(solids)
98}
99
100// --- shared schema-field builders --------------------------------------------
101
102pub(super) fn id_field() -> Value {
103    serde_json::json!({
104        "type": "string",
105        "default_value": null,
106        "hint": "Unique identifier for the annotation"
107    })
108}
109
110pub(super) fn reference_field(
111    label: &str,
112    filter: &[&str],
113    multiple: bool,
114    min: usize,
115    max: usize,
116    hint: &str,
117) -> Value {
118    serde_json::json!({
119        "type": "reference_selection",
120        "label": label,
121        "selectionFilter": filter,
122        "multiple": multiple,
123        "minSelections": min,
124        "maxSelections": max,
125        "default_value": null,
126        "hint": hint
127    })
128}
129
130pub(super) fn number_field(label: &str, default: f64, step: f64, hint: &str) -> Value {
131    serde_json::json!({
132        "type": "number",
133        "label": label,
134        "default_value": default,
135        "step": step,
136        "hint": hint
137    })
138}
139
140pub(super) fn boolean_field(label: &str, default: bool, hint: &str) -> Value {
141    serde_json::json!({
142        "type": "boolean",
143        "label": label,
144        "default_value": default,
145        "hint": hint
146    })
147}
148
149pub(super) fn string_field(label: &str, default: &str, hint: &str) -> Value {
150    serde_json::json!({
151        "type": "string",
152        "label": label,
153        "default_value": default,
154        "hint": hint
155    })
156}
157
158pub(super) fn options_field(label: &str, options: &[&str], default: &str, hint: &str) -> Value {
159    serde_json::json!({
160        "type": "options",
161        "label": label,
162        "options": options,
163        "default_value": default,
164        "hint": hint
165    })
166}
167
168/// The dimension tolerance block + decimals + reference flag, appended to a
169/// dimension schema's params in this order.
170pub(super) fn dimension_fields(params: &mut Map<String, Value>, decimals_default: u64) {
171    params.insert(
172        "decimals".into(),
173        serde_json::json!({
174            "type": "number",
175            "label": "Decimals",
176            "default_value": decimals_default,
177            "step": 1,
178            "hint": "Decimal places shown (0–8)"
179        }),
180    );
181    params.insert(
182        "isReference".into(),
183        boolean_field("Reference", false, "A reference dimension: shown in parentheses, no tolerance"),
184    );
185    params.insert(
186        "tolMode".into(),
187        options_field(
188            "Tolerance",
189            &["none", "symmetric", "deviation", "limits"],
190            "none",
191            "none · ± symmetric · +upper/−lower deviation · upper/lower limit values",
192        ),
193    );
194    params.insert(
195        "tolUpper".into(),
196        number_field("Upper (+)", 0.0, 0.01, "The upper deviation (the ± value for symmetric)"),
197    );
198    params.insert(
199        "tolLower".into(),
200        number_field("Lower (−)", 0.0, 0.01, "The lower deviation (deviation / limits modes)"),
201    );
202}
203
204/// The annotation-plane field every drawn type carries (explode draws
205/// nothing): a planar face or reference plane the annotation lies in;
206/// empty aligns it to the view camera.
207pub(super) fn plane_field() -> Value {
208    reference_field(
209        "Annotation plane",
210        &["FACE", "PLANE"],
211        false,
212        0,
213        1,
214        "A planar face or reference plane the annotation lies in — leave empty to align it to the view camera",
215    )
216}
217
218pub(super) fn schema_entry(def: &PmiTypeDef, mut params: Map<String, Value>) -> Value {
219    if def.type_id != "explode" {
220        params.insert("plane".into(), plane_field());
221    }
222    serde_json::json!({
223        "type": def.type_id,
224        "shortName": def.short_name,
225        "longName": def.long_name,
226        "label": def.label,
227        "icon": def.icon,
228        "inputParamsSchema": Value::Object(params),
229    })
230}
231
232/// Build a params object in insertion order.
233pub(super) fn params(entries: Vec<(&str, Value)>) -> Map<String, Value> {
234    let mut map = Map::new();
235    for (key, value) in entries {
236        map.insert(key.to_string(), value);
237    }
238    map
239}
240
241/// The decimals param clamped to `0..=8`.
242pub(super) fn decimals_of(annotation: &PmiAnnotation, context: &PmiContext<'_>, default: f64) -> usize {
243    annotation
244        .number("decimals", context.env, default)
245        .unwrap_or(default)
246        .round()
247        .clamp(0.0, 8.0) as usize
248}