Merge pull request #3052 from dotCooCoo/feat/live-paint-selection-options

Live Paint: Live Paint Selection Tool Options set what a click selects and the highlight
This commit is contained in:
BFlat
2026-10-19 22:07:12 +07:00
committed by GitHub
31 changed files with 347 additions and 17 deletions
+145 -16
View File
@@ -628,16 +628,14 @@ fn lp_hover(cx: &ToolContext, cache: &mut RegionCache, p: Point, edge: bool) ->
LpHover::None
}
fn lp_overlays(h: &LpHover, cache: &RegionCache) -> Vec<Overlay> {
/// The highlight of `h`, `width` screen pixels wide in `color`.
fn lp_overlays(h: &LpHover, cache: &RegionCache, color: [u8; 3], width: f32) -> Vec<Overlay> {
match h {
LpHover::None => vec![],
LpHover::Part(bp) => vec![Overlay::Path { path: bp.clone(), color: HIGHLIGHT_RED, width: 4.0, dashed: false }],
LpHover::Region(i) => cache
.map
.regions
.get(*i)
.map(|(_, bp, _)| vec![Overlay::Path { path: bp.clone(), color: HIGHLIGHT_RED, width: 4.0, dashed: false }])
.unwrap_or_default(),
LpHover::Part(bp) => vec![Overlay::Path { path: bp.clone(), color, width, dashed: false }],
LpHover::Region(i) => {
cache.map.regions.get(*i).map(|(_, bp, _)| vec![Overlay::Path { path: bp.clone(), color, width, dashed: false }]).unwrap_or_default()
}
}
}
@@ -701,7 +699,7 @@ impl Tool for LivePaintBucketTool {
}
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
lp_overlays(&self.hover, &self.cache)
lp_overlays(&self.hover, &self.cache, HIGHLIGHT_RED, 4.0)
}
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
@@ -711,18 +709,54 @@ impl Tool for LivePaintBucketTool {
// ---------- Live Paint Selection ----------
/// Live Paint Selection: click selects a face (Shift adds); Alt-click selects an edge.
/// The Live Paint Selection tool's highlight width (screen pixels) by default.
pub const LP_SELECT_WIDTH: f64 = 4.0;
/// The least and greatest highlight width (screen pixels) the Live Paint Selection tool takes.
pub const LP_SELECT_WIDTH_RANGE: (f64, f64) = (1.0, 20.0);
/// Live Paint Selection: click selects a face (Shift adds); Alt-click selects an edge. Its Tool
/// Options (double-click the tool) are `selectFills` and `selectStrokes` (both on), the highlight
/// `color` (`#rrggbb`, red by default) and its `width` ([`LP_SELECT_WIDTH`] pixels). With Select
/// Strokes off, Alt-click selects the face under the pointer as a click does; with Select Fills
/// off, a click selects the edge nearest the pointer as Alt-click does; with both off, a click
/// selects no face or edge.
pub struct LivePaintSelectionTool {
cache: RegionCache,
hover: LpHover,
fills: bool,
strokes: bool,
color: [u8; 3],
width: f64,
}
impl Default for LivePaintSelectionTool {
fn default() -> Self {
Self { cache: RegionCache::live_paint(), hover: LpHover::None }
Self { cache: RegionCache::live_paint(), hover: LpHover::None, fills: true, strokes: true, color: HIGHLIGHT_RED, width: LP_SELECT_WIDTH }
}
}
impl LivePaintSelectionTool {
/// What a click picks, Alt held or not: Some(true) an edge, Some(false) a face, None nothing.
fn picks_edge(&self, alt: bool) -> Option<bool> {
match (self.fills, self.strokes) {
(true, true) => Some(alt),
(true, false) => Some(false),
(false, true) => Some(true),
(false, false) => None,
}
}
}
/// A highlight color option: `#rrggbb` or a layer color's name (any case).
fn lp_color(v: &Value) -> Option<[u8; 3]> {
let s = v.as_str()?.trim();
if let Some((_, c)) = vectorcraft_doc::LAYER_COLORS.iter().find(|(name, _)| name.eq_ignore_ascii_case(s)) {
return Some(*c);
}
let [r, g, b, _] = vectorcraft_color::Color::from_hex(s)?.to_rgba8(1.0);
Some([r, g, b])
}
impl Tool for LivePaintSelectionTool {
fn id(&self) -> &'static str {
"livePaintSelection"
@@ -730,18 +764,21 @@ impl Tool for LivePaintSelectionTool {
fn pointer(&mut self, cx: &ToolContext, ev: &PointerEvent) -> Vec<Action> {
let p = ev.pos;
let edge = self.picks_edge(ev.mods.alt);
match ev.kind {
PointerKind::Move => {
self.hover = match lp_hover(cx, &mut self.cache, p, ev.mods.alt) {
h @ LpHover::Part(_) => h,
self.hover = match edge.map(|e| lp_hover(cx, &mut self.cache, p, e)) {
Some(h @ LpHover::Part(_)) => h,
_ => LpHover::None,
};
vec![]
}
PointerKind::Down => {
let mut hit = None;
if let Some(g) = live_paint_at(cx.doc, p).and_then(|id| cx.doc.node(id)) {
let part = if ev.mods.alt { edge_near(g, p, cx.tol(4.0)) } else { face_at(g, p) };
if let Some(edge) = edge
&& let Some(g) = live_paint_at(cx.doc, p).and_then(|id| cx.doc.node(id))
{
let part = if edge { edge_near(g, p, cx.tol(4.0)) } else { face_at(g, p) };
hit = part.map(|n| n.id);
}
match (hit, ev.mods.shift) {
@@ -756,12 +793,36 @@ impl Tool for LivePaintSelectionTool {
}
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
lp_overlays(&self.hover, &self.cache)
lp_overlays(&self.hover, &self.cache, self.color, self.width as f32)
}
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
Cursor::Arrow
}
fn options(&self) -> Value {
let [r, g, b] = self.color;
json!({
"selectFills": self.fills,
"selectStrokes": self.strokes,
"color": format!("#{r:02x}{g:02x}{b:02x}"),
"width": self.width,
})
}
fn set_option(&mut self, key: &str, v: &Value) {
match key {
"selectFills" => self.fills = v.as_bool().unwrap_or(self.fills),
"selectStrokes" => self.strokes = v.as_bool().unwrap_or(self.strokes),
"color" => self.color = lp_color(v).unwrap_or(self.color),
"width" => {
let (min, max) = LP_SELECT_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;
}
}
/// Create a builder-family tool by id (None = not one of ours).
@@ -1014,6 +1075,74 @@ mod tests {
assert_eq!(acts, vec![Action::Exec("select.none".into(), json!({}))]);
}
#[test]
fn live_paint_selection_options_choose_what_a_click_selects() {
let (d, _gid, f2, e) = live_group_doc();
let s = Selection::default();
let p = paint();
let cx = cx(&d, &s, &p);
let alt = Mods { alt: true, ..Default::default() };
let set = |ids: &[NodeId]| vec![Action::Exec("select.set".into(), json!({ "ids": ids.iter().map(|i| i.0).collect::<Vec<_>>() }))];
let none = vec![Action::Exec("select.none".into(), json!({}))];
let mut t = LivePaintSelectionTool::default();
assert_eq!(t.options(), json!({"selectFills": true, "selectStrokes": true, "color": "#ff2a2a", "width": 4.0}));
// Select Fills off: a click inside a face selects nothing, and a click by an edge selects it.
t.set_option("selectFills", &json!(false));
assert_eq!(t.pointer(&cx, &ev(PointerKind::Down, 75.0, 25.0)), none);
t.pointer(&cx, &ev(PointerKind::Move, 75.0, 25.0));
assert!(t.overlays(&cx).is_empty());
assert_eq!(t.pointer(&cx, &ev(PointerKind::Down, 51.0, 25.0)), set(&[e]));
assert_eq!(t.pointer(&cx, &ev(PointerKind::Down, 51.0, 25.0).with_mods(alt)), set(&[e]));
// Select Strokes off: Alt-click by an edge selects the face under the pointer.
t.set_option("selectFills", &json!(true));
t.set_option("selectStrokes", &json!(false));
assert_eq!(t.pointer(&cx, &ev(PointerKind::Down, 51.0, 25.0).with_mods(alt)), set(&[f2]));
t.pointer(&cx, &ev(PointerKind::Move, 51.0, 25.0).with_mods(alt));
let ov = t.overlays(&cx);
let [Overlay::Path { path, .. }] = ov.as_slice() else { panic!("{ov:?}") };
assert_eq!(path.bounding_box(), Rect::new(50.0, 0.0, 100.0, 50.0), "the face, not the edge");
// Both off: nothing is selected or highlighted.
t.set_option("selectFills", &json!(false));
for m in [Mods::default(), alt] {
assert_eq!(t.pointer(&cx, &ev(PointerKind::Down, 51.0, 25.0).with_mods(m)), none);
t.pointer(&cx, &ev(PointerKind::Move, 51.0, 25.0).with_mods(m));
assert!(t.overlays(&cx).is_empty());
}
let shift = Mods { shift: true, ..Default::default() };
assert!(t.pointer(&cx, &ev(PointerKind::Down, 75.0, 25.0).with_mods(shift)).is_empty());
}
#[test]
fn live_paint_selection_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 = LivePaintSelectionTool::default();
let highlight = |t: &mut LivePaintSelectionTool| {
t.pointer(&cx, &ev(PointerKind::Move, 75.0, 25.0));
match t.overlays(&cx).as_slice() {
[Overlay::Path { color, width, .. }] => (*color, *width),
ov => panic!("{ov:?}"),
}
};
assert_eq!(highlight(&mut t), (HIGHLIGHT_RED, 4.0));
t.set_option("color", &json!("#00FF00"));
t.set_option("width", &json!(9));
assert_eq!(highlight(&mut t), ([0, 0xff, 0], 9.0));
// A layer color's name; a value that is not a color, or a width that is not a number, is ignored.
t.set_option("color", &json!("light blue"));
t.set_option("width", &json!("wide"));
assert_eq!(highlight(&mut t), ([0x4f, 0x80, 0xff], 9.0));
t.set_option("color", &json!("nope"));
assert_eq!(t.options()["color"], "#4f80ff");
// The width stays within its range.
t.set_option("width", &json!(100));
assert_eq!(t.options()["width"], json!(LP_SELECT_WIDTH_RANGE.1));
t.set_option("width", &json!(0));
assert_eq!(t.options()["width"], json!(LP_SELECT_WIDTH_RANGE.0));
}
#[test]
fn create_builds_the_three_tools() {
for id in ["shapeBuilder", "livePaintBucket", "livePaintSelection"] {
+2
View File
@@ -34,6 +34,8 @@ const ROWS: &[Row] = &[
(&["smooth"], None, &["fidelity"]),
(&["blobBrush", "eraser"], None, &["size"]),
(&["blobBrush"], None, &["keepSelected", "mergeOnlySelection"]),
// Live Paint Selection Tool Options (`builder::LivePaintSelectionTool`).
(&["livePaintSelection"], None, &["selectFills", "selectStrokes", "color", "width"]),
(&["polygon"], None, &["sides"]),
(&["star"], None, &["points"]),
(&["freeTransform"], None, &["constrain"]),
@@ -0,0 +1,142 @@
//! Live Paint Selection Tool Options (double-click the Live Paint Selection tool): what a click
//! with the tool selects 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: "livePaintSelection", values}`.
//!
//! Fields: `selectFills` and `selectStrokes` (the faces and the edges of Live Paint groups),
//! `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_SELECT_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 Selection Tool Options.
pub const KIND: &str = "livePaintSelectionOptions";
/// The tool the dialog serves.
const TOOL: &str = "livePaintSelection";
/// The options the dialog edits.
const KEYS: [&str; 4] = ["selectFills", "selectStrokes", "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 Selection Tool 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, "selectFills", tl!("Select Fills"));
form::check(ui, d, "selectStrokes", tl!("Select Strokes"));
ui.add_space(6.0);
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();
widgets::label_row(ui, tl!("Color:"), LABEL_W, |ui| {
if let Some((_, [r, g, b])) =
widgets::dropdown(ui, "lp-select-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_SELECT_WIDTH_RANGE;
let v = d.f64("width", min);
widgets::label_row(ui, tl!("Width:"), LABEL_W, |ui| {
if let Some(x) = widgets::plain_field(ui, "lp-select-width", v, "px", 0, 72.0) {
d.fields.insert("width".into(), json!(x.round().clamp(min, max)));
}
});
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 click = |app: &mut VectorcraftApp, x: f64| {
let view = app.view_info();
crate::canvas::dispatch(app, &PointerEvent::new(PointerKind::Down, x, 100.0), view);
crate::canvas::dispatch(app, &PointerEvent::new(PointerKind::Up, x, 100.0), view);
app.session.doc().unwrap().selection.objects.len()
};
assert_eq!(click(&mut app, 100.0), 1, "a face is selected by default");
app.run("tool.options", json!({"tool": TOOL})).unwrap();
let text = painted(&mut app);
for label in ["Live Paint Selection Tool Options", "Select Fills", "Select Strokes", "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("selectFills"), d.bool("selectStrokes"), d.str("color"), d.f64("width", 0.0)), (true, true, "#ff2a2a".to_string(), 4.0));
d.fields.insert("selectFills".into(), json!(false));
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: a click inside a face selects nothing, and the highlight shows the edge
// in the new color and width.
assert_eq!(click(&mut app, 100.0), 0);
let view = app.view_info();
crate::canvas::dispatch(&mut app, &PointerEvent::new(PointerKind::Move, 151.0, 100.0), view);
let ov = app.session.overlays(view);
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["selectFills"].as_bool(), o["color"].as_str(), o["width"].as_f64()), (Some(false), Some("#4fff4f"), Some(8.0)));
app.run("tool.options", json!({"tool": TOOL})).unwrap();
let d = app.ui.dialog.as_ref().unwrap();
assert_eq!((d.bool("selectFills"), d.str("color")), (false, "#4fff4f".to_string()));
}
}
+2
View File
@@ -51,6 +51,7 @@ pub mod layer_options;
pub mod layers_panel_options;
pub mod liquify;
pub mod live_paint_gaps;
pub mod live_paint_selection;
pub mod missing_fonts;
pub mod missing_links;
pub(crate) mod modal;
@@ -320,6 +321,7 @@ registry! {
PerspectiveGridOptions: [perspective_options::KIND] => perspective_options::SPEC,
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,
BrushOptions: [brush_options::KIND] => brush_options::SPEC,
EditSelection: [edit_selection::KIND] => edit_selection::SPEC,
PerspectivePlane: [perspective_plane::KIND] => perspective_plane::SPEC,
+2
View File
@@ -1174,6 +1174,8 @@
Large Gaps Големи пролуки
Gap Preview Color: Цвят за преглед на пролуките:
Close Gaps With Paths Затваряне на пролуките с пътища
Select Fills Избиране на запълвания
Select Strokes Избиране на щрихове
Gaussian Blur… Гаусово размазване…
General Общи
General › Placement › X Общи › Разположение › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -264,6 +264,8 @@
Large Gaps Velké mezery
Gap Preview Color: Barva náhledu mezer:
Close Gaps With Paths Uzavřít mezery cestami
Select Fills Vybírat výplně
Select Strokes Vybírat tahy
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.
+2
View File
@@ -5076,6 +5076,8 @@ swatch Bright Violet Kraftig violet
Chop Design Beskær design
Clear Events Ryd hændelser
Close Gaps With Paths Luk huller med stier
Select Fills Markér udfyldninger
Select Strokes Markér konturer
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.
+2
View File
@@ -1192,6 +1192,8 @@ file-filter Templates Vorlagen
Large Gaps Große Lücken
Gap Preview Color: Vorschaufarbe für Lücken:
Close Gaps With Paths Lücken mit Pfaden schließen
Select Fills Flächen auswählen
Select Strokes Konturen auswählen
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.
+2
View File
@@ -1139,6 +1139,8 @@
Large Gaps Μεγάλα διάκενα
Gap Preview Color: Χρώμα προεπισκόπησης διακένων:
Close Gaps With Paths Κλείσιμο διακένων με διαδρομές
Select Fills Επιλογή γεμισμάτων
Select Strokes Επιλογή περιγραμμάτων
Gaussian Blur… Θόλωση Γκάους…
General Γενικά
General › Placement › X Γενικά › Τοποθέτηση › X
1 # Ελληνικά (el) — VectorCraft
1139
1140
1141
1142
1143
1144
1145
1146
+2
View File
@@ -1199,6 +1199,8 @@
Large Gaps Huecos grandes
Gap Preview Color: Color de previsualización de huecos:
Close Gaps With Paths Cerrar huecos con trazados
Select Fills Seleccionar rellenos
Select Strokes Seleccionar trazos
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.
+2
View File
@@ -271,6 +271,8 @@
Large Gaps Suuret aukot
Gap Preview Color: Aukkojen esikatseluväri:
Close Gaps With Paths Sulje aukot poluilla
Select Fills Valitse täytöt
Select Strokes Valitse viivat
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.
+2
View File
@@ -1198,6 +1198,8 @@
Large Gaps Grands espaces
Gap Preview Color: Couleur d’aperçu des espaces :
Close Gaps With Paths Fermer les espaces avec des tracés
Select Fills Sélectionner les fonds
Select Strokes Sélectionner les contours
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.
+2
View File
@@ -1504,6 +1504,8 @@ file-filter Templates Sablonok
Large Gaps Nagy hézagok
Gap Preview Color: Hézagok előnézeti színe:
Close Gaps With Paths Hézagok lezárása útvonalakkal
Select Fills Kitöltések kijelölése
Select Strokes Körvonalak kijelölése
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.
+2
View File
@@ -5076,6 +5076,8 @@ swatch Bright Violet Violet cerah
Chop Design Potong desain
Clear Events Hapus peristiwa
Close Gaps With Paths Tutup celah dengan jalur
Select Fills Pilih isian
Select Strokes Pilih stroke
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 3770 and column 19.
+2
View File
@@ -1208,6 +1208,8 @@ file-filter Templates Modelli
Large Gaps Spazi vuoti grandi
Gap Preview Color: Colore anteprima spazi vuoti:
Close Gaps With Paths Chiudi spazi vuoti con tracciati
Select Fills Seleziona riempimenti
Select Strokes Seleziona tracce
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.
+2
View File
@@ -262,6 +262,8 @@
Large Gaps 大きい間隔
Gap Preview Color: 間隔のプレビューカラー:
Close Gaps With Paths パスで間隔を閉じる
Select Fills 塗りを選択
Select Strokes 線を選択
Image Trace 画像トレース
Image Tracing 画像トレースオブジェクト
Tracing Presets トレースプリセット
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5035,6 +5035,8 @@ imageTrace Outline კონტური
Resize Type Area ტექსტის არის ზომის შეცვლა
Scale Design დიზაინის მასშტაბირება
Scale in Perspective პერსპექტივაში მასშტაბირება
Select Fills შევსებების მონიშვნა
Select Strokes კონტურების მონიშვნა
Set History Limit… ისტორიის ლიმიტის დაყენება…
Shape Builder Delete ფიგურების კონსტრუქტორით წაშლა
Sliding სრიალა
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -258,6 +258,8 @@
Large Gaps 큰 간격
Gap Preview Color: 간격 미리 보기 색상:
Close Gaps With Paths 패스로 간격 닫기
Select Fills 칠 선택
Select Strokes 선 선택
Image Trace 이미지 추적
Image Tracing 이미지 추적
Tracing Presets 추적 프리셋
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -5088,6 +5088,8 @@ swatch Bright Violet Felviolet
Chop Design Ontwerp afknippen
Clear Events Gebeurtenissen wissen
Close Gaps With Paths Openingen sluiten met paden
Select Fills Vullingen selecteren
Select Strokes Lijnen selecteren
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.
+2
View File
@@ -229,6 +229,8 @@
Large Gaps Duże przerwy
Gap Preview Color: Kolor podglądu przerw:
Close Gaps With Paths Zamknij przerwy ścieżkami
Select Fills Zaznaczaj wypełnienia
Select Strokes Zaznaczaj obrysy
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.
+2
View File
@@ -1056,6 +1056,8 @@
Large Gaps Lacunas Grandes
Gap Preview Color: Cor de Visualização das Lacunas:
Close Gaps With Paths Fechar Lacunas com Caminhos
Select Fills Selecionar Preenchimentos
Select Strokes Selecionar Traçados
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.
+2
View File
@@ -1206,6 +1206,8 @@ file-filter Templates Шаблоны
Large Gaps Большие зазоры
Gap Preview Color: Цвет просмотра зазоров:
Close Gaps With Paths Закрыть зазоры контурами
Select Fills Выделять заливки
Select Strokes Выделять обводки
Gaussian Blur… Размытие по Гауссу…
General Общие
General › Placement › X Общие › Размещение › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1176,6 +1176,8 @@
Large Gaps Stora mellanrum
Gap Preview Color: Förhandsgranskningsfärg för mellanrum:
Close Gaps With Paths Stäng mellanrum med banor
Select Fills Markera fyllningar
Select Strokes Markera konturer
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.
+2
View File
@@ -1202,6 +1202,8 @@ file-filter Templates Шаблони
Large Gaps Великі проміжки
Gap Preview Color: Колір перегляду проміжків:
Close Gaps With Paths Закрити проміжки контурами
Select Fills Виділяти заливки
Select Strokes Виділяти обведення
Gaussian Blur… Розмиття за Гауссом…
General Загальні
General › Placement › X Загальні › Розміщення › X
Can't render this file because it contains an unexpected character in line 725 and column 40.
+2
View File
@@ -1394,6 +1394,8 @@ file-filter Templates Mẫu
Large Gaps Khoảng cách lớn
Gap Preview Color: Màu xem trước khoảng cách:
Close Gaps With Paths Đóng khoảng cách bằng đường dẫn
Select Fills Chọn vùng tô
Select Strokes Chọn nét
Gaussian Blur Mờ Gaussian
Gaussian Blur… Làm mờ Gaussian…
General Chung
1 # Vietnamese (vi) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1394
1395
1396
1397
1398
1399
1400
1401
+2
View File
@@ -1155,6 +1155,8 @@ file-filter Templates 模板
Large Gaps 大间隙
Gap Preview Color: 间隙预览颜色:
Close Gaps With Paths 用路径封闭间隙
Select Fills 选择填色
Select Strokes 选择描边
Gaussian Blur… 高斯模糊…
General 常规
General › Placement › X 一般 › 位置 › X
Can't render this file because it contains an unexpected character in line 2 and column 73.
+2
View File
@@ -1179,6 +1179,8 @@ file-filter Templates 範本
Large Gaps 大間隙
Gap Preview Color: 間隙預覽顏色:
Close Gaps With Paths 以路徑封閉間隙
Select Fills 選取填色
Select Strokes 選取筆畫
Gaussian Blur… 高斯模糊…
General 一般
General › Placement › X 一般 › 位置 › X
Can't render this file because it contains an unexpected character in line 6 and column 47.
+1 -1
View File
@@ -471,7 +471,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
"tool.options",
"Tool Options…",
"",
"{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height; OK runs artboard.setProps), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x and y, draws one there with them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (pt), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), size (pt); OK runs tool.setOption {tool, values})",
"{tool: id} what double-clicking a tool button opens: hand → fits the artboard in the window (view.fitArtboard), zoom → 100% (view.actualSize), rotate|scale|reflect|shear → that Object › Transform dialog (dialog `rotate`, `scale`, `reflect` or `shear`; error `nothing selected` without a selection), selection|directSelection|groupSelection → the Move dialog (dialog `move`; the same error), gradient|magicWand → the Gradient or Magic Wand panel (window.panel), artboard → Artboard Options of the active artboard (dialog `artboardOptions`, fields index, name, x, y, width, height; OK runs artboard.setProps), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x and y, draws one there with them), columnGraph|stackedColumnGraph|barGraph|stackedBarGraph|lineGraph|areaGraph|scatterGraph|pieGraph|radarGraph → Graph Type for the selected graph (dialog `command` running graph.setType; an error without one), symbolSprayer|symbolShifter|symbolScruncher|symbolSizer|symbolSpinner|symbolStainer|symbolScreener|symbolStyler → Symbolism Tools Options (dialog `symbolismOptions`, fields tool, diameter (pt), intensity and density (1–10), shared by the eight tools; OK runs tool.setOption {tool, values}), blend → Blend Options, perspectiveGrid → Perspective Grid Options, eyedropper → Eyedropper Options (dialog `eyedropperOptions`, fields sampleSize, pickUp, apply; OK runs eyedropper.setOptions), printTiling → resets the print tiling (print.tiling.set {reset: true}), warp|twirl|pucker|bloat|scallop|crystallize|wrinkle → that tool's Tool Options (dialog `liquifyOptions`, fields tool, width, height, angle, intensity %, usePressure, detail, simplify, simplifyOn, rate, complexity, horizontal %, vertical %, affectAnchors, affectIn, affectOut, showBrush; OK runs tool.setOption {tool, values}), pencil|paintbrush|smooth|blobBrush|eraser → that tool's Tool Options (dialog `freehandOptions`, fields tool and the options the tool keeps: fidelity (pt), fill, keepSelected (on for pencil and paintbrush, off for blobBrush), mergeOnlySelection (off), closeWithin and editWithin (px, 0 is off), size (pt); OK runs tool.setOption {tool, values}), livePaintSelection → Live Paint Selection Tool Options (dialog `livePaintSelectionOptions`, fields selectFills and selectStrokes (both on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the highlight's width, 1–20 px; 4); OK runs tool.setOption {tool: \"livePaintSelection\", values})",
),
(
"ui.colorGuideOptions",
+2
View File
@@ -366,6 +366,8 @@ pub fn open_options(app: &mut VectorcraftApp, tool: &str) -> Result<serde_json::
_ if vectorcraft_tools::settings::LIQUIFY.contains(&tool) => crate::dialogs::liquify::open(app, tool),
// The freehand tools: their Tool Options (Fidelity, fill, the tolerances, the brush size).
_ 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 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.
+6
View File
@@ -1233,6 +1233,12 @@ already walks Image Trace. Short agent entry points:
and `livePaint.info` list the gaps up to `size` wide that no path closes, as `[x0, y0, x1, y1]`. `livePaint.merge`
keeps the bottom group's options when its Gap Detection is on, and otherwise 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.
The Live Paint Selection tool's options (`tool.setOption {tool: "livePaintSelection", values}`; double-clicking the
tool opens them, dialog `livePaintSelectionOptions`) are `selectFills` and `selectStrokes` (both 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 `selectStrokes` off, Alt-click selects the face under the pointer as a click does; with
`selectFills` off, a click selects the edge nearest the pointer as Alt-click does; with both off, a click selects
no face or edge.
`dataset.select`, `dataset.next` and `dataset.prev` reply `{name, applied, skipped}`. They add `overflow: [id…]`
when area type or type on a path whose text they rewrote doesn't fit, as `text.setText` does (see
+1
View File
@@ -654,6 +654,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Advanced art:** live Blends (steps/distance/smooth colour, editable spine, anchor-targeted Blend tool, faithful interpolation, knockout), Envelope Distort (warp/mesh/top object, Reset, full Envelope Options, mesh handles, Edit Contents; type, images, symbols, gradients, patterns and appearance distorted everywhere), Liquify tools with options and pen pressure, Puppet Warp with rotating pins, Perspective Grid with presets, movable planes and projected type (choosing a perspective tool shows the grid; Hide Grid hides it with the tool still chosen), Gradient Mesh, Shape Builder (stroke-only open paths and lines cut the regions they cross and enclose regions of their own, and regions they bound merge without seams where the lines cross at rounded coordinates (#893); filled open paths count as closed; erase mode while Alt/Option is held: a minus pointer, red highlights of what will go, a click or drag deletes the regions touched, the pieces of open lines and the edges (outline pieces between the points where paths meet, opening their paths there) it passes over, one undo step, `shapeBuilder.merge {erase}` and `shapeBuilder.regions` edges over MCP), Live Paint (every path an edge, open lines included), Image Trace (12 presets, colour layers traced in parallel; a trace past two million anchor points is refused with advice rather than running out of memory, an image over 64 megapixels is traced scaled down to that size (aspect kept, paths at the image's own size) and one over 192 megapixels is refused, and a selection too large to outline shows its bounds (#525); Object › Image Trace › Make and Make and Expand trace at once with the Default preset, #543; with an image selected the Control bar and the Properties panel show Image Trace, whose arrow traces with any preset, Mask (`object.maskImage`: a clipping path on the image's outline, selected so its handles crop), Crop Image (a crop box on the image, starting on its part over the artboard, dragged by its handles or inside it, the outside dimmed, with X/Y/W/H, Apply and Cancel in the Control bar, Enter and Escape, #734; `object.cropImage {rect}` for agents, `{trim: true}` cuts an image to its pixels that aren't fully transparent, pixel for pixel) and Embed or Unembed in place of Fill and Stroke; with an Image Trace object selected, its Preset (choosing another traces again), View, the Image Trace panel and Expand, plus Release in Properties; the five View modes (Tracing Result, with Outlines, Outlines, Outlines with Source Image, Source Image) redraw a trace on screen without tracing again, are saved with it and `imageTrace.setView` sets them, while exports and Expand keep the result; the panel's Advanced section is a header that stays open or closed as it was left (part of #641); Color mode's Palette: Limited, Full Tone (as many colours as the tones need, Less … More), Automatic (flat art with its own colours, else Full Tone; a heuristic) and Document Library (the most used colours of the document's swatches or a swatch library, filled with the swatch colours as they are), part of #641; Advanced › Create: Fills and/or Strokes, where lines up to the Stroke width trace as stroked centre lines (distance transform, thinning, a skeleton graph with spurs pruned, curve fitting; one width per line; without Fills, wider areas become stroked outlines), part of #641), pattern swatches with pattern editing mode, live Repeat (radial/grid/mirror). A single selected mirror repeat shows a prominent axis with a center move handle and two rotation handles. Drag the line or center to change spacing, or an end to change angle; Shift snaps rotation to 45°. The original artwork stays in place, the reflected copy previews live, Escape cancels and each drag takes one undo step through `object.repeat.options`. Controls follow zoom/view rotation and hide with edges or the bounding box. Engine and headless canvas regressions passed for gestures, history and visibility; native move, rotation and Escape screenshots were reviewed. [Public mirror-repeat reference](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/create-and-edit-patterns/set-mirror-repeat-options.html). Object › Expand turns radial, grid and mirror repeats into groups of their instances (#1765, #1756).
- 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.
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.
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"}`).
Window › Blend opens the Blend panel: Make, the selected blend's Spacing and Orientation (with no blend selected, the options new blends start with), Reverse Front to Back, Reverse Spine, Replace Spine, Expand and Release, each running its Object › Blend command. Make is dimmed until two or more objects are selected. Spacing and Orientation are dimmed until a blend or two or more objects are selected, and the other buttons are dimmed until a blend is selected.
- **Paint and appearance (M3):**
- **Swatches:** names follow the colour model and are unique; Swatch Options (process/spot, Global, Gray/RGB/HSB/CMYK/Web, live preview); editing a global or spot swatch recolours every linked fill, stroke and text run in one undo step; multi-select and delete with confirmation; New Swatch and New Color Group dialogs (a group from the selected artwork's colours); swatches in colour groups convert, separate and count like any other. The Swatches panel selects ranges and whole groups, has a find field, and drags swatches to reorder, regroup or paint art; Add Used Colors, Select All Unused, Merge, Ungroup and Sort by Kind; a CMYK document keeps applied colours in CMYK. Swatch libraries: nine generated colour libraries and five gradient libraries in a library panel, saved as `.vcswatches`, `.gpl`, swatch exchange (`.ase`) or CSS, and loaded from `.vcswatches`, `.gpl` and `.ase` files (`.ase` both ways: RGB, CMYK, Lab and Gray; global, spot and process; groups) and from color books (`.acb`, read only: the RGB, CMYK or Lab colors of a book such as a Pantone book a user owns, named with its prefix and suffix, spot unless it says process, #831); `swatch.library.copyToUser` copies a loaded library into the User Defined folder without replacing a file, and a file with the same extension and bytes as a User Defined library opens as that library. Tints of global and spot colours ("Name 40%") stay linked and separate as a percentage of their plate. Spot colours can be defined in Lab (Spot Colors options). A built-in [Registration] swatch prints on every plate.