Merge pull request #2557 from dotCooCoo/feat/shape-builder-options

Shape Builder: double-clicking the tool opens Shape Builder Tool Options
This commit is contained in:
BFlat
2026-10-21 17:34:15 +07:00
committed by GitHub
33 changed files with 707 additions and 31 deletions
+18 -6
View File
@@ -39,7 +39,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Shape Builder",
[],
None,
"{ids?|id?: [id…] (default: selection), points: [[x,y]…] (drag path; one point = click), erase?: bool (erase mode, Alt: deletes the regions touched, and the pieces of unfilled open paths and the edges (outline pieces between the points where paths meet; see shapeBuilder.regions) within tolerance; a deleted edge opens its path there, and edges bounding a deleted region go with it), tolerance?: pt (2, erase only), fill?: \"#rrggbb\" | {color|swatch|gradient|none} (default: the fill of the object under the first point; the current fill for an area only lines enclose)} → {ids, merged, regions, lines, edges} (counts deleted or merged)",
"{ids?|id?: [id…] (default: selection), points: [[x,y]…] (drag path; one point = click), erase?: bool (erase mode, Alt: deletes the regions touched, and the pieces of open paths that cut regions and the edges (outline pieces between the points where paths meet; see shapeBuilder.regions) within tolerance; a deleted edge opens its path there, and edges bounding a deleted region go with it), tolerance?: pt (2, erase only), openFilledAsClosed?: true (Consider Open Filled Paths as Closed: a filled open path bounds a region as if an invisible edge closed it; false: it cuts the regions it crosses like an unfilled one), pickColorFrom?: \"artwork\" (default: the merged shape takes the fill of the front-most object over the first region the drag touches that an object covers; the current fill when only lines enclose every region touched) | \"swatches\" (the current fill), fill?: \"#rrggbb\" | {color|swatch|gradient|none} (overrides pickColorFrom)} → {ids, merged, regions, lines, edges} (counts deleted or merged)",
has_selection_or_ids,
sb_merge
),
@@ -48,7 +48,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Shape Builder Regions",
[],
None,
"{ids?|id?} → {regions: [{bounds: [x0,y0,x1,y1], area, sources: [id…]}], lines: [{bounds, source: id}], edges: [{bounds, source: id}]} faces of the planar arrangement (unfilled open paths cut them; sources is empty for an area only they enclose), the pieces of those open paths and the edges (the other paths' outlines cut where paths meet; source: the front-most path along it), which erasing deletes",
"{ids?|id?, openFilledAsClosed?: true (as shapeBuilder.merge)} → {regions: [{bounds: [x0,y0,x1,y1], area, sources: [id…]}], lines: [{bounds, source: id}], edges: [{bounds, source: id}]} faces of the planar arrangement (unfilled open paths cut them, and filled ones too with openFilledAsClosed false; sources is empty for an area only they enclose), the pieces of those open paths and the edges (the other paths' outlines cut where paths meet; source: the front-most path along it), which erasing deletes",
has_selection_or_ids,
sb_regions
),
@@ -245,12 +245,24 @@ fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
let erase = bool_or(p, "erase", false);
// Line pieces and edges are only deleted (erase mode), never merged.
let piece_tol = erase.then(|| f64_or(p, "tolerance", 2.0).clamp(0.0, 1000.0));
let fill = fill_param(s, p, C)?;
let open_filled_as_closed = bool_or(p, "openFilledAsClosed", true);
let swatches = match p.get("pickColorFrom") {
None | Some(Value::Null) => false,
Some(v) => match v.as_str() {
Some("artwork") => false,
Some("swatches") => true,
_ => return Err(bad(C, format!("pickColorFrom is \"artwork\" or \"swatches\", not {v}"))),
},
};
let current_fill = s.paint.fill.clone();
let fill = match fill_param(s, p, C)? {
None if swatches => Some(current_fill.clone()),
f => f,
};
let roots = root_ids(s, p)?;
let label = if erase { "Shape Builder Delete" } else { "Shape Builder" };
let (ids, merged, touched) = s.edit(label, |d, sel| {
let (map, leaves) = BuilderMap::new(d, &roots, erase);
let (map, leaves) = BuilderMap::new(d, &roots, erase, open_filled_as_closed);
let leaves: Vec<Node> = leaves.into_iter().cloned().collect();
if map.shapes.is_empty() {
return Err(EngineError::Other("Shape Builder: select paths or compound paths".into()));
@@ -311,7 +323,7 @@ fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
} else {
b::node_outline(leaf).map_or_else(|| sh.path.clone(), |o| o.0)
};
let fill_closes = !leaf.appearance.fill_paint().is_none();
let fill_closes = open_filled_as_closed && !leaf.appearance.fill_paint().is_none();
let mut cut = false;
for piece in &cuts {
if let Some(r) = po::cut_out(&rest, piece, fill_closes, cut_tol) {
@@ -366,7 +378,7 @@ fn replace_roots(d: &mut Document, roots: &[NodeId], nodes: Vec<Node>) -> Result
fn sb_regions(s: &mut Session, p: &Value) -> Result<Value> {
let roots = root_ids(s, p)?;
let d = &s.doc()?.doc;
let (map, leaves) = BuilderMap::new(d, &roots, true);
let (map, leaves) = BuilderMap::new(d, &roots, true, bool_or(p, "openFilledAsClosed", true));
let id_of = |i: usize| leaves.get(i).map(|n| n.id.0);
let regions: Vec<Value> = map
.regions
+137 -1
View File
@@ -7,7 +7,7 @@ use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{AppearanceItem, ImageBlob, ImageObject, LineCap, LineJoin, Node, NodeKind};
use vectorcraft_geom::{Affine, FillRule, PathData, Rect};
use vectorcraft_testkit::pdf::{PdfPage, pdf};
use vectorcraft_tools::{Mods, PointerEvent, PointerKind};
use vectorcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
use super::*;
@@ -380,6 +380,142 @@ fn shape_builder_closes_filled_open_paths() {
assert_eq!(r["lines"].as_array().unwrap().len(), 1);
}
/// Consider Open Filled Paths as Closed off: a filled open path is only an edge, as an unfilled
/// one is, so it forms no region and a click inside it merges nothing.
#[test]
fn shape_builder_open_filled_paths_cut_without_open_filled_as_closed() {
let mut s = session();
let r = s.execute("path.create", &json!({"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 100, "y": 100}]})).unwrap();
let p = NodeId(r["id"].as_u64().unwrap());
set_fill(&mut s, p, Color::rgb(0.0, 1.0, 0.0));
s.execute("select.set", &json!({"ids": [p.0]})).unwrap();
let r = s.execute("shapeBuilder.regions", &json!({"openFilledAsClosed": false})).unwrap();
assert_eq!(r["regions"], json!([]));
assert_eq!(r["lines"].as_array().unwrap().len(), 1);
assert!(s.execute("shapeBuilder.merge", &json!({"points": [[75, 25]], "openFilledAsClosed": false})).is_err());
// On (the default), the click makes the area the fill closes a shape.
let r = s.execute("shapeBuilder.merge", &json!({"points": [[75, 25]]})).unwrap();
assert!((area(&s, NodeId(r["merged"].as_u64().unwrap())) - 5000.0).abs() < 1e-6);
}
/// Pick Color From Color Swatches: the merged shape takes the current fill, not the fill of the
/// object under the first point; setting the current fill without `ids` leaves the selection's.
#[test]
fn shape_builder_pick_color_from_swatches_fills_with_the_current_fill() {
let mut s = session();
let (a, _) = two(&mut s);
s.execute("paint.setFill", &json!({"color": "#00ff00", "ids": []})).unwrap();
assert_eq!(node(&s, a).appearance.fill_paint(), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), "A keeps its fill");
let green = s.paint.fill.clone();
let r = s.execute("shapeBuilder.merge", &json!({"points": [[25, 50], [75, 50]], "pickColorFrom": "swatches"})).unwrap();
assert_eq!(node(&s, NodeId(r["merged"].as_u64().unwrap())).appearance.fill_paint(), green);
s.execute("edit.undo", &json!({})).unwrap();
s.execute("select.all", &json!({})).unwrap();
let r = s.execute("shapeBuilder.merge", &json!({"points": [[25, 50], [75, 50]], "pickColorFrom": "artwork"})).unwrap();
assert_eq!(node(&s, NodeId(r["merged"].as_u64().unwrap())).appearance.fill_paint(), Paint::solid(Color::rgb(1.0, 0.0, 0.0)));
s.execute("edit.undo", &json!({})).unwrap();
s.execute("select.all", &json!({})).unwrap();
assert!(s.execute("shapeBuilder.merge", &json!({"points": [[25, 50]], "pickColorFrom": "layers"})).is_err());
}
/// The Shape Builder Tool Options reach what the tool does: Straight Line sends the drag's first
/// and last points, and the other options go with them to `shapeBuilder.merge`.
#[test]
fn shape_builder_tool_options_reach_the_command() {
let mut s = session();
two(&mut s);
let v = ViewInfo::default();
s.select_tool("shapeBuilder", v).unwrap();
let values = json!({"openFilledAsClosed": false, "pickColorFrom": "swatches", "selection": "straightLine"});
s.set_tool_option_cmd(&json!({"tool": "shapeBuilder", "values": values})).unwrap();
// A drag that bends up through (60, 20) on its way.
for (kind, x, y) in
[(PointerKind::Down, 25.0, 50.0), (PointerKind::Drag, 60.0, 20.0), (PointerKind::Drag, 75.0, 50.0), (PointerKind::Up, 75.0, 50.0)]
{
s.pointer(&PointerEvent::new(kind, x, y), v).unwrap();
}
let (cmd, p) = s.journal.last().unwrap().clone();
assert_eq!(cmd, "shapeBuilder.merge");
assert_eq!(
(&p["points"], &p["openFilledAsClosed"], &p["pickColorFrom"]),
(&json!([[25.0, 50.0], [75.0, 50.0]]), &json!(false), &json!("swatches"))
);
}
/// Highlight Fill off: hovering a region in merge mode highlights nothing; erase mode still shows
/// what a click would delete.
#[test]
fn shape_builder_highlight_fill_off_leaves_regions_unshaded() {
let mut s = session();
two(&mut s);
let v = ViewInfo::default();
s.select_tool("shapeBuilder", v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Move, 75.0, 50.0), v).unwrap();
assert!(!s.overlays(v).is_empty());
s.set_tool_option_cmd(&json!({"key": "highlightFill", "value": false})).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Move, 76.0, 50.0), v).unwrap();
assert!(s.overlays(v).is_empty());
let alt = Mods { alt: true, ..Default::default() };
s.pointer(&PointerEvent::new(PointerKind::Move, 75.0, 50.0).with_mods(alt), v).unwrap();
assert!(!s.overlays(v).is_empty());
}
/// Pick Color From Color Swatches: Left and Right step the current fill through the document's
/// color swatches (the selection keeps its fills), and Cursor Swatch Preview shows the current fill
/// between the swatches before and after it.
#[test]
fn shape_builder_arrow_keys_step_the_current_fill_through_the_swatches() {
let mut s = session();
let (a, _) = two(&mut s);
let v = ViewInfo::default();
s.select_tool("shapeBuilder", v).unwrap();
let chips = |s: &mut Session| s.overlays(v).iter().filter(|o| matches!(o, vectorcraft_tools::Overlay::Swatch { .. })).count();
assert!(!s.tool_claims_key(ToolKey::Right, v), "Artwork leaves the arrows to nudging");
let values = json!({"pickColorFrom": "swatches", "cursorSwatchPreview": true});
s.set_tool_option_cmd(&json!({"tool": "shapeBuilder", "values": values})).unwrap();
assert!(s.tool_claims_key(ToolKey::Right, v));
let (first, swatches): (String, Vec<Paint>) = {
let list = vectorcraft_tools::builder::color_swatches(&s.doc().unwrap().doc);
(list[0].name.clone(), list.iter().take(3).map(|sw| sw.paint.clone()).collect())
};
assert_eq!(swatches.len(), 3);
s.execute("paint.setFill", &json!({"swatch": first, "ids": []})).unwrap();
s.tool_key(ToolKey::Right, Mods::default(), v).unwrap();
s.tool_key(ToolKey::Right, Mods::default(), v).unwrap();
assert_eq!(s.paint.fill, swatches[2]);
s.tool_key(ToolKey::Left, Mods::default(), v).unwrap();
assert_eq!(s.paint.fill, swatches[1]);
assert_eq!(node(&s, a).appearance.fill_paint(), Paint::solid(Color::rgb(1.0, 0.0, 0.0)), "A keeps its fill");
s.pointer(&PointerEvent::new(PointerKind::Move, 75.0, 50.0), v).unwrap();
assert_eq!(chips(&mut s), 3);
s.set_tool_option_cmd(&json!({"key": "cursorSwatchPreview", "value": false})).unwrap();
assert_eq!(chips(&mut s), 0);
}
/// Two swatches of the same color next to each other: the arrow keys step through both and on to
/// the swatch after them.
#[test]
fn shape_builder_arrow_keys_pass_swatches_of_the_same_color() {
let mut s = session();
two(&mut s);
for name in ["Twin A", "Twin B"] {
s.execute("swatch.new", &json!({"name": name, "color": "#123456"})).unwrap();
}
let v = ViewInfo::default();
s.select_tool("shapeBuilder", v).unwrap();
s.set_tool_option_cmd(&json!({"tool": "shapeBuilder", "values": {"pickColorFrom": "swatches"}})).unwrap();
let names: Vec<String> = vectorcraft_tools::builder::color_swatches(&s.doc().unwrap().doc).iter().map(|sw| sw.name.clone()).collect();
let a = names.iter().position(|n| n == "Twin A").unwrap();
assert_eq!(names[a + 1], "Twin B");
s.execute("paint.setFill", &json!({"swatch": "Twin A", "ids": []})).unwrap();
let twin = s.paint.fill.clone();
s.tool_key(ToolKey::Right, Mods::default(), v).unwrap();
assert_eq!(s.paint.fill, twin, "Twin B");
s.tool_key(ToolKey::Right, Mods::default(), v).unwrap();
let after = vectorcraft_tools::builder::color_swatches(&s.doc().unwrap().doc)[(a + 2) % names.len()].paint.clone();
assert_eq!(s.paint.fill, after, "the swatch after Twin B");
}
#[test]
fn shape_builder_tool_alt_deletes_line_pieces() {
let mut s = session();
+188 -20
View File
@@ -14,11 +14,12 @@
use std::sync::Arc;
use serde_json::{Value, json};
use vectorcraft_color::{Paint, Swatch};
use vectorcraft_doc::{Document, Node, NodeId, NodeKind};
use vectorcraft_geom::{BezPath, FillRule, PathData, Point, Rect, Shape as _};
use vectorcraft_pathops as po;
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, json_ids};
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, json_ids};
/// Names starting with this mark a Live Paint group.
pub const LIVE_PAINT_PREFIX: &str = "Live Paint";
@@ -169,12 +170,13 @@ pub fn shapes_for<'a>(doc: &'a Document, roots: &[NodeId]) -> (Vec<po::Shape>, V
}
/// Shape Builder inputs: [`shapes_for`], with the open subpaths of filled paths closed where that
/// encloses an area (the fill bounds a region, as if an invisible edge closed it). Unfilled open
/// paths and straight lines stay open and cut the regions they cross.
fn shape_builder_shapes<'a>(doc: &'a Document, roots: &[NodeId]) -> (Vec<po::Shape>, Vec<&'a Node>) {
/// encloses an area (the fill bounds a region, as if an invisible edge closed it) when
/// `open_filled_as_closed`. Unfilled open paths and straight lines stay open and cut the regions
/// they cross, and so do filled open paths without `open_filled_as_closed`.
fn shape_builder_shapes<'a>(doc: &'a Document, roots: &[NodeId], open_filled_as_closed: bool) -> (Vec<po::Shape>, Vec<&'a Node>) {
let (mut shapes, leaves) = shapes_for(doc, roots);
for (s, l) in shapes.iter_mut().zip(&leaves) {
if l.appearance.fill_paint().is_none() {
if !open_filled_as_closed || l.appearance.fill_paint().is_none() {
continue;
}
for sp in s.path.subpaths.iter_mut().filter(|sp| !sp.closed) {
@@ -204,7 +206,8 @@ pub struct BuilderMap {
/// The inputs, back → front (see [`po::shape_builder`]).
pub shapes: Vec<po::Shape>,
pub regions: Vec<Face>,
/// Pieces of the unfilled open paths, keyed by input index, with their bounds.
/// Pieces of the open paths that cut regions (see [`BuilderMap::new`]), keyed by input index,
/// with their bounds.
pub lines: Vec<(po::Shape, Rect)>,
/// Pieces of the other inputs' outlines (cut wherever paths meet), keyed by input index, with
/// their bounds: the edges erasing deletes. Only in a map made `with_edges`.
@@ -236,9 +239,11 @@ fn piece_near(pieces: &[(po::Shape, Rect)], p: Point, tol: f64) -> Option<(f64,
impl BuilderMap {
/// The map of `roots` (sorted back → front), with the leaves the inputs came from; the outline
/// edges only `with_edges` (they cost a planar map of their own when no open path is in).
pub fn new<'a>(doc: &'a Document, roots: &[NodeId], with_edges: bool) -> (Self, Vec<&'a Node>) {
let (shapes, leaves) = shape_builder_shapes(doc, roots);
/// edges only `with_edges` (they cost a planar map of their own when no open path is in). With
/// `open_filled_as_closed` (Consider Open Filled Paths as Closed), a filled open path bounds a
/// region as if an invisible edge closed it; without it, the path cuts like a line.
pub fn new<'a>(doc: &'a Document, roots: &[NodeId], with_edges: bool, open_filled_as_closed: bool) -> (Self, Vec<&'a Node>) {
let (shapes, leaves) = shape_builder_shapes(doc, roots, open_filled_as_closed);
if shapes.is_empty() {
return (Self::default(), leaves);
}
@@ -417,10 +422,11 @@ pub fn hatch(path: &BezPath, gap: f64) -> BezPath {
out
}
/// The planar map of the current selection, cached by document identity and selection.
/// The planar map of the current selection, cached by document identity, selection and whether
/// filled open paths count as closed.
#[derive(Default)]
struct RegionCache {
key: Option<(usize, Vec<NodeId>)>,
key: Option<(usize, Vec<NodeId>, bool)>,
map: BuilderMap,
/// Live Paint faces (every path an edge, filled or not) rather than Shape Builder regions.
live: bool,
@@ -432,9 +438,10 @@ impl RegionCache {
fn live_paint() -> Self {
Self { live: true, ..Default::default() }
}
/// The map, with the outline edges when `edges` (built the first time they're wanted).
fn get(&mut self, cx: &ToolContext, edges: bool) -> &BuilderMap {
let key = (cx.doc as *const Document as usize, cx.selection.objects.clone());
/// The map, with the outline edges when `edges` (built the first time they're wanted), and
/// filled open paths closed when `open_filled_as_closed` (Shape Builder only).
fn get(&mut self, cx: &ToolContext, edges: bool, open_filled_as_closed: bool) -> &BuilderMap {
let key = (cx.doc as *const Document as usize, cx.selection.objects.clone(), open_filled_as_closed);
if self.key.as_ref() != Some(&key) || (edges && !self.edges) {
let roots = sorted_roots(cx.doc, &cx.selection.objects);
self.map = if self.live {
@@ -442,7 +449,7 @@ impl RegionCache {
let regions = if shapes.is_empty() { vec![] } else { faces_of(po::live_paint(&shapes).0) };
BuilderMap { regions, ..Default::default() }
} else {
BuilderMap::new(cx.doc, &roots, edges).0
BuilderMap::new(cx.doc, &roots, edges, open_filled_as_closed).0
};
self.key = Some(key);
self.edges = edges;
@@ -450,16 +457,79 @@ impl RegionCache {
&self.map
}
fn at(&mut self, cx: &ToolContext, p: Point) -> Option<usize> {
self.get(cx, false).region_at(p)
self.get(cx, false, true).region_at(p)
}
}
// ---------- Shape Builder ----------
/// Shape Builder Tool Options (double-click the tool; `tool.setOption`), kept across tool switches
/// and saved with the preferences.
#[derive(Clone, Debug, PartialEq)]
pub struct ShapeBuilderOptions {
/// Consider Open Filled Paths as Closed (`openFilledAsClosed`, on): a filled open path bounds a
/// region as if an invisible edge closed it. When it is off, the path cuts the regions it crosses
/// as a line does.
pub open_filled_as_closed: bool,
/// Pick Color From Color Swatches (`pickColorFrom: "swatches"`; off: `"artwork"`): a merge
/// fills the new shape with the current fill, and the Left and Right arrow keys step the current
/// fill through the document's color swatches. With Artwork, the shape takes the look of the
/// art it is merged from.
pub swatches: bool,
/// Cursor Swatch Preview (`cursorSwatchPreview`, off): with Color Swatches, the current fill
/// and the swatches before and after it show as chips above the pointer while no drag is on.
pub cursor_swatch_preview: bool,
/// Selection Straight Line (`selection: "straightLine"`; off: `"freeform"`): a drag touches
/// what the straight line from where it started to the pointer crosses, not what the pointer
/// passed over.
pub straight_line: bool,
/// Highlight Fill (`highlightFill`, on): in merge mode, the regions under the pointer or
/// touched by the drag are shaded with a gray mesh. When it is off, they are not highlighted.
pub highlight_fill: bool,
}
impl Default for ShapeBuilderOptions {
fn default() -> Self {
Self { open_filled_as_closed: true, swatches: false, cursor_swatch_preview: false, straight_line: false, highlight_fill: true }
}
}
/// The document's color swatches in the Swatches panel's order (the swatches outside groups, then
/// each group's): the solid colors, without None.
pub fn color_swatches(doc: &Document) -> Vec<&Swatch> {
let groups = doc.swatch_groups.iter().flat_map(|g| &g.swatches);
doc.swatches.iter().chain(groups).filter(|s| matches!(s.paint, Paint::Solid { .. })).collect()
}
/// Is `fill` swatch `s`: linked to it by name, or the same paint?
fn is_swatch(s: &Swatch, fill: &Paint) -> bool {
s.paint == *fill || matches!(fill, Paint::Solid { swatch: Some(name), tint, .. } if *tint >= 1.0 && *name == s.name)
}
/// The index in `list` of the swatch `fill` is: `last` (the swatch picked last) while `fill` is still
/// that swatch, so swatches of the same color are each reached in turn; otherwise the first swatch
/// it is.
fn swatch_index(list: &[&Swatch], fill: &Paint, last: Option<usize>) -> Option<usize> {
last.filter(|&i| list.get(i).is_some_and(|s| is_swatch(s, fill))).or_else(|| list.iter().position(|s| is_swatch(s, fill)))
}
/// The index of the swatch of `list` after (`forward`) or before the one `fill` is (see
/// [`swatch_index`]), wrapping around; the first or the last one when `fill` is none of them.
fn step_swatch(list: &[&Swatch], fill: &Paint, last: Option<usize>, forward: bool) -> Option<usize> {
let n = list.len();
match (swatch_index(list, fill, last), forward) {
(Some(i), true) => Some((i + 1) % n),
(Some(i), false) => Some((i + n - 1) % n),
(None, true) => (n > 0).then_some(0),
(None, false) => n.checked_sub(1),
}
}
/// Shape Builder tool: drag across regions to merge them. Holding Alt (Option) switches to erase
/// mode while it is held: the pointer shows a minus, what a click or drag would delete highlights in
/// red, and the gesture deletes the regions it touches, the pieces of open paths and the edges
/// (outline pieces between the points where paths meet) it passes over.
/// (outline pieces between the points where paths meet) it passes over. Its options are
/// [`ShapeBuilderOptions`].
#[derive(Default)]
pub struct ShapeBuilderTool {
cache: RegionCache,
@@ -469,6 +539,11 @@ pub struct ShapeBuilderTool {
dragging: bool,
/// Erase mode: Alt held while hovering, or when the drag started.
alt: bool,
/// Where the pointer last moved (the cursor swatch preview sits above it).
at: Option<Point>,
/// The index in [`color_swatches`] of the swatch the arrow keys picked last.
picked: Option<usize>,
opts: ShapeBuilderOptions,
}
/// How close (screen px) the pointer must come to a line piece or an edge to delete it.
@@ -485,11 +560,43 @@ impl ShapeBuilderTool {
None => vec![to],
};
let tol = Self::line_tol(cx, self.alt);
let map = self.cache.get(cx, self.alt);
let map = self.cache.get(cx, self.alt, self.opts.open_filled_as_closed);
for p in pts {
self.touched.add(map.hit(p, tol));
}
}
/// Selection Straight Line: the drag touches what the line from its first point to `p` crosses.
fn straight_to(&mut self, cx: &ToolContext, p: Point) {
let Some(&start) = self.points.first() else { return };
self.touched = Touched::default();
if start == p {
self.points = vec![start];
self.touch(cx, None, start);
} else {
self.points = vec![start, p];
self.touch(cx, Some(start), p);
}
}
/// The cursor swatch preview: the swatch before the current fill, the current fill (ringed)
/// and the swatch after it, side by side above the pointer.
fn swatch_chips(&self, cx: &ToolContext) -> Vec<Overlay> {
let Some(at) = self.at else { return vec![] };
let list = color_swatches(cx.doc);
let fill = &cx.paint.fill;
let rgba = |p: &Paint| match p {
Paint::Solid { color, .. } => Some(color.to_rgba8(1.0)),
_ => None,
};
let chip = |forward| step_swatch(&list, fill, self.picked, forward).and_then(|i| rgba(&list.get(i)?.paint));
let (before, after) = (chip(false), chip(true));
let y = at.y - cx.tol(22.0);
[(before, -16.0, false), (rgba(fill), 0.0, true), (after, 16.0, false)]
.into_iter()
.filter_map(|(color, dx, selected)| Some(Overlay::Swatch { p: Point::new(at.x + cx.tol(dx), y), color: color?, selected }))
.collect()
}
}
impl Tool for ShapeBuilderTool {
@@ -502,7 +609,8 @@ impl Tool for ShapeBuilderTool {
match ev.kind {
PointerKind::Move => {
self.alt = ev.mods.alt;
self.hover = self.cache.get(cx, self.alt).hit(p, Self::line_tol(cx, self.alt));
self.at = Some(p);
self.hover = self.cache.get(cx, self.alt, self.opts.open_filled_as_closed).hit(p, Self::line_tol(cx, self.alt));
vec![]
}
PointerKind::Down => {
@@ -517,6 +625,11 @@ impl Tool for ShapeBuilderTool {
vec![]
}
PointerKind::Drag if self.dragging => {
self.at = Some(p);
if self.opts.straight_line {
self.straight_to(cx, p);
return vec![];
}
let last = self.points.last().copied();
if last.is_none_or(|q| q.distance(p) >= cx.tol(1.0)) {
self.touch(cx, last, p);
@@ -526,7 +639,9 @@ impl Tool for ShapeBuilderTool {
}
PointerKind::Up if self.dragging => {
self.dragging = false;
if self.points.last().is_none_or(|q| *q != p) {
if self.opts.straight_line {
self.straight_to(cx, p);
} else if self.points.last().is_none_or(|q| *q != p) {
let last = self.points.last().copied();
self.touch(cx, last, p);
self.points.push(p);
@@ -544,6 +659,12 @@ impl Tool for ShapeBuilderTool {
if let Some(t) = Self::line_tol(cx, erase) {
params["tolerance"] = json!(t);
}
if !self.opts.open_filled_as_closed {
params["openFilledAsClosed"] = json!(false);
}
if self.opts.swatches {
params["pickColorFrom"] = json!("swatches");
}
vec![Action::Exec("shapeBuilder.merge".into(), params)]
}
_ => vec![],
@@ -563,6 +684,8 @@ impl Tool for ShapeBuilderTool {
};
for hit in show {
match hit {
// Highlight Fill off: merge mode leaves the regions as they are.
Hit::Region(_) if !self.alt && !self.opts.highlight_fill => {}
Hit::Region(i) => {
if let Some((_, bp, _)) = map.regions.get(i) {
out.push(Overlay::Path { path: hatch(bp, cx.tol(4.0)), color, width: 1.0, dashed: false });
@@ -585,6 +708,9 @@ impl Tool for ShapeBuilderTool {
}
out.push(Overlay::Path { path: bp, color, width: 1.0, dashed: false });
}
if self.opts.swatches && self.opts.cursor_swatch_preview && !self.dragging {
out.extend(self.swatch_chips(cx));
}
out
}
@@ -597,6 +723,48 @@ impl Tool for ShapeBuilderTool {
fn busy(&self) -> bool {
self.dragging
}
/// With Pick Color From Color Swatches, the Left and Right arrow keys pick the color swatch
/// before or after the current fill.
fn claims_key(&self, _cx: &ToolContext, key: ToolKey) -> bool {
self.opts.swatches && !self.dragging && matches!(key, ToolKey::Left | ToolKey::Right)
}
fn key(&mut self, cx: &ToolContext, key: ToolKey, _mods: Mods) -> Vec<Action> {
if !self.claims_key(cx, key) {
return vec![];
}
let list = color_swatches(cx.doc);
let Some((i, s)) = step_swatch(&list, &cx.paint.fill, self.picked, key == ToolKey::Right).and_then(|i| Some((i, list.get(i)?))) else {
return vec![];
};
self.picked = Some(i);
// The current fill only (no `ids`): the selected art keeps its own.
vec![Action::Exec("paint.setFill".into(), json!({ "swatch": s.name, "ids": [] }))]
}
fn options(&self) -> Value {
let o = &self.opts;
json!({
"openFilledAsClosed": o.open_filled_as_closed,
"pickColorFrom": if o.swatches { "swatches" } else { "artwork" },
"cursorSwatchPreview": o.cursor_swatch_preview,
"selection": if o.straight_line { "straightLine" } else { "freeform" },
"highlightFill": o.highlight_fill,
})
}
fn set_option(&mut self, key: &str, v: &Value) {
let o = &mut self.opts;
match key {
"openFilledAsClosed" => o.open_filled_as_closed = v.as_bool().unwrap_or(o.open_filled_as_closed),
"pickColorFrom" => o.swatches = v.as_str().map_or(o.swatches, |s| s == "swatches"),
"cursorSwatchPreview" => o.cursor_swatch_preview = v.as_bool().unwrap_or(o.cursor_swatch_preview),
"selection" => o.straight_line = v.as_str().map_or(o.straight_line, |s| s == "straightLine"),
"highlightFill" => o.highlight_fill = v.as_bool().unwrap_or(o.highlight_fill),
_ => {}
}
}
}
// ---------- Live Paint Bucket ----------
+2
View File
@@ -41,6 +41,8 @@ const ROWS: &[Row] = &[
(&["polygon"], None, &["sides"]),
(&["star"], None, &["points"]),
(&["freeTransform"], None, &["constrain"]),
// Shape Builder Tool Options (`builder::ShapeBuilderOptions`).
(&["shapeBuilder"], None, &["openFilledAsClosed", "pickColorFrom", "cursorSwatchPreview", "selection", "highlightFill"]),
(&["artboard"], None, &["moveArt", "scaleArt"]),
// Flare Tool Options (`extra::FLARE_OPTIONS` and the Rays and Rings checkboxes).
(
+2
View File
@@ -87,6 +87,7 @@ mod save_pdf;
pub mod save_style_library;
pub mod save_swatch_library;
pub mod script_check;
pub mod shape_builder_options;
mod shapes;
pub mod shortcut_import;
pub(crate) mod simplify_slider;
@@ -332,6 +333,7 @@ registry! {
Envelope: [envelope::WARP, envelope::MESH, envelope::OPTIONS] => envelope::SPEC,
LiquifyOptions: [liquify::KIND] => liquify::SPEC,
FreehandOptions: [freehand::KIND] => freehand::SPEC,
ShapeBuilderOptions: [shape_builder_options::KIND] => shape_builder_options::SPEC,
FlareOptions: [flare_options::KIND] => flare_options::SPEC,
SymbolOptions: [symbol_options::KIND] => symbol_options::SPEC,
SymbolismOptions: [symbolism_options::KIND] => symbolism_options::SPEC,
@@ -0,0 +1,169 @@
//! Shape Builder Tool Options (double-click the Shape Builder tool): the options the tool keeps,
//! which last across tool switches and are saved with the preferences. OK sets them with
//! `tool.setOption {tool: "shapeBuilder", values}`.
//!
//! Fields: `openFilledAsClosed` (Consider Open Filled Paths as Closed), `pickColorFrom`
//! (`artwork` or `swatches`), `cursorSwatchPreview` (with `swatches`), `selection` (`freeform` or
//! `straightLine`) and `highlightFill` (Highlight: Fill).
use serde_json::{Map, Value, json};
use super::{DialogSpec, form, run_and_close};
use crate::state::Dialog;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of Shape Builder Tool Options.
pub const KIND: &str = "shapeBuilderOptions";
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_width: 360.0, ..DialogSpec::FORM };
/// The options the dialog edits.
const KEYS: [&str; 5] = ["openFilledAsClosed", "pickColorFrom", "cursorSwatchPreview", "selection", "highlightFill"];
/// Width of the label column.
const LABEL_W: f32 = 110.0;
fn heading(_: &Dialog) -> String {
let label = vectorcraft_tools::tool_info("shapeBuilder").map_or("", |t| t.label);
crate::i18n::fmt(tl!("{tool} Options"), &[("tool", tl!(label))])
}
/// Open the Shape Builder's options with their current values.
pub fn open(app: &mut VectorcraftApp) -> Value {
let o = app.session.tool_options_of("shapeBuilder");
let fields: Map<String, Value> = KEYS.iter().filter_map(|key| Some((key.to_string(), o.get(*key)?.clone()))).collect();
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields });
json!({ "dialog": KIND })
}
fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
form::check(ui, d, "openFilledAsClosed", tl!("Consider Open Filled Paths as Closed"));
ui.add_space(8.0);
let sources = [("artwork", tl!("Artwork")), ("swatches", tl!("Color Swatches"))];
form::choice(ui, d, "pickColorFrom", tl!("Pick Color From:"), (LABEL_W, 160.0), &sources);
// Cursor Swatch Preview can be changed with Color Swatches only.
let preview = d.bool("cursorSwatchPreview");
if widgets::check(ui, tl!("Cursor Swatch Preview"), preview, d.str("pickColorFrom") == "swatches") {
d.fields.insert("cursorSwatchPreview".into(), json!(!preview));
}
ui.add_space(8.0);
let selection = d.str("selection");
widgets::label_row(ui, tl!("Selection:"), LABEL_W, |ui| {
for (value, label) in [("freeform", tl!("Freeform")), ("straightLine", tl!("Straight Line"))] {
if widgets::radio(ui, label, selection == value, true) {
d.fields.insert("selection".into(), json!(value));
}
}
});
widgets::label_row(ui, tl!("Highlight:"), LABEL_W, |ui| form::check(ui, d, "highlightFill", tl!("Fill")));
false
}
/// OK: set the Shape Builder's options.
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let values: Map<String, Value> = KEYS.iter().filter_map(|k| Some((k.to_string(), d.fields.get(*k)?.clone()))).collect();
run_and_close(app, "tool.setOption", json!({ "tool": "shapeBuilder", "values": values }))
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
fn app() -> VectorcraftApp {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app
}
/// The labels the open dialog paints.
fn painted(app: &mut VectorcraftApp) -> String {
crate::tests_labels::painted_text(app, |app, ui| super::super::show(app, ui.ctx()))
}
#[test]
fn double_clicking_the_tool_opens_its_options_and_ok_keeps_them() {
let mut app = app();
let r = app.run("tool.options", json!({"tool": "shapeBuilder"})).unwrap();
assert_eq!(r["dialog"], json!(KIND));
let text = painted(&mut app);
for label in [
"Shape Builder Tool Options",
"Consider Open Filled Paths as Closed",
"Pick Color From:",
"Artwork",
"Cursor Swatch Preview",
"Selection:",
"Freeform",
"Straight Line",
"Highlight:",
"Fill",
] {
assert!(text.contains(label), "{label} in {text}");
}
// The defaults: VectorCraft's Shape Builder as it works without the dialog.
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!(
(d.bool("openFilledAsClosed"), d.str("pickColorFrom"), d.bool("cursorSwatchPreview"), d.str("selection"), d.bool("highlightFill")),
(true, "artwork".into(), false, "freeform".into(), true)
);
for (k, v) in [
("openFilledAsClosed", json!(false)),
("pickColorFrom", json!("swatches")),
("cursorSwatchPreview", json!(true)),
("selection", json!("straightLine")),
("highlightFill", json!(false)),
] {
d.fields.insert(k.into(), v);
}
super::super::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
// They last while another tool is chosen, and the dialog opens on them again.
app.select_tool("shapeBuilder");
app.select_tool("selection");
let o = app.session.tool_options_of("shapeBuilder");
assert_eq!(
o,
json!({"openFilledAsClosed": false, "pickColorFrom": "swatches", "cursorSwatchPreview": true, "selection": "straightLine", "highlightFill": false})
);
app.run("tool.options", json!({"tool": "shapeBuilder"})).unwrap();
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!((d.str("selection"), d.bool("highlightFill")), ("straightLine".into(), false));
}
#[test]
fn a_straight_line_drag_merges_only_the_regions_the_line_crosses() {
// A wide rectangle and a tall one over the middle of its top edge: three regions, the wide
// one's own, the overlap and the tall one's top.
let mut app = app();
app.run("shape.rectangle", json!({"x": 100, "y": 100, "width": 200, "height": 100})).unwrap();
app.run("shape.rectangle", json!({"x": 180, "y": 50, "width": 40, "height": 100})).unwrap();
// A drag that goes up into the tall rectangle's top and back down; the bounds of each
// object afterwards.
let drag = |app: &mut VectorcraftApp| {
app.run("select.all", json!({})).unwrap();
app.select_tool("shapeBuilder");
let view = app.view_info();
let ev = |kind, (x, y): (f64, f64)| vectorcraft_tools::PointerEvent::new(kind, x, y);
let pts = [(120.0, 180.0), (200.0, 75.0), (280.0, 180.0)];
crate::canvas::dispatch(app, &ev(vectorcraft_tools::PointerKind::Down, pts[0]), view);
for p in &pts[1..] {
crate::canvas::dispatch(app, &ev(vectorcraft_tools::PointerKind::Drag, *p), view);
}
crate::canvas::dispatch(app, &ev(vectorcraft_tools::PointerKind::Up, pts[2]), view);
let doc = &app.session.doc().unwrap().doc;
let ids: Vec<_> = doc.layers[0].children().unwrap().iter().map(|n| n.id).collect();
ids.iter().filter_map(|id| doc.bounds_of(&[*id], false)).map(|b| [b.x0, b.y0, b.x1, b.y1].map(f64::round)).collect::<Vec<_>>()
};
// Freeform: the drag merges all three regions into one shape.
assert_eq!(drag(&mut app), vec![[100.0, 50.0, 300.0, 200.0]]);
app.run("edit.undo", json!({})).unwrap();
// Straight Line: the line from the first point to the last stays in the wide rectangle's
// own region, so only that region is merged: the shape made from it leaves the tall
// rectangle's top out, and the overlap stays an object of its own.
app.run("tool.setOption", json!({"tool": "shapeBuilder", "values": {"selection": "straightLine"}})).unwrap();
let after = drag(&mut app);
assert!(!after.iter().any(|b| b[1] == 50.0 && b[0] == 100.0), "{after:?}");
assert!(after.contains(&[180.0, 100.0, 220.0, 150.0]), "{after:?}");
}
}
+7
View File
@@ -5130,6 +5130,13 @@ theme Appearance Външен вид
Isolate Selected Object Изолиране на избрания обект
Keep selected Запазване на избора
Merge only with selection Сливане само с избраното
Consider Open Filled Paths as Closed Отворените запълнени пътища се смятат за затворени
Pick Color From: Цвят от:
Artwork Графика
Cursor Swatch Preview Преглед на образеца до курсора
Selection: Селекция:
Straight Line Права линия
Highlight: Открояване:
Sample Color from Screen Взимане на цвят от екрана
The active document changed while the screen color sampler was open Активният документ се промени, докато взимането на цвят от екрана беше отворено
This system has no screen color sampler Тази система няма инструмент за взимане на цвят от екрана
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -2267,6 +2267,13 @@ lighting Light Světlo
Alt key toggles to Smooth Tool Klávesa Alt přepíná na nástroj Vyhlazení
Keep selected Ponechat vybrané
Merge only with selection Sloučit jen s výběrem
Consider Open Filled Paths as Closed Považovat otevřené vyplněné cesty za uzavřené
Pick Color From: Brát barvu z:
Artwork Kresba
Cursor Swatch Preview Náhled vzorku u kurzoru
Selection: Výběr:
Straight Line Přímka
Highlight: Zvýraznění:
The document has no colour swatches. Dokument nemá ve vzorníku žádné barvy.
Style Name: Název stylu:
Crop To Oříznout na
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -5087,6 +5087,13 @@ swatch Bright Violet Kraftig violet
@msg select the art to export first markér først grafikken, der skal eksporteres
@msg no saved selection recalled ingen gemt markering hentet
Merge only with selection Flet kun med markering
Consider Open Filled Paths as Closed Behandl åbne udfyldte kurver som lukkede
Pick Color From: Hent farve fra:
Artwork Grafik
Cursor Swatch Preview Forhåndsvisning af farveprøver ved markøren
Selection: Markering:
Straight Line Lige linje
Highlight: Fremhævning:
Sketch Skitse
Halftone Pattern… Rastermønster…
Halftone Pattern Rastermønster
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1066,6 +1066,14 @@
Alt key toggles to Smooth Tool Alt-Taste schaltet auf Glätten-Werkzeug um
Keep selected Ausgewählt lassen
Merge only with selection Nur mit Auswahl zusammenfügen
Consider Open Filled Paths as Closed Offene gefüllte Pfade als geschlossen behandeln
Pick Color From: Farbe aufnehmen aus:
Artwork Bildmaterial
Cursor Swatch Preview Farbfeldvorschau am Zeiger
Selection: Auswahl:
Freeform Freiform
Straight Line Gerade Linie
Highlight: Hervorheben:
Fill Color Flächenfarbe
Fill New Type Objects With Placeholder Text Neue Textobjekte mit Platzhaltertext füllen
Fill Rule Füllregel
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -5083,6 +5083,13 @@ effect Points Σημεία
Isolate Selected Object Απομόνωση επιλεγμένου αντικειμένου
Keep selected Διατήρηση επιλογής
Merge only with selection Συγχώνευση μόνο με την επιλογή
Consider Open Filled Paths as Closed Οι ανοιχτές γεμισμένες διαδρομές θεωρούνται κλειστές
Pick Color From: Λήψη χρώματος από:
Artwork Γραφικά
Cursor Swatch Preview Προεπισκόπηση δείγματος δίπλα στον δείκτη
Selection: Επιλογή:
Straight Line Ευθεία γραμμή
Highlight: Επισήμανση:
Legacy (version 7) Παλαιού τύπου (έκδοση 7)
Legacy .ai Options Επιλογές .ai παλαιού τύπου
Linked Files Συνδεδεμένα αρχεία
Can't render this file because it contains an unexpected character in line 2021 and column 19.
+7
View File
@@ -1082,6 +1082,13 @@
Alt key toggles to Smooth Tool La tecla Alt cambia a la herramienta Suavizar
Keep selected Mantener seleccionado
Merge only with selection Combinar solo con la selección
Consider Open Filled Paths as Closed Tratar los trazados abiertos con relleno como cerrados
Pick Color From: Tomar el color de:
Artwork Ilustración
Cursor Swatch Preview Vista previa de muestras junto al cursor
Selection: Selección:
Straight Line Línea recta
Highlight: Resaltado:
Fill Color Color de relleno
Fill New Type Objects With Placeholder Text Rellenar nuevos objetos de texto con texto de muestra
Fill Rule Regla de relleno
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -5095,6 +5095,13 @@ mark-style Japanese Japanilainen
Isolate Selected Object Eristä valittu objekti
Keep selected Pidä valittuna
Merge only with selection Yhdistä vain valintaan
Consider Open Filled Paths as Closed Käsittele avoimia täytettyjä polkuja suljettuina
Pick Color From: Värin lähde:
Artwork Kuvitus
Cursor Swatch Preview Värimallien esikatselu osoittimen vieressä
Selection: Valinta:
Straight Line Suora viiva
Highlight: Korostus:
Legacy (version 7) Vanha (versio 7)
Legacy .ai Options Vanhan .ai-muodon asetukset
Linked Files Linkitetyt tiedostot
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -1082,6 +1082,13 @@
Alt key toggles to Smooth Tool La touche Alt bascule sur l’outil Arrondi
Keep selected Garder sélectionné
Merge only with selection Fusionner uniquement avec la sélection
Consider Open Filled Paths as Closed Traiter les tracés ouverts avec fond comme fermés
Pick Color From: Prendre la couleur dans :
Artwork Illustration
Cursor Swatch Preview Aperçu des nuances près du pointeur
Selection: Sélection :
Straight Line Ligne droite
Highlight: Mise en évidence :
Fill Color Couleur de fond
Fill New Type Objects With Placeholder Text Remplir les nouveaux objets texte avec un texte de substitution
Fill Rule Règle de remplissage
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -1370,6 +1370,13 @@
Alt key toggles to Smooth Tool Az Alt billentyű a Simítás eszközre vált
Keep selected Kijelölés megtartása
Merge only with selection Összeolvasztás csak a kijelöléssel
Consider Open Filled Paths as Closed A nyitott kitöltött útvonalak zártnak számítanak
Pick Color From: Szín forrása:
Artwork Grafika
Cursor Swatch Preview Színminta-előnézet a mutató mellett
Selection: Kijelölés:
Straight Line Egyenes vonal
Highlight: Kiemelés:
Fill New Type Objects With Placeholder Text Új szövegobjektumok kitöltése helykitöltő szöveggel
Fill Rule Kitöltési szabály
Fill with Placeholder Text Kitöltés helykitöltő szöveggel
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -5087,6 +5087,13 @@ swatch Bright Violet Violet cerah
@msg select the art to export first pilih dulu karya yang akan diekspor
@msg no saved selection recalled tidak ada seleksi tersimpan yang dipanggil
Merge only with selection Gabungkan hanya dengan seleksi
Consider Open Filled Paths as Closed Anggap jalur terbuka yang terisi sebagai tertutup
Pick Color From: Ambil warna dari:
Artwork Karya seni
Cursor Swatch Preview Pratinjau swatch di kursor
Selection: Seleksi:
Straight Line Garis lurus
Highlight: Sorotan:
Sketch Sketsa
Halftone Pattern… Pola halftone…
Halftone Pattern Pola halftone
Can't render this file because it contains an unexpected character in line 3781 and column 19.
+8
View File
@@ -1082,6 +1082,14 @@
Alt key toggles to Smooth Tool Il tasto Alt attiva lo strumento Smussa
Keep selected Mantieni selezionato
Merge only with selection Unisci solo con la selezione
Consider Open Filled Paths as Closed Considera chiusi i tracciati aperti con riempimento
Pick Color From: Prendi il colore da:
Artwork Grafica
Cursor Swatch Preview Anteprima dei campioni accanto al cursore
Selection: Selezione:
Freeform Libera
Straight Line Linea retta
Highlight: Evidenziazione:
Fill Color Colore riempimento
Fill New Type Objects With Placeholder Text Riempi i nuovi oggetti di testo con testo segnaposto
Fill Rule Regola di riempimento
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1491,6 +1491,14 @@
Alt key toggles to Smooth Tool Alt キーでスムーズツールに切り替え
Keep selected 選択を保持
Merge only with selection 選択範囲とのみ結合
Consider Open Filled Paths as Closed 塗りのある開いたパスを閉じたパスとみなす
Pick Color From: 色の取得元:
Artwork アートワーク
Cursor Swatch Preview ポインター横のスウォッチプレビュー
Selection: 選択方法:
Freeform 自由形状
Straight Line 直線
Highlight: 強調表示:
Fill: 塗り:
Find 検索
Find Font フォントを検索
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -4917,6 +4917,13 @@ imageTrace Outline კონტური
Time (hhmmss) დრო (hhmmss)
PDF Print PDF (ბეჭდვა)
Merge only with selection შერწყმა მხოლოდ მონიშნულთან
Consider Open Filled Paths as Closed შევსებული ღია ტრაექტორიების შეკრულად მიჩნევა
Pick Color From: ფერის წყარო:
Artwork ნახატი
Cursor Swatch Preview ფერის ნიმუშები მაჩვენებელთან
Selection: მონიშვნა:
Straight Line სწორი ხაზი
Highlight: გამოკვეთა:
Words Longer Than: სიტყვები, რომელთა სიგრძე აღემატება:
After First: პირველის შემდეგ:
Before Last: ბოლომდე:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1486,6 +1486,14 @@
Alt key toggles to Smooth Tool Alt 키로 매끄럽게 도구로 전환
Keep selected 선택 유지
Merge only with selection 선택 영역과만 병합
Consider Open Filled Paths as Closed 칠이 있는 열린 패스를 닫힌 패스로 처리
Pick Color From: 색상 출처:
Artwork 아트워크
Cursor Swatch Preview 포인터 옆 견본 미리 보기
Selection: 선택 방식:
Freeform 자유 형태
Straight Line 직선
Highlight: 강조:
Fill: 칠:
Find 찾기
Find Font 글꼴 찾기
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -5099,6 +5099,13 @@ swatch Bright Violet Felviolet
@msg select the art to export first selecteer eerst de illustratie die je wilt exporteren
@msg no saved selection recalled geen opgeslagen selectie opgeroepen
Merge only with selection Alleen samenvoegen met selectie
Consider Open Filled Paths as Closed Open gevulde paden als gesloten beschouwen
Pick Color From: Kleur kiezen uit:
Artwork Artwork
Cursor Swatch Preview Staalvoorbeeld bij de cursor
Selection: Selectie:
Straight Line Rechte lijn
Highlight: Markering:
Sketch Schets
Halftone Pattern… Rasterpatroon…
Halftone Pattern Rasterpatroon
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -2182,6 +2182,13 @@ lighting Light Światło
Alt key toggles to Smooth Tool Klawisz Alt przełącza na narzędzie Wygładzanie
Keep selected Zachowaj zaznaczenie
Merge only with selection Scalaj tylko z zaznaczeniem
Consider Open Filled Paths as Closed Traktuj otwarte wypełnione ścieżki jako zamknięte
Pick Color From: Źródło koloru:
Artwork Grafika
Cursor Swatch Preview Podgląd próbek przy kursorze
Selection: Zaznaczanie:
Straight Line Linia prosta
Highlight: Podświetlenie:
The document has no colour swatches. Dokument nie zawiera próbek kolorów.
Style Name: Nazwa stylu:
Crop To Kadruj do
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -967,6 +967,13 @@
Alt key toggles to Smooth Tool A tecla Alt alterna para a ferramenta Suavizar
Keep selected Manter selecionado
Merge only with selection Mesclar somente com a seleção
Consider Open Filled Paths as Closed Tratar caminhos abertos preenchidos como fechados
Pick Color From: Origem da cor:
Artwork Ilustração
Cursor Swatch Preview Visualização de amostras junto ao cursor
Selection: Seleção:
Straight Line Linha reta
Highlight: Destaque:
Fill with Placeholder Text Preencher com Texto de Placeholder
Insert Inline Symbol Inserir Símbolo no Texto
Fill: Preenchimento:
Can't render this file because it contains an unexpected character in line 6 and column 47.
+8
View File
@@ -1080,6 +1080,14 @@
Alt key toggles to Smooth Tool Клавиша Alt переключает на инструмент «Сглаживание»
Keep selected Оставлять выбранным
Merge only with selection Объединять только с выделенным
Consider Open Filled Paths as Closed Считать открытые контуры с заливкой замкнутыми
Pick Color From: Источник цвета:
Artwork Графика
Cursor Swatch Preview Образцы у указателя
Selection: Выделение:
Freeform Произвольно
Straight Line Прямая линия
Highlight: Подсветка:
Fill Color Цвет заливки
Fill New Type Objects With Placeholder Text Заполнять новые текстовые объекты текстом-заполнителем
Fill Rule Правило заливки
Can't render this file because it contains an unexpected character in line 2 and column 73.
+7
View File
@@ -5087,6 +5087,13 @@ theme Appearance Utseende
Isolate Selected Object Isolera markerat objekt
Keep selected Behåll markerad
Merge only with selection Sammanfoga endast med markeringen
Consider Open Filled Paths as Closed Behandla öppna fyllda banor som slutna
Pick Color From: Hämta färg från:
Artwork Bildmaterial
Cursor Swatch Preview Förhandsvisning av färgprover vid pekaren
Selection: Markering:
Straight Line Rak linje
Highlight: Framhävning:
Legacy (version 7) Äldre (version 7)
Legacy .ai Options Alternativ för äldre .ai
Linked Files Länkade filer
Can't render this file because it contains an unexpected character in line 5 and column 25.
+8
View File
@@ -1076,6 +1076,14 @@
Alt key toggles to Smooth Tool Клавіша Alt перемикає на інструмент «Згладжування»
Keep selected Залишати вибраним
Merge only with selection Об’єднувати лише з виділеним
Consider Open Filled Paths as Closed Вважати відкриті контури із заливкою замкненими
Pick Color From: Джерело кольору:
Artwork Графіка
Cursor Swatch Preview Зразки біля вказівника
Selection: Виділення:
Freeform Довільно
Straight Line Пряма лінія
Highlight: Підсвічування:
Fill Color Колір заливки
Fill New Type Objects With Placeholder Text Заповнювати нові текстові об’єкти текстом-заповнювачем
Fill Rule Правило заливки
Can't render this file because it contains an unexpected character in line 726 and column 40.
+8
View File
@@ -1248,6 +1248,14 @@
Alt key toggles to Smooth Tool Phím Alt chuyển sang công cụ Làm mượt
Keep selected Giữ trạng thái đã chọn
Merge only with selection Chỉ hợp nhất với vùng chọn
Consider Open Filled Paths as Closed Coi đường dẫn mở có tô là đường dẫn đóng
Pick Color From: Lấy màu từ:
Artwork Tác phẩm
Cursor Swatch Preview Xem trước mẫu màu cạnh con trỏ
Selection: Cách chọn:
Freeform Tự do
Straight Line Đường thẳng
Highlight: Làm nổi bật:
Fill New Type Objects With Placeholder Text Tô đối tượng chữ mới bằng văn bản giữ chỗ
Fill Rule Quy tắc tô
Fill with Placeholder Text Tô bằng văn bản giữ chỗ
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
+8
View File
@@ -1030,6 +1030,14 @@
Alt key toggles to Smooth Tool Alt 键切换为平滑工具
Keep selected 保持选择状态
Merge only with selection 仅与所选对象合并
Consider Open Filled Paths as Closed 将有填色的开放路径视为闭合路径
Pick Color From: 颜色来源:
Artwork 图稿
Cursor Swatch Preview 在指针旁预览色板
Selection: 选择方式:
Freeform 自由
Straight Line 直线
Highlight: 突出显示:
Fill Color 填色
Fill New Type Objects With Placeholder Text 用占位文本填充新文字对象
Fill Rule 填充规则
Can't render this file because it contains an unexpected character in line 2 and column 73.
+8
View File
@@ -1054,6 +1054,14 @@
Alt key toggles to Smooth Tool Alt 鍵切換為平滑工具
Keep selected 保持選取狀態
Merge only with selection 僅與選取物件合併
Consider Open Filled Paths as Closed 將有填色的開放路徑視為封閉路徑
Pick Color From: 顏色來源:
Artwork 圖稿
Cursor Swatch Preview 在指標旁預覽色票
Selection: 選取方式:
Freeform 自由
Straight Line 直線
Highlight: 強調顯示:
Fill New Type Objects With Placeholder Text 以預留位置文字填滿新文字物件
Fill Rule 填色規則
Fill with Placeholder Text 以預留位置文字填滿
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -484,7 +484,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; 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})",
"{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; 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 openFilledAsClosed (on), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on); OK runs tool.setOption {tool: \"shapeBuilder\", values})",
),
(
"ui.colorGuideOptions",
+3 -2
View File
@@ -326,8 +326,8 @@ fn flyout_press(ui: &Ui, resp: &egui::Response, rect: egui::Rect) -> Option<Flyo
/// Options; the Print Tiling tool's resets the print tiling. As in the reference app, the Hand
/// 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 and Eraser tools' are their Tool
/// Options.
/// Move dialog, and the Pencil, Paintbrush, Smooth, Blob Brush, Eraser and Shape Builder tools' are
/// their Tool Options.
pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::Value, String> {
match tool {
"dimension" => crate::dialogs::dimension::open(app),
@@ -358,6 +358,7 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
crate::dialogs::perspective_options::open(app);
Ok(json!({ "dialog": crate::dialogs::perspective_options::KIND }))
}
"shapeBuilder" => Ok(crate::dialogs::shape_builder_options::open(app)),
// 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.
+16 -1
View File
@@ -4196,7 +4196,7 @@ active tool, `tool` names another one, and `{}` just reads them; the result is t
tool keeps last across tool switches (and documents) and are saved with the preferences (`toolSettings` in
`prefs.get`'s full object), as the reference app keeps them: the Liquify tools' brush and tool options, Mirror & Cut's
axis and side, Puppet Warp's mesh, the Symbolism brush, the line, grid, pencil, brush and eraser tools' options,
polygon sides and star points, and the Flare Tool Options. The Liquify tools share one set of Global Brush Dimensions (width, height, angle,
polygon sides and star points, the Flare Tool Options and the Shape Builder Tool Options. The Liquify tools share one set of Global Brush Dimensions (width, height, angle,
intensity), and the Symbolism tools one brush. Interaction state (pins, a reference point) starts afresh.
```json
@@ -4224,6 +4224,21 @@ default name, "Artboard N" for the Nth artboard.
{"name":"run_command","arguments":{"command":"tool.setOption","params":{"tool":"flare","values":{"rays":24,"ringsOn":false}}}}
```
The Shape Builder tool's options are its Shape Builder Tool Options (dialog `shapeBuilderOptions`):
`openFilledAsClosed` (on: a filled open path bounds a region as if an invisible edge closed it; off: it cuts regions
as an unfilled path does), `pickColorFrom` (`artwork`, the default: a merged shape takes the fill of the front-most
object over the first region the drag touches that an object covers, and the current fill when only lines enclose
every region touched; `swatches`: the current fill, and the Left and Right arrow keys step the current fill through the
document's color swatches), `cursorSwatchPreview` (off; with `swatches`, chips above the pointer show the current fill
and the swatches before and after it), `selection` (`freeform`, the default, or `straightLine`: a drag touches what
the line from its first point to the pointer crosses) and `highlightFill` (on by default; when it is off, merge mode
shades no regions). The tool sends `openFilledAsClosed` and `pickColorFrom` with `shapeBuilder.merge`, which takes
both as params, and `shapeBuilder.regions` takes `openFilledAsClosed`.
```json
{"name":"run_command","arguments":{"command":"tool.setOption","params":{"tool":"shapeBuilder","values":{"selection":"straightLine","pickColorFrom":"swatches"}}}}
```
## Perspective grid
The grid is document data (`perspective.grid.set` edits the model directly). `perspective.grid.get` reads it:
+1
View File
@@ -590,6 +590,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Tool double-clicks:** double-clicking a tool's button opens its options: Hand fits the artboard in the window, Zoom shows 100 %, Rotate, Scale, Reflect and Shear open their Object › Transform dialogs, the Selection, Direct Selection and Group Selection tools the Move dialog (Enter opens these seven tools' dialogs too; Move opens on the offset of the last drag or Move and with the checkboxes it was last confirmed with, so Enter, Enter repeats a move or a copy at the same spacing, as [an Adobe Community thread](https://community.adobe.com/questions-652/move-tool-771221) describes), Gradient, Eyedropper, Blend and the Liquify tools their panels or options, the Pencil, Paintbrush, Smooth, Blob Brush and Eraser tools their Tool Options (Fidelity, fill, Keep selected, the tolerances, the brush size), and (#812) the Flare tool its Flare Tool Options, the Artboard tool the active artboard's Artboard Options, the graph tools Graph Type for the selected graph, the Symbolism tools the Symbolism Tools Options (Diameter, Intensity, Symbol Set Density) and the Magic Wand tool its panel.
In the Pencil and Paintbrush Tool Options, Edit selected paths within 0 px turns editing off, so a stroke that starts exactly on an end of a selected open path draws a new path; Close paths when ends are within 0 px turns closing off, so a stroke that ends exactly where it started stays open.
Keep selected in the Pencil and Paintbrush Tool Options is on by default; while it is off, the path a stroke draws or continues is not selected afterwards and the rest of the selection stays as it was (`path.freehand` `keepSelected`).
Double-clicking the Shape Builder tool opens Shape Builder Tool Options ([Shape Builder tool options](https://web.archive.org/web/20260105172244/https://helpx.adobe.com/illustrator/using/building-new-shapes-using-shape.html)): with Consider Open Filled Paths as Closed off, a filled open path cuts regions as an unfilled one does; Pick Color From Color Swatches fills merged shapes with the current fill, the Left and Right arrow keys step it through the document's color swatches, and Cursor Swatch Preview shows the current fill and the swatches before and after it above the pointer; Selection Straight Line merges what the line from a drag's first point to its last crosses ([Setting Illustrator's Shape Builder Options](https://web.archive.org/web/20260615204453/https://creativepro.com/setting-illustrators-shape-builder-options/)); with Highlight Fill off, merge mode shades no regions (`shapeBuilder.merge` `openFilledAsClosed` and `pickColorFrom`). Gap Detection, "In Merge Mode, Clicking Stroke Splits the Path" and Highlight Stroke When Editable are not built.
The Blob Brush Tool Options have Keep selected and Merge only with selection, both off by default: while Keep selected is off, the blob a stroke draws or merges into is not selected afterwards, and while Merge only with selection is on, a stroke merges only with selected blobs of its color that it touches (`path.blob` `keepSelected` and `mergeOnlySelection`).
- **Ruler guides as layer objects:** a guide goes on the current layer, shows, hides, locks and is deleted with it, is listed in the Layers panel (its row selects it) and moves to another layer (`guide.setLayer`, or its row dragged onto that layer's rows); guides on a hidden or locked layer can't be selected, moved or deleted, Duplicate copies a layer's guides and Merge Selected and Flatten Artwork keep them on the merged layer; guides from files saved before guides had layers go on the top visible, unlocked layer when the document opens. Not done: guides stacked among the art of their layer (they are listed above it), a guide row's own eye and lock. Lock Guides also locks the objects made into guides: they can't be selected, moved or deleted (#1543); they are drawn in the guide colour and style of Preferences, and a dragged selection snaps to ruler guides with Smart Guides on (#1583, fixes #1534). The Transform panel shows a selected ruler guide's X or Y, and typing a value moves it (#1659, part of #1597). This is not established by the documentation we could find: the public help pages on guides, rulers and the Transform, Control and Properties panels (read again on 2026-10-14) describe selecting and dragging unlocked guides and the panels' X and Y for a selection, not a guide's position in a panel, and the one public statement of it is a user's [community answer](https://community.adobe.com/questions-652/adding-a-new-custom-guide-785586), which is not documentation. It is kept as a convenience pending the owner's decision, with no parity claim and nothing counted towards a score.
- **Releasing ruler guides:** View › Guides › Release Guides turns the selected ruler guides into paths with no fill or stroke. An artboard guide becomes a line across its artboard, and a canvas guide becomes a line across the artboards (`view.guides.release {index?, ids?}`). A Cmd/Ctrl+Shift double-click with the Selection, Direct Selection or Group Selection tool releases the ruler guide or guide path under the pointer.