Skip to main content

brep_plugins/
annotations.rs

1//! Synchronous typed PMI callbacks. No resident geometry handles enter JavaScript.
2use super::*;
3use kernel::pmi::{PluginAnnotationOutput, PmiAnnotation, PmiContext, PmiGeometry};
4
5impl Runtime {
6    pub fn execute_annotation(
7        &self,
8        pins: &[PluginPin],
9        annotation: &PmiAnnotation,
10        context: &PmiContext<'_>,
11    ) -> Result<PluginAnnotationOutput, String> {
12        let package = self.package(pins, &annotation.kind)?;
13        let pin = package.pin()?;
14        let registration = self
15            .registry
16            .annotations
17            .iter()
18            .find(|r| r.id == annotation.kind && r.package == pin)
19            .ok_or("annotation registration unavailable")?;
20        let mut params = annotation.params.clone();
21        // Use the document's expression environment, then the same schema validator
22        // as actions/features. Reference fields are resolved separately below.
23        if let Some(fields) = params.as_object_mut() {
24            for (key, schema) in registration.input_params_schema.as_object().unwrap() {
25                if schema["type"] == "number" {
26                    if let Some(source) = fields.get(key).and_then(Value::as_str) {
27                        let value = context.env.eval(source)?;
28                        if !value.is_finite() {
29                            return Err("annotation parameter must be finite".into());
30                        }
31                        fields.insert(key.clone(), json!(value));
32                    }
33                }
34            }
35        }
36        validate_params(&registration.input_params_schema, &mut params, None)?;
37        let mut names = BTreeSet::new();
38        fn collect(value: &Value, names: &mut BTreeSet<String>) -> Result<(), String> {
39            match value {
40                Value::String(name) => {
41                    if !name.trim().is_empty() {
42                        names.insert(name.clone());
43                    }
44                }
45                Value::Object(object) => {
46                    let name = object["name"].as_str().ok_or("reference missing name")?;
47                    if !name.trim().is_empty() {
48                        names.insert(name.to_owned());
49                    }
50                }
51                Value::Array(values) => {
52                    for value in values {
53                        collect(value, names)?;
54                    }
55                }
56                _ => return Err("invalid annotation reference".into()),
57            }
58            Ok(())
59        }
60        for (key, schema) in registration.input_params_schema.as_object().unwrap() {
61            if schema["type"] == "reference_selection" {
62                collect(&params[key], &mut names)?;
63            }
64        }
65        if names.len() > 256 {
66            return Err("annotation dependency limit exceeded".into());
67        }
68        let mut dependencies = serde_json::Map::new();
69        for name in &names {
70            if name.is_empty() || name.len() > 1024 {
71                return Err("invalid annotation dependency name".into());
72            }
73            let point = kernel::pmi::resolve::resolve_reference(context.scene, name)?
74                .representative_point();
75            let point = [point.x, point.y, point.z];
76            if !point.iter().all(|v| v.is_finite()) {
77                return Err("nonfinite annotation dependency".into());
78            }
79            dependencies.insert(name.clone(), json!({"point":point}));
80        }
81        let input = json!({"params":params,"persistentData":annotation.persistent_data,"annotationId":annotation.id(),"units":"mm","dependencies":dependencies});
82        if input.to_string().len() > 1024 * 1024 {
83            return Err("annotation input exceeds 1 MiB".into());
84        }
85        let (registry, value, _) = invocation::invoke(
86            package,
87            Some(("annotation", &annotation.kind)),
88            Some(&input),
89            None,
90            Some(&self.registry.for_document(&[pin])?),
91        )?;
92        self.verify(package, &registry)?;
93        let mut output = validate_output(value)?;
94        if output.references.iter().any(|r| !names.contains(r)) {
95            return Err("annotation output contains undeclared reference".into());
96        }
97        // Include every declared dependency, even if the callback omits references.
98        output.references = names.into_iter().collect();
99        Ok(output)
100    }
101}
102
103pub(super) fn validate_output(value: Value) -> Result<PluginAnnotationOutput, String> {
104    let object = value
105        .as_object()
106        .ok_or("annotation result must be an object")?;
107    if object.keys().any(|k| {
108        ![
109            "text",
110            "geometry",
111            "defaultLabel",
112            "references",
113            "value",
114            "unit",
115            "persistentData",
116        ]
117        .contains(&k.as_str())
118    }) {
119        return Err("unknown annotation result property".into());
120    }
121    let geometry = object
122        .get("geometry")
123        .and_then(Value::as_object)
124        .ok_or("annotation geometry must be an object")?;
125    let allowed: &[&str] = match geometry.get("kind").and_then(Value::as_str) {
126        Some("note") => &["kind", "position"],
127        Some("leader") => &["kind", "targets", "dot"],
128        Some("linear") => &["kind", "a", "b", "component"],
129        _ => return Err("unsupported annotation geometry; expected note, leader or linear".into()),
130    };
131    if geometry.keys().any(|k| !allowed.contains(&k.as_str())) {
132        return Err("unknown annotation geometry property".into());
133    }
134    let output: PluginAnnotationOutput =
135        serde_json::from_value(value).map_err(|e| format!("invalid annotation result: {e}"))?;
136    output.validate()?;
137    let finite = |p: &[f64; 3]| p.iter().all(|v| v.is_finite() && v.abs() <= 1e12);
138    if output.text.is_empty()
139        || output.text.len() > 16384
140        || !matches!(output.unit.as_str(), "" | "mm" | "deg")
141        || !finite(&output.default_label)
142        || output.value.is_some_and(|v| !v.is_finite())
143        || output.references.len() > 256
144        || output
145            .references
146            .iter()
147            .any(|s| s.is_empty() || s.len() > 1024)
148        || output.persistent_data.to_string().len() > 1024 * 1024
149    {
150        return Err("invalid annotation text, value, references, persistent data or label".into());
151    }
152    match &output.geometry {
153        PmiGeometry::Note { position } if finite(position) => {}
154        PmiGeometry::Leader { targets, .. }
155            if !targets.is_empty() && targets.len() <= 256 && targets.iter().all(finite) => {}
156        PmiGeometry::Linear { a, b, component }
157            if finite(a)
158                && finite(b)
159                && a != b
160                && component.is_none_or(|c| match c {
161                    'X' => a[1] == b[1] && a[2] == b[2],
162                    'Y' => a[0] == b[0] && a[2] == b[2],
163                    'Z' => a[0] == b[0] && a[1] == b[1],
164                    _ => false,
165                }) => {}
166        _ => return Err("invalid annotation geometry".into()),
167    }
168    Ok(output)
169}
170
171pub(super) fn primitive(op: &str, params: &Value) -> Result<Value, String> {
172    let mut output =
173        json!({"text":params["text"], "defaultLabel":params["defaultLabel"], "persistentData":{}});
174    output["geometry"] = match op {
175        "annotation.note" => {
176            output["defaultLabel"] = params["position"].clone();
177            json!({"kind":"note", "position":params["position"]})
178        }
179        "annotation.leader" => {
180            json!({"kind":"leader", "targets":params["targets"], "dot":params.get("dot").cloned().unwrap_or(json!(false))})
181        }
182        "annotation.linear" => {
183            json!({"kind":"linear", "a":params["a"], "b":params["b"], "component":params.get("component").cloned().unwrap_or(Value::Null)})
184        }
185        _ => return Err("unknown annotation primitive".into()),
186    };
187    // PMI layout draws a→b directly. Project the endpoint before validation so
188    // component measurements and the rendered dimension describe the same span.
189    if op == "annotation.linear" {
190        if let Some(axis) = match params["component"].as_str() {
191            Some("X") => Some(0),
192            Some("Y") => Some(1),
193            Some("Z") => Some(2),
194            _ => None,
195        } {
196            let a: [f64; 3] = serde_json::from_value(params["a"].clone())
197                .map_err(|e| format!("invalid linear endpoint: {e}"))?;
198            let original_b: [f64; 3] = serde_json::from_value(params["b"].clone())
199                .map_err(|e| format!("invalid linear endpoint: {e}"))?;
200            if !a
201                .iter()
202                .chain(original_b.iter())
203                .all(|v| v.is_finite() && v.abs() <= 1e12)
204            {
205                return Err("invalid linear endpoint".into());
206            }
207            let mut b = a;
208            b[axis] = original_b[axis];
209            output["geometry"]["b"] = json!(b);
210        }
211    }
212    let mut validated = validate_output(output)?;
213    if let PmiGeometry::Linear { a, b, component } = &validated.geometry {
214        let delta = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
215        validated.value = Some(match component {
216            Some('X') => delta[0].abs(),
217            Some('Y') => delta[1].abs(),
218            Some('Z') => delta[2].abs(),
219            _ => delta.iter().map(|v| v * v).sum::<f64>().sqrt(),
220        });
221        validated.unit = "mm".into();
222    }
223    serde_json::to_value(validated).map_err(|e| e.to_string())
224}