G12: General › Corner Radius is the Rounded Rectangle tool's radius (#394)

Squashed from #4074 onto main.
This commit is contained in:
Angelo Bernardini
2026-10-26 09:09:47 +07:00
committed by bflatastic
parent f6a20c9f56
commit ea36291f30
7 changed files with 138 additions and 11 deletions
+11
View File
@@ -541,7 +541,18 @@ impl Session {
let tile_edge_changed = p.pattern_tile_edge_color != self.prefs.pattern_tile_edge_color;
let fonts_folder_changed = p.fonts_folder != self.prefs.fonts_folder;
let hyphen_exceptions_changed = p.hyphenation_exceptions != self.prefs.hyphenation_exceptions;
let corner_radius_changed = p.corner_radius != self.prefs.corner_radius;
self.prefs = p;
if corner_radius_changed {
// General › Corner Radius is the Rounded Rectangle tool's radius (#394): a tool already
// made (active, or lent a selection tool for a drag) takes the new value.
let r = serde_json::Value::from(self.prefs.corner_radius);
for t in std::iter::once(&mut self.tool).chain(self.lender.as_mut()) {
if t.id() == "roundedRectangle" {
t.set_option("cornerRadius", &r);
}
}
}
if fonts_folder_changed {
let folder = self.prefs.fonts_folder.trim();
// A folder an automation caller chose is read without following its links out of it.
+59 -2
View File
@@ -1,10 +1,11 @@
//! General › Constrain Angle turns the axes the arrow keys, Shift-constrained drags and the
//! Rectangle, Rounded Rectangle and Ellipse tools work along (G12, #394, first batch).
//! Rectangle, Rounded Rectangle and Ellipse tools work along, and General › Corner Radius is the
//! Rounded Rectangle tool's radius (G12, #394, first batch).
use serde_json::{Value, json};
use vectorcraft_doc::{LiveShape, NodeKind};
use vectorcraft_geom::{Point, Vec2};
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
use super::*;
@@ -177,3 +178,59 @@ fn constrain_angle_keeps_guides_selected_with_art_on_the_page_axes() {
s.execute("object.nudge", &json!({"dx": 1, "dy": 0, "copy": true})).unwrap();
assert_eq!(pos(&s), [60.0, 60.0, 70.0, 60.0]);
}
/// General › Corner Radius is the Rounded Rectangle tool's radius, a change while the tool is
/// active reaches it, and the radius ↑ sets while drawing one becomes the preference.
#[test]
fn corner_radius_is_the_rounded_rectangle_tools_radius() {
use PointerKind::{Down, Drag, Up};
let mut s = new_doc();
let v = ViewInfo::default();
set_pref(&mut s, "cornerRadius", json!(7));
s.select_tool("roundedRectangle", v).unwrap();
let draw = |s: &mut Session, y: f64, up: bool| {
gesture(s, Mods::default(), &[(Down, 100.0, y), (Drag, 150.0, y + 30.0), (Drag, 200.0, y + 60.0)]);
if up {
s.tool_key(ToolKey::Up, Mods::default(), v).unwrap();
}
gesture(s, Mods::default(), &[(Up, 200.0, y + 60.0)]);
live_box(s, last_id(s)).3
};
assert!(close(draw(&mut s, 10.0, false), 7.0), "the preference's radius");
// Set while the tool is active: the tool takes it.
set_pref(&mut s, "cornerRadius", json!(4));
assert!(close(draw(&mut s, 100.0, false), 4.0), "the new preference's radius");
assert!(close(draw(&mut s, 200.0, true), 5.0), "↑ adds a point");
assert_eq!(s.execute("prefs.get", &json!({"key": "cornerRadius"})).unwrap(), json!(5.0), "and it is the preference now");
s.select_tool("selection", v).unwrap();
s.select_tool("roundedRectangle", v).unwrap();
assert_eq!(s.tool_options()["cornerRadius"], json!(5.0), "chosen again, the tool keeps it");
// A tool option set from the Tool Options dialog is the preference too, within its range.
s.set_tool_option("cornerRadius", &json!(5000));
assert_eq!(s.execute("prefs.get", &json!({"key": "cornerRadius"})).unwrap(), json!(1296.0));
assert_eq!(s.tool_options()["cornerRadius"], json!(1296.0), "and the tool itself stops at the range");
draw(&mut s, 300.0, true);
assert_eq!(s.tool_options()["cornerRadius"], json!(1296.0), "↑ at the top stays there");
}
/// General › Corner Radius set for the Rounded Rectangle tool by name (`tool.setOption {tool}`)
/// while it has lent a selection tool for a Cmd-drag reaches the tool kept aside: back from the
/// drag, it draws the radius that is the preference now, not the one from before.
#[test]
fn corner_radius_set_by_name_reaches_the_tool_that_lent_a_selection_tool() {
use PointerKind::{Down, Drag, Up};
let mut s = new_doc();
let v = ViewInfo::default();
set_pref(&mut s, "cornerRadius", json!(7));
s.select_tool("roundedRectangle", v).unwrap();
let cmd = Mods { cmd: true, ..Mods::default() };
gesture(&mut s, cmd, &[(Down, 300.0, 300.0), (Drag, 320.0, 320.0)]);
assert_eq!(s.tool_id(), "selection", "Cmd lends the Selection tool");
s.set_tool_option_cmd(&json!({"tool": "roundedRectangle", "key": "cornerRadius", "value": 9})).unwrap();
assert_eq!(s.execute("prefs.get", &json!({"key": "cornerRadius"})).unwrap(), json!(9.0), "the preference");
gesture(&mut s, cmd, &[(Up, 320.0, 320.0)]);
assert_eq!(s.tool_id(), "roundedRectangle", "the drag over, the tool is back");
assert_eq!(s.tool_options()["cornerRadius"], json!(9.0), "with the radius set while it was aside");
gesture(&mut s, Mods::default(), &[(Down, 100.0, 100.0), (Drag, 150.0, 130.0), (Drag, 200.0, 160.0), (Up, 200.0, 160.0)]);
assert!(close(live_box(&s, last_id(&s)).3, 9.0), "and draws it");
}
+29 -1
View File
@@ -5,10 +5,14 @@ use vectorcraft_doc::Selection;
use vectorcraft_geom::Point;
use vectorcraft_tools::distort::perspective::widget::WidgetPlace;
use vectorcraft_tools::draw2::FEEDBACK;
use vectorcraft_tools::shape::MAX_CORNER_RADIUS;
use vectorcraft_tools::{Action, Cursor, ImeSurrounding, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, settings};
use crate::{EngineError, Prefs, Result, Session};
/// The tool whose radius is General › Corner Radius.
const ROUNDED_RECTANGLE: &str = "roundedRectangle";
/// View state the tools need from the frontend.
#[derive(Clone, Copy, Debug)]
pub struct ViewInfo {
@@ -186,6 +190,10 @@ impl Session {
pub(crate) fn make_tool(&self, id: &str) -> Box<dyn Tool> {
let mut t = vectorcraft_tools::create(id);
Self::restore_tool_settings(&self.prefs, t.as_mut());
// General › Corner Radius is the Rounded Rectangle tool's radius (#394).
if id == ROUNDED_RECTANGLE {
t.set_option("cornerRadius", &Value::from(self.prefs.corner_radius));
}
t
}
@@ -204,6 +212,14 @@ impl Session {
/// Store the persistent options of `t` in `prefs` (so they are saved with them too).
fn store_tool_settings(prefs: &mut Prefs, t: &dyn Tool) {
let opts = t.options();
// The radius ↑ and ↓ set while drawing a rounded rectangle (or `tool.setOption`) is the
// Corner Radius preference from then on, as in the reference app
// (<https://helpx.adobe.com/illustrator/using/drawing-simple-lines-shapes.html>).
if t.id() == ROUNDED_RECTANGLE
&& let Some(r) = opts.get("cornerRadius").and_then(Value::as_f64).filter(|r| r.is_finite())
{
prefs.corner_radius = r.clamp(0.0, MAX_CORNER_RADIUS);
}
for (store, keys) in settings::stores(t.id()) {
let saved = prefs.tool_settings.entry(store.to_string()).or_default();
for k in keys {
@@ -601,7 +617,19 @@ impl Session {
}
Self::store_tool_settings(&mut self.prefs, t);
let out = t.options();
if other.is_some() {
if let Some(other) = other {
// The tool that lent a selection tool for a Cmd-drag is kept aside, not made again when
// it is back: when it is the tool set, it takes the writes too (the Rounded Rectangle's
// radius stays the Corner Radius preference it just set) and what it shares, like the
// active tool.
if let Some(lender) = self.lender.as_deref_mut() {
if lender.id() == other.id() {
for (k, v) in values {
lender.set_option(k, v);
}
}
Self::restore_tool_settings(&self.prefs, lender);
}
Self::restore_tool_settings(&self.prefs, self.tool.as_mut());
}
out
+30 -2
View File
@@ -13,6 +13,11 @@ use vectorcraft_geom::{Affine, Point, Rect, Vec2};
use crate::guides::{DrawSnap, Leave, square};
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey};
/// The largest corner radius the Rounded Rectangle tool draws, points: the range of General ›
/// Corner Radius, which the tool's radius is (its `prefs.list` spec), so ↑ and a Tool Options
/// value stop there too.
pub const MAX_CORNER_RADIUS: f64 = 1296.0;
pub struct ShapeTool {
id: &'static str,
start: Option<Point>,
@@ -279,7 +284,7 @@ impl Tool for ShapeTool {
true
}
("roundedRectangle", ToolKey::Up) => {
self.corner_radius += 1.0;
self.corner_radius = (self.corner_radius + 1.0).min(MAX_CORNER_RADIUS);
true
}
("roundedRectangle", ToolKey::Down) => {
@@ -351,7 +356,7 @@ impl Tool for ShapeTool {
match key {
"sides" => self.sides = v.as_u64().unwrap_or(6).clamp(3, 1000) as u32,
"points" => self.points = v.as_u64().unwrap_or(5).clamp(3, 1000) as u32,
"cornerRadius" => self.corner_radius = v.as_f64().unwrap_or(12.0).max(0.0),
"cornerRadius" => self.corner_radius = finite.unwrap_or(12.0).clamp(0.0, MAX_CORNER_RADIUS),
"starRatio" => self.star_ratio = v.as_f64().unwrap_or(0.5).clamp(0.01, 1.0),
"length" => self.line_length = finite.map_or(self.line_length, |n| n.abs().min(1e6)),
"angle" => self.line_angle = finite.map_or(self.line_angle, |n| n.rem_euclid(360.0)),
@@ -597,6 +602,29 @@ mod tests {
assert!(matches!(&a[0], Action::Preview(_, v) if v["sides"] == 7));
}
/// The Rounded Rectangle radius stops at General › Corner Radius's range on both of its input
/// paths: a Tool Options value is clamped to it, and ↑ at the top stays there.
#[test]
fn rounded_rectangle_radius_is_clamped_to_its_range() {
let (d, _) = doc_with_rect();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = ShapeTool::new("roundedRectangle");
t.set_option("cornerRadius", &json!(5000));
assert_eq!(t.options()["cornerRadius"], json!(MAX_CORNER_RADIUS));
t.set_option("cornerRadius", &json!(f64::INFINITY));
assert_eq!(t.options()["cornerRadius"], json!(12.0), "a non-finite value is the default");
t.set_option("cornerRadius", &json!(-3));
assert_eq!(t.options()["cornerRadius"], json!(0.0));
t.set_option("cornerRadius", &json!(MAX_CORNER_RADIUS));
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, 10.0, 10.0));
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, 50.0, 40.0));
let a = t.key(&cx, ToolKey::Up, Mods::default());
assert!(matches!(&a[0], Action::Preview(_, v) if v["radius"] == json!(MAX_CORNER_RADIUS)), "{a:?}");
assert_eq!(t.options()["cornerRadius"], json!(MAX_CORNER_RADIUS));
}
/// The start and the dragged corner land on another object's anchors (#506), hovering shows
/// it before the press, and the measurement label gives the size drawn.
#[test]
+3 -2
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+3 -2
View File
File diff suppressed because one or more lines are too long