//! Move/Copy: the move types (Free Move, Translate, Rotate, Point to Point, Point to Position), //! their rows and inputs, the triad on the canvas and the FusionMoveCommand they make. Nothing //! moves until a value is typed or the triad is dragged. use serde_json::{Value, json}; use solvecraft_engine::Sel; use solvecraft_engine::doc::expr::Kind as ValueKind; use solvecraft_engine::geom::Vec3; use crate::SolveApp; use crate::dialogs::{Dialog, Kind, combo, field, row_label}; use crate::gizmo::{self, Change, Drag}; use crate::selection::{AXES, BODIES, EDGES, FACES, SelInput, VERTICES}; pub const TYPES: [&str; 5] = ["Free Move", "Translate", "Rotate", "Point to Point", "Point to Position"]; const FREE: usize = 0; const TRANSLATE: usize = 1; const ROTATE: usize = 2; const POINT_TO_POINT: usize = 3; const POINT_TO_POSITION: usize = 4; const POINTS: u16 = VERTICES | FACES | EDGES; /// A fresh Move dialog of a type (all values zero). pub fn kind(mode: usize) -> Kind { let z = || "0 mm".to_string(); let a = || "0 deg".to_string(); Kind::Move { mode, x: z(), y: z(), z: z(), rx: a(), ry: a(), rz: a(), angle: a() } } /// The selection inputs of a move type (the bodies first). pub fn inputs(mode: usize) -> Vec { let mut v = vec![SelInput::new("Bodies", BODIES, true)]; match mode { FREE => v.push(SelInput::new("Pivot", POINTS, false)), ROTATE => v.push(SelInput::new("Axis", AXES | EDGES, false)), POINT_TO_POINT => { v.push(SelInput::new("From", POINTS, false)); v.push(SelInput::new("To", POINTS, false)); } POINT_TO_POSITION => v.push(SelInput::new("Point", POINTS, false)), _ => {} } v } /// The Move Type row (above the inputs); switching keeps the bodies and resets the values. pub fn type_row(d: &mut Dialog, ui: &mut egui::Ui) { let Kind::Move { mode, .. } = d.kind else { return }; row_label(ui, "Move Type"); let mut m = mode; combo(ui, "move_type", &TYPES, &mut m); ui.end_row(); if m != mode { let bodies = d.inputs.first().map(|i| i.items.clone()).unwrap_or_default(); d.kind = kind(m); d.inputs = inputs(m); if let Some(i) = d.inputs.first_mut() { i.items = bodies; } d.extra.remove("pivot"); d.active = if bodies_empty(d) { 0 } else { 1.min(d.inputs.len() - 1) }; } } fn bodies_empty(d: &Dialog) -> bool { d.inputs.first().is_none_or(|i| i.items.is_empty()) } /// The value rows after the inputs; true when Enter was pressed in one. pub fn rows(ui: &mut egui::Ui, k: &mut Kind) -> bool { let Kind::Move { mode, x, y, z, rx, ry, rz, angle } = k else { return false }; let mut enter = false; let mut row = |ui: &mut egui::Ui, l: &str, v: &mut String| { row_label(ui, l); enter |= field(ui, v); ui.end_row(); }; match *mode { FREE | TRANSLATE => { row(ui, "X Distance", x); row(ui, "Y Distance", y); row(ui, "Z Distance", z); if *mode == FREE { row(ui, "X Angle", rx); row(ui, "Y Angle", ry); row(ui, "Z Angle", rz); } } ROTATE => row(ui, "Angle", angle), POINT_TO_POSITION => { row(ui, "X", x); row(ui, "Y", y); row(ui, "Z", z); } _ => {} } enter } fn sel_point(s: Option<&Sel>) -> Option { match s? { Sel::Vertex { point, .. } | Sel::Face { point, .. } | Sel::Edge { point, .. } => Some(*point), _ => None, } } fn items(d: &Dialog, i: usize) -> &[Sel] { d.inputs.get(i).map(|x| x.items.as_slice()).unwrap_or(&[]) } fn body_names(d: &Dialog) -> Vec { items(d, 0).iter().filter_map(|x| if let Sel::Body { name } = x { Some(name.clone()) } else { None }).collect() } /// The centre of the picked bodies where they are before this move. fn bodies_center(app: &SolveApp, d: &Dialog) -> Option { let st = match d.editing { Some((id, _)) => app.session.model.state_before(id), None => app.session.model.state(), }; let mut bb: Option<(Vec3, Vec3)> = None; for n in body_names(d) { let Some(b) = st.body(&n) else { continue }; let x = b.mesh().bounds(); bb = Some(match bb { None => (x.min, x.max), Some((lo, hi)) => ( Vec3::new(lo.x.min(x.min.x), lo.y.min(x.min.y), lo.z.min(x.min.z)), Vec3::new(hi.x.max(x.max.x), hi.y.max(x.max.y), hi.z.max(x.max.z)), ), }); } bb.map(|(lo, hi)| (lo + hi) * 0.5) } /// What a Free Move turns about: the picked pivot, else the kept one (editing), else the /// bodies' centre. fn pivot(app: &SolveApp, d: &Dialog) -> Option { if let Some(p) = sel_point(items(d, 1).first()) { return Some(p); } if let Some(a) = d.extra.get("pivot").and_then(|v| serde_json::from_value::<[f64; 3]>(v.clone()).ok()) { return Some(Vec3::new(a[0], a[1], a[2])); } bodies_center(app, d) } fn len(app: &SolveApp, s: &str) -> Result { app.session.doc.eval(s, ValueKind::Length).map_err(|e| e.to_string()).and_then(|v| if v.is_finite() { Ok(v) } else { Err("not a number".into()) }) } fn ang(app: &SolveApp, s: &str) -> Result { app.session.doc.eval(s, ValueKind::Angle).map_err(|e| e.to_string()).and_then(|v| if v.is_finite() { Ok(v) } else { Err("not a number".into()) }) } fn mm(v: f64) -> String { let s = format!("{v:.6}"); let s = s.trim_end_matches('0').trim_end_matches('.'); format!("{} mm", if s == "-0" || s.is_empty() { "0" } else { s }) } fn deg(v: f64) -> String { let s = format!("{:.4}", v.to_degrees()); let s = s.trim_end_matches('0').trim_end_matches('.'); format!("{} deg", if s == "-0" || s.is_empty() { "0" } else { s }) } /// The FusionMoveCommand parameters (bodies, translate, axis, angle). pub fn params(app: &SolveApp, d: &Dialog) -> Result { let Kind::Move { mode, x, y, z, rx, ry, rz, angle } = &d.kind else { return Err("not a move".into()) }; let bodies = body_names(d); if bodies.is_empty() { return Err("select bodies first".into()); } let need_point = |i: usize, what: &str| sel_point(items(d, i).first()).ok_or(format!("select {what} first")); let num = |t: Vec3| json!([mm(t.x), mm(t.y), mm(t.z)]); let mut p = json!({ "bodies": bodies }); match *mode { TRANSLATE => p["translate"] = json!([x, y, z]), FREE => { let a = [ang(app, rx)?, ang(app, ry)?, ang(app, rz)?]; let turned: Vec = (0..3).filter(|i| a[*i].abs() > 1e-12).collect(); if turned.is_empty() { p["translate"] = json!([x, y, z]); // A rotation kept from an edited move about another axis. for k in ["axis", "angle"] { if let Some(v) = d.extra.get(k) { p[k] = v.clone(); } } } else { let piv = pivot(app, d).ok_or("select bodies first")?; let r = gizmo::mul(&gizmo::mul(&gizmo::rotation(Vec3::Z, a[2]), &gizmo::rotation(Vec3::Y, a[1])), &gizmo::rotation(Vec3::X, a[0])); let (axis, th) = gizmo::axis_angle(&r); match turned.as_slice() { // One turn about the origin: the typed values stay as they are. [i] if piv.len() < 1e-9 => { p["translate"] = json!([x, y, z]); p["axis"] = json!([[1, 0, 0], [0, 1, 0], [0, 0, 1]][*i]); p["angle"] = json!([rx, ry, rz][*i]); } _ => { let t = Vec3::new(len(app, x)?, len(app, y)?, len(app, z)?); p["translate"] = num(piv - gizmo::apply(&r, piv) + t); p["axis"] = json!([axis.x, axis.y, axis.z]); p["angle"] = json!(deg(th)); } } } } ROTATE => { let (o, a) = items(d, 1).first().and_then(|s| axis_line(app, s)).ok_or("select an axis or a straight edge first")?; let th = ang(app, angle)?; let r = gizmo::rotation(a, th); p["translate"] = num(o - gizmo::apply(&r, o)); p["axis"] = json!([a.x, a.y, a.z]); p["angle"] = json!(angle); } POINT_TO_POINT => { let (from, to) = (need_point(1, "the point to move from")?, need_point(2, "the point to move to")?); p["translate"] = num(to - from); } _ => { let from = need_point(1, "the point to move")?; let at = Vec3::new(len(app, x)?, len(app, y)?, len(app, z)?); p["translate"] = num(at - from); } } Ok(p) } /// An axis line: origin axes and sketch lines, or a straight body edge. fn axis_line(app: &SolveApp, s: &Sel) -> Option<(Vec3, Vec3)> { if let Some(a) = crate::dialogs::axis_of(app, s) { return Some(a); } let Sel::Edge { body, index, .. } = s else { return None }; let st = app.session.model.state(); let m = st.body(body)?.mesh(); let e = m.edges.get(*index)?; let (a, b) = (*e.first()?, *e.last()?); let dir = (b - a).normalized()?; // Straight: every point on the chord. e.iter().all(|p| (*p - a).cross(dir).len() < 1e-6 * (1.0 + (b - a).len())).then_some((a, dir)) } /// Where the triad sits: the pivot carried by the move so far (Free Move and Translate only). pub fn triad(app: &SolveApp, d: &Dialog) -> Option { let Kind::Move { mode, x, y, z, .. } = &d.kind else { return None }; if !matches!(*mode, FREE | TRANSLATE) || bodies_empty(d) { return None; } let t = Vec3::new(len(app, x).unwrap_or(0.0), len(app, y).unwrap_or(0.0), len(app, z).unwrap_or(0.0)); Some(gizmo::Triad { center: pivot(app, d)? + t, translate: true, rotate: *mode == FREE }) } /// Apply a triad drag to the dialog's values (added to the values when the drag began). pub fn drag(app: &SolveApp, d: &mut Dialog, g: Drag) { let Kind::Move { x, y, z, rx, ry, rz, .. } = &d.kind else { return }; if g.started || !d.extra.contains_key("drag0") { let v = [len(app, x), len(app, y), len(app, z), ang(app, rx), ang(app, ry), ang(app, rz)].map(|r| r.unwrap_or(0.0)); d.extra.insert("drag0".into(), json!(v)); } let v0: [f64; 6] = d.extra.get("drag0").and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default(); let Kind::Move { x, y, z, rx, ry, rz, .. } = &mut d.kind else { return }; match g.change { Change::Translate(t) => { *x = mm(v0[0] + t.x); *y = mm(v0[1] + t.y); *z = mm(v0[2] + t.z); } Change::Rotate { axis, angle } => { let slot = match axis { 0 => rx, 1 => ry, _ => rz, }; *slot = deg(v0.get(3 + axis.min(2)).copied().unwrap_or(0.0) + angle); } } if g.done { d.extra.remove("drag0"); } } /// The dialog for editing a move: Free Move with its translation and, about a world axis /// through the origin, its angle (another axis is kept as it is). pub fn for_feature(d: &mut Dialog, bodies: &[String], translate: &[String; 3], axis: Option, angle: Option<&String>) { let [x, y, z] = translate.clone(); let mut k = kind(FREE); if let Kind::Move { x: kx, y: ky, z: kz, rx, ry, rz, .. } = &mut k { (*kx, *ky, *kz) = (x, y, z); if let Some(a) = angle { let dir = axis.unwrap_or(Vec3::Z); let world = [Vec3::X, Vec3::Y, Vec3::Z].iter().position(|w| dir.cross(*w).len() < 1e-9 && dir.dot(*w) > 0.0); match world { Some(0) => *rx = a.clone(), Some(1) => *ry = a.clone(), Some(_) => *rz = a.clone(), None => { d.extra.insert("axis".into(), json!([dir.x, dir.y, dir.z])); d.extra.insert("angle".into(), json!(a)); } } // The stored move turns about the origin. d.extra.insert("pivot".into(), json!([0.0, 0.0, 0.0])); } } d.kind = k; d.inputs = inputs(FREE); if let Some(inp) = d.inputs.first_mut() { inp.items = bodies.iter().map(|n| Sel::Body { name: n.clone() }).collect(); } }