Merge pull request #3815 from Taha-Mahmoodi/feat/2691-grid-arc-tool-options

Tools: double-clicking the Line Segment, Arc, Rectangular Grid and Polar Grid tools opens their Tool Options
This commit is contained in:
BFlat
2026-10-24 03:11:37 +07:00
committed by GitHub
43 changed files with 456 additions and 55 deletions
+4 -3
View File
@@ -30,7 +30,7 @@ pub fn specs() -> Vec<CommandSpec> {
has_doc,
flare
),
cmd!("shape.line", "Line Segment", [], None, "{x1, y1, x2, y2} → {id}", has_doc, line),
cmd!("shape.line", "Line Segment", [], None, "{x1, y1, x2, y2, fill?: bool (Fill Line: the current fill, else none)} → {id}", has_doc, line),
cmd!("shape.spiral", "Spiral", [], None, "{cx, cy, radius, decay=80 (%), segments=10, clockwise?} → {id}", has_doc, spiral),
cmd!(
"shape.arc",
@@ -486,9 +486,10 @@ fn line(s: &mut Session, p: &Value) -> Result<Value> {
let a = Point::new(f64_req(p, "x1", "shape.line")?, f64_req(p, "y1", "shape.line")?);
let b = Point::new(f64_req(p, "x2", "shape.line")?, f64_req(p, "y2", "shape.line")?);
let live = LiveShape::Line { a, b };
// Lines are stroked, never filled (Illustrator ignores the fill for new open lines).
// Lines are stroked and, unless Fill Line asks for the current fill, not filled.
let fill = if switch_or(p, "fill", false, "shape.line")? { s.paint.fill.clone() } else { vectorcraft_color::Paint::None };
let look = s.new_art_look(
vectorcraft_color::Paint::None,
fill,
if s.paint.stroke.is_none() { vectorcraft_color::Paint::solid(vectorcraft_color::Color::BLACK) } else { s.paint.stroke.clone() },
s.paint.stroke_width.max(0.1),
);
+14
View File
@@ -141,3 +141,17 @@ fn the_option_round_trips_with_the_preferences() {
let old: Prefs = serde_json::from_value(json!({"keyboardIncrement": 2})).unwrap();
assert!(old.new_art_basic);
}
/// Line Segment Tool Options' Fill Line: `shape.line {fill: true}` gives the line the current
/// fill; without it a new line has none.
#[test]
fn fill_line_gives_a_new_line_the_current_fill() {
let mut s = session();
run(&mut s, "paint.setFill", json!({"color": "#ff0000"}));
let plain = NodeId(run(&mut s, "shape.line", json!({"x1": 10, "y1": 20, "x2": 200, "y2": 80}))["id"].as_u64().unwrap());
let filled = NodeId(run(&mut s, "shape.line", json!({"x1": 10, "y1": 20, "x2": 200, "y2": 80, "fill": true}))["id"].as_u64().unwrap());
assert_eq!(node(&s, plain).appearance.fill_paint(), Paint::None);
assert_eq!(node(&s, filled).appearance.fill_paint(), s.paint.fill);
assert_ne!(s.paint.fill, Paint::None);
assert!(s.execute("shape.line", &json!({"x1": 0, "y1": 0, "x2": 1, "y2": 1, "fill": "yes"})).is_err());
}
+1 -1
View File
@@ -56,7 +56,7 @@ fn an_unknown_key_is_named_with_the_nearest_accepted_one() {
assert_eq!(refusal("select.all", json!({"zzz": 1})), "unknown parameter `zzz`; the command takes no parameters");
assert_eq!(
refusal("shape.line", json!({"x1": 0, "y1": 0, "x2": 5, "y2": 5, "colour": "red"})),
"unknown parameter `colour`; the command takes: x1, y1, x2, y2"
"unknown parameter `colour`; the command takes: x1, y1, x2, y2, fill"
);
for (id, ok) in [
("file.new", json!({"width": 300, "title": "a", "artboardLayout": {"anything": 1}})),
+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));
}
}
+2
View File
@@ -84,6 +84,8 @@ fn suffix(kind: &str, key: &str) -> Option<&'static str> {
fn own_rows(kind: &str) -> &'static [&'static str] {
match kind {
"artboardOptions" => &["color"],
// The grids' Reference Point is their 9-point locator.
"rectangularGrid" | "polarGrid" => &["reference"],
_ => &[],
}
}
+1 -1
View File
@@ -90,7 +90,7 @@ pub mod save_style_library;
pub mod save_swatch_library;
pub mod script_check;
pub mod shape_builder_options;
mod shapes;
pub(crate) mod shapes;
pub mod shortcut_import;
pub(crate) mod simplify_slider;
pub mod slices;
+128 -19
View File
@@ -1,5 +1,8 @@
//! Shape size dialogs (a click with a shape tool): Rectangle, Rounded Rectangle, Ellipse, Polygon,
//! Star, Line Segment, Arc and Spiral tool options, and the two grids (size, divider counts and skews, switches).
//! The Line Segment, Arc and grid dialogs are also their tools' Tool Options (a double-click on the
//! tool, [`open_options`]): OK then sets the options the tool's next drag and click use, and draws
//! nothing.
use serde_json::{Value, json};
use vectorcraft_doc::Unit;
@@ -11,23 +14,97 @@ use crate::panels::transform::artboard_rect;
use crate::state::Dialog;
use crate::widgets;
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| title(&d.kind).into(), body, confirm, ..DialogSpec::FORM };
pub(super) const SPEC: DialogSpec = DialogSpec { heading: |d| title(d).into(), body, confirm, ..DialogSpec::FORM };
fn title(kind: &str) -> &'static str {
match kind {
/// The field that marks a dialog opened as its tool's Tool Options rather than by a click.
const TOOL_OPTIONS: &str = "__toolOptions";
/// The tools whose Tool Options are this dialog.
pub(crate) const OPTION_TOOLS: [&str; 4] = ["rectangularGrid", "polarGrid", "arc", "lineSegment"];
fn title(d: &Dialog) -> &'static str {
let options = d.bool(TOOL_OPTIONS);
match d.kind.as_str() {
"rectangle" => tl!("Rectangle"),
"roundedRectangle" => tl!("Rounded Rectangle"),
"ellipse" => tl!("Ellipse"),
"polygon" => tl!("Polygon"),
"star" => tl!("Star"),
"spiral" => tl!("Spiral"),
"rectangularGrid" if options => tl!("Rectangular Grid Tool Options"),
"rectangularGrid" => tl!("Rectangular Grid"),
"polarGrid" if options => tl!("Polar Grid Tool Options"),
"polarGrid" => tl!("Polar Grid"),
"arc" => tl!("Arc Segment Tool Options"),
_ => tl!("Line Segment Tool Options"),
}
}
/// Open `tool`'s Tool Options (one of [`OPTION_TOOLS`]), as a double-click on the tool does
/// (<https://helpx.adobe.com/illustrator/using/tool-techniques/rectangular-grid-tool.html>,
/// <https://helpx.adobe.com/illustrator/using/tool-techniques/arc-tool.html>): the click dialog's
/// fields at the tool's options, with no click to draw at.
pub(crate) fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<Value, String> {
if !OPTION_TOOLS.contains(&tool) {
return Err(format!("the {tool} tool has no options"));
}
super::open_tool_dialog(app, tool, json!({}));
let d = app.ui.dialog.as_mut().filter(|d| d.kind == tool).ok_or_else(|| format!("the {tool} tool has no options"))?;
for key in ["x", "y", "__anchorX", "__anchorY"] {
d.fields.remove(key);
}
d.fields.insert(TOOL_OPTIONS.into(), json!(true));
Ok(json!({ "dialog": tool }))
}
/// What OK sets as the options of `d`'s tool ([`OPTION_TOOLS`]), from its fields: a grid's are its
/// switches as they are, its typed fields as numbers, its Default Size and Reference Point.
fn tool_options(d: &Dialog) -> Value {
match d.kind.as_str() {
"lineSegment" => {
let (length, angle) = line_length_angle(d);
json!({"length": length, "angle": angle, "fillLine": d.bool("fillLine")})
}
"arc" => arc_options(d),
kind => {
let (mut o, (counts, skews)) = (form::params(d), grid_keys(kind));
if let Some(m) = o.as_object_mut() {
m.remove("x");
m.remove("y");
}
for (count, skew) in counts.into_iter().zip(skews) {
o[count] = json!((d.f64(count, 5.0) as u64).min(999));
o[skew] = json!(d.f64(skew, 0.0));
}
o["width"] = json!(d.f64("width", 100.0));
o["height"] = json!(d.f64("height", 100.0));
o["reference"] = json!(grid_reference(d));
o
}
}
}
/// The Line Segment dialog's length and angle (degrees), a negative length turned into the same
/// line drawn the other way: -80 pt at 0° is 80 pt at 180°. The line a click draws and the options
/// OK keeps both use it, so the next line goes the way this one went.
fn line_length_angle(d: &Dialog) -> (f64, f64) {
let (length, angle) = (d.f64("length", 100.0), d.f64("angle", 0.0));
if length < 0.0 { (-length, (angle + 180.0).rem_euclid(360.0)) } else { (length, angle) }
}
/// The Arc tool's options from its dialog's fields.
fn arc_options(d: &Dialog) -> Value {
json!({
"closed": d.str("arcType").eq_ignore_ascii_case("closed"),
"lengthX": d.f64("lengthX", 100.0),
"lengthY": d.f64("lengthY", 100.0),
"reference": arc_reference(d),
"baseAlong": d.str("baseAlong"),
"slope": d.f64("slope", 50.0),
"fillArc": d.bool("fillArc"),
})
}
/// The grids' divider counts and skews: (rows / concentric, columns / radial).
fn grid_keys(kind: &str) -> ([&'static str; 2], [&'static str; 2]) {
if kind == "polarGrid" { (["concentric", "radial"], ["concentricSkew", "radialSkew"]) } else { (["rows", "columns"], ["rowSkew", "columnSkew"]) }
@@ -109,6 +186,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
let own = match d.kind.as_str() {
"rectangularGrid" => ("frame", tl!("Use Outside Rectangle As Frame")),
"polarGrid" => ("compound", tl!("Create Compound Path From Ellipses")),
"lineSegment" => {
ui.add_space(8.0);
form::check(ui, d, "fillLine", tl!("Fill Line"));
return false;
}
_ => return false,
};
ui.add_space(8.0);
@@ -173,6 +255,12 @@ fn box_origin(d: &Dialog) -> (f64, f64) {
}
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
// Tool Options: the options the tool's next drag and click use; nothing is drawn.
if d.bool(TOOL_OPTIONS) {
let r = app.run("tool.setOption", json!({"tool": d.kind, "values": tool_options(d)}));
app.ui.dialog = None;
return r;
}
let count = match d.kind.as_str() {
"polygon" => Some(("sides", 6.0, 3.0)),
"star" => Some(("points", 5.0, 2.0)),
@@ -184,19 +272,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
values[key] = json!(d.f64(key, default).clamp(min, 1000.0) as u64);
values
})
.or_else(|| {
(d.kind == "arc").then(|| {
json!({
"closed": d.str("arcType").eq_ignore_ascii_case("closed"),
"lengthX": d.f64("lengthX", 100.0),
"lengthY": d.f64("lengthY", 100.0),
"reference": arc_reference(d),
"baseAlong": d.str("baseAlong"),
"slope": d.f64("slope", 50.0),
"fillArc": d.bool("fillArc"),
})
})
});
.or_else(|| OPTION_TOOLS.contains(&d.kind.as_str()).then(|| tool_options(d)));
let (x, y) = (d.f64("x", 0.0), d.f64("y", 0.0));
let (bx, by) = box_origin(d);
let (id, params) = match d.kind.as_str() {
@@ -231,8 +307,13 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
(if kind == "polarGrid" { "shape.polarGrid" } else { "shape.rectangularGrid" }, p)
}
"lineSegment" => {
let (l, a) = (d.f64("length", 100.0), d.f64("angle", 0.0).to_radians());
("shape.line", json!({"x1": x, "y1": y, "x2": x + l * a.cos(), "y2": y - l * a.sin()}))
let (l, a) = line_length_angle(d);
let a = a.to_radians();
let mut p = json!({"x1": x, "y1": y, "x2": x + l * a.cos(), "y2": y - l * a.sin()});
if d.bool("fillLine") {
p["fill"] = json!(true);
}
("shape.line", p)
}
"arc" => {
let anchor = Point::new(d.f64("__anchorX", x), d.f64("__anchorY", y));
@@ -259,7 +340,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
// On the active plane while the perspective grid shows, sized in plane units from the click;
// else along General › Constrain Angle, turned about the point the dialog places at the click
// (a grid's reference point, the other shapes' top-left corner or centre).
let options = options.or_else(|| matches!(d.kind.as_str(), "spiral" | "rectangularGrid" | "polarGrid").then(|| params.clone()));
let options = options.or_else(|| (d.kind == "spiral").then(|| params.clone()));
let result = match vectorcraft_engine::perspective_click(&app.session, id, &params, vectorcraft_geom::Point::new(x, y)) {
Some((id, params)) => run_and_close(app, &id, params),
None => {
@@ -314,4 +395,32 @@ mod tests {
assert_eq!(box_origin(&fields(false)), (100.0, 50.0));
assert_eq!(box_origin(&fields(true)), (80.0, 40.0));
}
#[test]
fn a_negative_line_length_draws_and_keeps_the_same_direction() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.run("tool.select", json!({"tool": "lineSegment"})).unwrap();
let line_bounds = |app: &VectorcraftApp, i: usize| {
let doc = &app.session.doc().unwrap().doc;
let n = &doc.layers[0].children().unwrap()[i];
n.path_data().and_then(|p| p.bounds()).expect("line bounds")
};
super::super::open_tool_dialog(&mut app, "lineSegment", json!({"x": 100.0, "y": 60.0}));
let d = app.ui.dialog.as_mut().expect("Line Segment dialog");
d.fields.insert("length".into(), json!(-80.0));
d.fields.insert("angle".into(), json!(0.0));
super::super::confirm(&mut app).unwrap();
let first = line_bounds(&app, 0);
assert!((first.x0 - 20.0).abs() < 1e-6 && (first.x1 - 100.0).abs() < 1e-6, "{first:?}");
let kept = app.session.tool_options_of("lineSegment");
assert_eq!((kept["length"].as_f64(), kept["angle"].as_f64()), (Some(80.0), Some(180.0)));
// The next click opens at what was kept and draws the same way.
super::super::open_tool_dialog(&mut app, "lineSegment", json!({"x": 100.0, "y": 120.0}));
super::super::confirm(&mut app).unwrap();
let second = line_bounds(&app, 1);
assert!((second.x0 - 20.0).abs() < 1e-6 && (second.x1 - 100.0).abs() < 1e-6, "{second:?}");
}
}
+4 -1
View File
@@ -1842,7 +1842,10 @@ fn polar_grid_dialog_edits_skews_compound_and_fill() {
assert!((widths[0] - 100.0).abs() < 1e-6 && (widths[1] - 93.75).abs() < 1e-6, "outward: {widths:?}");
let tips: Vec<f64> = children[3..].iter().map(|n| n.path_data().unwrap().subpaths[0].anchors[1].p.x).collect();
assert!(tips.iter().all(|x| *x > 60.0), "right of the top divider: {tips:?}");
let saved = json!({"concentric": 3, "radial": 4, "concentricSkew": 100.0, "radialSkew": 500.0, "compound": true, "fill": true});
let saved = json!({
"concentric": 3, "radial": 4, "concentricSkew": 100.0, "radialSkew": 500.0, "compound": true, "fill": true,
"width": 100.0, "height": 100.0, "reference": 0,
});
assert_eq!(app.session.tool_options_of("polarGrid"), saved);
}
@@ -222,7 +222,7 @@ fn rectangular_grid_creates_exact_dividers_as_one_undo_redo_step() {
assert_eq!(app.session.doc().unwrap().doc, before.doc);
assert_eq!(app.session.doc().unwrap().selection.objects, before.selected);
assert_eq!(app.session.doc().unwrap().history.undo.len(), before.undo);
let saved = json!({"rows": 2, "columns": 3, "rowSkew": 0.0, "columnSkew": 0.0, "frame": true, "fill": false});
let saved = json!({"rows": 2, "columns": 3, "rowSkew": 0.0, "columnSkew": 0.0, "frame": true, "fill": false, "width": 120.0, "height": 60.0, "reference": 0});
assert_eq!(app.session.tool_options_of(KIND), saved, "undo does not undo the saved tool settings");
app.run("edit.redo", json!({})).unwrap();
assert_eq!(app.session.doc().unwrap().doc, created);
@@ -380,7 +380,7 @@ fn rectangular_grid_dialog_edits_skews_frame_and_fill() {
}
let x = paths[7].subpaths[0].anchors[0].p.x;
assert!(x > 10.0 && x < 30.0, "toward the left: {x}");
let saved = json!({"rows": 3, "columns": 1, "rowSkew": 100.0, "columnSkew": -250.0, "frame": false, "fill": true});
let saved = json!({"rows": 3, "columns": 1, "rowSkew": 100.0, "columnSkew": -250.0, "frame": false, "fill": true, "width": 100.0, "height": 100.0, "reference": 0});
assert_eq!(app.session.tool_options_of(KIND), saved);
// A drag draws with the same options.
pointer(&mut app, PointerKind::Down, Point::new(300.0, 300.0));
+23 -6
View File
@@ -19,7 +19,15 @@ pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) {
"star" => {
Dialog::new(kind, json!({"x": x, "y": y, "radius1": "50 pt", "radius2": "25 pt", "points": app.session.tool_options_of(kind)["points"]}))
}
"lineSegment" => Dialog::new(kind, json!({"x": x, "y": y, "length": "100 pt", "angle": 0})),
// The Line Segment Tool Options: Length, Angle and Fill Line as the tool keeps them.
"lineSegment" => {
let o = app.session.tool_options_of(kind);
let fill = o.get("fillLine").and_then(Value::as_bool).unwrap_or(false);
Dialog::new(
kind,
json!({"x": x, "y": y, "length": pt(o.get("length")), "angle": o.get("angle").cloned().unwrap_or(json!(0)), "fillLine": fill}),
)
}
"spiral" => {
let options = app.session.tool_options_of(kind);
Dialog::new(
@@ -56,16 +64,20 @@ pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) {
super::shapes::arc_pin_to_anchor(&mut d);
d
}
// The grids: the tool's stored options (divider counts and skews, switches) at the default size.
// The grids: the tool's stored options (Default Size, Reference Point, divider counts and
// skews, switches).
"rectangularGrid" | "polarGrid" => {
let mut obj = match app.session.tool_options_of(kind) {
Value::Object(o) => o,
_ => serde_json::Map::new(),
};
for (key, value) in [("__anchorX", json!(x)), ("__anchorY", json!(y)), ("width", json!("100 pt")), ("height", json!("100 pt"))] {
obj.insert(key.into(), value);
}
obj.insert("reference".into(), json!(0));
obj.insert("__anchorX".into(), json!(x));
obj.insert("__anchorY".into(), json!(y));
// The Default Size in points, as typed lengths.
let (w, h) = (pt(obj.get("width")), pt(obj.get("height")));
obj.insert("width".into(), w);
obj.insert("height".into(), h);
obj.entry("reference").or_insert(json!(0));
let mut d = Dialog::new(kind, Value::Object(obj));
grid_pin_to_anchor(&mut d);
d
@@ -150,3 +162,8 @@ pub fn open_tool_dialog(app: &mut VectorcraftApp, kind: &str, p: Value) {
};
app.ui.dialog = Some(d);
}
/// A stored length option (points) as a dialog's typed length, `"N pt"`; 100 pt when unset.
fn pt(v: Option<&Value>) -> Value {
json!(format!("{} pt", v.and_then(Value::as_f64).filter(|n| n.is_finite()).unwrap_or(100.0)))
}
+3
View File
@@ -5412,3 +5412,6 @@ axis Y Axis Y ос
The plug-in is computing… Приставката изчислява…
Inspect Plug-in Проверка на приставка
Cancel Plug-in Run Отказ от изпълнението на приставката
Rectangular Grid Tool Options Опции на инструмента „Правоъгълна мрежа“
Polar Grid Tool Options Опции на инструмента „Полярна мрежа“
Fill Line Запълване на линия
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5463,3 +5463,6 @@ axis Y Axis osa Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` živý efekt běží během vykreslování dokumentu, takže nemůže být `heavy`
@msg an exporter always gets an export's budget: leave `budget` out exportní zásuvný modul dostává vždy rozpočet exportu: klíč `budget` vynechte
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees „{_1}“ žádá o velký výpočetní rozpočet (každé spuštění až {minutes} minut a {times}krát víc instrukcí než obvykle): nainstalujte ho s `allowHeavy: true`, jakmile uživatel souhlasí
Rectangular Grid Tool Options Možnosti nástroje Obdélníková mřížka
Polar Grid Tool Options Možnosti nástroje Polární mřížka
Fill Line Výplň úsečky
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5428,3 +5428,6 @@ axis Y Axis Y-akse
The plug-in is computing… Plug-in'en regner…
Inspect Plug-in Undersøg plug-in
Cancel Plug-in Run Annullér plug-in-kørsel
Rectangular Grid Tool Options Indstillinger for rektangulært gitterværktøj
Polar Grid Tool Options Indstillinger for polært gitterværktøj
Fill Line Udfyld linje
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5046,3 +5046,6 @@ axis Y Axis Y-Achse
@msg a live effect runs while the document is drawn, so it can't be `heavy` ein Live-Effekt läuft, während das Dokument gezeichnet wird, daher kann er nicht `heavy` sein
@msg an exporter always gets an export's budget: leave `budget` out ein Export-Plug-in erhält immer das Budget eines Exports: Lassen Sie `budget` weg
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees „{_1}“ verlangt ein großes Rechenbudget (jede Ausführung bis zu {minutes} Minuten und {times}-mal so viele Anweisungen wie üblich): Installieren Sie es mit `allowHeavy: true`, sobald der Benutzer zustimmt
Rectangular Grid Tool Options Optionen für das Rechteckiges-Raster-Werkzeug
Polar Grid Tool Options Optionen für das Radiales-Raster-Werkzeug
Fill Line Linie füllen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5415,3 +5415,6 @@ axis Y Axis Άξονας Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` ένα δυναμικό εφέ εκτελείται κατά τη σχεδίαση του εγγράφου, οπότε δεν μπορεί να είναι `heavy`
@msg an exporter always gets an export's budget: leave `budget` out ένα πρόσθετο εξαγωγής παίρνει πάντα τον προϋπολογισμό μιας εξαγωγής: παραλείψτε το `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees το «{_1}» ζητά μεγάλο υπολογιστικό προϋπολογισμό (κάθε εκτέλεση έως {minutes} λεπτά και {times} φορές τις συνήθεις εντολές): εγκαταστήστε το με `allowHeavy: true` μόλις συμφωνήσει ο χρήστης
Rectangular Grid Tool Options Επιλογές εργαλείου ορθογώνιου πλέγματος
Polar Grid Tool Options Επιλογές εργαλείου πολικού πλέγματος
Fill Line Γέμισμα γραμμής
Can't render this file because it contains an unexpected character in line 2049 and column 19.
+3
View File
@@ -5405,3 +5405,6 @@ axis Y Axis Eje Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` un efecto interactivo se ejecuta mientras se dibuja el documento, así que no puede ser `heavy`
@msg an exporter always gets an export's budget: leave `budget` out un plugin de exportación siempre recibe el presupuesto de una exportación: omite `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” pide un presupuesto de cálculo grande (cada ejecución hasta {minutes} minutos y {times} veces las instrucciones habituales): instálalo con `allowHeavy: true` cuando el usuario lo acepte
Rectangular Grid Tool Options Opciones de la herramienta Cuadrícula rectangular
Polar Grid Tool Options Opciones de la herramienta Cuadrícula polar
Fill Line Rellenar línea
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5494,3 +5494,6 @@ axis Y Axis Y-akseli
@msg a live effect runs while the document is drawn, so it can't be `heavy` live-tehoste suoritetaan asiakirjaa piirrettäessä, joten se ei voi olla `heavy`
@msg an exporter always gets an export's budget: leave `budget` out vientilaajennus saa aina viennin budjetin: jätä `budget` pois
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees ”{_1}” pyytää suurta laskentabudjettia (jokainen ajo enintään {minutes} minuuttia ja {times} kertaa tavallinen määrä käskyjä): asenna se asetuksella `allowHeavy: true`, kun käyttäjä hyväksyy
Rectangular Grid Tool Options Suorakulmaisen ruudukon työkalun asetukset
Polar Grid Tool Options Napakoordinaattiruudukon työkalun asetukset
Fill Line Täytä viiva
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5462,3 +5462,6 @@ axis Y Axis Axe Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` un effet dynamique s’exécute pendant le tracé du document, il ne peut donc pas être `heavy`
@msg an exporter always gets an export's budget: leave `budget` out un module externe d’exportation reçoit toujours le budget d’une exportation : omettez `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees « {_1} » demande un budget de calcul important (chaque exécution jusqu’à {minutes} minutes et {times} fois les instructions habituelles) : installez-le avec `allowHeavy: true` une fois que l’utilisateur l’accepte
Rectangular Grid Tool Options Options de l’outil Grille rectangulaire
Polar Grid Tool Options Options de l’outil Grille à coordonnées polaires
Fill Line Remplir le trait
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5449,3 +5449,6 @@ axis Y Axis Y tengely
@msg a live effect runs while the document is drawn, so it can't be `heavy` az élő effektus a dokumentum rajzolása közben fut, ezért nem lehet `heavy`
@msg an exporter always gets an export's budget: leave `budget` out az exportáló bővítmény mindig az exportálás keretét kapja: hagyd ki a `budget` kulcsot
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees a(z) „{_1}” nagy számítási keretet kér (futásonként legfeljebb {minutes} perc és a szokásos utasításszám {times}-szorosa): telepítsd az `allowHeavy: true` megadásával, miután a felhasználó beleegyezett
Rectangular Grid Tool Options Téglalaprács beállításai
Polar Grid Tool Options Polárrács beállításai
Fill Line Szakasz kitöltése
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5428,3 +5428,6 @@ axis Y Axis Sumbu Y
The plug-in is computing… Plug-in sedang menghitung…
Inspect Plug-in Periksa plug-in
Cancel Plug-in Run Batalkan jalannya plug-in
Rectangular Grid Tool Options Opsi alat kisi persegi panjang
Polar Grid Tool Options Opsi alat kisi polar
Fill Line Isi garis
Can't render this file because it contains an unexpected character in line 3820 and column 19.
+3
View File
@@ -5080,3 +5080,6 @@ axis Y Axis Asse Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` un effetto dinamico viene eseguito mentre il documento viene disegnato, quindi non può essere `heavy`
@msg an exporter always gets an export's budget: leave `budget` out un plug-in di esportazione riceve sempre il budget di un’esportazione: ometti `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” chiede un budget di calcolo elevato (ogni esecuzione fino a {minutes} minuti e {times} volte le istruzioni consuete): installalo con `allowHeavy: true` quando l’utente acconsente
Rectangular Grid Tool Options Opzioni strumento Griglia rettangolare
Polar Grid Tool Options Opzioni strumento Griglia polare
Fill Line Riempi linea
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5142,3 +5142,6 @@ axis Y Axis Y 軸
@msg a live effect runs while the document is drawn, so it can't be `heavy` ライブ効果はドキュメントの描画中に実行されるため、`heavy` にはできません
@msg an exporter always gets an export's budget: leave `budget` out 書き出しプラグインには常に書き出しの予算が割り当てられます。`budget` は省略してください
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees 「{_1}」は大きな計算予算を求めています (1 回の実行につき最大 {minutes} 分、通常の {times} 倍の命令)。ユーザーが同意したら `allowHeavy: true` を指定してインストールしてください
Rectangular Grid Tool Options 長方形グリッドツールオプション
Polar Grid Tool Options 同心円グリッドツールオプション
Fill Line 線を塗りつぶす
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5379,3 +5379,6 @@ axis Y Axis Y ღერძი
@msg a live effect runs while the document is drawn, so it can't be `heavy` ცოცხალი ეფექტი დოკუმენტის დახატვისას ეშვება, ამიტომ ის ვერ იქნება `heavy`
@msg an exporter always gets an export's budget: leave `budget` out ექსპორტის პლაგინი ყოველთვის ექსპორტის ბიუჯეტს იღებს: გამოტოვეთ `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees „{_1}“ დიდ გამოთვლით ბიუჯეტს ითხოვს (თითო გაშვება {minutes} წუთამდე და ჩვეულებრივზე {times}-ჯერ მეტი ინსტრუქცია): მომხმარებლის თანხმობის შემდეგ დააყენეთ ის `allowHeavy: true` პარამეტრით
Rectangular Grid Tool Options მართკუთხა ბადის ხელსაწყოს პარამეტრები
Polar Grid Tool Options პოლარული ბადის ხელსაწყოს პარამეტრები
Fill Line ხაზის შევსება
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5043,3 +5043,6 @@ axis Y Axis Y축
@msg a live effect runs while the document is drawn, so it can't be `heavy` 라이브 효과는 문서를 그리는 동안 실행되므로 `heavy`일 수 없습니다
@msg an exporter always gets an export's budget: leave `budget` out 내보내기 플러그인은 항상 내보내기 예산을 받습니다. `budget`은 생략하세요
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}”은(는) 큰 계산 예산을 요청합니다(실행당 최대 {minutes}분, 평소의 {times}배 명령어). 사용자가 동의하면 `allowHeavy: true`로 설치하세요
Rectangular Grid Tool Options 사각형 격자 도구 옵션
Polar Grid Tool Options 극좌표 격자 도구 옵션
Fill Line 선 채우기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5471,3 +5471,6 @@ axis Y Axis Y-as
The plug-in is computing… De plug-in is aan het rekenen…
Inspect Plug-in Plug-in inspecteren
Cancel Plug-in Run Plug-in-run annuleren
Rectangular Grid Tool Options Opties rechthoekig-rastergereedschap
Polar Grid Tool Options Opties polair-rastergereedschap
Fill Line Lijn vullen
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5315,3 +5315,6 @@ axis Y Axis Oś Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` efekt na żywo działa podczas rysowania dokumentu, więc nie może być `heavy`
@msg an exporter always gets an export's budget: leave `budget` out wtyczka eksportu zawsze dostaje budżet eksportu: pomiń `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees „{_1}” prosi o duży budżet obliczeń (każde uruchomienie do {minutes} minut i {times} razy więcej instrukcji niż zwykle): zainstaluj wtyczkę z `allowHeavy: true`, gdy użytkownik się zgodzi
Rectangular Grid Tool Options Opcje narzędzia Siatka prostokątna
Polar Grid Tool Options Opcje narzędzia Siatka biegunowa
Fill Line Wypełnij odcinek
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5380,3 +5380,6 @@ axis Y Axis Eixo Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` um efeito dinâmico é executado enquanto o documento é desenhado, então não pode ser `heavy`
@msg an exporter always gets an export's budget: leave `budget` out um plug-in de exportação sempre recebe o orçamento de uma exportação: omita `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” pede um orçamento de processamento grande (cada execução até {minutes} minutos e {times} vezes as instruções habituais): instale-o com `allowHeavy: true` depois que o usuário concordar
Rectangular Grid Tool Options Opções da Ferramenta Grade Retangular
Polar Grid Tool Options Opções da Ferramenta Grade Polar
Fill Line Preencher linha
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -5076,3 +5076,6 @@ axis Y Axis Ось Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` динамический эффект выполняется во время отрисовки документа, поэтому он не может быть `heavy`
@msg an exporter always gets an export's budget: leave `budget` out модуль экспорта всегда получает бюджет экспорта: не указывайте `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees «{_1}» запрашивает большой вычислительный бюджет (каждый запуск до {minutes} минут и в {times} раз больше инструкций, чем обычно): установите его с `allowHeavy: true`, когда пользователь согласится
Rectangular Grid Tool Options Параметры инструмента «Прямоугольная сетка»
Polar Grid Tool Options Параметры инструмента «Полярная сетка»
Fill Line Заливка отрезка
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5419,3 +5419,6 @@ axis Y Axis Y-axel
@msg a live effect runs while the document is drawn, so it can't be `heavy` en live-effekt körs medan dokumentet ritas, så den kan inte vara `heavy`
@msg an exporter always gets an export's budget: leave `budget` out ett exportinsticksprogram får alltid en exports budget: utelämna `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees ”{_1}” begär en stor beräkningsbudget (varje körning upp till {minutes} minuter och {times} gånger de vanliga instruktionerna): installera det med `allowHeavy: true` när användaren har godkänt
Rectangular Grid Tool Options Alternativ för verktyget för rektangulärt rutnät
Polar Grid Tool Options Alternativ för verktyget för polärt rutnät
Fill Line Fyll linje
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -5426,3 +5426,6 @@ axis Y Axis Y ekseni
@msg a live effect runs while the document is drawn, so it can't be `heavy` canlı efekt belge çizilirken çalışır, bu yüzden `heavy` olamaz
@msg an exporter always gets an export's budget: leave `budget` out dışa aktarma eklentisi her zaman bir dışa aktarmanın bütçesini alır: `budget` anahtarını çıkarın
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” büyük bir hesaplama bütçesi istiyor (her çalıştırma en fazla {minutes} dakika ve olağan komutların {times} katı): kullanıcı onayladıktan sonra `allowHeavy: true` ile yükleyin
Rectangular Grid Tool Options Dikdörtgen Izgara Aracı Seçenekleri
Polar Grid Tool Options Yuvarlak Izgara Aracı Seçenekleri
Fill Line Çizgiyi doldur
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -5060,3 +5060,6 @@ axis Y Axis Вісь Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` динамічний ефект виконується під час відмальовування документа, тому він не може бути `heavy`
@msg an exporter always gets an export's budget: leave `budget` out модуль експорту завжди отримує бюджет експорту: не вказуйте `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees «{_1}» запитує великий обчислювальний бюджет (кожен запуск до {minutes} хвилин і в {times} разів більше інструкцій, ніж зазвичай): встановіть його з `allowHeavy: true`, коли користувач погодиться
Rectangular Grid Tool Options Параметри інструменту «Прямокутна сітка»
Polar Grid Tool Options Параметри інструменту «Полярна сітка»
Fill Line Заливка відрізка
Can't render this file because it contains an unexpected character in line 737 and column 40.
+3
View File
@@ -5060,3 +5060,6 @@ axis Y Axis Trục Y
@msg a live effect runs while the document is drawn, so it can't be `heavy` hiệu ứng trực tiếp chạy trong lúc tài liệu được vẽ nên không thể là `heavy`
@msg an exporter always gets an export's budget: leave `budget` out plug-in xuất luôn nhận ngân sách của một lần xuất: hãy bỏ `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}” yêu cầu ngân sách tính toán lớn (mỗi lần chạy tới {minutes} phút và gấp {times} lần số lệnh thông thường): hãy cài đặt với `allowHeavy: true` khi người dùng đồng ý
Rectangular Grid Tool Options Tùy chọn công cụ Lưới chữ nhật
Polar Grid Tool Options Tùy chọn công cụ Lưới cực
Fill Line Tô đoạn thẳng
Can't render this file because it contains an unexpected character in line 4034 and column 10.
+3
View File
@@ -5303,3 +5303,6 @@ axis Y Axis Y 轴
@msg a live effect runs while the document is drawn, so it can't be `heavy` 实时效果在绘制文档时运行,因此不能为 `heavy`
@msg an exporter always gets an export's budget: leave `budget` out 导出增效工具始终使用导出的预算:请省略 `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees “{_1}”需要大量计算预算(每次运行最多 {minutes} 分钟,指令数为通常的 {times} 倍):请在用户同意后使用 `allowHeavy: true` 安装
Rectangular Grid Tool Options 矩形网格工具选项
Polar Grid Tool Options 极坐标网格工具选项
Fill Line 填充线段
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5099,3 +5099,6 @@ axis Y Axis Y 軸
@msg a live effect runs while the document is drawn, so it can't be `heavy` 即時效果會在繪製文件時執行,因此不能是 `heavy`
@msg an exporter always gets an export's budget: leave `budget` out 匯出增效模組一律使用匯出的預算:請移除 `budget`
@msg “{_1}” asks for a heavy budget (each run up to {minutes} minutes and {times} times the usual instructions): install it with `allowHeavy: true` once the user agrees 「{_1}」需要大量運算預算(每次執行最多 {minutes} 分鐘,指令數為一般的 {times} 倍):請在使用者同意後以 `allowHeavy: true` 安裝
Rectangular Grid Tool Options 矩形格線工具選項
Polar Grid Tool Options 極座標格點工具選項
Fill Line 填色線段
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -596,7 +596,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"tool.options",
"Tool Options…",
"",
"{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x and y, draws one there with them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (pt), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), size (pt); OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values}), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values})",
"{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x and y, draws one there with them), lineSegment|arc|rectangularGrid|polarGrid → that tool's Tool Options, its click dialog without x and y (dialog kind = the tool id, `__toolOptions: true`; lineSegment: length (pt), angle (°), fillLine; arc: lengthX, lengthY (pt), reference (0–8), arcType: open|closed, baseAlong: x|y, slope (−100…100), fillArc; rectangularGrid: width, height (pt, the Default Size), reference (0–8), rows, columns, rowSkew, columnSkew (%), frame, fill; polarGrid: the same with concentric, radial, concentricSkew, radialSkew and compound for rows, columns, the skews and frame; OK runs tool.setOption {tool, values} and draws nothing: the next drag uses them (Fill Line, the arc's type, base, slope and fill, the grids' dividers, skews and switches) and a click's dialog opens at them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (pt), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), size (pt); OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values}), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values})",
),
(
"ui.colorGuideOptions",
+79 -1
View File
@@ -327,7 +327,8 @@ fn flyout_press(ui: &Ui, resp: &egui::Response, rect: egui::Rect) -> Option<Flyo
/// tool's fits the artboard in the window, the Zoom tool's shows it at 100%, the Rotate, Scale,
/// Reflect and Shear tools' are their Object › Transform dialogs, the selection tools' are the
/// Move dialog, and the Pencil, Paintbrush, Smooth, Blob Brush, Eraser and Shape Builder tools' are
/// their Tool Options.
/// their Tool Options, as are the Line Segment, Arc, Rectangular Grid and Polar Grid tools'
/// ([`crate::dialogs::shapes::open_options`]).
pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::Value, String> {
match tool {
"dimension" => crate::dialogs::dimension::open(app),
@@ -359,6 +360,9 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
Ok(json!({ "dialog": crate::dialogs::perspective_options::KIND }))
}
"shapeBuilder" => Ok(crate::dialogs::shape_builder_options::open(app)),
// The Line Segment, Arc and grid tools: their Tool Options, the options of their next
// drag and click.
_ if crate::dialogs::shapes::OPTION_TOOLS.contains(&tool) => crate::dialogs::shapes::open_options(app, tool),
// The Flare tool: its options, which draw the next flare (OK draws none).
"flare" => Ok(crate::dialogs::flare_options::open(app, None)),
// A double click on the Print Tiling tool puts the pages back where the placement puts them.
@@ -1061,6 +1065,80 @@ pub(crate) mod tests {
}
}
/// #2691: a double-click on the Line Segment, Arc, Rectangular Grid or Polar Grid tool opens
/// its Tool Options (the Rectangular Grid and Arc help pages, step 3). Cancel leaves the next
/// drag as it was; OK draws nothing and the next drag, and a click's dialog, use the options.
#[test]
fn double_clicking_the_line_arc_and_grid_tools_opens_their_tool_options() {
// (tool, the option changed, its new value, the drag's command, the param it shows in)
let cases = [
("lineSegment", "fillLine", json!(true), "shape.line", "fill"),
("arc", "slope", json!(-30.0), "shape.arc", "slope"),
("rectangularGrid", "rows", json!(2), "shape.rectangularGrid", "rows"),
("polarGrid", "radial", json!(3), "shape.polarGrid", "radial"),
];
let objects =
|app: &VectorcraftApp| app.session.active().unwrap().doc.layers.iter().map(|l| l.children().map_or(0, |c| c.len())).sum::<usize>();
let drag = |app: &mut VectorcraftApp| {
let view = app.view_info();
for (kind, x, y) in [
(vectorcraft_tools::PointerKind::Down, 50.0, 50.0),
(vectorcraft_tools::PointerKind::Drag, 150.0, 120.0),
(vectorcraft_tools::PointerKind::Up, 150.0, 120.0),
] {
app.session.pointer(&vectorcraft_tools::PointerEvent::new(kind, x, y), view).unwrap();
}
app.session.journal.last().cloned().expect("the drag's command")
};
for (tool, key, value, cmd, param) in cases {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.ui.toolbar_advanced = true;
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
app.select_tool(tool);
let index = slots(&app).iter().position(|(_, tools)| tools.contains(&tool)).expect("a toolbar slot");
let ctx = egui::Context::default();
let at = frame_in(&mut app, &ctx, 0.0, vec![], 2000.0)[index].center();
let b = |pressed| Event::PointerButton { pos: at, button: PointerButton::Primary, pressed, modifiers: Default::default() };
frame_in(&mut app, &ctx, 1.0, vec![Event::PointerMoved(at), b(true), b(false), b(true), b(false)], 2000.0);
let d = app.ui.dialog.clone().unwrap_or_else(|| panic!("{tool}: Tool Options"));
assert_eq!((d.kind.as_str(), d.bool("__toolOptions")), (tool, true));
assert!(!d.fields.contains_key("x"), "{tool}: no click to draw at");
let text = crate::tests_labels::painted_text(&mut app, |app, ui| crate::dialogs::show(app, ui.ctx()));
let title = match tool {
"lineSegment" => "Line Segment Tool Options",
"arc" => "Arc Segment Tool Options",
"rectangularGrid" => "Rectangular Grid Tool Options",
_ => "Polar Grid Tool Options",
};
assert!(text.contains(title) && !text.contains("Reference:"), "{tool}: {text}");
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert(key.into(), value.clone());
crate::dialogs::cancel(&mut app);
let (c, p) = drag(&mut app);
assert_eq!(c, cmd);
assert_ne!(p.get(param), Some(&value), "{tool}: Cancel changes nothing");
assert_eq!(objects(&app), 1);
app.run("tool.options", json!({ "tool": tool })).unwrap();
let d = app.ui.dialog.as_mut().unwrap();
d.fields.insert(key.into(), value.clone());
if tool.ends_with("Grid") {
d.fields.insert("width".into(), json!("150 pt"));
}
crate::dialogs::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
assert_eq!(objects(&app), 1, "{tool}: OK draws nothing");
let (c, p) = drag(&mut app);
assert_eq!((c.as_str(), &p[param]), (cmd, &value), "{tool}: the next drag uses the option");
// A click's dialog opens at them too, a grid at its Default Size.
crate::dialogs::open_tool_dialog(&mut app, tool, json!({"x": 100, "y": 100}));
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!(d.fields[key], value, "{tool}");
if tool.ends_with("Grid") {
assert_eq!(d.f64("width", 0.0), 150.0);
}
}
}
#[test]
fn double_clicking_the_hand_zoom_and_transform_tools() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
+5 -2
View File
@@ -182,8 +182,11 @@ Transform (dialog kind = the tool id), or fails with `nothing selected`. For `se
drag or Move (`dx`, `dy`) and with the `copy`, `objects` and `preview` it was last confirmed with (its `patterns` is
the `transformPatternTiles` preference, which its OK sets). `ui.key` Enter with one of those seven
tools opens its dialog too (nothing happens without a selection), with the transform tools' `origin` at their reference
point. Gradient tool handles snap to anchors, edges and smart guides; Shift constrains them to 45° steps from the
`constrainAngle` preference.
point. For `lineSegment`, `arc`, `rectangularGrid` and `polarGrid` it opens the tool's Tool Options: the dialog its
click opens (kind = the tool id), without `x` and `y` and with `__toolOptions: true`, whose `ui.dialog.confirm` runs
`tool.setOption {tool, values}` and draws nothing; the next drag with the tool and its click dialog use the values
(the line's `fillLine`, the grids' Default Size `width`, `height` and `reference` too). Gradient tool handles snap to
anchors, edges and smart guides; Shift constrains them to 45° steps from the `constrainAngle` preference.
Effect dialogs: `engine.execute {command: "effect.dialog", params: {effect, index?, item?}}` opens the `effect` dialog
(fields: the effect's parameters, `preview`). With `index` it edits that applied effect of `item` (null: the object's
+3 -1
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4 -2
View File
File diff suppressed because one or more lines are too long