Tools: double-clicking the Line Segment, Arc, Rectangular Grid and Polar Grid tools opens their Tool Options

Step 3 of the Rectangular Grid tool and Arc tool help pages
(https://helpx.adobe.com/illustrator/using/tool-techniques/rectangular-grid-tool.html,
https://helpx.adobe.com/illustrator/using/tool-techniques/arc-tool.html)
and the Tools page (https://helpx.adobe.com/illustrator/using/tools.html)
say a double-click on the tool opens its Tool Options. For these four tools
the double-click (tool.options, and Enter once #2891 lands) said the tool
had no options (#2691).

The Tool Options are the tool's click dialog in dialogs/shapes.rs, opened
without a place to draw (shapes::open_options; a `__toolOptions` field
marks it), as the review of #2979 asked: no copied form code. Its OK runs
tool.setOption and draws nothing; Cancel changes nothing. The grids' dialogs
are titled Rectangular Grid / Polar Grid Tool Options there. The options
are what the next drag draws with and what a click's dialog opens at:
- Line Segment: Length and Angle (now kept by the tool and used by the
  click dialog) and Fill Line (new: shape.line {fill: true} gives the line
  the current fill; the click dialog has it too);
- Arc: lengths, Reference Point, Type, Base Along, Slope, Fill Arc;
- the grids: dividers, skews and switches, plus the Default Size and
  Reference Point, now kept by the tool (width, height, reference) for the
  click dialog; a drag leaves them out of its command.
The grids' Reference Point is drawn only as the 9-point locator (it also
showed as a plain number field).

New strings (Rectangular Grid Tool Options, Polar Grid Tool Options, Fill
Line) have rows in all 24 catalogs.

Verification:
toolbar::tests::double_clicking_the_line_arc_and_grid_tools_opens_their_tool_options
double-clicks each tool's toolbar button: the dialog and its title, Cancel
leaves the next drag as it was, OK draws nothing, the next drag uses the
option and a click's dialog opens at it (a grid at its Default Size);
line_segment_options_and_fill_line, grid_default_size_is_an_option_the_drag_leaves_out,
fill_line_gives_a_new_line_the_current_fill. Grid dialog tests now expect
the stored Default Size. Live: double-clicking the Rectangular Grid tool in
the release build opens Rectangular Grid Tool Options; OK with 2 and 3
dividers, then a drag, draws a 3 × 4 grid.
This commit is contained in:
Taha-Mahmoodi
2026-10-24 03:09:07 +07:00
committed by bflatastic
parent b7477e3cd8
commit 7c8840b93a
43 changed files with 415 additions and 54 deletions
+58 -9
View File
@@ -41,6 +41,9 @@ pub struct FamilyTool {
pub compound: bool,
/// The grids take the current fill.
pub fill: bool,
/// The grids' Default Size (width, height): the size a click's dialog opens with (a drag sets
/// its own). The Reference Point is `reference`.
pub grid_size: (f64, f64),
snap: DrawSnap,
}
@@ -74,6 +77,7 @@ impl FamilyTool {
frame: true,
compound: false,
fill: false,
grid_size: (100.0, 100.0),
snap: DrawSnap::default(),
}
}
@@ -99,6 +103,20 @@ impl FamilyTool {
matches!(self.id, "rectangularGrid" | "polarGrid")
}
/// A grid's options a drag draws with: its dividers, skews and switches (the Default Size and
/// Reference Point are for a click's dialog).
fn grid_options(&self) -> Value {
if self.id == "polarGrid" {
json!({
"concentric": self.rows, "radial": self.columns, "concentricSkew": self.skew[0], "radialSkew": self.skew[1], "compound": self.compound, "fill": self.fill,
})
} else {
json!({
"rows": self.rows, "columns": self.columns, "rowSkew": self.skew[0], "columnSkew": self.skew[1], "frame": self.frame, "fill": self.fill,
})
}
}
/// The command for a drag from `start` to `p`.
pub fn command(&self, start: Point, p: Point, m: Mods) -> (String, Value) {
self.command_at(start, p, m, 0.0)
@@ -117,7 +135,7 @@ impl FamilyTool {
}
// The grids: the dragged box (along the Constrain Angle's axes) with the tool's options.
"rectangularGrid" | "polarGrid" => {
let mut params = self.options();
let mut params = self.grid_options();
if let (Some(o), Value::Object(b)) = (params.as_object_mut(), box_params(start, p, m, base)) {
o.extend(b);
}
@@ -252,12 +270,13 @@ impl Tool for FamilyTool {
fn options(&self) -> Value {
match self.id {
"spiral" => json!({"decay": self.decay, "segments": self.segments, "clockwise": self.clockwise}),
"rectangularGrid" => json!({
"rows": self.rows, "columns": self.columns, "rowSkew": self.skew[0], "columnSkew": self.skew[1], "frame": self.frame, "fill": self.fill,
}),
"polarGrid" => json!({
"concentric": self.rows, "radial": self.columns, "concentricSkew": self.skew[0], "radialSkew": self.skew[1], "compound": self.compound, "fill": self.fill,
}),
"rectangularGrid" | "polarGrid" => {
let mut o = self.grid_options();
o["width"] = json!(self.grid_size.0);
o["height"] = json!(self.grid_size.1);
o["reference"] = json!(self.reference);
o
}
_ => json!({
"closed": self.closed,
"lengthX": self.length_x,
@@ -292,6 +311,8 @@ impl Tool for FamilyTool {
"frame" => self.frame = v.as_bool().unwrap_or(self.frame),
"compound" => self.compound = v.as_bool().unwrap_or(self.compound),
"fill" => self.fill = v.as_bool().unwrap_or(self.fill),
"width" => self.grid_size.0 = grid_length(v, self.grid_size.0),
"height" => self.grid_size.1 = grid_length(v, self.grid_size.1),
_ => {}
}
}
@@ -302,6 +323,12 @@ fn arc_length(v: &Value, current: f64) -> f64 {
v.as_f64().filter(|n| n.is_finite()).map_or(current, |n| n.abs().max(1e-9))
}
/// A grid's Default Size side from option value `v`: a finite positive length (at most 1e6 pt),
/// else `current`.
fn grid_length(v: &Value, current: f64) -> f64 {
v.as_f64().filter(|n| n.is_finite() && *n > 0.0).map_or(current, |n| n.min(1e6))
}
/// A grid divider skew from option value `v`: a finite number within the range, else `current`.
fn grid_skew(v: &Value, current: f64) -> f64 {
let max = vectorcraft_geom::shapes::GRID_SKEW_MAX;
@@ -361,7 +388,9 @@ mod tests {
t.set_option(key, &value);
}
let options = json!({"concentric": 2, "radial": 5, "concentricSkew": -40.0, "radialSkew": 500.0, "compound": true, "fill": true});
assert_eq!(t.options(), options);
let mut reported = options.clone();
reported.as_object_mut().unwrap().extend(json!({"width": 100.0, "height": 100.0, "reference": 0}).as_object().unwrap().clone());
assert_eq!(t.options(), reported);
let (command, params) = t.command(Point::new(10.0, 20.0), Point::new(40.0, 80.0), Mods::default());
assert_eq!(command, "shape.polarGrid");
let mut want = options;
@@ -370,7 +399,10 @@ mod tests {
let mut t = FamilyTool::new("rectangularGrid");
t.set_option("frame", &json!(false));
t.set_option("rowSkew", &json!(12.5));
assert_eq!(t.options(), json!({"rows": 5, "columns": 5, "rowSkew": 12.5, "columnSkew": 0.0, "frame": false, "fill": false}));
assert_eq!(
t.options(),
json!({"rows": 5, "columns": 5, "rowSkew": 12.5, "columnSkew": 0.0, "frame": false, "fill": false, "width": 100.0, "height": 100.0, "reference": 0})
);
}
#[test]
@@ -475,4 +507,21 @@ mod tests {
);
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Label { text, .. } if text == "align")));
}
/// A grid's Default Size and Reference Point are options for a click's dialog; the drag
/// draws the box it is dragged to and leaves them out of its command.
#[test]
fn grid_default_size_is_an_option_the_drag_leaves_out() {
for id in ["rectangularGrid", "polarGrid"] {
let mut t = FamilyTool::new(id);
for (k, v) in [("width", json!(150.0)), ("height", json!(-3.0)), ("reference", json!(4))] {
t.set_option(k, &v);
}
let o = t.options();
assert_eq!((o["width"].clone(), o["height"].clone(), o["reference"].clone()), (json!(150.0), json!(100.0), json!(4)), "{id}");
let (_, p) = t.command(Point::new(0.0, 0.0), Point::new(40.0, 30.0), Mods::default());
assert!(p.get("reference").is_none(), "{id}: {p}");
assert_eq!((p["width"].clone(), p["height"].clone()), (json!(40.0), json!(30.0)), "{id}");
}
}
}
+6 -2
View File
@@ -28,8 +28,12 @@ const ROWS: &[Row] = &[
(&["puppetWarp"], None, &["showMesh", "expand"]),
(&["arc"], None, &["closed", "lengthX", "lengthY", "reference", "baseAlong", "slope", "fillArc"]),
(&["spiral"], None, &["decay", "segments", "clockwise"]),
(&["rectangularGrid"], None, &["rows", "columns", "rowSkew", "columnSkew", "frame", "fill"]),
(&["polarGrid"], None, &["concentric", "radial", "concentricSkew", "radialSkew", "compound", "fill"]),
// The grids' Tool Options: the dividers and switches the drag uses, the Default Size and
// Reference Point a click's dialog opens with.
(&["rectangularGrid"], None, &["rows", "columns", "rowSkew", "columnSkew", "frame", "fill", "width", "height", "reference"]),
(&["polarGrid"], None, &["concentric", "radial", "concentricSkew", "radialSkew", "compound", "fill", "width", "height", "reference"]),
// Line Segment Tool Options: the Length and Angle a click's dialog opens with, Fill Line.
(&["lineSegment"], None, &["length", "angle", "fillLine"]),
(&["pencil", "paintbrush"], None, &["fidelity", "fill", "keepSelected", "editWithin", "closeWithin"]),
// Pencil Tool Options' Alt key toggles to Smooth Tool (the Paintbrush has no such option).
(&["pencil"], None, &["altSmooth"]),
+44 -2
View File
@@ -26,6 +26,11 @@ pub struct ShapeTool {
pub star_ratio: f64,
/// The star's inner radius held while Ctrl/Cmd is down in a drag.
inner: Option<f64>,
/// Line Segment Tool Options: the length and angle (degrees) a click's dialog opens with, and
/// Fill Line (new lines take the current fill).
pub line_length: f64,
pub line_angle: f64,
pub fill_line: bool,
snap: DrawSnap,
}
@@ -59,6 +64,9 @@ impl ShapeTool {
star_ratio: 0.5,
inner: None,
snap: DrawSnap::default(),
line_length: 100.0,
line_angle: 0.0,
fill_line: false,
}
}
@@ -131,7 +139,11 @@ impl ShapeTool {
}
"lineSegment" => {
let (a, b) = if m.alt { (start - (p - start), p) } else { (start, p) };
("shape.line".into(), json!({ "x1": a.x, "y1": a.y, "x2": b.x, "y2": b.y }))
let mut v = json!({ "x1": a.x, "y1": a.y, "x2": b.x, "y2": b.y });
if self.fill_line {
v["fill"] = json!(true);
}
("shape.line".into(), v)
}
_ => {
let cmd = if self.id == "ellipse" { "shape.ellipse" } else { "shape.rectangle" };
@@ -314,14 +326,26 @@ impl Tool for ShapeTool {
Cursor::Crosshair
}
fn options(&self) -> Value {
json!({ "sides": self.sides, "points": self.points, "cornerRadius": self.corner_radius, "starRatio": self.star_ratio })
json!({
"sides": self.sides,
"points": self.points,
"cornerRadius": self.corner_radius,
"starRatio": self.star_ratio,
"length": self.line_length,
"angle": self.line_angle,
"fillLine": self.fill_line,
})
}
fn set_option(&mut self, key: &str, v: &Value) {
let finite = v.as_f64().filter(|n| n.is_finite());
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),
"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)),
"fillLine" => self.fill_line = v.as_bool().unwrap_or(self.fill_line),
_ => {}
}
}
@@ -532,4 +556,22 @@ mod tests {
let to = Point::new(200.0, 300.0);
assert!(t.overlays(&cx).iter().any(|o| matches!(o, Overlay::Measure { text, .. } if text == &cx.segment_measure_label(from, to))));
}
/// Line Segment Tool Options: Length and Angle are options (for a click's dialog), and Fill
/// Line makes the drag's line take the current fill.
#[test]
fn line_segment_options_and_fill_line() {
let mut t = ShapeTool::new("lineSegment");
let (a, b) = (Point::new(0.0, 0.0), Point::new(30.0, 40.0));
assert!(t.command(a, b, Mods::default()).1.get("fill").is_none());
for (k, v) in [("length", json!(-80.0)), ("angle", json!(-90.0)), ("fillLine", json!(true))] {
t.set_option(k, &v);
}
t.set_option("length", &json!(f64::NAN));
assert_eq!(
(t.options()["length"].clone(), t.options()["angle"].clone(), t.options()["fillLine"].clone()),
(json!(80.0), json!(270.0), json!(true))
);
assert_eq!(t.command(a, b, Mods::default()).1["fill"], json!(true));
}
}