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, target_solids, 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 solids = target_solids(&annotation.references("targets"), context)?;
67    let mut low = Vec3::new(f64::INFINITY, f64::INFINITY, f64::INFINITY);
68    let mut high = Vec3::new(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY);
69    for (_, handle) in &solids {
70        let (lo, hi) = solid_bbox(*handle)?;
71        low = Vec3::new(low.x.min(lo.x), low.y.min(lo.y), low.z.min(lo.z));
72        high = Vec3::new(high.x.max(hi.x), high.y.max(hi.y), high.z.max(hi.z));
73    }
74    let center = low.add(high).scale(0.5);
75    let transform = annotation.params.get("transform");
76    let translate = triple(transform.and_then(|t| t.get("position")), [0.0; 3], context.env)?;
77    let rotate_deg = triple(transform.and_then(|t| t.get("rotationEuler")), [0.0; 3], context.env)?;
78    let scale = triple(transform.and_then(|t| t.get("scale")), [1.0; 3], context.env)?;
79    if scale.iter().any(|v| *v <= 0.) { return Err("explode scale must be positive".into()); }
80    let exploded = center.add(Vec3::new(translate[0], translate[1], translate[2]));
81    let names: Vec<String> = solids.into_iter().map(|(name, _)| name).collect();
82    Ok(Resolved {
83        text: format!("Explode {}", names.join(", ")),
84        value: None,
85        unit: "",
86        references: names.clone(),
87        geometry: PmiGeometry::Explode {
88            solids: names,
89            translate,
90            rotate_deg,
91            scale,
92            center: a3(center),
93            trace: annotation
94                .params
95                .get("showTraceLine")
96                .and_then(serde_json::Value::as_bool)
97                .unwrap_or(true),
98        },
99        default_label: a3(exploded),
100    })
101}