diff --git a/crates/tools/src/select.rs b/crates/tools/src/select.rs index 8f59c3503..babaa4712 100644 --- a/crates/tools/src/select.rs +++ b/crates/tools/src/select.rs @@ -178,12 +178,18 @@ fn resize_edges_rect(from: Rect, handle: usize, p: Point) -> Rect { } fn finish_resize(from: Rect, handle: usize, rect: Rect, m: Mods) -> Rect { - let rect = if m.shift && from.width() > 1e-9 && from.height() > 1e-9 { - apply_aspect(from, handle, rect, aspect_scale(from, handle, rect)) - } else { - rect - }; - if m.alt { apply_alt(from, handle, rect) } else { rect } + // Alt first: Shift then measures the drag from the centre and keeps it there. + let rect = if m.alt { apply_alt(from, handle, rect) } else { rect }; + if !(m.shift && from.width() > 1e-9 && from.height() > 1e-9) { + return rect; + } + let s = aspect_scale(from, handle, rect); + if m.alt { centered_rect(from, from.width() * s, from.height() * s) } else { apply_aspect(from, handle, rect, s) } +} + +fn centered_rect(from: Rect, w: f64, h: f64) -> Rect { + let c = from.center(); + Rect::new(c.x - w / 2.0, c.y - h / 2.0, c.x + w / 2.0, c.y + h / 2.0) } fn aspect_scale(from: Rect, handle: usize, rect: Rect) -> f64 { @@ -214,31 +220,17 @@ fn drives_x(handle: usize, from: Rect, rect: Rect) -> bool { sx > sy } +/// Scales `from` by `s` with the edges opposite the handle fixed. An axis the handle doesn't move stays centred. fn apply_aspect(from: Rect, handle: usize, rect: Rect, s: f64) -> Rect { - let (mut x0, mut y0, mut x1, mut y1) = (rect.x0, rect.y0, rect.x1, rect.y1); - let (w, h) = (from.width() * s, from.height() * s); - match handle { - 0 => { - x0 = x1 - w; - y0 = y1 - h; - } - 2 => { - x1 = x0 + w; - y0 = y1 - h; - } - 3..=5 => { - x1 = x0 + w; - y1 = y0 + h; - } - 6 | 7 => { - x0 = x1 - w; - y1 = y0 + h; - } - _ => { - x0 = x1 - w; - y0 = y1 - h; - } - } + let (x_edges, y_edges) = resize_edge_flags(handle); + let span = |edges: [bool; 3], lo: f64, hi: f64, mid: f64, len: f64| match edges { + [true, _, _] => (hi - len, hi), + [_, _, true] => (lo, lo + len), + _ => (mid - len / 2.0, mid + len / 2.0), + }; + let c = from.center(); + let (x0, x1) = span(x_edges, rect.x0, rect.x1, c.x, from.width() * s); + let (y0, y1) = span(y_edges, rect.y0, rect.y1, c.y, from.height() * s); Rect::new(x0, y0, x1, y1) } @@ -285,8 +277,7 @@ fn finish_snapped_resize( if !(w.is_finite() && h.is_finite()) { return proposed; } - let c = from.center(); - Rect::new(c.x - w / 2.0, c.y - h / 2.0, c.x + w / 2.0, c.y + h / 2.0) + centered_rect(from, w, h) } /// Length match: add the difference once. Position match: the flagged edge moves and the other side mirrors. @@ -313,16 +304,18 @@ fn axis_size_delta(edges: [bool; 3], delta: f64) -> f64 { } } +/// Mirrors the dragged sides around the centre of `from`. Shrinking and crossing the centre follow the pointer. fn apply_alt(from: Rect, handle: usize, rect: Rect) -> Rect { - let (x0, y0, x1, y1) = (rect.x0, rect.y0, rect.x1, rect.y1); + let (x_edges, y_edges) = resize_edge_flags(handle); let c = from.center(); - let (hw, hh) = ((x1 - x0) / 2.0, (y1 - y0) / 2.0); - let (hw, hh) = match handle { - 1 | 5 => (from.width() / 2.0, (if handle == 1 { c.y - y0 } else { y1 - c.y })), - 3 | 7 => ((if handle == 7 { c.x - x0 } else { x1 - c.x }), from.height() / 2.0), - _ => (hw.abs().max((c.x - x0).abs()).max((x1 - c.x).abs()), hh.abs().max((c.y - y0).abs()).max((y1 - c.y).abs())), + let size = |edges: [bool; 3], lo: f64, hi: f64, mid: f64, len: f64| match edges { + [true, _, _] => 2.0 * (mid - lo), + [_, _, true] => 2.0 * (hi - mid), + _ => len, }; - Rect::new(c.x - hw, c.y - hh, c.x + hw, c.y + hh) + let w = size(x_edges, rect.x0, rect.x1, c.x, from.width()); + let h = size(y_edges, rect.y0, rect.y1, c.y, from.height()); + centered_rect(from, w, h) } /// Left/center/right and top/center/bottom for a handle. Centers stay off. diff --git a/crates/tools/src/tests.rs b/crates/tools/src/tests.rs index 6c75b0da6..f62a400aa 100644 --- a/crates/tools/src/tests.rs +++ b/crates/tools/src/tests.rs @@ -60,6 +60,31 @@ fn resize_rect_modifiers() { assert_eq!(c, Rect::new(-50.0, 0.0, 150.0, 50.0)); } +#[test] +fn resize_side_shift_keeps_the_other_axis_centred() { + let r = Rect::new(0.0, 0.0, 100.0, 50.0); + let shift = Mods { shift: true, ..Default::default() }; + assert_eq!(select::resize_rect(r, 3, Point::new(150.0, 25.0), shift), Rect::new(0.0, -12.5, 150.0, 62.5)); + assert_eq!(select::resize_rect(r, 5, Point::new(50.0, 100.0), shift), Rect::new(-50.0, 0.0, 150.0, 100.0)); +} + +#[test] +fn resize_corner_alt_shift_scales_around_the_centre() { + let r = Rect::new(0.0, 0.0, 100.0, 50.0); + let mods = Mods { shift: true, alt: true, ..Default::default() }; + // From the centre the pointer is at 0.2 by 0.6; the larger factor drives and the corner stays on y = 40. + assert_eq!(select::resize_rect(r, 4, Point::new(60.0, 40.0), mods), Rect::new(20.0, 10.0, 80.0, 40.0)); + assert_eq!(select::resize_rect(r, 0, Point::new(-50.0, 10.0), mods), Rect::new(-50.0, -25.0, 150.0, 75.0)); +} + +#[test] +fn resize_corner_alt_mirrors_the_opposite_side() { + let r = Rect::new(0.0, 0.0, 100.0, 50.0); + let alt = Mods { alt: true, ..Default::default() }; + assert_eq!(select::resize_rect(r, 0, Point::new(10.0, 5.0), alt), Rect::new(10.0, 5.0, 90.0, 45.0)); + assert_eq!(select::resize_rect(r, 4, Point::new(120.0, 60.0), alt), Rect::new(-20.0, -10.0, 120.0, 60.0)); +} + #[test] fn cmd_shift_click_overrides_parent_item() { let mut d = Document::new(&NewDocument::default());