brep_render/engine_state/
assembly_flip.rs1use 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
29fn 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 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 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 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 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