mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Shape Builder: double-clicking the tool opens Shape Builder Tool Options
Double-clicking the Shape Builder tool reported that the tool has no options. The reference app opens Shape Builder Tool Options there. The dialog sets Consider Open Filled Paths as Closed, Pick Color From (Artwork or Color Swatches), Cursor Swatch Preview, Selection (Freeform or Straight Line) and Highlight Fill. The defaults keep today's behavior. With Consider Open Filled Paths as Closed off, a filled open path cuts regions as an unfilled one does. With Color Swatches, 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. Cursor Swatch Preview shows the current fill and the swatches before and after it above the pointer. With Straight Line, a drag merges what the line from its first point to its last crosses. With Highlight Fill off, merge mode shades no regions. The options persist like other tools' options. shapeBuilder.merge takes openFilledAsClosed and pickColorFrom, and shapeBuilder.regions takes openFilledAsClosed. Gap Detection, "In Merge Mode, Clicking Stroke Splits the Path" and Highlight Stroke When Editable are not part of this change. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 ----------
|
||||
|
||||
@@ -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).
|
||||
(
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
}
|
||||
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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.
|
@@ -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ỗ
|
||||
|
||||
|
@@ -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.
|
@@ -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.
|
@@ -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",
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user