mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 12:24:19 +00:00
Shape Builder: an open path counts as filled by the fill rows that draw
Consider Open Filled Paths as Closed decided whether an open path was filled from its top fill row only. A path whose top fill is None over a painted lower fill stayed open, and one whose only painted fill was hidden or at 0% opacity closed. The region map, merge inputs and the Clicking Stroke Splits the Path cut now use Appearance::has_drawn_fill, so any shown, painted fill row above 0% opacity counts, as the canvas paints it. The Shape Builder's Live Paint face and edge filters had the same top-row check; they now use has_drawn_fill and has_drawn_stroke, so a face or edge is taken when one of its rows draws.
This commit is contained in:
@@ -54,7 +54,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Shape Builder",
|
||||
[],
|
||||
None,
|
||||
"{ids?|id?: [id…] (default: selection), points: [[x,y]…] (drag path; one point = click; may be left out with rect), rect?: [x, y, width, height] (Shift-drag marquee from where the drag started, x and y, to x+width and y+height; width and height may be negative: touches every region whose outline crosses the rectangle or has an anchor inside it and the region holding the whole rectangle, the region under x, y comes first and the one under the opposite corner comes next; with points, it also touches the regions along them; for pickColorFrom, x, y is the drag's first point and the opposite corner is its last), 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, except that a point in a region no more than twice the tolerance across there touches the region; a deleted edge opens its path there, and edges bounding a deleted region go with it; what is left of each path keeps its look, with gradients fitted to its bounds kept where they were on it, its name, opacity mask and attributes, and its fill rule unless merged regions shaped it), splitStroke?: false (In Merge Mode, Clicking Stroke Splits the Path; not with erase or rect: when the one point of a click is within tolerance of a stroke, the path that stroke is on splits in two instead of a merge: the path keeps the rest of it, and a copy of the path holding the run of it from the anchor or crossing point before the click to the one after it, in the path's direction, goes directly above it; both keep the path's gradients where they were on it; the other objects stay as they are; with pickColorFrom \"swatches\" or fill, the run's stroke takes that paint; a path in a Live Paint group or a Shaper Group does not split, and nor does a path the run is all of, such as a lone line, or all but 0.05 pt or less of (a ten-thousandth of the larger side of a path over 500 pt)), tolerance?: pt (2, erase or splitStroke), gap?: pt (0–72; 0: Gap Detection off), 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 look of the front-most object under the first point, else under the last point, else of the front-most object) | \"swatches\" (the current fill, with the rest of the look of the front-most object over the first region the drag touches that an object covers, else of the front-most object), fill?: \"#rrggbb\" | {color|swatch|gradient|none} (overrides pickColorFrom)} replaces the paths it uses where they are; other selected objects (type, hidden or locked objects, clipping paths) stay, and a group goes only when it is left empty → {ids (selected after: each selected object still there, or what took its place), merged, regions, lines, edges (counts deleted, merged or split), split?: id (the run split off, after a split)}; after a split, ids are the roots, with the rest and the run in place of the path when it was one",
|
||||
"{ids?|id?: [id…] (default: selection), points: [[x,y]…] (drag path; one point = click; may be left out with rect), rect?: [x, y, width, height] (Shift-drag marquee from where the drag started, x and y, to x+width and y+height; width and height may be negative: touches every region whose outline crosses the rectangle or has an anchor inside it and the region holding the whole rectangle, the region under x, y comes first and the one under the opposite corner comes next; with points, it also touches the regions along them; for pickColorFrom, x, y is the drag's first point and the opposite corner is its last), 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, except that a point in a region no more than twice the tolerance across there touches the region; a deleted edge opens its path there, and edges bounding a deleted region go with it; what is left of each path keeps its look, with gradients fitted to its bounds kept where they were on it, its name, opacity mask and attributes, and its fill rule unless merged regions shaped it), splitStroke?: false (In Merge Mode, Clicking Stroke Splits the Path; not with erase or rect: when the one point of a click is within tolerance of a stroke, the path that stroke is on splits in two instead of a merge: the path keeps the rest of it, and a copy of the path holding the run of it from the anchor or crossing point before the click to the one after it, in the path's direction, goes directly above it; both keep the path's gradients where they were on it; the other objects stay as they are; with pickColorFrom \"swatches\" or fill, the run's stroke takes that paint; a path in a Live Paint group or a Shaper Group does not split, and nor does a path the run is all of, such as a lone line, or all but 0.05 pt or less of (a ten-thousandth of the larger side of a path over 500 pt)), tolerance?: pt (2, erase or splitStroke), gap?: pt (0–72; 0: Gap Detection off), openFilledAsClosed?: true (Consider Open Filled Paths as Closed: a filled open path bounds a region as if an invisible edge closed it, where filled means any fill row is shown, painted and above 0% opacity, whichever row is on top; false: it cuts the regions it crosses like an unfilled one), pickColorFrom?: \"artwork\" (default: the merged shape takes the look of the front-most object under the first point, else under the last point, else of the front-most object) | \"swatches\" (the current fill, with the rest of the look of the front-most object over the first region the drag touches that an object covers, else of the front-most object), fill?: \"#rrggbb\" | {color|swatch|gradient|none} (overrides pickColorFrom)} replaces the paths it uses where they are; other selected objects (type, hidden or locked objects, clipping paths) stay, and a group goes only when it is left empty → {ids (selected after: each selected object still there, or what took its place), merged, regions, lines, edges (counts deleted, merged or split), split?: id (the run split off, after a split)}; after a split, ids are the roots, with the rest and the run in place of the path when it was one",
|
||||
has_selection_or_ids,
|
||||
sb_merge
|
||||
),
|
||||
|
||||
@@ -63,7 +63,7 @@ pub(super) fn sb_merge(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
&& let Some(&q) = pts.first()
|
||||
&& let Some((i, run)) = map.stroke_at(&outlines, q, tol)
|
||||
&& let (Some(leaf), Some(drawn)) = (leaves.get(i), outlines.get(i))
|
||||
&& let Some(rest) = po::cut_out(drawn, &run, open_filled_as_closed && !leaf.appearance.fill_paint().is_none(), b::outline_snap(drawn))
|
||||
&& let Some(rest) = po::cut_out(drawn, &run, open_filled_as_closed && leaf.appearance.has_drawn_fill(), b::outline_snap(drawn))
|
||||
&& rest.length() > b::outline_snap(drawn)
|
||||
{
|
||||
// The path keeps the rest of it, and a copy of it with the run under the click goes
|
||||
|
||||
@@ -1490,6 +1490,93 @@ fn shape_builder_open_filled_paths_cut_without_open_filled_as_closed() {
|
||||
assert!((area(&s, NodeId(r["merged"].as_u64().unwrap())) - 5000.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
/// The fill rows of an open path's appearance (bottom first) for the open-path tests below, with
|
||||
/// a 1 pt black stroke on top: a painted fill under an unpainted top fill, a painted fill that is
|
||||
/// hidden, one with an opacity of 0, and a painted fill under a hidden painted top fill.
|
||||
fn open_path_fill_stacks() -> [(&'static str, Vec<AppearanceItem>, bool); 4] {
|
||||
use vectorcraft_doc::{FillLayer, StrokeLayer};
|
||||
let fill = |p: Paint, visible: bool, opacity: f32| {
|
||||
let mut f = FillLayer::new(p);
|
||||
f.visible = visible;
|
||||
f.opacity = opacity;
|
||||
AppearanceItem::Fill(f)
|
||||
};
|
||||
let red = || Paint::solid(Color::rgb(1.0, 0.0, 0.0));
|
||||
let blue = || Paint::solid(Color::rgb(0.0, 0.0, 1.0));
|
||||
let stroke = || AppearanceItem::Stroke(StrokeLayer::new(Paint::solid(Color::BLACK), 1.0));
|
||||
[
|
||||
("a painted fill under an unpainted top fill", vec![fill(red(), true, 1.0), fill(Paint::None, true, 1.0), stroke()], true),
|
||||
("a hidden painted fill", vec![fill(red(), false, 1.0), stroke()], false),
|
||||
("a painted fill with an opacity of 0", vec![fill(red(), true, 0.0), stroke()], false),
|
||||
("a painted fill under a hidden painted top fill", vec![fill(red(), true, 1.0), fill(blue(), false, 1.0), stroke()], true),
|
||||
]
|
||||
}
|
||||
|
||||
/// Consider Open Filled Paths as Closed: an open path counts as filled when any of its fill rows
|
||||
/// draws (shown, painted and above 0% opacity), whichever row is on top. The map, a merge inside
|
||||
/// the area and what a merge keeps of the path follow the rows that draw; with the option off, none
|
||||
/// of the paths closes.
|
||||
#[test]
|
||||
fn shape_builder_closes_open_paths_by_the_fill_rows_that_draw() {
|
||||
for (what, items, closes) in open_path_fill_stacks() {
|
||||
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());
|
||||
s.edit("t", |d, _| {
|
||||
d.node_mut(p).unwrap().appearance.items = items;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("select.set", &json!({"ids": [p.0]})).unwrap();
|
||||
let r = s.execute("shapeBuilder.regions", &json!({})).unwrap();
|
||||
assert_eq!(
|
||||
(r["regions"].as_array().unwrap().len(), r["lines"].as_array().unwrap().len()),
|
||||
if closes { (1, 0) } else { (0, 1) },
|
||||
"{what}: {r}"
|
||||
);
|
||||
let r = s.execute("shapeBuilder.regions", &json!({"openFilledAsClosed": false})).unwrap();
|
||||
assert_eq!((r["regions"].as_array().unwrap().len(), r["lines"].as_array().unwrap().len()), (0, 1), "{what}, option off: {r}");
|
||||
let n = node(&s, p);
|
||||
for on in [true, false] {
|
||||
let inputs = vectorcraft_tools::builder::merge_inputs(&[&n], on);
|
||||
assert_eq!(inputs.iter().map(|i| i.fill_closes).collect::<Vec<_>>(), vec![closes && on], "{what}, option {on}");
|
||||
}
|
||||
let merged = s.execute("shapeBuilder.merge", &json!({"points": [[75, 25]]}));
|
||||
if closes {
|
||||
assert!((area(&s, NodeId(merged.unwrap()["merged"].as_u64().unwrap())) - 5000.0).abs() < 1e-6, "{what}");
|
||||
} else {
|
||||
assert!(merged.is_err(), "{what}: the open path encloses no region");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Clicking Stroke Splits the Path on the middle side of an open U cuts it along the outline its
|
||||
/// drawn fill closes: with a fill row that draws, what is left runs on from the cut round the
|
||||
/// implied closing edge (the top side, then the bottom side); without one, what is left is the
|
||||
/// open path's two ends in path order (the bottom side, then the top side).
|
||||
#[test]
|
||||
fn shape_builder_stroke_split_closes_open_paths_by_the_fill_rows_that_draw() {
|
||||
for (what, items, closes) in open_path_fill_stacks() {
|
||||
let mut s = session();
|
||||
let r =
|
||||
s.execute("path.create", &json!({"anchors": [{"x": 0, "y": 0}, {"x": 100, "y": 0}, {"x": 100, "y": 100}, {"x": 0, "y": 100}]})).unwrap();
|
||||
let p = NodeId(r["id"].as_u64().unwrap());
|
||||
s.edit("t", |d, _| {
|
||||
d.node_mut(p).unwrap().appearance.items = items;
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
s.execute("select.set", &json!({"ids": [p.0]})).unwrap();
|
||||
let r = s.execute("shapeBuilder.merge", &json!({"points": [[101, 50]], "splitStroke": true})).unwrap();
|
||||
let out = ids(&r);
|
||||
assert_eq!(shapes_of(&s, &out), vec![(false, 4, 200.0), (false, 2, 100.0)], "{what}");
|
||||
let (rest, _) = outline(&node(&s, out[0]));
|
||||
let starts: Vec<(f64, f64)> = rest.subpaths.iter().map(|sp| (sp.anchors[0].p.x.round(), sp.anchors[0].p.y.round())).collect();
|
||||
let order = if closes { vec![(100.0, 100.0), (0.0, 0.0)] } else { vec![(0.0, 0.0), (100.0, 100.0)] };
|
||||
assert_eq!(starts, order, "{what}");
|
||||
}
|
||||
}
|
||||
|
||||
/// 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]
|
||||
@@ -4004,6 +4091,49 @@ fn shape_builder_uses_the_filled_faces_of_a_live_paint_group() {
|
||||
assert!((stroked_length(&s, &out) - 600.0).abs() < 1.0, "{}", stroked_length(&s, &out));
|
||||
}
|
||||
|
||||
/// The Shape Builder takes a Live Paint face or edge when any of its fill or stroke rows draws,
|
||||
/// whichever row is on top: a painted lower row under an unpainted top row counts, and a part
|
||||
/// whose only painted row is hidden, transparent or (for an edge) 0 pt wide does not.
|
||||
#[test]
|
||||
fn shape_builder_takes_the_live_paint_faces_and_edges_that_draw() {
|
||||
use vectorcraft_doc::{FillLayer, StrokeLayer};
|
||||
let fill = |p: Paint| AppearanceItem::Fill(FillLayer::new(p));
|
||||
let stroke = |p: Paint, w: f64| AppearanceItem::Stroke(StrokeLayer::new(p, w));
|
||||
let red = || Paint::solid(Color::rgb(1.0, 0.0, 0.0));
|
||||
let hidden = |mut i: AppearanceItem| {
|
||||
match &mut i {
|
||||
AppearanceItem::Fill(f) => f.visible = false,
|
||||
AppearanceItem::Stroke(st) => st.visible = false,
|
||||
}
|
||||
i
|
||||
};
|
||||
let clear = |mut i: AppearanceItem| {
|
||||
match &mut i {
|
||||
AppearanceItem::Fill(f) => f.opacity = 0.0,
|
||||
AppearanceItem::Stroke(st) => st.opacity = 0.0,
|
||||
}
|
||||
i
|
||||
};
|
||||
let mut s = session();
|
||||
let g = live(&mut s);
|
||||
let lower_fill = set_live_part(&mut s, g, Some((25.0, 50.0)), None, vec![fill(red()), fill(Paint::None)], 1.0);
|
||||
let hidden_fill = set_live_part(&mut s, g, Some((125.0, 50.0)), None, vec![hidden(fill(red()))], 1.0);
|
||||
let clear_fill = set_live_part(&mut s, g, Some((75.0, 50.0)), None, vec![clear(fill(red()))], 1.0);
|
||||
let lower_stroke = set_live_part(&mut s, g, None, Some((0.0, 50.0)), vec![stroke(red(), 2.0), stroke(Paint::None, 1.0)], 1.0);
|
||||
let hidden_stroke = set_live_part(&mut s, g, None, Some((150.0, 50.0)), vec![hidden(stroke(red(), 2.0))], 1.0);
|
||||
let thin_stroke = set_live_part(&mut s, g, None, Some((100.0, 50.0)), vec![stroke(red(), 0.0)], 1.0);
|
||||
let clear_stroke = set_live_part(&mut s, g, None, Some((50.0, 50.0)), vec![clear(stroke(red(), 2.0))], 1.0);
|
||||
let parts = [lower_fill, hidden_fill, clear_fill, lower_stroke, hidden_stroke, thin_stroke, clear_stroke];
|
||||
assert_eq!(parts.iter().collect::<std::collections::HashSet<_>>().len(), parts.len());
|
||||
let d = s.doc().unwrap();
|
||||
let (_, leaves) = vectorcraft_tools::builder::shape_builder_shapes(&d.doc, &[g], true);
|
||||
let taken: Vec<NodeId> = leaves.iter().map(|l| l.id).collect();
|
||||
assert!(taken.contains(&lower_fill) && taken.contains(&lower_stroke), "a painted lower row draws: {taken:?}");
|
||||
for id in [hidden_fill, clear_fill, hidden_stroke, thin_stroke, clear_stroke] {
|
||||
assert!(!taken.contains(&id), "{id:?} draws nothing: {taken:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An edge that closes on itself is a line around its face, not a second shape over it.
|
||||
#[test]
|
||||
fn shape_builder_takes_a_closed_live_paint_edge_as_a_line() {
|
||||
|
||||
@@ -162,8 +162,8 @@ fn collect_members<'a>(n: &'a Node, builder: bool, out: &mut Vec<&'a Node>) {
|
||||
NodeKind::Path { guide: true, .. } | NodeKind::Path { clipping: true, .. } => {}
|
||||
NodeKind::Path { .. } | NodeKind::Compound { .. } => out.push(n),
|
||||
NodeKind::Group { .. } if builder && is_live_paint(n) => {
|
||||
out.extend(faces(n).into_iter().filter(|f| !f.visible || f.locked || !f.appearance.fill_paint().is_none()).map(|f| &**f));
|
||||
out.extend(edges(n).into_iter().filter(|e| !e.visible || e.locked || !e.appearance.stroke_paint().is_none()).map(|e| &**e));
|
||||
out.extend(faces(n).into_iter().filter(|f| !f.visible || f.locked || f.appearance.has_drawn_fill()).map(|f| &**f));
|
||||
out.extend(edges(n).into_iter().filter(|e| !e.visible || e.locked || e.appearance.has_drawn_stroke()).map(|e| &**e));
|
||||
}
|
||||
NodeKind::Group { children, .. } | NodeKind::Layer { children, .. } => {
|
||||
for c in children {
|
||||
@@ -244,10 +244,12 @@ fn shapes_of<'a>(doc: &'a Document, roots: &[NodeId], builder: bool) -> (Vec<po:
|
||||
/// 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) 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`. A Live Paint group
|
||||
/// contributes its filled faces as shapes and its stroked edges as open lines; its unfilled faces
|
||||
/// and its edges without a stroke are left out. Locked objects, and objects in a locked group or
|
||||
/// layer, give nothing.
|
||||
/// they cross, and so do filled open paths without `open_filled_as_closed`. A path, face or edge
|
||||
/// counts as filled or stroked when any of its fill or stroke rows draws
|
||||
/// ([`vectorcraft_doc::Appearance::has_drawn_fill`], [`vectorcraft_doc::Appearance::has_drawn_stroke`]),
|
||||
/// whichever row is on top. A Live Paint group contributes its filled faces as shapes and its
|
||||
/// stroked edges as open lines; its unfilled faces and its edges without a stroke are left out.
|
||||
/// Locked objects, and objects in a locked group or layer, give nothing.
|
||||
pub 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_of(doc, roots, true);
|
||||
for (s, l) in shapes.iter_mut().zip(&leaves) {
|
||||
@@ -257,7 +259,7 @@ pub fn shape_builder_shapes<'a>(doc: &'a Document, roots: &[NodeId], open_filled
|
||||
s.path.subpaths = s.path.subpaths.iter().flat_map(|sp| sp.cut_at(&start)).collect();
|
||||
continue;
|
||||
}
|
||||
if !open_filled_as_closed || l.appearance.fill_paint().is_none() {
|
||||
if !open_filled_as_closed || !l.appearance.has_drawn_fill() {
|
||||
continue;
|
||||
}
|
||||
for sp in s.path.subpaths.iter_mut().filter(|sp| !sp.closed) {
|
||||
|
||||
@@ -364,7 +364,8 @@ impl BuilderMap {
|
||||
}
|
||||
|
||||
/// What a merge needs of each input besides the map: its outline as drawn ([`node_outline`]) and
|
||||
/// whether its fill closes its open subpaths (Consider Open Filled Paths as Closed, and a fill).
|
||||
/// whether its fill closes its open subpaths (Consider Open Filled Paths as Closed, and a fill row
|
||||
/// that draws).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MergeInput {
|
||||
pub outline: Option<PathData>,
|
||||
@@ -375,7 +376,7 @@ pub struct MergeInput {
|
||||
pub fn merge_inputs(leaves: &[&Node], open_filled_as_closed: bool) -> Vec<MergeInput> {
|
||||
leaves
|
||||
.iter()
|
||||
.map(|l| MergeInput { outline: node_outline(l).map(|o| o.0), fill_closes: open_filled_as_closed && !l.appearance.fill_paint().is_none() })
|
||||
.map(|l| MergeInput { outline: node_outline(l).map(|o| o.0), fill_closes: open_filled_as_closed && l.appearance.has_drawn_fill() })
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -10109,7 +10109,8 @@ A canvas-click dialog also has `preview` (initially on): tentative settings draw
|
||||
```
|
||||
|
||||
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
|
||||
`openFilledAsClosed` (on: a filled open path bounds a region as if an invisible edge closed it; a path counts as
|
||||
filled when any of its fill rows is shown, painted and above 0% opacity, whichever row is on top; off: it cuts regions
|
||||
as an unfilled path does), `pickColorFrom` (`artwork`, the default: a merged shape takes the appearance, opacity and
|
||||
blending mode of the front-most selected object under the drag's first point, else of the one under its last point,
|
||||
else of the front-most selected object; `swatches`: the current fill, and the Left and Right arrow keys without
|
||||
|
||||
@@ -1190,6 +1190,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Keep selected in the Pencil and Paintbrush Tool Options is on by default: while it is on, a stroke selects the path it draws or continues and replaces the earlier selection. While it is off, a new stroke leaves nothing selected, the earlier selection included, as the reference application starts the stroke by dropping it (#5752); a continued or redrawn path is deselected while the rest of the selection stays (`path.freehand` `keepSelected`).
|
||||
With two open paths selected, a Pencil stroke from an end of one that ends within Edit selected paths' distance of an end of the other connects them into one path with the first path's appearance, in one undo step, as [Connect two paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/connect-two-paths-with-the-pencil-tool.html) describes (`path.freehand` `connect`).
|
||||
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`).
|
||||
As VectorCraft's own rule, Consider Open Filled Paths as Closed counts an open path as filled when any of its fill rows draws (shown, painted and above 0% opacity), whichever row is on top, as the canvas paints it: a painted fill under an unpainted top fill closes the path, and a painted fill that is hidden or at 0% opacity does not. The region map, merges, erasing and Clicking Stroke Splits the Path follow that rule, and the Shape Builder takes the faces and edges of a Live Paint group that one of their fill or stroke rows draws. Regression tests: `tests::build::shape_builder_closes_open_paths_by_the_fill_rows_that_draw`, `tests::build::shape_builder_stroke_split_closes_open_paths_by_the_fill_rows_that_draw` and `tests::build::shape_builder_takes_the_live_paint_faces_and_edges_that_draw`.
|
||||
With In Merge Mode, Clicking Stroke Splits the Path on in the same dialog, a click on a stroke in merge mode splits its path into the run of stroke under the pointer and the rest of the path, as the [Shape Builder tool options](https://web.archive.org/web/20260105172244/https://helpx.adobe.com/illustrator/using/building-new-shapes-using-shape.html) page describes; the run goes from the anchor or crossing point before the pointer to the one after it, and the pointer shows scissors over it. Highlight Stroke When Editable draws that run in the color chosen in its Color list (Red by default), and with Pick Color From Color Swatches the run takes the current fill as its stroke (`shapeBuilder.merge` `splitStroke`).
|
||||
As VectorCraft's own rule, OK in Shape Builder Tool Options gives an error for a highlight Color with an alpha part (`#rrggbbaa` or `#rgba`) and changes nothing. Regression test: `dialogs::shape_builder_options::tests::a_highlight_color_with_an_alpha_part_is_refused`.
|
||||
With Pick Color From Artwork, a merged shape takes the appearance, opacity and blending mode of the object where the drag starts, else of the object where it ends, else of the front-most selected object, as the [Shape Builder combine page](https://web.archive.org/web/20251006211643/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/combine-shapes-with-shape-builder-tool.html) describes. A click in an area that no selected object covers, such as a hole other shapes enclose, takes the front-most selected object's look (`shapeBuilder.merge` `points`: the first point is where the drag starts, and the last point is where it ends).
|
||||
|
||||
Reference in New Issue
Block a user