Merge pull request #4432 from dotCooCoo/fix/star-ctrl-holds-shown-inner-radius

Star tool: Ctrl/Cmd holds the inner radius the star shows when the key goes down
This commit is contained in:
BFlat
2026-10-29 12:15:12 +07:00
committed by GitHub
3 changed files with 69 additions and 9 deletions
+15 -5
View File
@@ -75,12 +75,15 @@ impl ShapeTool {
}
}
/// Ctrl/Cmd in a star drag from `start` to `p`: the inner radius stays as it was when the key
/// went down, so the ratio follows the outer radius (and stays so once the key is let go).
fn hold_inner(&mut self, start: Point, p: Point, m: Mods) {
/// Ctrl/Cmd in a star drag from `start` to `p`, where the last drag showed a star with the inner
/// radius `shown`: once the star has begun, the inner radius stays as the star showed it when
/// the key went down, so the ratio follows the outer radius (and stays so once the key is let
/// go). When the key goes down before the star has begun, the inner radius stays as the first
/// star the drag shows has it.
fn hold_inner(&mut self, shown: f64, start: Point, p: Point, m: Mods) {
let outer = (p - start).hypot();
match (m.cmd, self.inner) {
(true, None) => self.inner = Some(outer * self.star_ratio),
(true, None) if self.began => self.inner = Some(shown),
(false, Some(_)) => self.inner = None,
_ => {}
}
@@ -224,17 +227,23 @@ impl Tool for ShapeTool {
self.start = Some(p);
self.last = ev.pos;
self.began = false;
self.inner = None;
vec![]
}
PointerKind::Drag => {
let Some(mut s) = self.start else { return vec![] };
let pos = self.snap.drag(cx, ev.pos, self.leave(cx, s, ev.mods).as_ref());
let ev = &PointerEvent { pos, ..*ev };
// The inner radius of the star the last drag showed, with the ratio it drew with:
// the canvas sends a drag only once the pointer moves, so a key pressed in between
// first arrives with the next position.
let ratio = if self.mods.alt { straight_ratio(self.points).unwrap_or(self.star_ratio) } else { self.star_ratio };
let shown = (self.last - s).hypot() * ratio;
space_moves(&mut s, &mut self.last, ev, self.began);
self.start = Some(s);
self.mods = ev.mods;
if self.id == "star" {
self.hold_inner(s, ev.pos, ev.mods);
self.hold_inner(shown, s, ev.pos, ev.mods);
}
let mut out = vec![];
if !self.began {
@@ -299,6 +308,7 @@ impl Tool for ShapeTool {
} else if key == ToolKey::Escape {
self.start = None;
self.began = false;
self.inner = None;
self.snap.clear();
vec![Action::Cancel]
} else {
+53 -4
View File
@@ -106,8 +106,10 @@ fn star_measure_label_matches_the_preview_with_modifiers_and_shrink() {
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).
/// During a star drag, Ctrl/Cmd keeps the inner radius the star showed when the key went down
/// while the outer one follows the pointer (the new ratio stays once the key is let go), and Alt
/// keeps the shoulders straight (#812). The canvas sends a drag only once the pointer moves, so
/// the key first arrives with a new position.
#[test]
fn a_star_drag_holds_its_inner_radius_with_ctrl_and_straightens_with_alt() {
let (d, _) = doc_with_rect();
@@ -123,8 +125,7 @@ fn a_star_drag_holds_its_inner_radius_with_ctrl_and_straightens_with_alt() {
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
let (r1, r2) = radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 50.0)));
assert_eq!((r1, r2), (40.0, 20.0));
// Ctrl: the inner radius stays 20 as the outer one grows to 80.
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 50.0).with_mods(ctrl));
// Ctrl, pressed with the pointer still: the inner radius stays 20 as the outer one grows to 80.
let (r1, r2) = radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 130.0, 50.0).with_mods(ctrl)));
assert!((r1 - 80.0).abs() < 1e-9 && (r2 - 20.0).abs() < 1e-9, "{r1} {r2}");
// Let go: the ratio (a quarter) stays.
@@ -138,6 +139,54 @@ fn a_star_drag_holds_its_inner_radius_with_ctrl_and_straightens_with_alt() {
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 150.0, 50.0)), vec![Action::Commit]);
}
/// Ctrl/Cmd pressed while Alt straightens the shoulders keeps the straightened inner radius, a drag
/// canceled with Escape leaves no held radius for the next star, and Ctrl/Cmd pressed inside the
/// drag threshold keeps the inner radius of the first star the drag shows. The canvas lends a
/// selection tool to a press with Ctrl/Cmd held, so every press here is without it.
#[test]
fn a_star_drag_holds_the_inner_radius_it_shows_when_ctrl_goes_down() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
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 close = |(r1, r2): (f64, f64), (w1, w2): (f64, f64)| assert!((r1 - w1).abs() < 1e-9 && (r2 - w2).abs() < 1e-9, "{r1} {r2}, not {w1} {w2}");
let ctrl = Mods { cmd: true, ..Default::default() };
let alt = Mods { alt: true, ..Default::default() };
let ctrl_alt = Mods { cmd: true, alt: true, ..Default::default() };
// Alt shows straight shoulders; Ctrl/Cmd pressed then keeps that inner radius.
let mut t = ShapeTool::new("star");
let straight = straight_ratio(5).unwrap_or(0.0);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
close(radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 50.0).with_mods(alt))), (40.0, 40.0 * straight));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 130.0, 50.0).with_mods(ctrl_alt));
close(radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 170.0, 50.0).with_mods(ctrl))), (120.0, 40.0 * straight));
// Escape with the key held, then a new press and the key before the first move: the next star
// holds what it shows, not the canceled radius.
let mut t = ShapeTool::new("star");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 50.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 130.0, 50.0).with_mods(ctrl));
assert_eq!(t.key(&cx, ToolKey::Escape, ctrl), vec![Action::Cancel]);
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
close(radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 60.0, 50.0).with_mods(ctrl))), (10.0, 2.5));
close(radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 70.0, 50.0).with_mods(ctrl))), (20.0, 2.5));
// Ctrl/Cmd pressed inside the drag threshold, where no star is shown yet.
let mut t = ShapeTool::new("star");
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 50.0, 50.0));
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 51.0, 50.0)), vec![]);
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 90.0, 50.0).with_mods(ctrl));
assert_eq!(a[0], Action::Begin("Star".into()));
close(radii(&a), (40.0, 20.0));
close(radii(&t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 130.0, 50.0).with_mods(ctrl))), (80.0, 20.0));
assert_eq!(t.pointer(&cx, &PointerEvent::new(PointerKind::Up, 130.0, 50.0).with_mods(ctrl)), vec![Action::Commit]);
}
#[test]
fn shift_square_alt_center() {
let t = ShapeTool::new("ellipse");
+1
View File
@@ -454,6 +454,7 @@ since 2026-10-01.
- **Status-bar pointer readout (2026-10-18):** uses the same effective ruler origin as the position fields, then formats the relative X/Y in General units. The absolute pointer position, document, selection, Undo/Redo and saved state are preserved. Three `chrome::tests::status_coordinates_*` painted-text regressions cover zero/positive/negative origins, unit conversion, global/artboard mode, active artboard, reset and missing pointer/document. Original headless status-bar captures were reviewed before and after the change. Scores and hours are unchanged.
**Drawing tools — Polygon and Star counts:** The existing canvas placement dialog reads the stored count and saves it only after successful placement. `polygon_and_star_canvas_counts_persist_without_new_toolbar_dialogs` covers both tools, repeated drags, tool switching, reopening and Cancel. Counts remain session tool options. Existing click and toolbar gestures are retained; no score or hour change.
**Drawing tools, Star inner radius held with Ctrl/Cmd:** While a star is dragged out, Ctrl/Cmd keeps the inner radius the star showed when the key went down, also when the key is pressed with the pointer still. `a_star_drag_holds_its_inner_radius_with_ctrl_and_straightens_with_alt` and `a_star_drag_holds_the_inner_radius_it_shows_when_ctrl_goes_down` cover it, with Alt's straight shoulders and a drag canceled with Escape.
**Live trace result bounds and selection highlights exclude the retained bitmap except in source-image views:** `live_trace_result_bounds_exclude_the_retained_bitmap_at_any_angle` checks every TraceView, geometric and visual bounds and rotated result bounds. `live_trace_selection_highlight_excludes_the_retained_source_rectangle` checks result highlights and the existing ordinary-group policy. Shared editing-child traversal also preserves Shaper handling.
**Artboard Options shares existing presets and orientation controls and can fit artwork without moving it; artwork fitting includes all drawn document art across artboards and off-board; artwork bounds are cached by active document and revision, and selection uses the shared bounds cache across artboards:** Shared existing preset definitions are reused; no external preset assets are added. Presets preserve each selected artboard's orientation, and orientation choices apply even when the active board already matches. Fit operations draft bounds until OK, affect only the active artboard and leave artwork in place. Regression coverage checks draft/Cancel, selected-set application, active-only fitting and Undo.