diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index ab45a983..cb3f24d0 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -59,13 +59,18 @@ pub struct TreeState { #[derive(Clone, Debug)] pub struct OccMove { pub occurrence: u64, + /// Distances along X, Y, Z (mm) and turns about X, Y, Z (degrees) from where it was. pub translate: [f64; 3], - pub angle_deg: f64, + pub angles: [f64; 3], + /// The occurrence's origin when the move began: what it turns about. + pub pivot: Option, + /// The values when the current triad drag began. + drag0: Option<([f64; 3], [f64; 3])>, } impl OccMove { pub fn new(occurrence: u64) -> Self { - OccMove { occurrence, translate: [0.0; 3], angle_deg: 0.0 } + OccMove { occurrence, translate: [0.0; 3], angles: [0.0; 3], pivot: None, drag0: None } } } @@ -493,7 +498,7 @@ pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) { if let Some((at, target)) = acts.menu { crate::context_menu::open_for(app, at, target); } - occurrence_panel(app, ui.ctx()); + occurrence_panel(app, ui); capture_prompt(app, ui.ctx()); canvas_panel(app, ui.ctx()); redefine_panel(app, ui.ctx()); @@ -1357,12 +1362,16 @@ fn apply_drop(app: &mut SolveApp, d: DropAction) { // --------------------------------------------------------------------------------------------- /// Move/Copy of an occurrence: offsets and a rotation about Z, pending until Capture Position. -fn occurrence_panel(app: &mut SolveApp, ctx: &egui::Context) { +fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) { let Some(mut m) = app.tree.occurrence_move.clone() else { return }; + let ctx = ui.ctx().clone(); + if m.pivot.is_none() { + m.pivot = occurrence_origin(app, m.occurrence); + } let name = app.session.doc.occurrences.iter().find(|o| o.id == m.occurrence).map(|o| o.name.clone()).unwrap_or_default(); let mut open = true; let mut action: Option<&str> = None; - let before = (m.translate, m.angle_deg); + let before = (m.translate, m.angles); crate::frame::window(ctx, format!("Move: {name}"), crate::frame::Width::Normal) .id(egui::Id::new("sc_occ_move")) .pivot(egui::Align2::LEFT_TOP) @@ -1370,7 +1379,7 @@ fn occurrence_panel(app: &mut SolveApp, ctx: &egui::Context) { .open(&mut open) .resizable(false) .collapsible(false) - .show(ctx, |ui| { + .show(&ctx, |ui| { egui::Grid::new("sc_occ_grid").num_columns(2).spacing([12.0, 6.0]).show(ui, |ui| { for (k, axis) in ["X distance", "Y distance", "Z distance"].iter().enumerate() { ui.label(*axis); @@ -1379,9 +1388,13 @@ fn occurrence_panel(app: &mut SolveApp, ctx: &egui::Context) { } ui.end_row(); } - ui.label("Z angle"); - ui.add(egui::DragValue::new(&mut m.angle_deg).speed(1.0).suffix(" deg")); - ui.end_row(); + for (k, axis) in ["X angle", "Y angle", "Z angle"].iter().enumerate() { + ui.label(*axis); + if let Some(v) = m.angles.get_mut(k) { + ui.add(egui::DragValue::new(v).speed(1.0).suffix(" deg")); + } + ui.end_row(); + } }); ui.add_space(6.0); ui.horizontal(|ui| { @@ -1393,14 +1406,9 @@ fn occurrence_panel(app: &mut SolveApp, ctx: &egui::Context) { } }); }); - gizmo(app, ctx, &mut m); - if (m.translate, m.angle_deg) != before { - // The pending move is the typed offset from where the occurrence is now. - let _ = app.session.execute("component.revert_position", &json!({})); - let _ = app.run( - "occurrence.move", - json!({ "occurrence": m.occurrence, "translate": m.translate, "axis": [0, 0, 1], "angle": m.angle_deg.to_radians(), "capture": false }), - ); + triad(app, ui, &mut m); + if (m.translate, m.angles) != before { + apply_occurrence_move(app, &m); } match action { Some("capture") => { @@ -1419,6 +1427,50 @@ fn occurrence_panel(app: &mut SolveApp, ctx: &egui::Context) { } } +/// The pending move: turned about X, then Y, then Z through the starting origin, then moved. +pub fn apply_occurrence_move(app: &mut SolveApp, m: &OccMove) { + use crate::gizmo; + use solvecraft_engine::geom::Vec3; + let a = m.angles.map(f64::to_radians); + 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, angle) = gizmo::axis_angle(&r); + let o = m.pivot.unwrap_or(Vec3::ZERO); + let _ = app.session.execute("component.revert_position", &json!({})); + let _ = app.run( + "occurrence.move", + json!({ "occurrence": m.occurrence, "translate": m.translate, "axis": [axis.x, axis.y, axis.z], "angle": angle, "origin": [o.x, o.y, o.z], "capture": false }), + ); +} + +/// The shared move triad at the occurrence's origin: arrows and squares move it, rings turn it. +fn triad(app: &mut SolveApp, ui: &mut egui::Ui, m: &mut OccMove) { + use crate::gizmo::{self, Change}; + let (Some(rect), Some(pivot)) = (app.viewport.rect, m.pivot) else { return }; + let proj = crate::viewport::projection(app, rect); + let center = pivot + solvecraft_engine::geom::Vec3::new(m.translate[0], m.translate[1], m.translate[2]); + let layer = egui::LayerId::new(egui::Order::Middle, egui::Id::new("sc_occ_triad_layer")); + let mut gui = ui.new_child(egui::UiBuilder::new().layer_id(layer).max_rect(rect)); + let painter = gui.painter().with_clip_rect(rect); + let t = gizmo::Triad { center, translate: true, rotate: true }; + let step = crate::canvas::snap_step(app.cam.half_height()); + let Some(g) = gizmo::show(&mut gui, &painter, &proj, egui::Id::new("sc_occ_triad"), &t, step) else { return }; + if g.started || m.drag0.is_none() { + m.drag0 = Some((m.translate, m.angles)); + } + let (t0, a0) = m.drag0.unwrap_or((m.translate, m.angles)); + match g.change { + Change::Translate(v) => m.translate = [t0[0] + v.x, t0[1] + v.y, t0[2] + v.z], + Change::Rotate { axis, angle } => { + if let Some(slot) = m.angles.get_mut(axis) { + *slot = a0.get(axis).copied().unwrap_or(0.0) + angle.to_degrees(); + } + } + } + if g.done { + m.drag0 = None; + } +} + /// Dimensions of finished sketches whose dimensions are shown (Show Dimensions), drawn over the /// viewport like the sketch being edited draws its own (`crate::dim_view`), but not editable. pub fn dims_overlay(app: &SolveApp, ctx: &egui::Context) { @@ -1563,65 +1615,6 @@ fn occurrence_origin(app: &SolveApp, occurrence: u64) -> Option