1use super::*;
2
3#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
10pub enum GizmoMode {
11 #[default]
13 None,
14 Transform,
16 Dimension,
18}
19
20#[derive(Default)]
22pub struct TransformArm {
23 pub(super) feature_id: Option<String>,
26 pub(super) mode: GizmoMode,
29 pub(super) drag: Option<TransformDrag>,
31}
32
33#[derive(Clone, Copy)]
38pub(super) struct TransformDrag {
39 handle: u32,
40 sx: f32,
41 sy: f32,
42 start: TransformPose,
43}
44
45#[derive(Clone, Copy, PartialEq, Debug)]
49struct TransformPose {
50 position: [f64; 3],
51 rotation_deg: [f64; 3],
52 scale: [f64; 3],
53}
54
55#[derive(Clone, Copy, PartialEq, Debug)]
57enum TransformDelta {
58 Translate([f64; 3]),
60 Rotate { axis: [f64; 3], radians: f64 },
63 None,
65}
66
67impl EngineState {
68 pub fn transform_armed(&self) -> bool {
72 matches!(self.transform_gizmo.mode, GizmoMode::Transform)
73 }
74
75 pub fn transform_armed_feature(&self) -> String {
77 if self.transform_armed() {
78 self.transform_gizmo.feature_id.clone().unwrap_or_default()
79 } else {
80 String::new()
81 }
82 }
83
84 pub fn transform_armed_for(&self, feature_id: &str) -> bool {
86 self.transform_armed() && self.transform_gizmo.feature_id.as_deref() == Some(feature_id)
87 }
88
89 pub fn arm_transform(&mut self, feature_id: &str) {
93 self.transform_gizmo.feature_id = Some(feature_id.to_string());
94 self.transform_gizmo.mode = GizmoMode::Transform;
95 self.transform_gizmo.drag = None;
96 self.clear_feature_dimension_overlay();
97 self.sync_transform_gizmo();
98 }
99
100 pub fn disarm_transform(&mut self) {
102 self.transform_gizmo.feature_id = None;
103 self.transform_gizmo.mode = GizmoMode::None;
104 self.transform_gizmo.drag = None;
105 let _ = self.widgets.set_transform_json("null");
106 self.clear_feature_dimension_overlay();
107 self.dirty = true;
108 }
109
110 fn armed_pose(&self) -> Option<TransformPose> {
113 let id = self.transform_gizmo.feature_id.as_deref()?;
114 let index = self.history.index_of(id)?;
115 let params = self.history.feature_params(index)?;
116 let transform = params.get("transform");
117 Some(TransformPose {
118 position: read_pose_vec3(transform, "position", [0.0, 0.0, 0.0]),
119 rotation_deg: read_pose_vec3(transform, "rotationEuler", [0.0, 0.0, 0.0]),
120 scale: read_pose_vec3(transform, "scale", [1.0, 1.0, 1.0]),
121 })
122 }
123
124 pub fn sync_transform_gizmo(&mut self) {
131 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
134 return;
135 }
136 let Some(pose) = self.armed_pose() else {
137 self.disarm_transform();
138 return;
139 };
140 let _ = self.widgets.set_transform_json(&transform_frame_json(&pose));
141 self.dirty = true;
142 }
143
144 pub fn feature_has_transform(&self, feature_id: &str) -> bool {
152 self.history
153 .index_of(feature_id)
154 .and_then(|i| self.history.feature_params(i))
155 .map(|params| params.get("transform").is_some())
156 .unwrap_or(false)
157 }
158
159 pub fn transform_gizmo_anchor(&self) -> Option<(f64, f64)> {
163 let pose = self.armed_pose()?;
164 let (sx, sy, depth) = self.camera.project(pose.position);
165 (depth > 0.0).then_some((sx, sy))
166 }
167
168 pub fn transform_axis_labels_json(&self) -> String {
173 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
174 return "[]".to_string();
175 }
176 let Some(pose) = self.armed_pose() else {
177 return "[]".to_string();
178 };
179 let euler = [
180 pose.rotation_deg[0].to_radians(),
181 pose.rotation_deg[1].to_radians(),
182 pose.rotation_deg[2].to_radians(),
183 ];
184 let axes = [
185 normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler)),
186 normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler)),
187 normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler)),
188 ];
189 let gap_px = 12.0_f64;
191 let dist = (brep_gizmos::transform::PX_AXIS_LEN as f64 + gap_px) * self.camera.world_per_pixel();
192 let labels: [(&str, [f32; 3]); 3] = [
193 ("XC", [0.92, 0.26, 0.28]), ("YC", [0.30, 0.78, 0.36]), ("ZC", [0.30, 0.52, 0.98]), ];
197 let out: Vec<serde_json::Value> = (0..3)
198 .map(|i| {
199 let o = pose.position;
200 let a = axes[i];
201 serde_json::json!({
202 "text": labels[i].0,
203 "rgb": labels[i].1,
204 "world": [o[0] + a[0] * dist, o[1] + a[1] * dist, o[2] + a[2] * dist],
205 })
206 })
207 .collect();
208 serde_json::to_string(&out).unwrap_or_else(|_| "[]".to_string())
209 }
210
211 pub fn transform_press(&mut self, x: f64, y: f64) -> bool {
216 if !matches!(self.transform_gizmo.mode, GizmoMode::Transform) {
219 return false;
220 }
221 let handle = self.transform_pick(x, y);
222 if handle == 0 {
223 return false;
224 }
225 let Some(pose) = self.armed_pose() else {
226 return false;
227 };
228 self.widgets.set_transform_active(handle);
229 self.transform_gizmo.drag = Some(TransformDrag {
230 handle,
231 sx: x as f32,
232 sy: y as f32,
233 start: pose,
234 });
235 self.dirty = true;
236 true
237 }
238
239 pub fn transform_dragging(&self) -> bool {
241 self.transform_gizmo.drag.is_some()
242 }
243
244 pub fn transform_drag_to(&mut self, cx: f64, cy: f64) {
251 let Some(drag) = self.transform_gizmo.drag else {
252 return;
253 };
254 let delta = self.resolve_transform_delta(&drag, cx, cy);
255 if matches!(delta, TransformDelta::None) {
256 return;
257 }
258 let pose = apply_transform_delta(&drag.start, &delta);
259 self.write_armed_pose(&pose);
260 self.sync_transform_gizmo();
265 }
266
267 pub fn transform_release(&mut self) {
270 if self.transform_gizmo.drag.take().is_some() {
271 self.widgets.set_transform_active(0);
272 self.sync_transform_gizmo();
273 }
274 }
275
276 fn resolve_transform_delta(&self, drag: &TransformDrag, cx: f64, cy: f64) -> TransformDelta {
288 let cam = gizmo_camera(&self.camera);
289 let frame_json = transform_frame_json(&drag.start);
290 let json = self.widgets.transform_drag_json_with_frame(
291 &cam,
292 &frame_json,
293 drag.handle,
294 drag.sx,
295 drag.sy,
296 cx as f32,
297 cy as f32,
298 );
299 let value: serde_json::Value =
300 serde_json::from_str(&json).unwrap_or(serde_json::Value::Null);
301 match value.get("kind").and_then(|k| k.as_str()) {
302 Some("translate") => {
303 TransformDelta::Translate(json_vec3(value.get("world").and_then(|a| a.as_array())))
304 }
305 Some("rotate") => TransformDelta::Rotate {
306 axis: json_vec3(value.get("axisWorld").and_then(|a| a.as_array())),
307 radians: value.get("radians").and_then(|n| n.as_f64()).unwrap_or(0.0),
308 },
309 _ => TransformDelta::None,
310 }
311 }
312
313 fn write_armed_pose(&mut self, pose: &TransformPose) {
316 let Some(id) = self.transform_gizmo.feature_id.clone() else {
317 return;
318 };
319 let Some(index) = self.history.index_of(&id) else {
320 return;
321 };
322 let mut params = self
323 .history
324 .feature_params(index)
325 .unwrap_or_else(|| serde_json::json!({}));
326 if !params.get("transform").map(|t| t.is_object()).unwrap_or(false) {
328 if let Some(object) = params.as_object_mut() {
329 object.insert("transform".into(), serde_json::json!({}));
330 }
331 }
332 if let Some(transform) = params.get_mut("transform").and_then(|t| t.as_object_mut()) {
333 transform.insert("position".into(), serde_json::json!(pose.position));
334 transform.insert("rotationEuler".into(), serde_json::json!(pose.rotation_deg));
335 }
336 let _ = self.update_feature_params(&id, ¶ms.to_string());
337 }
338}
339
340fn apply_transform_delta(start: &TransformPose, delta: &TransformDelta) -> TransformPose {
346 match delta {
347 TransformDelta::Translate(world) => TransformPose {
348 position: [
349 start.position[0] + world[0],
350 start.position[1] + world[1],
351 start.position[2] + world[2],
352 ],
353 ..*start
354 },
355 TransformDelta::Rotate { axis, radians } => {
356 let q0 = quat_from_euler_xyz_deg(start.rotation_deg);
357 let dq = quat_from_axis_angle(*axis, *radians);
358 let nq = quat_mul(dq, q0);
359 TransformPose {
360 rotation_deg: euler_xyz_deg_from_quat(nq),
361 ..*start
362 }
363 }
364 TransformDelta::None => *start,
365 }
366}
367
368fn read_pose_vec3(transform: Option<&serde_json::Value>, key: &str, default: [f64; 3]) -> [f64; 3] {
371 let array = transform.and_then(|t| t.get(key)).and_then(|v| v.as_array());
372 let mut out = default;
373 if let Some(array) = array {
374 for (index, slot) in out.iter_mut().enumerate() {
375 if let Some(number) = array.get(index).and_then(|v| v.as_f64()) {
376 *slot = number;
377 }
378 }
379 }
380 out
381}
382
383fn json_vec3(array: Option<&Vec<serde_json::Value>>) -> [f64; 3] {
385 let mut out = [0.0; 3];
386 if let Some(array) = array {
387 for (index, slot) in out.iter_mut().enumerate() {
388 if let Some(number) = array.get(index).and_then(|v| v.as_f64()) {
389 *slot = number;
390 }
391 }
392 }
393 out
394}
395
396fn transform_frame_json(pose: &TransformPose) -> String {
400 let euler = [
401 pose.rotation_deg[0].to_radians(),
402 pose.rotation_deg[1].to_radians(),
403 pose.rotation_deg[2].to_radians(),
404 ];
405 let x = normalize3(rotate_euler_xyz_f64([1.0, 0.0, 0.0], euler));
406 let y = normalize3(rotate_euler_xyz_f64([0.0, 1.0, 0.0], euler));
407 let z = normalize3(rotate_euler_xyz_f64([0.0, 0.0, 1.0], euler));
408 serde_json::json!({
409 "origin": pose.position,
410 "x": x,
411 "y": y,
412 "z": z,
413 "showCenter": true,
414 })
415 .to_string()
416}
417
418fn normalize3(v: [f64; 3]) -> [f64; 3] {
419 let length = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
420 if length < 1e-12 {
421 [0.0, 0.0, 1.0]
422 } else {
423 [v[0] / length, v[1] / length, v[2] / length]
424 }
425}
426
427pub(crate) fn rotate_euler_xyz_f64(v: [f64; 3], euler: [f64; 3]) -> [f64; 3] {
431 let (c1, s1) = (euler[0].cos(), euler[0].sin());
432 let (c2, s2) = (euler[1].cos(), euler[1].sin());
433 let (c3, s3) = (euler[2].cos(), euler[2].sin());
434 let m00 = c2 * c3;
435 let m01 = -c2 * s3;
436 let m02 = s2;
437 let m10 = c1 * s3 + c3 * s1 * s2;
438 let m11 = c1 * c3 - s1 * s2 * s3;
439 let m12 = -c2 * s1;
440 let m20 = s1 * s3 - c1 * c3 * s2;
441 let m21 = c3 * s1 + c1 * s2 * s3;
442 let m22 = c1 * c2;
443 [
444 m00 * v[0] + m01 * v[1] + m02 * v[2],
445 m10 * v[0] + m11 * v[1] + m12 * v[2],
446 m20 * v[0] + m21 * v[1] + m22 * v[2],
447 ]
448}
449
450type Quat = [f64; 4]; fn quat_from_axis_angle(axis: [f64; 3], angle: f64) -> Quat {
455 let n = normalize3(axis);
456 let half = angle * 0.5;
457 let s = half.sin();
458 [n[0] * s, n[1] * s, n[2] * s, half.cos()]
459}
460
461fn quat_from_euler_xyz_deg(deg: [f64; 3]) -> Quat {
463 let (c1, s1) = ((deg[0].to_radians() * 0.5).cos(), (deg[0].to_radians() * 0.5).sin());
464 let (c2, s2) = ((deg[1].to_radians() * 0.5).cos(), (deg[1].to_radians() * 0.5).sin());
465 let (c3, s3) = ((deg[2].to_radians() * 0.5).cos(), (deg[2].to_radians() * 0.5).sin());
466 [
467 s1 * c2 * c3 + c1 * s2 * s3,
468 c1 * s2 * c3 - s1 * c2 * s3,
469 c1 * c2 * s3 + s1 * s2 * c3,
470 c1 * c2 * c3 - s1 * s2 * s3,
471 ]
472}
473
474fn quat_mul(a: Quat, b: Quat) -> Quat {
476 [
477 a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
478 a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
479 a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
480 a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
481 ]
482}
483
484fn euler_xyz_deg_from_quat(q: Quat) -> [f64; 3] {
488 let [x, y, z, w] = q;
489 let (x2, y2, z2) = (x + x, y + y, z + z);
490 let (xx, xy, xz) = (x * x2, x * y2, x * z2);
491 let (yy, yz, zz) = (y * y2, y * z2, z * z2);
492 let (wx, wy, wz) = (w * x2, w * y2, w * z2);
493 let m11 = 1.0 - (yy + zz);
495 let m12 = xy - wz;
496 let m13 = xz + wy;
497 let m22 = 1.0 - (xx + zz);
498 let m23 = yz - wx;
499 let m32 = yz + wx;
500 let m33 = 1.0 - (xx + yy);
501 let ey = m13.clamp(-1.0, 1.0).asin();
502 let (ex, ez) = if m13.abs() < 0.9999999 {
503 ((-m23).atan2(m33), (-m12).atan2(m11))
504 } else {
505 (m32.atan2(m22), 0.0)
506 };
507 [ex.to_degrees(), ey.to_degrees(), ez.to_degrees()]
508}
509
510#[cfg(test)]
511mod transform_gizmo_tests {
512 use super::*;
513
514 fn cube_request(name: &str, size: f64) -> String {
517 serde_json::json!({
518 "expressions": "",
519 "configurator": {},
520 "features": [{
521 "type": "P.CU",
522 "inputParams": {
523 "id": name,
524 "sizeX": size, "sizeY": size, "sizeZ": size,
525 "transform": {
526 "position": [0.0, 0.0, 0.0],
527 "rotationEuler": [0.0, 0.0, 0.0],
528 "scale": [1.0, 1.0, 1.0]
529 },
530 "boolean": { "targets": [], "operation": "NONE" }
531 },
532 "persistentData": {}
533 }]
534 })
535 .to_string()
536 }
537
538 fn ident() -> TransformPose {
539 TransformPose {
540 position: [1.0, 2.0, 3.0],
541 rotation_deg: [0.0, 0.0, 0.0],
542 scale: [1.0, 1.0, 1.0],
543 }
544 }
545
546 #[test]
547 fn translate_delta_adds_world_to_position() {
548 let pose = apply_transform_delta(&ident(), &TransformDelta::Translate([4.0, -1.0, 0.5]));
549 assert_eq!(pose.position, [5.0, 1.0, 3.5]);
550 assert_eq!(pose.rotation_deg, [0.0, 0.0, 0.0]);
551 assert_eq!(pose.scale, [1.0, 1.0, 1.0]);
552 }
553
554 #[test]
555 fn rotate_delta_about_z_yields_z_euler() {
556 let pose = apply_transform_delta(
558 &ident(),
559 &TransformDelta::Rotate {
560 axis: [0.0, 0.0, 1.0],
561 radians: std::f64::consts::FRAC_PI_2,
562 },
563 );
564 assert!((pose.rotation_deg[0]).abs() < 1e-6, "{:?}", pose.rotation_deg);
565 assert!((pose.rotation_deg[1]).abs() < 1e-6, "{:?}", pose.rotation_deg);
566 assert!((pose.rotation_deg[2] - 90.0).abs() < 1e-4, "{:?}", pose.rotation_deg);
567 assert_eq!(pose.position, [1.0, 2.0, 3.0]);
569 }
570
571 #[test]
572 fn euler_quat_roundtrip_is_identity() {
573 let deg = [30.0, 45.0, 60.0];
575 let back = euler_xyz_deg_from_quat(quat_from_euler_xyz_deg(deg));
576 for k in 0..3 {
577 assert!((back[k] - deg[k]).abs() < 1e-4, "axis {k}: {back:?} vs {deg:?}");
578 }
579 }
580
581 #[test]
582 fn rotate_about_local_axis_composes_onto_existing_orientation() {
583 let start = TransformPose {
587 rotation_deg: [0.0, 0.0, 90.0],
588 ..ident()
589 };
590 let pose = apply_transform_delta(
591 &start,
592 &TransformDelta::Rotate {
593 axis: [0.0, 0.0, 1.0],
594 radians: std::f64::consts::FRAC_PI_2,
595 },
596 );
597 let got = quat_from_euler_xyz_deg(pose.rotation_deg);
600 let want = quat_from_euler_xyz_deg([0.0, 0.0, 180.0]);
601 let dot = got[0] * want[0] + got[1] * want[1] + got[2] * want[2] + got[3] * want[3];
602 assert!(dot.abs() > 0.9999, "orientation mismatch: {got:?} vs {want:?}");
603 }
604
605 #[test]
606 fn arm_press_drag_moves_the_feature_then_disarms() {
607 let mut engine = EngineState::new();
608 engine.set_history_json(&cube_request("Pin", 10.0)).unwrap();
609 engine.resize(800.0, 600.0);
610 engine.camera.eye = [0.0, 0.0, 40.0];
611 engine.camera.target = [0.0, 0.0, 0.0];
612 engine.camera.up = [0.0, 1.0, 0.0];
613 engine.camera.projection = crate::view::Projection::Orthographic { half_height: 20.0 };
614
615 assert!(!engine.transform_armed());
616 engine.arm_transform("Pin");
617 assert!(engine.transform_armed() && engine.transform_armed_for("Pin"));
618 assert!(engine.widgets.has_transform(), "arming feeds the widget gizmo");
619
620 let (ax, ay) = engine.transform_gizmo_anchor().expect("armed anchor");
623 assert!((ax - 400.0).abs() < 1.0 && (ay - 300.0).abs() < 1.0, "anchor {ax},{ay}");
624
625 let origin_before = engine.widgets.transform_origin().expect("gizmo shown");
627 assert!(origin_before[0].abs() < 1e-4, "gizmo starts at x=0: {origin_before:?}");
628
629 assert!(engine.transform_press(ax, ay), "press grabs a handle");
631 assert!(engine.transform_dragging());
632 engine.transform_drag_to(ax + 60.0, ay);
633
634 let index = engine.history.index_of("Pin").unwrap();
635 let params = engine.history.feature_params(index).unwrap();
636 let moved_x = params["transform"]["position"][0].as_f64().unwrap();
637 assert!(moved_x > 0.0, "Pin moved +X, got {:?}", params["transform"]["position"]);
638
639 let origin_mid = engine.widgets.transform_origin().expect("gizmo still shown");
644 assert!(
645 origin_mid[0] > 0.0 && (origin_mid[0] as f64 - moved_x).abs() < 1e-3,
646 "gizmo should follow to x={moved_x} mid-drag, got {origin_mid:?}"
647 );
648 assert!(engine.transform_dragging(), "still dragging after the live re-sync");
650
651 engine.transform_release();
652 assert!(!engine.transform_dragging());
653
654 engine.disarm_transform();
655 assert!(!engine.transform_armed());
656 assert!(!engine.widgets.has_transform(), "disarm hides the gizmo");
657 }
658
659 #[test]
660 fn deleting_the_armed_feature_auto_disarms() {
661 let mut engine = EngineState::new();
662 engine.set_history_json(&cube_request("Pin", 10.0)).unwrap();
663 engine.arm_transform("Pin");
664 assert!(engine.transform_armed());
665 engine.delete_feature("Pin");
668 assert!(!engine.transform_armed(), "armed feature gone → auto-disarm");
669 assert!(!engine.widgets.has_transform());
670 }
671}
672
673