Live Paint: a triple-click matches the paint of the face its last clicks painted

A double-click or triple-click compares the paint the face or edge had
before the first of its last clicks that painted it. A first click that
made the Live Paint group counts only when the face it filled is that
face, so clicks that move from one face to another no longer match the
paint under the first click.

With Space held for the view tools, a press, a double-click or a
triple-click with the Live Paint Bucket paints nothing.

The ui.pointer row in docs/control-protocol.md escapes the pipes in its
parameters cell, so the table keeps three columns.
This commit is contained in:
dotCooCoo
2026-11-13 18:19:05 -08:00
parent 33319b2c4d
commit f59381ab59
5 changed files with 83 additions and 22 deletions
+29 -17
View File
@@ -179,7 +179,8 @@ enum LpPaint {
/// paints every edge of the group with the edge's stroke (`scope: "samePaint"`). Each click of a
/// double-click or triple-click paints as it arrives, as its own undo step, and the paint compared
/// is the one the face or edge had before the first of its clicks. Shift and the options choose a
/// face or an edge as for a click; with Alt held a double-click or triple-click paints nothing.
/// face or an edge as for a click; with Alt held a double-click or triple-click paints nothing. With
/// Space held for the view tools, a click, double-click or triple-click paints nothing.
pub struct LivePaintBucketTool {
pub(super) cache: RegionCache,
pub(super) hover: LpHover,
@@ -202,6 +203,8 @@ pub struct LivePaintBucketTool {
struct LpPress {
/// The face or edge; None for the face of the Live Paint group the press makes.
part: Option<NodeId>,
/// Where the press was.
at: Point,
/// Its fill (a face) or stroke paint (an edge) before the press.
before: Paint,
}
@@ -295,20 +298,23 @@ impl LivePaintBucketTool {
self.presses.push(press);
}
/// Note a press that paints face or edge (`edge`) `n`.
fn note_part(&mut self, n: &Node, edge: bool) {
/// Note a press at `at` that paints face or edge (`edge`) `n`.
fn note_part(&mut self, n: &Node, edge: bool, at: Point) {
let before = if edge { n.appearance.stroke_paint() } else { n.appearance.fill_paint() };
self.note_press(Some(LpPress { part: Some(n.id), before }));
self.note_press(Some(LpPress { part: Some(n.id), at, before }));
}
/// The paint `part` had before the first press of a click sequence of `clicks` presses, when
/// those presses all painted `part` (the first may have made the group `part` is in); else
/// `now`.
fn before(&self, part: NodeId, clicks: usize, now: Paint) -> Paint {
/// The paint `part` had before the first of the last presses of a click sequence of `clicks`
/// presses that all painted `part` (the sequence's first press may have made the group `part`
/// is in, when the face it filled there is `part`); `now` when the last press did not paint it.
fn before(&self, cx: &ToolContext, part: NodeId, clicks: usize, now: Paint) -> Paint {
let Some(run) = self.presses.len().checked_sub(clicks).and_then(|from| self.presses.get(from..)) else { return now };
let on_part = |(i, q): (usize, &Option<LpPress>)| q.as_ref().is_some_and(|q| q.part == Some(part) || (i == 0 && q.part.is_none()));
match run.first() {
Some(Some(first)) if run.iter().enumerate().all(on_part) => first.before.clone(),
let made_part = |q: &LpPress| live_paint_at(cx.doc, q.at).and_then(|g| visible_face_at(cx.doc.node(g)?, q.at)).is_some_and(|f| f.id == part);
let on_part =
|(i, q): (usize, &Option<LpPress>)| q.as_ref().is_some_and(|q| q.part == Some(part) || (i == 0 && q.part.is_none() && made_part(q)));
let from = run.len().saturating_sub(run.iter().enumerate().rev().take_while(|&iq| on_part(iq)).count());
match run.get(from) {
Some(Some(first)) => first.before.clone(),
_ => now,
}
}
@@ -316,7 +322,8 @@ impl LivePaintBucketTool {
/// What a double-click (`scope` "connected", 2 `clicks`) or a triple-click ("samePaint", 3) at
/// `p` runs: the command a click there runs, with `scope`, and the paint it compares as `match`.
fn multi_click(&self, cx: &ToolContext, p: Point, mods: Mods, scope: &str, clicks: usize) -> Vec<Action> {
if mods.alt {
// With Space held the press pans, zooms or rotates the view and does not reach the tool.
if mods.alt || mods.space {
return vec![];
}
let point = json!([p.x, p.y]);
@@ -325,7 +332,7 @@ impl LivePaintBucketTool {
&& let Some((g, Some(e))) = lp_part_at(cx.doc, p, cx.tol(4.0), true, &|id| lp_visible(cx, id))
{
let mut v = json!({ "group": g.0, "point": point, "tolerance": cx.tol(4.0), "scope": scope });
if let Ok(like) = serde_json::to_value(self.before(e.id, clicks, e.appearance.stroke_paint())) {
if let Ok(like) = serde_json::to_value(self.before(cx, e.id, clicks, e.appearance.stroke_paint())) {
v["match"] = like;
}
return vec![Action::Exec("livePaint.strokeEdge".into(), v)];
@@ -336,7 +343,7 @@ impl LivePaintBucketTool {
let Some((g, f)) = live_paint_at(cx.doc, p).and_then(|g| Some((g, visible_face_at(cx.doc.node(g)?, p)?))) else { return vec![] };
let mut v = json!({ "group": g.0, "point": point, "scope": scope });
if scope == "samePaint"
&& let Ok(like) = serde_json::to_value(self.before(f.id, clicks, f.appearance.fill_paint()))
&& let Ok(like) = serde_json::to_value(self.before(cx, f.id, clicks, f.appearance.fill_paint()))
{
v["match"] = like;
}
@@ -431,6 +438,11 @@ impl Tool for LivePaintBucketTool {
}
PointerKind::Down => {
self.hover = LpHover::None;
// With Space held the press pans, zooms or rotates the view.
if ev.mods.space {
self.note_press(None);
return vec![];
}
if ev.mods.alt {
self.note_press(None);
if ev.mods.shift {
@@ -459,7 +471,7 @@ impl Tool for LivePaintBucketTool {
if matches!(paint, LpPaint::Strokes | LpPaint::Both)
&& let Some((g, Some(e))) = lp_part_at(cx.doc, p, cx.tol(4.0), true, &|id| lp_visible(cx, id))
{
self.note_part(e, true);
self.note_part(e, true, p);
return vec![Action::Exec("livePaint.strokeEdge".into(), json!({ "group": g.0, "point": point, "tolerance": cx.tol(4.0) }))];
}
if !matches!(paint, LpPaint::Fills | LpPaint::Both) {
@@ -468,14 +480,14 @@ impl Tool for LivePaintBucketTool {
}
if let Some(g) = live_paint_at(cx.doc, p) {
match cx.doc.node(g).and_then(|n| visible_face_at(n, p)) {
Some(f) => self.note_part(f, false),
Some(f) => self.note_part(f, false, p),
None => self.note_press(None),
}
return self.begin_fill(json!({ "group": g.0, "point": point }), p);
}
if self.cache.at(cx, p).is_some() {
let roots = sorted_roots(cx.doc, &cx.selection.objects);
self.note_press(Some(LpPress { part: None, before: made_face_fill(cx.doc, &roots, p) }));
self.note_press(Some(LpPress { part: None, at: p, before: made_face_fill(cx.doc, &roots, p) }));
return self.begin_fill(json!({ "ids": json_ids(&roots), "point": point }), p);
}
self.note_press(None);
+49 -1
View File
@@ -1145,10 +1145,19 @@ fn bucket_double_and_triple_click_widen_what_a_click_paints() {
assert_eq!(t.pointer(&later, &ev(PointerKind::DoubleClick, 51.0, 25.0).with_mods(shift)), stroke("connected", &edge_before));
click(&mut t, &later, 51.0, shift);
assert_eq!(t.pointer(&later, &ev(PointerKind::TripleClick, 51.0, 25.0).with_mods(shift)), stroke("samePaint", &edge_before));
// With Alt held, or with both options off, a double-click or triple-click paints nothing.
// With Alt or Space held, or with both options off, a double-click or triple-click paints nothing.
let alt = Mods { alt: true, ..Default::default() };
let space = Mods { space: true, ..Default::default() };
for k in [PointerKind::DoubleClick, PointerKind::TripleClick] {
assert!(t.pointer(&cx, &ev(k, 75.0, 25.0).with_mods(alt)).is_empty(), "{k:?}");
assert!(t.pointer(&cx, &ev(k, 75.0, 25.0).with_mods(space)).is_empty(), "{k:?} with Space");
assert!(t.pointer(&cx, &ev(k, 51.0, 25.0).with_mods(Mods { shift: true, ..space })).is_empty(), "{k:?} with Shift and Space");
}
// A press with Space held paints nothing either.
for m in [space, Mods { shift: true, ..space }] {
for (k, x) in [(PointerKind::Down, 75.0), (PointerKind::Up, 75.0), (PointerKind::Down, 51.0), (PointerKind::Up, 51.0)] {
assert!(t.pointer(&cx, &ev(k, x, 25.0).with_mods(m)).is_empty(), "{k:?} at {x} with {m:?}");
}
}
t.set_option("paintFills", &json!(false));
for k in [PointerKind::DoubleClick, PointerKind::TripleClick] {
@@ -1206,6 +1215,45 @@ fn bucket_triple_click_on_a_made_group_skips_open_paths() {
assert_eq!(v["match"], serde_json::to_value(&red).unwrap());
}
/// A triple-click whose first click makes the Live Paint group on one face and whose other two
/// clicks fill another face matches the fill the second face had before the first click on it,
/// not the fill under the first click or the fill the clicks gave it.
#[test]
fn bucket_triple_click_after_making_the_group_on_another_face_matches_its_own_face() {
use vectorcraft_color::{Color, Paint};
let (red, blue) = (Paint::solid(Color::rgb(1.0, 0.0, 0.0)), Paint::solid(Color::rgb(0.0, 0.0, 1.0)));
let mut d = Document::new(500.0, 500.0);
let l = d.layers[0].id;
let square = d.alloc_id();
let mut look = Appearance::default_art();
look.set_fill(red.clone());
d.insert(Some(l), 0, Node::path(square, shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 50.0)), look)).unwrap();
let mut s = Selection::default();
s.set([square]);
let p = paint();
let (made, _, f2, _) = live_group_doc();
let mut painted = made.clone();
painted.node_mut(f2).unwrap().appearance.set_fill(blue.clone());
let none = Selection::default();
let (cx, later, after) = (cx(&d, &s, &p), cx(&made, &none, &p), cx(&painted, &none, &p));
let mut t = LivePaintBucketTool::default();
// The first click makes the group and fills the face at (25, 25); the next two fill (75, 25),
// the first of them painting it blue.
t.pointer(&cx, &ev(PointerKind::Down, 25.0, 25.0));
t.pointer(&cx, &ev(PointerKind::Up, 25.0, 25.0));
for ctx in [&later, &after] {
t.pointer(ctx, &ev(PointerKind::Down, 75.0, 25.0));
t.pointer(ctx, &ev(PointerKind::Up, 75.0, 25.0));
}
let acts = t.pointer(&after, &ev(PointerKind::TripleClick, 75.0, 25.0));
let [Action::Exec(id, v)] = acts.as_slice() else { panic!("{acts:?}") };
assert_eq!(id, "livePaint.fill");
let own = serde_json::to_value(made.node(f2).unwrap().appearance.fill_paint()).unwrap();
assert_ne!(own, serde_json::to_value(&red).unwrap());
assert_ne!(own, serde_json::to_value(&blue).unwrap());
assert_eq!(v["match"], own);
}
#[test]
fn bucket_options_choose_what_a_click_paints() {
let (d, gid, ..) = live_group_doc();
+1 -1
View File
@@ -96,7 +96,7 @@ the two can still win that race.
| `ui.menu.list` / `ui.menu.invoke` | `{command, params}` | the full menu tree / invoke an item |
| `ui.contextMenu.list` | | the canvas context menu for the current selection, flattened like `ui.menu.list` (`path` holds its submenus). `ui.click {x, y, button: "right"}` on the canvas opens it, after selecting the object there unless it is already selected. With the Artboard tool it lists the artboard entries (see `docs/mcp.md`), after selecting the artboard there unless it is already selected. While a menu opened over locked objects is open, it has an `Unlock` submenu with one item for each locked object under the pointer, topmost first and at most 20, labeled with the object's name, which runs `layer.setProps {ids: [id], locked: false}`. A locked group gets one item, and the locked objects inside it get none. Hidden objects, objects on hidden, locked or template layers, and in isolation mode objects outside the isolated group get no item |
| `ui.tool.select` / `ui.tool.list` | `{tool}` | |
| `ui.pointer` | `{events:[{kind: down|drag|up|move|doubleclick|tripleclick, x, y, space?: "doc"|"screen", mods?}]}` | drive the active tool exactly like the mouse (`doubleclick` follows the second click's `up`, and `tripleclick` the third's): `mods.cmd` held at a press with any tool but a selection tool drags with the selection tool chosen last (Direct Selection with the Pen until one is), and the release gives the tool back as it was; the reply lists `selection`, `key` (the key object, null without one) and `tool`. With the Shape Builder or the Live Paint Bucket and a selection of more than 128 segments, the app finds the regions off the UI thread, so a `move` replies before the region under the pointer is highlighted, and the highlight shows in the first frame after the regions are found. With the Shape Builder and such a selection, the app also merges off the UI thread: the reply to the events that end a click or drag comes once the merge is applied. A click or drag made before the regions are found does not wait for them; the app records it and makes it once they are found. Until then the canvas keeps drawing, with the touched regions highlighted and the busy pointer. A click or drag sent meanwhile, with `mods.cmd` too, and the Shape Builder's arrow keys are made after the merge in the order sent, and the reply to such a request comes once its events are made and any merge they start is applied; a request of `move` events alone is answered at once. A command that can edit, select, save or redraw the documents (an edit, an undo, a selection, a save or an export, a tool option, another tool, another document, closing, a dialog's preview, a menu item, a command of the app's own such as `ui.recolorDialog`, the proof and separations views, a preference, added fonts) waits for the merge and the input sent before it, and runs after them; queries that only read, such as `document.inspect`, run at once on the document before the merge |
| `ui.pointer` | `{events:[{kind: down\|drag\|up\|move\|doubleclick\|tripleclick, x, y, space?: "doc"\|"screen", mods?}]}` | drive the active tool exactly like the mouse (`doubleclick` follows the second click's `up`, and `tripleclick` the third's): `mods.cmd` held at a press with any tool but a selection tool drags with the selection tool chosen last (Direct Selection with the Pen until one is), and the release gives the tool back as it was; the reply lists `selection`, `key` (the key object, null without one) and `tool`. With the Shape Builder or the Live Paint Bucket and a selection of more than 128 segments, the app finds the regions off the UI thread, so a `move` replies before the region under the pointer is highlighted, and the highlight shows in the first frame after the regions are found. With the Shape Builder and such a selection, the app also merges off the UI thread: the reply to the events that end a click or drag comes once the merge is applied. A click or drag made before the regions are found does not wait for them; the app records it and makes it once they are found. Until then the canvas keeps drawing, with the touched regions highlighted and the busy pointer. A click or drag sent meanwhile, with `mods.cmd` too, and the Shape Builder's arrow keys are made after the merge in the order sent, and the reply to such a request comes once its events are made and any merge they start is applied; a request of `move` events alone is answered at once. A command that can edit, select, save or redraw the documents (an edit, an undo, a selection, a save or an export, a tool option, another tool, another document, closing, a dialog's preview, a menu item, a command of the app's own such as `ui.recolorDialog`, the proof and separations views, a preference, added fonts) waits for the merge and the input sent before it, and runs after them; queries that only read, such as `document.inspect`, run at once on the document before the merge |
| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | synthetic keyboard input |
| `ui.move` | `{x, y}` | screen-point pointer move |
| `ui.click` / `ui.drag` | `{x, y, button?, count?, shift?, alt?, ctrl?, cmd?}` / `{x, y, toX, toY, steps?, count?, button?, shift?, alt?, ctrl?, cmd?}` | egui pointer input; see synthetic input budgets above |
+3 -2
View File
@@ -3530,14 +3530,15 @@ already walks Image Trace. Short agent entry points:
Shift-click runs `livePaint.fill`. With both off, a click runs nothing. With `highlight` off, the face or edge under
the pointer is not outlined.
A double-click runs the command a click there runs with `scope: "connected"`, and a triple-click with
`scope: "samePaint"`; Alt held runs nothing. On a face, `connected` fills the face and every face reached from it
`scope: "samePaint"`; Alt held runs nothing. With Space held (`mods.space`, the view tools), a press, a
double-click or a triple-click runs nothing. On a face, `connected` fills the face and every face reached from it
across edges that draw no stroke (hidden, stroke paint none, or weight 0); a stroked edge, a hidden face, a gap
that Gap Detection closes and the group's outside stop it. `samePaint` fills every face
of the group, joined to it or not, whose fill is the `match` paint. On an edge, `connected` paints the edge and the
edges joined to it end to end whose stroke paint is `match`, and `samePaint` paints every edge of the group with
that stroke paint; the stroke's weight is not compared. Each click of a double-click or triple-click runs as it
arrives and is its own undo step. `match` is the paint the face or edge had before the first of the double-click's
two clicks or the triple-click's three, so the clicks before the last do not change what it matches. Hidden faces and edges are not painted. These rules
two clicks or the triple-click's three that painted it, so the clicks before the last do not change what it matches. Hidden faces and edges are not painted. These rules
for what faces and edges join and what paints match are VectorCraft's own. The help page describes the gestures:
https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html.
`livePaint.fill {scope}` and `livePaint.strokeEdge {scope}` take `scope` as `"one"` (the default), `"connected"` or
+1 -1
View File
@@ -1437,7 +1437,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
Object › Live Paint › Merge adds the other selected paths and Live Paint groups to the bottom selected Live Paint group ([Live Paint groups](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/live-paint-groups.html)), which keeps its id, the paint of its faces and edges, and its name, lock, visibility, transparency, appearance, opacity mask and graphic style link. Object › Live Paint › Make with a Live Paint group selected, and a Live Paint Bucket click on a selected path beside a selected Live Paint group, add the paths to that group the same way. Make, Merge and the Live Paint Bucket take paths, compound paths and groups of them; with type, an image or another object among the selection they report that it has to be converted to paths first and change nothing, as type and raster images can't go into a Live Paint group directly ([Create Live Paint groups](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html)); before, such an object went among the group's hidden sources and vanished from the canvas. Evidence: `tests_build::live_paint_refuses_type_and_images_and_changes_nothing`.
With the Live Paint Bucket selected, Alt-click on vector art samples its fill, stroke and stroke weight into the paint defaults ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)). On a placed image it samples the image pixel as a solid fill with no stroke. Alt+Shift-click samples only the color under the pointer, including an intermediate gradient color or image pixels, into the active fill or stroke ([Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). Neither click paints or changes the selection or selected art, and the next Bucket click paints with the sampled paint. Evidence: `tests_build::live_paint_bucket_alt_sample_paints_next_face_without_changing_selection`.
Double-clicking the Live Paint Bucket opens Live Paint Bucket Options ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)): Paint Fills (on) and Paint Strokes (off) set whether a click paints faces, edges or both, Highlight turns the outline of the face or edge under the pointer on or off, and Color and Width set it (red and 4 px by default). Faces are outlined at the Width and edges at half of it, as the page says faces are highlighted with a thick line and edges with a thin line. With both on, a click paints the edge within 4 px of the pointer, else the face, and Shift switches to Paint Fills only; with one on, Shift switches to the other. The options last across tool switches and are saved with the preferences (`tool.setOption {tool: "livePaintBucket"}`). Evidence: `builder::tests::{bucket_options_choose_what_a_click_paints, bucket_options_set_the_highlight, bucket_highlights_an_edge_thinner_than_a_face}`, `dialogs::live_paint_bucket::tests::double_clicking_the_tool_opens_its_options_and_ok_sets_them`.
Live Paint Bucket double-click and triple-click, as [the help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html) describes them: a double-click on a face fills it and the faces reached across edges that draw no stroke, and on an edge it paints the edges joined to it with the same stroke; a triple-click fills every face of the group with the face's fill, or paints every edge with the edge's stroke (`livePaint.fill` and `livePaint.strokeEdge` `{scope: "connected" | "samePaint", match?}`; `pointer_gesture` and `ui.pointer` take `tripleclick`). Each click paints as it arrives, as its own undo step, and the paint compared is the one the face or edge had before the first of its clicks. Which faces and edges join and which paints match are VectorCraft's own rules. Evidence: `tests_build::{live_paint_bucket_double_click_floods_across_unstroked_edges, live_paint_bucket_double_click_floods_curved_and_narrow_faces, live_paint_bucket_double_click_floods_across_hidden_edges_and_past_gaps, live_paint_bucket_triple_click_fills_every_face_of_the_same_fill, live_paint_bucket_triple_click_after_making_the_group_matches_the_made_fill, live_paint_bucket_double_and_triple_click_paint_joined_and_same_stroke_edges, live_paint_scope_and_match_are_checked}`, `builder::tests::bucket_double_and_triple_click_widen_what_a_click_paints`, `tests::livepaint::a_triple_click_fills_every_face_of_the_same_fill` (MCP).
Live Paint Bucket double-click and triple-click, as [the help page](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html) describes them: a double-click on a face fills it and the faces reached across edges that draw no stroke, and on an edge it paints the edges joined to it with the same stroke; a triple-click fills every face of the group with the face's fill, or paints every edge with the edge's stroke (`livePaint.fill` and `livePaint.strokeEdge` `{scope: "connected" | "samePaint", match?}`; `pointer_gesture` and `ui.pointer` take `tripleclick`). Each click paints as it arrives, as its own undo step, and the paint compared is the one the face or edge had before the first of its clicks. Which faces and edges join and which paints match are VectorCraft's own rules. Evidence: `tests_build::{live_paint_bucket_double_click_floods_across_unstroked_edges, live_paint_bucket_double_click_floods_curved_and_narrow_faces, live_paint_bucket_double_click_floods_across_hidden_edges_and_past_gaps, live_paint_bucket_triple_click_fills_every_face_of_the_same_fill, live_paint_bucket_triple_click_after_making_the_group_matches_the_made_fill, live_paint_bucket_double_and_triple_click_paint_joined_and_same_stroke_edges, live_paint_scope_and_match_are_checked}`, `builder::tests::{bucket_double_and_triple_click_widen_what_a_click_paints, bucket_triple_click_after_making_the_group_on_another_face_matches_its_own_face}`, `tests::livepaint::a_triple_click_fills_every_face_of_the_same_fill` (MCP).
Live Paint Make preserves the front-most covering source's fill when compound paths share a curved boundary, and Expand keeps the face fills. Regression evidence: `planar::tests::coincident_curves_keep_their_source_faces` checks source order, winding, fill rules and nearby boundaries; `tests::build::live_paint_make_and_expand_keep_abutting_compound_colors` checks native compound paths, Make, Expand and Undo/Redo. This preserves VectorCraft's source-fill rule without changing arrangement precision.
Live Paint Gap Options (Object › Live Paint › Gap Options…, `livePaint.gapOptions`): with Gap Detection on, paint stops at the gaps that run from a loose end of a path to the nearest path, up to Small (3 pt), Medium (6 pt), Large (12 pt) or a custom size; Close Gaps With Paths adds an unpainted path across each gap; Preview applies the options live and draws the gaps in the Gap Preview Color. The options are saved, copied and undone with the group, and Merge keeps the bottom group's options when its Gap Detection is on, or else turns Gap Detection on with Small when another merged group has it on. When Gap Detection is off in every merged group, it stays off.
View › Show Live Paint Gaps (`view.livePaintGaps`) draws the gaps of the selected Live Paint groups up to their Paint Stops At size, each in its group's Gap Preview Color, until Hide Live Paint Gaps or Hide Edges hides them ([Live Paint gaps](https://web.archive.org/web/20260105111201/https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/find-and-close-gaps-in-live-paint-groups.html)).