Skip to main content

brep_kernel/feature_pipeline/pmi/annotations/
mod.rs

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