diff --git a/crates/tools/src/shape.rs b/crates/tools/src/shape.rs index 07593a72b..2ac761417 100644 --- a/crates/tools/src/shape.rs +++ b/crates/tools/src/shape.rs @@ -201,7 +201,9 @@ impl Tool for ShapeTool { self.began = false; vec![Action::Commit] } else { - vec![Action::Dialog(self.id.into(), json!({ "x": s.x, "y": s.y }))] + // Alt (draw from centre) is passed on, so the size dialog can centre the shape on the click. + let args = if ev.mods.alt { json!({ "x": s.x, "y": s.y, "alt": true }) } else { json!({ "x": s.x, "y": s.y }) }; + vec![Action::Dialog(self.id.into(), args)] } } _ => vec![], @@ -385,6 +387,21 @@ mod tests { ); } + #[test] + fn alt_click_passes_draw_from_centre_to_the_dialog() { + let (d, _) = doc_with_rect(); + let s = Selection::default(); + let p = paint(); + let cx = cx(&d, &s, &p); + let alt = Mods { alt: true, ..Default::default() }; + let mut t = ShapeTool::new("rectangle"); + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0).with_mods(alt)); + assert_eq!( + t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 10.0, 10.0).with_mods(alt)), + vec![Action::Dialog("rectangle".into(), json!({"x": 10.0, "y": 10.0, "alt": true}))] + ); + } + #[test] fn arrow_keys_change_sides() { let (d, _) = doc_with_rect(); diff --git a/crates/ui-egui/src/dialogs/shapes.rs b/crates/ui-egui/src/dialogs/shapes.rs index c8e2dd535..b0a3a1fd0 100644 --- a/crates/ui-egui/src/dialogs/shapes.rs +++ b/crates/ui-egui/src/dialogs/shapes.rs @@ -93,6 +93,16 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { false } +/// The top-left corner of a rectangle or ellipse: the click itself, or with Alt held (draw from centre) the click minus half the size. +fn box_origin(d: &Dialog) -> (f64, f64) { + let (x, y) = (d.f64("x", 0.0), d.f64("y", 0.0)); + if d.fields.get("alt").and_then(Value::as_bool).unwrap_or(false) { + (x - d.f64("width", 100.0) / 2.0, y - d.f64("height", 100.0) / 2.0) + } else { + (x, y) + } +} + fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let count = match d.kind.as_str() { "polygon" => Some(("sides", 6.0, 3.0)), @@ -105,8 +115,9 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { values }); let (x, y) = (d.f64("x", 0.0), d.f64("y", 0.0)); + let (bx, by) = box_origin(d); let (id, params) = match d.kind.as_str() { - "ellipse" => ("shape.ellipse", json!({"x": x, "y": y, "width": d.f64("width", 100.0), "height": d.f64("height", 100.0)})), + "ellipse" => ("shape.ellipse", json!({"x": bx, "y": by, "width": d.f64("width", 100.0), "height": d.f64("height", 100.0)})), "polygon" => ("shape.polygon", json!({"cx": x, "cy": y, "radius": d.f64("radius", 50.0), "sides": d.f64("sides", 6.0) as u64})), "star" => ( "shape.star", @@ -131,7 +142,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { } _ => ( "shape.rectangle", - json!({"x": x, "y": y, "width": d.f64("width", 100.0), "height": d.f64("height", 100.0), "radius": d.f64("radius", 0.0)}), + json!({"x": bx, "y": by, "width": d.f64("width", 100.0), "height": d.f64("height", 100.0), "radius": d.f64("radius", 0.0)}), ), }; // On the active plane while the perspective grid shows, sized in plane units from the click. @@ -145,3 +156,15 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { } Ok(result) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn alt_click_centres_the_typed_size_on_the_click() { + let fields = |alt: bool| Dialog::new("rectangle", json!({"x": 100.0, "y": 50.0, "alt": alt, "width": "40 pt", "height": "20 pt"})); + assert_eq!(box_origin(&fields(false)), (100.0, 50.0)); + assert_eq!(box_origin(&fields(true)), (80.0, 40.0)); + } +} diff --git a/crates/ui-egui/src/dialogs/tools.rs b/crates/ui-egui/src/dialogs/tools.rs index 9087cc80d..1947742ac 100644 --- a/crates/ui-egui/src/dialogs/tools.rs +++ b/crates/ui-egui/src/dialogs/tools.rs @@ -11,9 +11,10 @@ use crate::state::Dialog; pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) { let x = p.get("x").and_then(Value::as_f64).unwrap_or(0.0); let y = p.get("y").and_then(Value::as_f64).unwrap_or(0.0); + let alt = p.get("alt").and_then(Value::as_bool).unwrap_or(false); let d = match kind { - "rectangle" | "ellipse" => Dialog::new(kind, json!({"x": x, "y": y, "width": "100 pt", "height": "100 pt"})), - "roundedRectangle" => Dialog::new(kind, json!({"x": x, "y": y, "width": "100 pt", "height": "100 pt", "radius": "12 pt"})), + "rectangle" | "ellipse" => Dialog::new(kind, json!({"x": x, "y": y, "alt": alt, "width": "100 pt", "height": "100 pt"})), + "roundedRectangle" => Dialog::new(kind, json!({"x": x, "y": y, "alt": alt, "width": "100 pt", "height": "100 pt", "radius": "12 pt"})), "polygon" => Dialog::new(kind, json!({"x": x, "y": y, "radius": "50 pt", "sides": app.session.tool_options_of(kind)["sides"]})), "star" => { Dialog::new(kind, json!({"x": x, "y": y, "radius1": "50 pt", "radius2": "25 pt", "points": app.session.tool_options_of(kind)["points"]}))