Skip to main content

brep_render/engine_state/
assembly_flip.rs

1//! **Flip** a constraint to its other solution (Touch Align facing the other
2//! way, a distance on the other side, a gear turning the other way …).
3//!
4//! The kernel says what a flip is, per type ([`brep_kernel::assembly_flip_plan_json`],
5//! `constraints::flip_rule`): the parameter patch and how to SEED the moving
6//! component so the solve lands on the other branch. Without the seed the
7//! solve starts from the current pose and may simply return to the solution it
8//! left (or not converge). The patch and the seeded pose are written as ONE
9//! undo step: the pose through `set_many_feature_params` (the checkpoint),
10//! the patched assembly block without one, then one rerun, as the drag
11//! release writes poses and mechanical references.
12
13use super::component_move::quat_rotate;
14use super::transform_gizmo::{
15    euler_xyz_deg_from_quat, normalize3, quat_from_axis_angle, quat_from_euler_xyz_deg, quat_mul,
16};
17use super::*;
18use serde_json::{json, Value};
19
20fn triple(value: &Value) -> Option<[f64; 3]> {
21    let array = value.as_array()?;
22    Some([array.first()?.as_f64()?, array.get(1)?.as_f64()?, array.get(2)?.as_f64()?])
23}
24
25use crate::geometry3d::dot3 as dot;
26
27/// A unit vector perpendicular to `n`.
28fn perpendicular(n: [f64; 3]) -> [f64; 3] {
29    let helper = if n[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
30    normalize3([
31        n[1] * helper[2] - n[2] * helper[1],
32        n[2] * helper[0] - n[0] * helper[2],
33        n[0] * helper[1] - n[1] * helper[0],
34    ])
35}
36
37impl EngineState {
38    /// Flip constraint `id` to its other solution, as one undo step. Returns
39    /// the component the seed moved (empty when the flip moves nothing up
40    /// front: a mechanical mate, or a path/slot measured from the other end).
41    /// `Err` names why the constraint has no other solution.
42    pub fn assembly_flip_constraint(&mut self, id: &str) -> Result<String, String> {
43        self.ensure_assembly_session();
44        let plan: Value = serde_json::from_str(&brep_kernel::assembly_flip_plan_json(id))
45            .map_err(|error| format!("flip: {error}"))?;
46        if plan["ok"] != true {
47            return Err(plan["reason"].as_str().or(plan["error"].as_str()).unwrap_or("this constraint cannot flip").to_string());
48        }
49        let seed = plan["seed"].as_str().unwrap_or("none").to_string();
50        let elements = plan["elements"].as_array().cloned().unwrap_or_default();
51
52        // The patched assembly block (written without a checkpoint below).
53        let mut block = self.history.assembly_block().cloned().ok_or("flip: the document has no assembly")?;
54        let entry = block["constraints"]
55            .as_array_mut()
56            .into_iter()
57            .flatten()
58            .find(|entry| entry["inputParams"]["id"] == id)
59            .ok_or_else(|| format!("flip: no constraint '{id}'"))?;
60        for (key, value) in plan["patch"].as_object().into_iter().flatten() {
61            entry["inputParams"][key] = value.clone();
62        }
63
64        let seeded = if seed == "none" { None } else { self.flip_seed(&seed, &elements, &plan)? };
65        match &seeded {
66            Some((component, translate, rotate_deg)) => {
67                let index = self.history.index_of(component).ok_or_else(|| format!("flip: no component '{component}'"))?;
68                let mut params = self.history.feature_params(index).unwrap_or_else(|| json!({}));
69                if !params["transform"].is_object() {
70                    params["transform"] = json!({});
71                }
72                params["transform"]["translate"] = json!(translate);
73                params["transform"]["rotateEulerDeg"] = json!(rotate_deg);
74                self.history.break_coalescing();
75                self.history.set_many_feature_params(&[(index, params)]);
76                self.history.set_assembly_block(block, false);
77            }
78            None => {
79                self.history.break_coalescing();
80                self.history.set_assembly_block(block, true);
81            }
82        }
83        self.history.break_coalescing();
84        let _ = self.rerun_history();
85        Ok(seeded.map(|(component, _, _)| component).unwrap_or_default())
86    }
87
88    /// The moving component and its seeded `(translate, rotateEulerDeg)`.
89    /// The moving element is the non-fixed one; with both free, the one the
90    /// mates leave freer (the last solve's `bodyFreedom`), the second on a tie.
91    fn flip_seed(&mut self, seed: &str, elements: &[Value], plan: &Value) -> Result<Option<(String, [f64; 3], [f64; 3])>, String> {
92        let freedom = self.assembly_dof_value()["bodyFreedom"].clone();
93        let free_of = |element: &Value| -> u64 {
94            let component = element["component"].as_str().unwrap_or("");
95            freedom[component]["rotations"].as_u64().unwrap_or(0) + freedom[component]["translations"].as_u64().unwrap_or(0)
96        };
97        let movable: Vec<usize> = (0..elements.len()).filter(|&i| elements[i]["fixed"] != true && elements[i]["component"].as_str().is_some_and(|c| !c.is_empty())).collect();
98        let moving = match movable.as_slice() {
99            [] => return Err("both components are fixed: unfix one to flip".into()),
100            [only] => *only,
101            several => *several.iter().max_by_key(|&&i| (free_of(&elements[i]), i)).expect("non-empty"),
102        };
103        let reference = if moving == 0 { 1.min(elements.len().saturating_sub(1)) } else { 0 };
104        let component = elements[moving]["component"].as_str().unwrap_or_default().to_string();
105        let info = self.component_info(&component).ok_or_else(|| format!("flip: no component '{component}'"))?;
106        let anchor = triple(&elements[moving]["anchor"]).ok_or("flip: the moving element has no anchor")?;
107        let own = triple(&elements[moving]["direction"]).map(normalize3);
108        let other = triple(&elements[reference]["direction"]).map(normalize3);
109        // (rotation quaternion, pivot, extra translation)
110        let (rotation, shift) = match seed {
111            "turn" => {
112                let n = own.or(other).ok_or("flip: no direction to turn about")?;
113                (quat_from_axis_angle(perpendicular(n), std::f64::consts::PI), [0.0; 3])
114            }
115            "turn-about-reference" => {
116                let n = other.or(own).ok_or("flip: no direction to turn about")?;
117                (quat_from_axis_angle(n, std::f64::consts::PI), [0.0; 3])
118            }
119            "mirror" => {
120                let n = other.ok_or("flip: the reference has no plane to mirror across")?;
121                let base = triple(&elements[reference]["anchor"]).ok_or("flip: the reference has no anchor")?;
122                let offset = dot([anchor[0] - base[0], anchor[1] - base[1], anchor[2] - base[2]], n);
123                ([0.0, 0.0, 0.0, 1.0], [-2.0 * offset * n[0], -2.0 * offset * n[1], -2.0 * offset * n[2]])
124            }
125            "rotate" => {
126                let axis = triple(&plan["angleAxis"]).map(normalize3).or(other).ok_or("flip: no angle axis")?;
127                let value = plan["value"].as_f64().unwrap_or(0.0);
128                (quat_from_axis_angle(axis, (-2.0 * value).to_radians()), [0.0; 3])
129            }
130            _ => return Ok(None),
131        };
132        let q0 = quat_from_euler_xyz_deg(info.rotate_deg);
133        let offset = [info.translate[0] - anchor[0], info.translate[1] - anchor[1], info.translate[2] - anchor[2]];
134        let turned = quat_rotate(rotation, offset);
135        let translate = [anchor[0] + turned[0] + shift[0], anchor[1] + turned[1] + shift[1], anchor[2] + turned[2] + shift[2]];
136        let rotate_deg = euler_xyz_deg_from_quat(quat_mul(rotation, q0));
137        Ok(Some((component, translate, rotate_deg)))
138    }
139}
140