diff --git a/crates/tools/src/lib.rs b/crates/tools/src/lib.rs index 29b77f38e..a87eb2cb7 100644 --- a/crates/tools/src/lib.rs +++ b/crates/tools/src/lib.rs @@ -392,6 +392,10 @@ impl ToolContext<'_> { pub fn size_label(&self, w: f64, h: f64) -> String { format!("W: {}\nH: {}", self.len(w), self.len(h)) } + /// Star tool drag: outer (R1) and inner (R2) radii from the centre to the pointer. + pub fn star_radii_label(&self, outer: f64, inner: f64) -> String { + format!("R1: {}\nR2: {}", self.len(outer), self.len(inner)) + } /// A move label: `dX: …` over `dY: …`. pub fn offset_label(&self, dx: f64, dy: f64) -> String { format!("dX: {}\ndY: {}", self.len(dx), self.len(dy)) diff --git a/crates/tools/src/shape.rs b/crates/tools/src/shape.rs index 47ddf7a84..34553a034 100644 --- a/crates/tools/src/shape.rs +++ b/crates/tools/src/shape.rs @@ -308,15 +308,25 @@ impl Tool for ShapeTool { cx.segment_measure_label(s, self.last) } else { // A box's size as drawn (from its centre with Alt, square with Shift). - let (w, h) = match self.id { - "polygon" | "star" => ((self.last - s).x.abs(), (self.last - s).y.abs()), - _ => { - // Along the Constrain Angle's axes, as the box is drawn (`command_at`). - let (r, _) = turned_box(s, self.last, self.mods, cx.constrain_angle); - (r.width(), r.height()) + match self.id { + "star" => { + let outer = (self.last - s).hypot().max(0.01); + // The same ratio `command_at` draws with, so the label matches the preview + // (Ctrl/Cmd's held radius is already in `star_ratio`, clamped; Alt overrides it). + let ratio = if self.mods.alt { straight_ratio(self.points).unwrap_or(self.star_ratio) } else { self.star_ratio }; + cx.star_radii_label(outer, outer * ratio) } - }; - cx.size_label(w, h) + _ => { + let (w, h) = match self.id { + "polygon" => ((self.last - s).x.abs(), (self.last - s).y.abs()), + _ => { + let (r, _) = turned_box(s, self.last, self.mods, cx.constrain_angle); + (r.width(), r.height()) + } + }; + cx.size_label(w, h) + } + } }; o.push(Overlay::Measure { p, text }); } @@ -396,6 +406,71 @@ mod tests { assert!(matches!(a.last(), Some(Action::Preview(_, v)) if v["cx"] == 60.0 && v["cy"] == 60.0), "{a:?}"); } + #[test] + fn star_drag_measure_label_shows_outer_and_inner_radii() { + let (d, _) = doc_with_rect(); + let s = Selection::default(); + let p = paint(); + let cx = ToolContext { measurement_labels: true, smart_guides: false, ..cx(&d, &s, &p) }; + let mut t = ShapeTool::new("star"); + t.star_ratio = 0.5; + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 50.0, 10.0)); + let want = cx.star_radii_label(40.0, 20.0); + let label = t + .overlays(&cx) + .into_iter() + .find_map(|o| match o { + Overlay::Measure { text, .. } => Some(text), + _ => None, + }) + .unwrap_or_default(); + assert_eq!(label, want, "{label}"); + } + + /// The star label shows the radii the preview draws, also with Ctrl/Cmd + Alt and when a + /// Ctrl/Cmd drag shrinks the outer radius below the held inner one (the ratio is clamped). + #[test] + fn star_measure_label_matches_the_preview_with_modifiers_and_shrink() { + let (d, _) = doc_with_rect(); + let s = Selection::default(); + let p = paint(); + let cx = ToolContext { measurement_labels: true, smart_guides: false, ..cx(&d, &s, &p) }; + let label = |t: &ShapeTool| { + t.overlays(&cx) + .into_iter() + .find_map(|o| match o { + Overlay::Measure { text, .. } => Some(text), + _ => None, + }) + .unwrap_or_default() + }; + let radii = |a: &[Action]| match a.last() { + Some(Action::Preview(_, v)) => (v["radius1"].as_f64().unwrap(), v["radius2"].as_f64().unwrap()), + other => panic!("{other:?}"), + }; + let cmd = Mods { cmd: true, ..Default::default() }; + let cmd_alt = Mods { cmd: true, alt: true, ..Default::default() }; + let mut t = ShapeTool::new("star"); + t.star_ratio = 0.5; + t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0)); + t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 50.0, 10.0)); + // Ctrl/Cmd holds the 20 inner radius while the outer one grows. + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 70.0, 10.0).with_mods(cmd)); + let (r1, r2) = radii(&a); + assert_eq!(label(&t), cx.star_radii_label(r1, r2)); + // Ctrl/Cmd + Alt: the preview straightens the shoulders, and so does the label. + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 70.0, 10.0).with_mods(cmd_alt)); + let (r1, r2) = radii(&a); + assert!((r2 - 20.0).abs() > 1e-6, "Alt should override the held radius: {r2}"); + assert_eq!(label(&t), cx.star_radii_label(r1, r2)); + // Shrinking below the held inner radius clamps the ratio; the label follows the preview. + let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 20.0, 10.0).with_mods(cmd)); + let (r1, r2) = radii(&a); + assert!(r2 <= r1 + 1e-9, "{r1} {r2}"); + assert_eq!(label(&t), cx.star_radii_label(r1, r2)); + } + /// During a star drag, Ctrl/Cmd keeps the inner radius while the outer one follows the pointer /// (the new ratio stays once the key is let go), and Alt keeps the shoulders straight (#812). #[test]