Live Paint: Live Paint Bucket Options set what a click paints and the highlight

Double-clicking the Live Paint Bucket did nothing, and the tool always
filled on a click, stroked on Shift-click and highlighted in red at
4 px. It now opens Live Paint Bucket Options with Paint Fills, Paint
Strokes, Highlight, Color and Width.

- Paint Fills (on) and Paint Strokes (off) set what a click paints.
  With Paint Strokes alone, a click strokes the edge within 4 px of the
  pointer. With both on, a click strokes an edge within 4 px and
  otherwise fills the face. With both off, a click paints nothing.
- Shift switches Paint Fills alone to Paint Strokes alone and back, and
  switches both to Paint Fills alone. With the defaults, Shift-click
  strokes the edge as before.
- Highlight turns the outline of the face or edge under the pointer on
  or off; Color (a layer color or a custom color) and Width (1 to 20 px)
  set it. With Paint Strokes alone, a selection of ordinary paths no
  longer highlights a region, since a click there paints nothing.
- The options are tool options (tool.setOption {tool:
  "livePaintBucket"}): they last across tool switches and are saved
  with the preferences.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-01 08:32:57 +07:00
committed by bflatastic
parent ece78f052f
commit f07b9ab765
36 changed files with 460 additions and 18 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ use cache::RegionCache;
#[cfg(test)]
use live_paint::LpHover;
use live_paint::lp_color;
pub use live_paint::{LP_SELECT_WIDTH, LP_SELECT_WIDTH_RANGE, LivePaintBucketTool, LivePaintSelectionTool};
pub use live_paint::{LP_BUCKET_WIDTH, LP_BUCKET_WIDTH_RANGE, LP_SELECT_WIDTH, LP_SELECT_WIDTH_RANGE, LivePaintBucketTool, LivePaintSelectionTool};
pub use lookup::{
EDGE_NAME, FACE_NAME, Face, LIVE_PAINT_NAME, LIVE_PAINT_PREFIX, SOURCES_NAME, builder_members, drawn_outlines, edge_near, edges, face_at, faces,
is_live_paint, leaf_shapes, live_paint_at, live_paint_takes, node_outline, outline_snap, path_contains, shape_builder_shapes, shapes_for,
+113 -16
View File
@@ -86,20 +86,91 @@ fn lp_overlays(h: &LpHover, cache: &RegionCache, color: [u8; 3], width: f32) ->
}
}
/// Live Paint Bucket: click fills the face under the cursor with the current fill; Shift-click
/// paints the nearest edge with the current stroke. Alt-click samples a fill or stroke into the
/// paint defaults; Alt+Shift-click samples only the color under the pointer. Clicking selected
/// ordinary paths without Alt makes them a Live Paint group first.
/// The Live Paint Bucket's highlight width (screen pixels) by default.
pub const LP_BUCKET_WIDTH: f64 = 4.0;
/// The least and greatest highlight width (screen pixels) the Live Paint Bucket takes.
pub const LP_BUCKET_WIDTH_RANGE: (f64, f64) = (1.0, 20.0);
/// What a Live Paint Bucket click paints.
#[derive(Clone, Copy, Debug, PartialEq)]
enum LpPaint {
Nothing,
/// The face under the pointer.
Fills,
/// The edge nearest the pointer.
Strokes,
/// The edge nearest the pointer when one is in reach, else the face under the pointer.
Both,
}
/// Live Paint Bucket: by default, a click fills the face under the cursor with the current fill,
/// and Shift-click paints the nearest edge with the current stroke. Alt-click samples a fill or
/// stroke into the paint defaults; Alt+Shift-click samples only the color under the pointer.
/// Clicking selected ordinary paths without Alt makes them a Live Paint group first. Its Tool
/// Options (double-click the tool) are `paintFills` (on) and `paintStrokes` (off), `highlight`
/// (on), the highlight `color` (`#rrggbb`, red by default) and its `width` ([`LP_BUCKET_WIDTH`]
/// pixels). With Paint Strokes alone on, a click paints the edge nearest the pointer and
/// Shift-click fills the face. With both on, a click paints the edge nearest the pointer when one
/// is within 4 pixels, else it fills the face, and Shift-click fills the face. With both off, a
/// click paints nothing. With Highlight off, the face or edge under the pointer is not
/// highlighted.
pub struct LivePaintBucketTool {
pub(super) cache: RegionCache,
pub(super) hover: LpHover,
/// Where the pointer last moved, and whether Shift and Alt were held.
at: Option<(Point, bool, bool)>,
fills: bool,
strokes: bool,
highlight: bool,
color: [u8; 3],
width: f64,
}
impl Default for LivePaintBucketTool {
fn default() -> Self {
Self { cache: RegionCache::live_paint(), hover: LpHover::None, at: None }
Self {
cache: RegionCache::live_paint(),
hover: LpHover::None,
at: None,
fills: true,
strokes: false,
highlight: true,
color: HIGHLIGHT_RED,
width: LP_BUCKET_WIDTH,
}
}
}
impl LivePaintBucketTool {
/// What a click paints, Shift held or not. Shift switches Paint Fills alone to Paint Strokes
/// alone and back, and switches both to Paint Fills alone.
fn paints(&self, shift: bool) -> LpPaint {
match (self.fills, self.strokes, shift) {
(false, false, _) => LpPaint::Nothing,
(true, true, false) => LpPaint::Both,
(true, false, false) | (_, true, true) => LpPaint::Fills,
(true, false, true) | (false, true, false) => LpPaint::Strokes,
}
}
/// The highlight at `p` for a pointer with Shift and Alt held as given: none with Alt or with
/// Highlight off, else the face or edge a click there paints.
fn hover_at(&mut self, cx: &ToolContext, p: Point, shift: bool, alt: bool) -> LpHover {
if alt || !self.highlight {
return LpHover::None;
}
let takes = |id| lp_visible(cx, id);
match self.paints(shift) {
LpPaint::Nothing => LpHover::None,
LpPaint::Fills => lp_hover(cx, Some(&mut self.cache), p, false, &takes),
// A click paints no region of the selection, so none is highlighted.
LpPaint::Strokes => lp_hover(cx, None, p, true, &takes),
LpPaint::Both => match lp_part_at(cx.doc, p, cx.tol(4.0), true, &takes) {
Some((_, Some(e))) => e.path_data().map(|pd| LpHover::Part(pd.to_bezpath())).unwrap_or_default(),
_ => lp_hover(cx, Some(&mut self.cache), p, false, &takes),
},
}
}
}
@@ -113,8 +184,7 @@ impl Tool for LivePaintBucketTool {
match ev.kind {
PointerKind::Move => {
self.at = Some((p, ev.mods.shift, ev.mods.alt));
self.hover =
if ev.mods.alt { LpHover::None } else { lp_hover(cx, Some(&mut self.cache), p, ev.mods.shift, &|id| lp_visible(cx, id)) };
self.hover = self.hover_at(cx, p, ev.mods.shift, ev.mods.alt);
vec![]
}
PointerKind::Down => {
@@ -142,13 +212,14 @@ impl Tool for LivePaintBucketTool {
});
}
let point = json!([p.x, p.y]);
if ev.mods.shift {
return match lp_part_at(cx.doc, p, cx.tol(4.0), true, &|id| lp_visible(cx, id)) {
Some((g, Some(_))) => {
vec![Action::Exec("livePaint.strokeEdge".into(), json!({ "group": g.0, "point": point, "tolerance": cx.tol(4.0) }))]
}
_ => vec![],
};
let paint = self.paints(ev.mods.shift);
if matches!(paint, LpPaint::Strokes | LpPaint::Both)
&& let Some((g, Some(_))) = lp_part_at(cx.doc, p, cx.tol(4.0), true, &|id| lp_visible(cx, id))
{
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) {
return vec![];
}
if let Some(g) = live_paint_at(cx.doc, p) {
return vec![Action::Exec("livePaint.fill".into(), json!({ "group": g.0, "point": point }))];
@@ -164,13 +235,39 @@ impl Tool for LivePaintBucketTool {
}
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
lp_overlays(&self.hover, &self.cache, HIGHLIGHT_RED, 4.0)
lp_overlays(&self.hover, &self.cache, self.color, self.width as f32)
}
fn cursor(&self, _cx: &ToolContext, _p: Point, m: Mods) -> Cursor {
if m.alt { Cursor::Eyedropper } else { Cursor::LivePaintBucket }
}
fn options(&self) -> Value {
let [r, g, b] = self.color;
json!({
"paintFills": self.fills,
"paintStrokes": self.strokes,
"highlight": self.highlight,
"color": format!("#{r:02x}{g:02x}{b:02x}"),
"width": self.width,
})
}
fn set_option(&mut self, key: &str, v: &Value) {
match key {
"paintFills" => self.fills = v.as_bool().unwrap_or(self.fills),
"paintStrokes" => self.strokes = v.as_bool().unwrap_or(self.strokes),
"highlight" => self.highlight = v.as_bool().unwrap_or(self.highlight),
"color" => self.color = lp_color(v).unwrap_or(self.color),
"width" => {
let (min, max) = LP_BUCKET_WIDTH_RANGE;
self.width = v.as_f64().filter(|w| w.is_finite()).map_or(self.width, |w| w.clamp(min, max));
}
_ => return,
}
self.hover = LpHover::None;
}
fn waiting(&self) -> bool {
self.cache.waiting()
}
@@ -184,7 +281,7 @@ impl Tool for LivePaintBucketTool {
loop {
self.cache.settle(wait);
if let Some((p, shift, alt)) = self.at {
self.hover = if alt { LpHover::None } else { lp_hover(cx, Some(&mut self.cache), p, shift, &|id| lp_visible(cx, id)) };
self.hover = self.hover_at(cx, p, shift, alt);
}
if !wait || !self.cache.waiting() {
break;
+100
View File
@@ -765,6 +765,106 @@ fn bucket_click_fills_face_and_shift_click_strokes_edge() {
assert_eq!(id, "livePaint.strokeEdge");
}
#[test]
fn bucket_options_choose_what_a_click_paints() {
let (d, gid, ..) = live_group_doc();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let shift = Mods { shift: true, ..Default::default() };
let fill = |x: f64| vec![Action::Exec("livePaint.fill".into(), json!({ "group": gid.0, "point": [x, 25.0] }))];
let stroke = |x: f64| vec![Action::Exec("livePaint.strokeEdge".into(), json!({ "group": gid.0, "point": [x, 25.0], "tolerance": 4.0 }))];
let click = |t: &mut LivePaintBucketTool, x: f64, m: Mods| t.pointer(&cx, &ev(PointerKind::Down, x, 25.0).with_mods(m));
let hovered = |t: &mut LivePaintBucketTool, x: f64, m: Mods| {
t.pointer(&cx, &ev(PointerKind::Move, x, 25.0).with_mods(m));
match t.overlays(&cx).as_slice() {
[] => None,
[Overlay::Path { path, .. }] => Some(path.bounding_box()),
ov => panic!("{ov:?}"),
}
};
let (face, edge) = (Some(Rect::new(50.0, 0.0, 100.0, 50.0)), Some(Rect::new(50.0, 0.0, 50.0, 50.0)));
let mut t = LivePaintBucketTool::default();
assert_eq!(t.options(), json!({"paintFills": true, "paintStrokes": false, "highlight": true, "color": "#ff2a2a", "width": 4.0}));
// Paint Fills alone: 1 pt from the edge, a click fills the face and Shift-click strokes the edge.
assert_eq!((click(&mut t, 51.0, Mods::default()), hovered(&mut t, 51.0, Mods::default())), (fill(51.0), face));
assert_eq!((click(&mut t, 51.0, shift), hovered(&mut t, 51.0, shift)), (stroke(51.0), edge));
// Paint Strokes alone: a click strokes the edge in reach and paints nothing farther away;
// Shift-click fills the face.
t.set_option("paintFills", &json!(false));
t.set_option("paintStrokes", &json!(true));
assert_eq!((click(&mut t, 51.0, Mods::default()), hovered(&mut t, 51.0, Mods::default())), (stroke(51.0), edge));
assert_eq!((click(&mut t, 75.0, Mods::default()), hovered(&mut t, 75.0, Mods::default())), (vec![], None));
assert_eq!((click(&mut t, 51.0, shift), hovered(&mut t, 51.0, shift)), (fill(51.0), face));
// Both: a click strokes the edge in reach, else fills the face; Shift-click fills the face.
t.set_option("paintFills", &json!(true));
assert_eq!((click(&mut t, 51.0, Mods::default()), hovered(&mut t, 51.0, Mods::default())), (stroke(51.0), edge));
assert_eq!((click(&mut t, 75.0, Mods::default()), hovered(&mut t, 75.0, Mods::default())), (fill(75.0), face));
assert_eq!((click(&mut t, 51.0, shift), hovered(&mut t, 51.0, shift)), (fill(51.0), face));
// Both off: nothing is painted or highlighted.
t.set_option("paintFills", &json!(false));
t.set_option("paintStrokes", &json!(false));
for m in [Mods::default(), shift] {
assert_eq!((click(&mut t, 51.0, m), hovered(&mut t, 51.0, m)), (vec![], None));
}
// A value that is not true or false is ignored.
t.set_option("paintFills", &json!("yes"));
assert_eq!(t.options()["paintFills"], false);
}
#[test]
fn bucket_strokes_alone_paints_and_highlights_no_region_of_the_selection() {
let (d, a, b) = two_rects();
let mut s = Selection::default();
s.set([a, b]);
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = LivePaintBucketTool::default();
t.pointer(&cx, &ev(PointerKind::Move, 75.0, 50.0));
t.poll(&cx, true);
assert!(!t.overlays(&cx).is_empty(), "Paint Fills highlights the region");
t.set_option("paintFills", &json!(false));
t.set_option("paintStrokes", &json!(true));
t.pointer(&cx, &ev(PointerKind::Move, 75.0, 50.0));
t.poll(&cx, true);
assert!(t.overlays(&cx).is_empty());
assert!(t.pointer(&cx, &ev(PointerKind::Down, 75.0, 50.0)).is_empty());
}
#[test]
fn bucket_options_set_the_highlight() {
let (d, ..) = live_group_doc();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let mut t = LivePaintBucketTool::default();
let highlight = |t: &mut LivePaintBucketTool| {
t.pointer(&cx, &ev(PointerKind::Move, 75.0, 25.0));
match t.overlays(&cx).as_slice() {
[] => None,
[Overlay::Path { color, width, .. }] => Some((*color, *width)),
ov => panic!("{ov:?}"),
}
};
assert_eq!(highlight(&mut t), Some((HIGHLIGHT_RED, 4.0)));
t.set_option("color", &json!("#00FF00"));
t.set_option("width", &json!(9));
assert_eq!(highlight(&mut t), Some(([0, 0xff, 0], 9.0)));
t.set_option("color", &json!("light blue"));
t.set_option("width", &json!("wide"));
assert_eq!(highlight(&mut t), Some(([0x4f, 0x80, 0xff], 9.0)));
t.set_option("width", &json!(100));
assert_eq!(t.options()["width"], json!(LP_BUCKET_WIDTH_RANGE.1));
t.set_option("width", &json!(0));
assert_eq!(t.options()["width"], json!(LP_BUCKET_WIDTH_RANGE.0));
// Highlight off: nothing is highlighted, and a click still paints.
t.set_option("highlight", &json!(false));
assert_eq!(highlight(&mut t), None);
assert_eq!(t.pointer(&cx, &ev(PointerKind::Down, 75.0, 25.0)).len(), 1);
t.set_option("highlight", &json!(true));
assert!(highlight(&mut t).is_some());
}
#[test]
fn bucket_alt_samples_face_edge_and_selected_region_without_painting() {
let (mut d, _gid, face, edge) = live_group_doc();
+2
View File
@@ -45,6 +45,8 @@ const ROWS: &[Row] = &[
(&["blobBrush", "eraser"], None, &["angle", "roundness"]),
// Live Paint Selection Tool Options (`builder::LivePaintSelectionTool`).
(&["livePaintSelection"], None, &["selectFills", "selectStrokes", "color", "width"]),
// Live Paint Bucket Options (`builder::LivePaintBucketTool`).
(&["livePaintBucket"], None, &["paintFills", "paintStrokes", "highlight", "color", "width"]),
(&["polygon"], None, &["sides"]),
(&["star"], None, &["points"]),
(&["freeTransform"], None, &["constrain"]),
+2
View File
@@ -56,6 +56,7 @@ pub mod justification;
pub mod layer_options;
pub mod layers_panel_options;
pub mod liquify;
pub mod live_paint_bucket;
pub mod live_paint_gaps;
pub mod live_paint_selection;
pub mod missing_fonts;
@@ -389,6 +390,7 @@ registry! {
BlendOptions: [blend_options::KIND] => blend_options::SPEC,
LivePaintGaps: [live_paint_gaps::KIND] => live_paint_gaps::SPEC,
LivePaintSelectionOptions: [live_paint_selection::KIND] => live_paint_selection::SPEC,
LivePaintBucketOptions: [live_paint_bucket::KIND] => live_paint_bucket::SPEC,
BrushOptions: [brush_options::KIND] => brush_options::SPEC,
NewBrush: [new_brush::KIND] => new_brush::SPEC,
EditSelection: [edit_selection::KIND] => edit_selection::SPEC,
@@ -0,0 +1,151 @@
//! Live Paint Bucket Options (double-click the Live Paint Bucket tool): what a click with the tool
//! paints and how the face or edge under the pointer highlights. The options last across tool
//! switches and are saved with the preferences. OK sets them with
//! `tool.setOption {tool: "livePaintBucket", values}`.
//!
//! Fields: `paintFills` and `paintStrokes` (the faces and the edges of Live Paint groups),
//! `highlight` (whether the face or edge under the pointer is outlined), `color` (the highlight
//! color: `#rrggbb`, or a layer color's name such as "Red") and `width` (the highlight's width in
//! screen pixels).
use serde_json::{Map, Value, json};
use vectorcraft_color::Color;
use vectorcraft_doc::LAYER_COLORS;
use vectorcraft_tools::builder::LP_BUCKET_WIDTH_RANGE;
use super::tile_edge_color::resolve;
use super::{DialogSpec, form, run_and_close};
use crate::state::Dialog;
use crate::{VectorcraftApp, widgets};
/// The dialog kind of the Live Paint Bucket Options.
pub const KIND: &str = "livePaintBucketOptions";
/// The tool the dialog serves.
const TOOL: &str = "livePaintBucket";
/// The options the dialog edits.
const KEYS: [&str; 5] = ["paintFills", "paintStrokes", "highlight", "color", "width"];
/// Width of the label column.
const LABEL_W: f32 = 60.0;
pub(super) const SPEC: DialogSpec = DialogSpec { heading, body, confirm, min_width: 300.0, ..DialogSpec::FORM };
fn heading(_: &Dialog) -> String {
let label = vectorcraft_tools::tool_info(TOOL).map_or("", |t| t.label);
crate::i18n::fmt(tl!("{tool} Options"), &[("tool", tl!(label))])
}
/// Open the Live Paint Bucket Options with the tool's current values.
pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
let o = app.session.tool_options_of(TOOL);
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 });
Ok(json!({ "dialog": KIND }))
}
fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
form::check(ui, d, "paintFills", tl!("Paint Fills"));
form::check(ui, d, "paintStrokes", tl!("Paint Strokes"));
ui.add_space(6.0);
form::check(ui, d, "highlight", tl!("Highlight"));
let color = resolve(&d.str("color")).unwrap_or(Color::BLACK);
let [r, g, b, _] = color.to_rgba8(1.0);
let preset = LAYER_COLORS.iter().position(|(_, c)| *c == [r, g, b]);
let names: Vec<&str> = LAYER_COLORS.iter().map(|(n, _)| *n).chain(["Custom"]).collect();
ui.add_enabled_ui(d.bool("highlight"), |ui| {
widgets::label_row(ui, tl!("Color:"), LABEL_W, |ui| {
if let Some((_, [r, g, b])) =
widgets::dropdown(ui, "lp-bucket-color", names[preset.unwrap_or(LAYER_COLORS.len())], &names, 130.0).and_then(|i| LAYER_COLORS.get(i))
{
d.fields.insert("color".into(), json!(Color::rgb8(*r, *g, *b).to_hex()));
}
// The color button sets any other color; the dropdown then shows Custom.
let mut rgb = [r, g, b];
if ui.color_edit_button_srgb(&mut rgb).changed() {
d.fields.insert("color".into(), json!(Color::rgb8(rgb[0], rgb[1], rgb[2]).to_hex()));
}
});
let (min, max) = LP_BUCKET_WIDTH_RANGE;
let v = d.f64("width", min);
widgets::label_row(ui, tl!("Width:"), LABEL_W, |ui| {
if let Some(x) = widgets::range_field(ui, "lp-bucket-width", v, min..=max, "px", 0, 72.0) {
d.fields.insert("width".into(), json!(x));
}
});
});
false
}
/// OK: set the tool'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": TOOL, "values": values }))
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
use vectorcraft_tools::{Overlay, PointerEvent, PointerKind};
/// 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_sets_them() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
// Two squares side by side, made a Live Paint group: two faces and the edge between them.
for x in [0, 100] {
app.run("shape.rectangle", json!({"x": 50 + x, "y": 50, "width": 100, "height": 100})).unwrap();
}
app.run("select.all", json!({})).unwrap();
app.run("livePaint.make", json!({})).unwrap();
app.run("select.set", json!({"ids": []})).unwrap();
app.select_tool(TOOL);
let hover = |app: &mut VectorcraftApp, x: f64| {
let view = app.view_info();
crate::canvas::dispatch(app, &PointerEvent::new(PointerKind::Move, x, 100.0), view);
app.session.overlays(view)
};
let ov = hover(&mut app, 151.0);
assert!(ov.iter().any(|o| matches!(o, Overlay::Path { color: [0xff, 0x2a, 0x2a], width: 4.0, .. })), "{ov:?}");
crate::toolbar::open_options(&mut app, TOOL).unwrap();
let text = painted(&mut app);
for label in ["Live Paint Bucket Options", "Paint Fills", "Paint Strokes", "Highlight", "Color:", "Width:"] {
assert!(text.contains(label), "{label} in {text}");
}
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!(d.kind, KIND);
assert_eq!(
(d.bool("paintFills"), d.bool("paintStrokes"), d.bool("highlight"), d.str("color"), d.f64("width", 0.0)),
(true, false, true, "#ff2a2a".to_string(), 4.0)
);
d.fields.insert("paintFills".into(), json!(false));
d.fields.insert("paintStrokes".into(), json!(true));
d.fields.insert("color".into(), json!("Green"));
d.fields.insert("width".into(), json!(8));
super::super::confirm(&mut app).unwrap();
assert!(app.ui.dialog.is_none());
// The tool takes them: by the edge, the edge highlights in the new color and width.
let ov = hover(&mut app, 151.0);
assert!(ov.iter().any(|o| matches!(o, Overlay::Path { color: [0x4f, 0xff, 0x4f], width: 8.0, .. })), "{ov:?}");
// They last while another tool is chosen, and the dialog opens on them again.
app.select_tool("selection");
let o = app.session.tool_options_of(TOOL);
assert_eq!((o["paintFills"].as_bool(), o["paintStrokes"].as_bool(), o["color"].as_str()), (Some(false), Some(true), Some("#4fff4f")));
app.run("tool.options", json!({"tool": TOOL})).unwrap();
let d = app.ui.dialog.as_mut().unwrap();
assert_eq!((d.bool("paintStrokes"), d.str("color")), (true, "#4fff4f".to_string()));
// Highlight off: the tool outlines nothing under the pointer.
d.fields.insert("highlight".into(), json!(false));
super::super::confirm(&mut app).unwrap();
app.select_tool(TOOL);
assert!(!hover(&mut app, 151.0).iter().any(|o| matches!(o, Overlay::Path { .. })));
}
}
+3
View File
@@ -1230,6 +1230,9 @@ font-technology Variable Променливи
Hide Live Paint Gaps Скриване на пролуките на динамичното оцветяване
Select Fills Избиране на запълвания
Select Strokes Избиране на щрихове
Paint Fills Боядисване на запълвания
Paint Strokes Боядисване на щрихове
Highlight Маркиране
Gaussian Blur… Гаусово размазване…
General Общи
General › Placement › X Общи › Разположение › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -267,6 +267,9 @@
Hide Live Paint Gaps Sakrij praznine živog bojenja
Select Fills Odaberi ispune
Select Strokes Odaberi poteze
Paint Fills Boji ispune
Paint Strokes Boji poteze
Highlight Isticanje
Image Trace Praćenje slike
Image Trace (Make and Expand) Praćenje slike (napravi i proširi)
Release Image Trace Oslobodi praćenje slike
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1859,6 +1859,7 @@ digits Malayalam Malaiàlam
High Quality Print Impressió d’alta qualitat
High Resolution Alta resolució
High resolution Alta resolució
Highlight Ressaltat
Highlight Alternate Glyphs Ressalta els glifs alternatius
Highlight anchors on mouse over Ressalta els punts d’ancoratge en passar el ratolí
Highlight Area Ressalta l’àrea
@@ -2844,8 +2845,10 @@ digits Malayalam Malaiàlam
Pages Pàgines
Paint Daubs Taques de pintura
Paint Daubs… Taques de pintura…
Paint Fills Pinta els farciments
Paint Opacity: Opacitat de pintura:
Paint Stops At: La pintura s’atura a:
Paint Strokes Pinta els traços
Paintbrush Pinzell
Paintbrush Tool Eina de pinzell
Palette Knife Espàtula
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -277,6 +277,9 @@
Hide Live Paint Gaps Skrýt mezery živé malby
Select Fills Vybírat výplně
Select Strokes Vybírat tahy
Paint Fills Malovat výplně
Paint Strokes Malovat tahy
Highlight Zvýraznění
Image Trace Vektorizace obrazu
Image Trace (Make and Expand) Vektorizace obrazu (vytvořit a rozložit)
Release Image Trace Uvolnit vektorizaci obrazu
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5312,6 +5312,9 @@ swatch Bright Violet Kraftig violet
Close Gaps With Paths Luk huller med stier
Select Fills Markér udfyldninger
Select Strokes Markér konturer
Paint Fills Mal udfyldninger
Paint Strokes Mal konturer
Highlight Fremhævning
Column Type: Søjletype:
Each Design Represents: Hvert design repræsenterer:
Event: Hændelse:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1254,6 +1254,9 @@ file-filter Templates Vorlagen
Hide Live Paint Gaps Lücken beim interaktiven Malen ausblenden
Select Fills Flächen auswählen
Select Strokes Konturen auswählen
Paint Fills Flächen malen
Paint Strokes Konturen malen
Highlight Hervorheben
Gaussian Blur… Gaußscher Weichzeichner…
General Allgemein
General › Placement › X Allgemein › Platzierung › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1196,6 +1196,9 @@ font-technology Variable Μεταβλητές
Hide Live Paint Gaps Απόκρυψη διακένων δυναμικής βαφής
Select Fills Επιλογή γεμισμάτων
Select Strokes Επιλογή περιγραμμάτων
Paint Fills Βαφή γεμισμάτων
Paint Strokes Βαφή περιγραμμάτων
Highlight Επισήμανση
Gaussian Blur… Θόλωση Γκάους…
General Γενικά
General › Placement › X Γενικά › Τοποθέτηση › X
Can't render this file because it contains an unexpected character in line 164 and column 5.
+3
View File
@@ -1267,6 +1267,9 @@ font-technology Variable Variables
Hide Live Paint Gaps Ocultar huecos de pintura interactiva
Select Fills Seleccionar rellenos
Select Strokes Seleccionar trazos
Paint Fills Pintar rellenos
Paint Strokes Pintar trazos
Highlight Resaltado
Gaussian Blur… Desenfoque gaussiano…
General General
General › Placement › X General › Colocación › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -284,6 +284,9 @@
Hide Live Paint Gaps Piilota elävän maalauksen aukot
Select Fills Valitse täytöt
Select Strokes Valitse viivat
Paint Fills Maalaa täytöt
Paint Strokes Maalaa viivat
Highlight Korostus
Image Trace Kuvan jäljitys
Image Trace (Make and Expand) Kuvan jäljitys (luo ja laajenna)
Release Image Trace Vapauta kuvan jäljitys
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1267,6 +1267,9 @@ font-technology Variable Variables
Hide Live Paint Gaps Masquer les espaces de peinture dynamique
Select Fills Sélectionner les fonds
Select Strokes Sélectionner les contours
Paint Fills Peindre les fonds
Paint Strokes Peindre les contours
Highlight Mise en évidence
Gaussian Blur… Flou gaussien…
General Général
General › Placement › X Général › Placement › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1584,6 +1584,9 @@ file-filter Templates Sablonok
Hide Live Paint Gaps Élő festés hézagainak elrejtése
Select Fills Kitöltések kijelölése
Select Strokes Körvonalak kijelölése
Paint Fills Kitöltések festése
Paint Strokes Körvonalak festése
Highlight Kiemelés
Gaussian Blur Gauss-elmosás
Gaussian Blur… Gauss-elmosás…
General Általános
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5312,6 +5312,9 @@ swatch Bright Violet Violet cerah
Close Gaps With Paths Tutup celah dengan jalur
Select Fills Pilih isian
Select Strokes Pilih stroke
Paint Fills Cat isian
Paint Strokes Cat stroke
Highlight Sorotan
Column Type: Jenis kolom:
Each Design Represents: Setiap desain mewakili:
Event: Peristiwa:
Can't render this file because it contains an unexpected character in line 2190 and column 5.
+3
View File
@@ -1277,6 +1277,9 @@ file-filter Templates Modelli
Hide Live Paint Gaps Nascondi spazi vuoti pittura dinamica
Select Fills Seleziona riempimenti
Select Strokes Seleziona tracce
Paint Fills Colora riempimenti
Paint Strokes Colora tracce
Highlight Evidenziazione
Gaussian Blur… Sfocatura gaussiana…
General Generale
General › Placement › X Generale › Posizionamento › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -292,6 +292,9 @@ digits Malayalam マラヤーラム数字
Hide Live Paint Gaps ライブペイントの間隔を隠す
Select Fills 塗りを選択
Select Strokes 線を選択
Paint Fills 塗りをペイント
Paint Strokes 線をペイント
Highlight ハイライト
Image Trace 画像トレース
Image Tracing 画像トレースオブジェクト
Tracing Presets トレースプリセット
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5339,6 +5339,9 @@ imageTrace Outline კონტური
Scale in Perspective პერსპექტივაში მასშტაბირება
Select Fills შევსებების მონიშვნა
Select Strokes კონტურების მონიშვნა
Paint Fills შევსებების შეღებვა
Paint Strokes კონტურების შეღებვა
Highlight გამოკვეთა
Set History Limit… ისტორიის ლიმიტის დაყენება…
Shape Builder Delete ფიგურების კონსტრუქტორით წაშლა
Sliding სრიალა
Can't render this file because it is too large.
+3
View File
@@ -285,6 +285,9 @@ digits Malayalam 말라얄람어
Hide Live Paint Gaps 라이브 페인트 간격 숨기기
Select Fills 칠 선택
Select Strokes 선 선택
Paint Fills 칠 페인트
Paint Strokes 선 페인트
Highlight 강조 표시
Image Trace 이미지 추적
Image Tracing 이미지 추적
Tracing Presets 추적 프리셋
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -5323,6 +5323,9 @@ swatch Bright Violet Felviolet
Close Gaps With Paths Openingen sluiten met paden
Select Fills Vullingen selecteren
Select Strokes Lijnen selecteren
Paint Fills Vullingen verven
Paint Strokes Lijnen verven
Highlight Markering
Column Type: Kolomtype:
Each Design Represents: Elk ontwerp staat voor:
Event: Gebeurtenis:
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -242,6 +242,9 @@
Hide Live Paint Gaps Ukryj przerwy aktywnego malowania
Select Fills Zaznaczaj wypełnienia
Select Strokes Zaznaczaj obrysy
Paint Fills Maluj wypełnienia
Paint Strokes Maluj obrysy
Highlight Podświetlenie
Image Trace Obrys obrazu
Text Wrap Oblewanie tekstem
Text Wrap Options… Opcje oblewania tekstem…
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1116,6 +1116,9 @@
Hide Live Paint Gaps Ocultar Lacunas da Pintura ao Vivo
Select Fills Selecionar Preenchimentos
Select Strokes Selecionar Traçados
Paint Fills Pintar Preenchimentos
Paint Strokes Pintar Traçados
Highlight Destaque
Gaussian Blur… Desfoque Gaussiano…
General Geral
Generate CSS Gerar CSS
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3
View File
@@ -1275,6 +1275,9 @@ file-filter Templates Шаблоны
Hide Live Paint Gaps Скрыть зазоры быстрой заливки
Select Fills Выделять заливки
Select Strokes Выделять обводки
Paint Fills Рисовать заливки
Paint Strokes Рисовать обводки
Highlight Подсветка
Gaussian Blur… Размытие по Гауссу…
General Общие
General › Placement › X Общие › Размещение › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1232,6 +1232,9 @@ font-technology Variable Variabla
Hide Live Paint Gaps Dölj mellanrum i direktmålning
Select Fills Markera fyllningar
Select Strokes Markera konturer
Paint Fills Måla fyllningar
Paint Strokes Måla konturer
Highlight Framhävning
Gaussian Blur… Gaussisk oskärpa…
General Allmänt
General › Placement › X Allmänt › Placering › X
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -5364,6 +5364,9 @@ theme Appearance Görünüm
Overlap Örtüşme
Select Fills Dolguları Seç
Select Strokes Konturları Seç
Paint Fills Dolguları Boya
Paint Strokes Konturları Boya
Highlight Vurgulama
Snap to Unit Birim Değerine Yasla
The plug-in writes the whole document. Eklenti tüm belgeyi yazar.
Attach İliştir
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -1271,6 +1271,9 @@ file-filter Templates Шаблони
Hide Live Paint Gaps Приховати проміжки швидкої заливки
Select Fills Виділяти заливки
Select Strokes Виділяти обведення
Paint Fills Фарбувати заливки
Paint Strokes Фарбувати обведення
Highlight Підсвічування
Gaussian Blur… Розмиття за Гауссом…
General Загальні
General › Placement › X Загальні › Розміщення › X
Can't render this file because it contains an unexpected character in line 178 and column 5.
+3
View File
@@ -1486,6 +1486,9 @@ file-filter Templates Mẫu
Hide Live Paint Gaps Ẩn khoảng cách tô trực tiếp
Select Fills Chọn vùng tô
Select Strokes Chọn nét
Paint Fills Tô vùng tô
Paint Strokes Tô nét
Highlight Làm nổi bật
Gaussian Blur Mờ Gaussian
Gaussian Blur… Làm mờ Gaussian…
General Chung
Can't render this file because it contains an unexpected character in line 200 and column 5.
+3
View File
@@ -1215,6 +1215,9 @@ file-filter Templates 模板
Hide Live Paint Gaps 隐藏实时上色间隙
Select Fills 选择填色
Select Strokes 选择描边
Paint Fills 为填色上色
Paint Strokes 为描边上色
Highlight 突出显示
Gaussian Blur… 高斯模糊…
General 常规
General › Placement › X 一般 › 位置 › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -1241,6 +1241,9 @@ file-filter Templates 範本
Hide Live Paint Gaps 隱藏即時上色間隙
Select Fills 選取填色
Select Strokes 選取筆畫
Paint Fills 為填色上色
Paint Strokes 為筆畫上色
Highlight 強調顯示
Gaussian Blur… 高斯模糊…
General 一般
General › Placement › X 一般 › 位置 › X
Can't render this file because it contains an unexpected character in line 6 and column 47.
File diff suppressed because one or more lines are too long
+2
View File
@@ -377,6 +377,8 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
_ if crate::dialogs::freehand::TOOLS.contains(&tool) => crate::dialogs::freehand::open(app, tool),
// The Live Paint Selection tool: its Tool Options (what a click selects, the highlight).
"livePaintSelection" => crate::dialogs::live_paint_selection::open(app),
// The Live Paint Bucket: its options (what a click paints, the highlight).
"livePaintBucket" => crate::dialogs::live_paint_bucket::open(app),
// The Symbolism tools: the brush they share.
_ if vectorcraft_tools::settings::SYMBOLISM.contains(&tool) => Ok(crate::dialogs::symbolism_options::open(app, tool)),
// The graph tools: Graph Type for the selected graph.
+9
View File
@@ -1701,6 +1701,15 @@ already walks Image Trace. Short agent entry points:
release, and Escape between the press and the release removes the marquee so that the release runs nothing. The
tool does not highlight or select the faces and edges of a Live Paint group that is locked, on a locked layer or on
a locked artboard, or in isolation mode outside the isolated art.
The Live Paint Bucket's options (`tool.setOption {tool: "livePaintBucket", values}`; double-clicking the tool opens
them, dialog `livePaintBucketOptions`) are `paintFills` (on), `paintStrokes` (off), `highlight` (on), `color` (the
highlight color, `#rrggbb` or a layer color's name; `#ff2a2a`) and `width` (the highlight's width, 1 to 20 screen
pixels; 4). With `paintFills` alone on, a click runs `livePaint.fill` and Shift-click runs `livePaint.strokeEdge`.
With `paintStrokes` alone on, a click runs `livePaint.strokeEdge` on the edge within 4 screen pixels of the pointer,
or nothing when none is in reach, and Shift-click runs `livePaint.fill`. With both on, a click runs
`livePaint.strokeEdge` when an edge is within 4 screen pixels of the pointer and `livePaint.fill` otherwise, and
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.
`livePaint.merge` keeps the bottom Live Paint group's id, name, lock, visibility, transparency, appearance, opacity
mask and graphic style link. With a Live Paint group among the objects, `livePaint.make` adds the others to it as
`livePaint.merge` does, and so does `livePaint.fill {ids}` when `point` is outside that group's faces.
+1
View File
@@ -928,6 +928,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- Create Strokes uses exact squared Euclidean distances and Guo–Hall thinning in bounded per-blob crops. Maximal-disc pruning, split-crossing merges and degree-two joins simplify its skeleton graph. The whole-blob width rule and ink fitting remain; 46 code-drawn inputs measure ends and junctions in [`image-trace-quality.md`](image-trace-quality.md#centre-lines). Junction degrees measure 44/45 correct in each input variant; remaining spurs are recorded. This is a measured skeleton improvement; the Image Trace score, missing per-feature selection and width controls, and estimates remain unchanged.
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). 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}`, `dialogs::live_paint_bucket::tests::double_clicking_the_tool_opens_its_options_and_ok_sets_them`.
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)).
Double-clicking the Live Paint Selection tool opens its Tool Options ([Live Paint Selection tool options](https://web.archive.org/web/20250904012025/https://helpx.adobe.com/illustrator/using/selecting-objects.html)): with Select Fills off, a click selects an edge instead of a face; with Select Strokes off, Alt-click selects a face instead of an edge; Color and Width set the color and width of the highlight on the face or edge under the pointer (red and 4 px by default). The options last across tool switches and are saved with the preferences (`tool.setOption {tool: "livePaintSelection"}`).