brep_kernel/feature_pipeline/pmi/annotations/
explode.rs1use super::{boolean_field, id_field, params, reference_field, schema_entry, PmiTypeDef};
6use crate::feature_pipeline::pmi::resolve::{a3, solid_bbox};
7use crate::feature_pipeline::pmi::{PmiAnnotation, PmiContext, PmiGeometry, Resolved};
8use crate::feature_pipeline::SelectionProbe;
9use crate::Vec3;
10
11pub const DEF: PmiTypeDef = PmiTypeDef {
12 type_id: "explode",
13 short_name: "EXP",
14 icon: "\u{29C9}",
15 long_name: "\u{29C9} Explode body",
16 label: "Explode body",
17 applicable,
18 schema,
19 resolve,
20};
21
22fn applicable(probe: &SelectionProbe) -> bool {
24 probe.solids + probe.components >= 1
25}
26
27fn schema() -> serde_json::Value {
28 schema_entry(
29 &DEF,
30 params(vec![
31 ("id", id_field()),
32 (
33 "targets",
34 reference_field("Targets", &["SOLID", "COMPONENT"], true, 1, 64, "The solids or components to move"),
35 ),
36 (
37 "transform",
38 serde_json::json!({
39 "type": "transform",
40 "default_value": { "position": [0, 0, 0], "rotationEuler": [0, 0, 0], "scale": [1, 1, 1] },
41 "hint": "The pose delta applied while the view is active"
42 }),
43 ),
44 ("showTraceLine", boolean_field("Trace line", true, "Draw a dashed line from the original to the exploded position")),
45 ]),
46 )
47}
48
49fn triple(value: Option<&serde_json::Value>, default: [f64; 3], env: &crate::feature_pipeline::Env) -> Result<[f64; 3], String> {
50 let Some(items) = value.and_then(serde_json::Value::as_array) else {
51 return Ok(default);
52 };
53 let mut out = default;
54 for (slot, item) in out.iter_mut().zip(items) {
55 if let Some(number) = item.as_f64() {
56 *slot = number;
57 } else if let Some(text) = item.as_str() {
58 *slot = env.eval(text.trim()).map_err(|e| e.to_string())?;
59 }
60 }
61 if out.iter().any(|v| !v.is_finite()) { return Err("explode transform must be finite".into()); }
62 Ok(out)
63}
64
65fn resolve(annotation: &PmiAnnotation, context: &PmiContext<'_>) -> Result<Resolved, String> {
66 let targets = annotation.references("targets");
67 if targets.is_empty() {
68 return Err("select at least one solid or component".into());
69 }
70 let mut solids: Vec<(String, u32)> = Vec::new();
71 for target in &targets {
72 if let Some(handle) = context.scene.resolve_solid(target) {
73 solids.push((target.clone(), handle));
74 } else if crate::is_component_reference(crate::split_component_namespace(target).1) {
75 let members = context.scene.occurrence_solids(target);
76 if members.is_empty() {
77 return Err(format!("component '{target}' has no solids"));
78 }
79 solids.extend(members);
80 } else {
81 return Err(format!("'{target}' is not a solid or component"));
82 }
83 }
84 let mut seen = std::collections::HashSet::new();
85 solids.retain(|(name, _)| seen.insert(name.clone()));
86 let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
87 let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
88 for (_, handle) in &solids {
89 let (lo, hi) = solid_bbox(*handle)?;
90 low = Vec3::new(low.x.min(lo.x), low.y.min(lo.y), low.z.min(lo.z));
91 high = Vec3::new(high.x.max(hi.x), high.y.max(hi.y), high.z.max(hi.z));
92 }
93 let center = low.add(high).scale(0.5);
94 let transform = annotation.params.get("transform");
95 let translate = triple(transform.and_then(|t| t.get("position")), [0.0; 3], context.env)?;
96 let rotate_deg = triple(transform.and_then(|t| t.get("rotationEuler")), [0.0; 3], context.env)?;
97 let scale = triple(transform.and_then(|t| t.get("scale")), [1.0; 3], context.env)?;
98 if scale.iter().any(|v| *v <= 0.) { return Err("explode scale must be positive".into()); }
99 let exploded = center.add(Vec3::new(translate[0], translate[1], translate[2]));
100 let names: Vec<String> = solids.into_iter().map(|(name, _)| name).collect();
101 Ok(Resolved {
102 text: format!("Explode {}", names.join(", ")),
103 value: None,
104 unit: "",
105 references: names.clone(),
106 geometry: PmiGeometry::Explode {
107 solids: names,
108 translate,
109 rotate_deg,
110 scale,
111 center: a3(center),
112 trace: annotation
113 .params
114 .get("showTraceLine")
115 .and_then(serde_json::Value::as_bool)
116 .unwrap_or(true),
117 },
118 default_label: a3(exploded),
119 })
120}