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