brep_render/engine_state/
component_move.rs1use crate::geometry3d::{add3, cross3 as cross, sub3};
2use super::transform_gizmo::{
3 euler_xyz_deg_from_quat, normalize3, quat_from_axis_angle, quat_from_euler_xyz_deg, quat_mul,
4 parse_drag_delta, Quat, TransformDelta,
5};
6use super::*;
7
8#[derive(Default)]
16pub struct ComponentMoveArm {
17 pub(super) feature_id: Option<String>,
19 pub(super) anchor: [f64; 3],
22 pub(super) drag: Option<ComponentMoveDrag>,
24}
25
26#[derive(Clone)]
30pub(super) struct ComponentMoveDrag {
31 handle: u32,
32 sx: f32,
33 sy: f32,
34 start: ComponentPose,
35 pending: Option<ComponentPose>,
36}
37
38#[derive(Clone, Copy, PartialEq, Debug)]
41pub(super) struct ComponentPose {
42 pub translate: [f64; 3],
43 pub rotate_deg: [f64; 3],
44 pub anchor: [f64; 3],
45}
46
47impl EngineState {
48 pub fn component_move_armed(&self) -> bool {
50 self.component_move.feature_id.is_some()
51 }
52
53 pub fn component_move_armed_feature(&self) -> String {
55 self.component_move.feature_id.clone().unwrap_or_default()
56 }
57
58 pub fn component_move_toggle(&mut self, feature_id: &str) {
63 let Some(info) = self.component_info(feature_id) else {
64 self.push_notice(format!("'{feature_id}' is not an assembly component"));
65 return;
66 };
67 if info.fixed {
68 self.push_notice(format!(
69 "{} ({feature_id}) is fixed — unfix it to move",
70 info.part_name
71 ));
72 return;
73 }
74 if self.component_move.feature_id.as_deref() == Some(feature_id) {
75 self.disarm_transform();
76 } else {
77 self.component_move_arm_widget(feature_id);
78 }
79 }
80
81 pub(super) fn component_move_arm(&mut self, feature_id: &str) {
85 match self.component_info(feature_id) {
86 Some(info) if !info.fixed => self.component_move_arm_widget(feature_id),
87 _ => {}
88 }
89 }
90
91 pub(super) fn component_move_reset(&mut self) {
95 self.component_move = ComponentMoveArm::default();
96 }
97
98 fn component_move_arm_widget(&mut self, feature_id: &str) {
102 self.transform_gizmo.feature_id = None;
105 self.transform_gizmo.mode = GizmoMode::None;
106 self.transform_gizmo.drag = None;
107 self.clear_feature_dimension_overlay();
108
109 let anchor = self
110 .component_bbox_center(feature_id)
111 .or_else(|| self.component_info(feature_id).map(|info| info.translate))
112 .unwrap_or([0.0; 3]);
113 self.component_move.feature_id = Some(feature_id.to_string());
114 self.component_move.anchor = anchor;
115 self.component_move.drag = None;
116 self.feed_component_widget();
117 self.dirty = true;
118 }
119
120 fn feed_component_widget(&mut self) {
122 let Some(id) = self.component_move.feature_id.clone() else {
123 return;
124 };
125 let Some(info) = self.component_info(&id) else {
126 return;
127 };
128 let pose = ComponentPose {
129 translate: info.translate,
130 rotate_deg: info.rotate_deg,
131 anchor: self.component_move.anchor,
132 };
133 let json = component_frame_json(&pose);
134 let _ = self.widgets.set_transform_json(&json);
135 }
136
137 pub(super) fn component_move_sync(&mut self) {
141 let Some(id) = self.component_move.feature_id.clone() else {
142 return;
143 };
144 match self.component_info(&id) {
145 Some(info) if !info.fixed => {
146 self.component_move.anchor =
147 self.component_bbox_center(&id).unwrap_or(info.translate);
148 self.feed_component_widget();
149 self.dirty = true;
150 }
151 _ => self.disarm_transform(),
152 }
153 }
154
155 pub fn component_press(&mut self, x: f64, y: f64) -> bool {
158 let Some(id) = self.component_move.feature_id.clone() else {
159 return false;
160 };
161 let handle = self.transform_pick(x, y);
162 if handle == 0 {
163 return false;
164 }
165 let Some(info) = self.component_info(&id) else {
166 return false;
167 };
168 self.widgets.set_transform_active(handle);
169 self.component_move.drag = Some(ComponentMoveDrag {
170 handle,
171 sx: x as f32,
172 sy: y as f32,
173 start: ComponentPose {
174 translate: info.translate,
175 rotate_deg: info.rotate_deg,
176 anchor: self.component_move.anchor,
177 },
178 pending: None,
179 });
180 self.dirty = true;
181 true
182 }
183
184 pub fn component_move_dragging(&self) -> bool {
186 self.component_move.drag.is_some()
187 }
188
189 pub fn component_drag_to(&mut self, cx: f64, cy: f64) {
193 let Some(drag) = self.component_move.drag.clone() else {
194 return;
195 };
196 let cam = gizmo_camera(&self.camera);
197 let frame = component_frame_json(&drag.start);
198 let json = self.widgets.transform_drag_json_with_frame(
199 &cam,
200 &frame,
201 drag.handle,
202 drag.sx,
203 drag.sy,
204 cx as f32,
205 cy as f32,
206 );
207 let Some(delta) = parse_drag_delta(&json) else {
208 return;
209 };
210 let pending = compose_component_delta(&drag.start, &delta);
211 let json = component_frame_json(&pending);
214 let _ = self.widgets.set_transform_json(&json);
215 if let Some(live) = self.component_move.drag.as_mut() {
216 live.pending = Some(pending);
217 }
218 self.dirty = true;
219 }
220
221 pub fn component_release(&mut self) {
227 let Some(drag) = self.component_move.drag.take() else {
228 return;
229 };
230 self.widgets.set_transform_active(0);
231 self.dirty = true;
232 let Some(pending) = drag.pending else {
233 return;
234 };
235 let Some(id) = self.component_move.feature_id.clone() else {
236 return;
237 };
238 self.component_move.anchor = pending.anchor;
239 let Some(index) = self.history.index_of(&id) else {
240 return;
241 };
242 let mut params = self
243 .history
244 .feature_params(index)
245 .unwrap_or_else(|| serde_json::json!({}));
246 if !params.get("transform").map(|t| t.is_object()).unwrap_or(false) {
247 if let Some(object) = params.as_object_mut() {
248 object.insert("transform".into(), serde_json::json!({}));
249 }
250 }
251 if let Some(transform) = params.get_mut("transform").and_then(|t| t.as_object_mut()) {
252 transform.insert("translate".into(), serde_json::json!(pending.translate));
253 transform.insert("rotateEulerDeg".into(), serde_json::json!(pending.rotate_deg));
254 }
255 let _ = self.update_feature_params(&id, ¶ms.to_string());
256 }
257
258 pub fn component_move_json(&self) -> String {
261 serde_json::json!({
262 "armed": self.component_move_armed(),
263 "feature": self.component_move_armed_feature(),
264 "anchor": self.component_move.anchor,
265 })
266 .to_string()
267 }
268}
269
270fn component_frame_json(pose: &ComponentPose) -> String {
275 let euler = [
276 pose.rotate_deg[0].to_radians(),
277 pose.rotate_deg[1].to_radians(),
278 pose.rotate_deg[2].to_radians(),
279 ];
280 let x = normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler));
281 let y = normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler));
282 let z = normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler));
283 serde_json::json!({
284 "origin": pose.anchor,
285 "x": x,
286 "y": y,
287 "z": z,
288 "showCenter": true,
289 "showAxes": true,
290 "showRings": true,
291 })
292 .to_string()
293}
294
295pub(super) fn compose_component_delta(
298 start: &ComponentPose,
299 delta: &TransformDelta,
300) -> ComponentPose {
301 match delta {
302 TransformDelta::Translate(d) => ComponentPose {
303 translate: add3(start.translate, *d),
304 rotate_deg: start.rotate_deg,
305 anchor: add3(start.anchor, *d),
306 },
307 TransformDelta::Rotate { axis, radians } => {
308 let dq = quat_from_axis_angle(*axis, *radians);
309 let q0 = quat_from_euler_xyz_deg(start.rotate_deg);
310 let rotate_deg = euler_xyz_deg_from_quat(quat_mul(dq, q0));
311 let offset = sub3(start.translate, start.anchor);
312 ComponentPose {
313 translate: add3(start.anchor, quat_rotate(dq, offset)),
314 rotate_deg,
315 anchor: start.anchor,
316 }
317 }
318 }
319}
320
321fn quat_rotate(q: Quat, v: [f64; 3]) -> [f64; 3] {
323 let u = [q[0], q[1], q[2]];
324 let w = q[3];
325 let t = cross(u, add3(cross(u, v), [w * v[0], w * v[1], w * v[2]]));
326 [v[0] + 2.0 * t[0], v[1] + 2.0 * t[1], v[2] + 2.0 * t[2]]
327}
328
329