Skip to main content

brep_kernel/feature_pipeline/pmi/annotations/
explode.rs

1//! Explode body (`EXP`): a display-only pose delta on solids / components
2//! while the owning view is active. Never touches ACOMP transforms or the
3//! assembly solver; the base pose is the modeling pose at apply time.
4
5use 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
22/// One or more solids / components.
23fn 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}