brep_kernel/feature_pipeline/pmi/
plugin.rs1use super::{PmiAnnotation, PmiContext, PmiGeometry, Resolved};
3use serde::{Deserialize, Serialize};
4use serde_json::{json, Value};
5use std::collections::BTreeMap;
6
7#[derive(Clone, Debug, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase", deny_unknown_fields)]
10pub struct PluginAnnotationOutput {
11 pub text: String,
12 pub geometry: PmiGeometry,
13 pub default_label: [f64; 3],
14 #[serde(default)]
15 pub references: Vec<String>,
16 #[serde(default)]
17 pub value: Option<f64>,
18 #[serde(default)]
19 pub unit: String,
20 #[serde(default)]
21 pub persistent_data: Value,
22}
23
24pub(super) fn resolve(
25 annotation: &PmiAnnotation,
26 context: &PmiContext<'_>,
27) -> Result<(Resolved, Value, PluginAnnotationReplay), String> {
28 let mut input = annotation.clone();
29 if let Some(replay) = &annotation.plugin_replay {
30 if replay.matches(annotation, context) {
31 input.persistent_data = replay.input_data.clone();
32 }
33 }
34 input.plugin_replay = None;
37 let output = super::super::extension::execute_annotation(&input, context)?;
38 output.validate()?;
39 if annotation
40 .label_world
41 .as_ref()
42 .is_some_and(|p| p.iter().any(|x| !x.is_finite()))
43 {
44 return Err("invalid plugin annotation label position".into());
45 }
46 for name in &output.references {
47 super::resolve::resolve_reference(context.scene, name)?;
48 }
49 let unit = match output.unit.as_str() {
50 "" => "",
51 "mm" => "mm",
52 "deg" => "deg",
53 _ => return Err("unsupported plugin annotation unit".into()),
54 };
55 let replay = PluginAnnotationReplay {
56 version: 1,
57 fingerprint: fingerprint(annotation, context),
58 dependencies: dependency_snapshots(context, output.references.iter().map(String::as_str))?,
59 input_data: input.persistent_data,
60 output_data: output.persistent_data.clone(),
61 };
62 Ok((
63 Resolved {
64 text: output.text,
65 value: output.value,
66 unit,
67 references: output.references,
68 geometry: output.geometry,
69 default_label: output.default_label,
70 },
71 output.persistent_data,
72 replay,
73 ))
74}
75
76impl PluginAnnotationOutput {
77 pub fn validate(&self) -> Result<(), String> {
79 let finite = |p: &[f64; 3]| p.iter().all(|x| x.is_finite());
80 if self.text.len() > 16_384 || self.references.len() > 256
81 || !finite(&self.default_label)
82 || self.value.is_some_and(|x| !x.is_finite())
83 || serde_json::to_vec(&self.persistent_data).map_err(|e| e.to_string())?.len() > 1_048_576
84 {
85 return Err("invalid plugin annotation output or output limit exceeded".into());
86 }
87 match &self.geometry {
88 PmiGeometry::Note { position } if finite(position) => {},
89 PmiGeometry::Leader { targets, .. }
90 if !targets.is_empty() && targets.len() <= 256 && targets.iter().all(finite) => {},
91 PmiGeometry::Linear { a, b, component }
92 if finite(a) && finite(b) && component.is_none_or(|c| matches!(c, 'X' | 'Y' | 'Z')) => {},
93 _ => return Err("unsupported or invalid plugin annotation geometry (expected note, leader or linear)".into()),
94 }
95 if let PmiGeometry::Linear { a, b, component: Some(axis) } = &self.geometry {
96 let index = match axis { 'X' => 0, 'Y' => 1, 'Z' => 2, _ => unreachable!() };
97 if (0..3).any(|i| i != index && (a[i] - b[i]).abs() > 1e-9) {
98 return Err("invalid plugin annotation geometry: component dimension endpoints must be projected onto its axis".into());
99 }
100 }
101 if !matches!(self.unit.as_str(), "" | "mm" | "deg") {
102 return Err("unsupported plugin annotation unit".into());
103 }
104 Ok(())
105 }
106}
107
108#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
111#[serde(rename_all = "camelCase")]
112pub struct PluginAnnotationReplay {
113 pub version: u32,
114 pub fingerprint: Value,
115 pub dependencies: BTreeMap<String, Value>,
116 pub input_data: Value,
117 pub output_data: Value,
118}
119
120impl PluginAnnotationReplay {
121 fn matches(&self, annotation: &PmiAnnotation, context: &PmiContext<'_>) -> bool {
122 self.version == 1
123 && self.output_data == annotation.persistent_data
124 && self.fingerprint == fingerprint(annotation, context)
125 && dependency_snapshots(context, self.dependencies.keys().map(String::as_str))
126 .is_ok_and(|current| current == self.dependencies)
127 }
128}
129
130fn fingerprint(annotation: &PmiAnnotation, context: &PmiContext<'_>) -> Value {
131 let owner = annotation.kind.split('/').next().unwrap_or("");
132 let pins: Vec<_> = context
133 .request
134 .plugins
135 .iter()
136 .filter(|p| p.id == owner)
137 .collect();
138 json!({"type": annotation.kind, "pins": pins, "params": annotation.params,
142 "expressions": context.request.expressions, "configurator": context.request.configurator})
143}
144
145fn dependency_snapshots<'a>(
146 context: &PmiContext<'_>,
147 names: impl IntoIterator<Item = &'a str>,
148) -> Result<BTreeMap<String, Value>, String> {
149 names
150 .into_iter()
151 .map(|name| {
152 let geometry = super::resolve::resolve_reference(context.scene, name)?;
153 let value = serde_json::to_value(geometry).map_err(|e| e.to_string())?;
154 Ok((name.to_owned(), value))
155 })
156 .collect()
157}