diff --git a/crates/ui-egui/src/dialogs/live_paint_bucket.rs b/crates/ui-egui/src/dialogs/live_paint_bucket.rs index e9267b0f3..c040cdcc3 100644 --- a/crates/ui-egui/src/dialogs/live_paint_bucket.rs +++ b/crates/ui-egui/src/dialogs/live_paint_bucket.rs @@ -13,7 +13,7 @@ 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::tile_edge_color::{checked, resolve}; use super::{DialogSpec, form, run_and_close}; use crate::state::Dialog; use crate::{VectorcraftApp, widgets}; @@ -78,8 +78,11 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { false } -/// OK: set the tool's options. +/// OK: set the tool's options, or an error for a `color` that is not a color. fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + if d.fields.contains_key("color") { + checked(&d.str("color"))?; + } let values: Map = 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 })) } @@ -148,4 +151,24 @@ mod tests { app.select_tool(TOOL); assert!(!hover(&mut app, 151.0).iter().any(|o| matches!(o, Overlay::Path { .. }))); } + + /// A highlight color with an alpha part is refused and leaves the tool's options as they were; + /// `#rrggbb` still sets them. + #[test] + fn a_color_with_an_alpha_part_is_refused() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app.run("tool.options", json!({"tool": TOOL})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("color".into(), json!("#ff000080")); + d.fields.insert("width".into(), json!(8)); + let err = super::super::confirm(&mut app).unwrap_err(); + assert_eq!(err, "`#ff000080` is not a colour (#rrggbb or a preset name)"); + assert!(app.ui.dialog.is_some(), "the dialog stays open"); + let o = app.session.tool_options_of(TOOL); + assert_eq!((o["color"].as_str(), o["width"].as_f64()), (Some("#ff2a2a"), Some(4.0)), "nothing changes"); + app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!("#ff0000")); + super::super::confirm(&mut app).unwrap(); + assert_eq!(app.session.tool_options_of(TOOL)["color"], json!("#ff0000")); + } } diff --git a/crates/ui-egui/src/dialogs/live_paint_gaps.rs b/crates/ui-egui/src/dialogs/live_paint_gaps.rs index 98c7abfba..e01a08e9a 100644 --- a/crates/ui-egui/src/dialogs/live_paint_gaps.rs +++ b/crates/ui-egui/src/dialogs/live_paint_gaps.rs @@ -15,7 +15,7 @@ use vectorcraft_doc::{GapSize, LAYER_COLORS}; use vectorcraft_geom::{BezPath, Point}; use vectorcraft_tools::Overlay; -use super::tile_edge_color::resolve; +use super::tile_edge_color::{checked, has_alpha, resolve}; use super::{DialogSpec, form}; use crate::state::Dialog; use crate::{VectorcraftApp, widgets}; @@ -116,7 +116,11 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { }); ui.add_space(6.0); form::check(ui, d, "closeGaps", tl!("Close Gaps With Paths")); - let p = params(d); + let mut p = params(d); + // OK refuses a color with an alpha part, so the preview leaves each group's own color. + if has_alpha(&d.str("color")) { + p["color"] = Value::Null; + } if let Some(r) = form::preview_result(app, ui, d, "Gap Options", CMD, p) { d.fields.insert(GAPS.into(), r["gaps"].clone()); } @@ -127,6 +131,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { } fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + checked(&d.str("color"))?; form::commit_preview(app, CMD, params(d)) } @@ -134,7 +139,7 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { /// off or another dialog is open). pub(crate) fn overlays(app: &VectorcraftApp) -> Vec { let Some(d) = app.ui.dialog.as_ref().filter(|d| d.kind == KIND && d.bool("preview")) else { return vec![] }; - let [r, g, b, _] = resolve(&d.str("color")).unwrap_or(Color::BLACK).to_rgba8(1.0); + let [r, g, b, _] = checked(&d.str("color")).unwrap_or(Color::BLACK).to_rgba8(1.0); let gaps = d.fields.get(GAPS).and_then(Value::as_array).map(Vec::as_slice).unwrap_or_default(); gaps.iter() .filter_map(|gap| { @@ -257,4 +262,57 @@ mod tests { let d = app.ui.dialog.as_ref().unwrap(); assert_eq!((d.str("size"), d.f64("custom", 0.0)), ("large".to_string(), GapSize::LARGE)); } + + /// A Gap Preview Color with an alpha part is refused and leaves the group's options as they + /// were; `#rrggbb` still sets them. + #[test] + fn a_color_with_an_alpha_part_is_refused() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 300})).unwrap(); + app.run("shape.rectangle", json!({"x": 50, "y": 50, "width": 100, "height": 100})).unwrap(); + app.run("select.all", json!({})).unwrap(); + let g = app.run("livePaint.make", json!({})).unwrap()["id"].as_u64().unwrap(); + app.run("ui.livePaintGapOptions", json!({})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("color".into(), json!("#ff000080")); + d.fields.insert("detect".into(), json!(true)); + let depth = app.session.doc().unwrap().history.undo.len(); + let err = crate::dialogs::confirm(&mut app).unwrap_err(); + assert_eq!(err, "`#ff000080` is not a colour (#rrggbb or a preset name)"); + assert!(app.ui.dialog.is_some(), "the dialog stays open"); + assert_eq!((options(&app, g), app.session.doc().unwrap().history.undo.len()), (GapOptions::default(), depth), "nothing changes"); + app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!("#ff0000")); + crate::dialogs::confirm(&mut app).unwrap(); + assert_eq!(options(&app, g).color, [0xff, 0, 0]); + } + + /// With Preview on, a Gap Preview Color with an alpha part is not previewed: the group keeps + /// its own color, the gaps draw in black, OK refuses the color and Cancel rolls the preview back. + #[test] + fn a_color_with_an_alpha_part_is_not_previewed() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 300})).unwrap(); + // An open path round a square, its end 2 pt short of its start. + let anchors = [(0, 0), (100, 0), (100, 100), (0, 100), (0, 2)].map(|(x, y)| json!({"x": x, "y": y})); + let p = app.run("path.create", json!({ "anchors": anchors })).unwrap(); + app.run("select.set", json!({"ids": [p["id"]]})).unwrap(); + let g = app.run("livePaint.make", json!({})).unwrap()["id"].as_u64().unwrap(); + let own = options(&app, g).color; + app.run("ui.livePaintGapOptions", json!({})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("preview".into(), json!(true)); + d.fields.insert("detect".into(), json!(true)); + d.fields.insert("color".into(), json!("#ff000080")); + let depth = app.session.doc().unwrap().history.undo.len(); + frame(&mut app); + assert_eq!((options(&app, g).detect, options(&app, g).color), (true, own), "the group keeps its own color"); + let shown = overlays(&app); + assert!(matches!(&shown[..], [Overlay::Path { color: [0, 0, 0], .. }]), "{shown:?}"); + let err = crate::dialogs::confirm(&mut app).unwrap_err(); + assert_eq!(err, "`#ff000080` is not a colour (#rrggbb or a preset name)"); + assert!(app.ui.dialog.is_some(), "the dialog stays open"); + crate::dialogs::cancel(&mut app); + let undo = app.session.doc().unwrap().history.undo.len(); + assert_eq!((options(&app, g), undo), (GapOptions::default(), depth), "Cancel rolls the preview back"); + } } diff --git a/crates/ui-egui/src/dialogs/live_paint_selection.rs b/crates/ui-egui/src/dialogs/live_paint_selection.rs index 8ba8cf49b..a1a91ad2b 100644 --- a/crates/ui-egui/src/dialogs/live_paint_selection.rs +++ b/crates/ui-egui/src/dialogs/live_paint_selection.rs @@ -12,7 +12,7 @@ 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::tile_edge_color::{checked, resolve}; use super::{DialogSpec, form, run_and_close}; use crate::state::Dialog; use crate::{VectorcraftApp, widgets}; @@ -74,8 +74,11 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { false } -/// OK: set the tool's options. +/// OK: set the tool's options, or an error for a `color` that is not a color. fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + if d.fields.contains_key("color") { + checked(&d.str("color"))?; + } let values: Map = 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 })) } @@ -139,4 +142,24 @@ mod tests { let d = app.ui.dialog.as_ref().unwrap(); assert_eq!((d.bool("selectFills"), d.str("color")), (false, "#4fff4f".to_string())); } + + /// A highlight color with an alpha part is refused and leaves the tool's options as they were; + /// `#rrggbb` still sets them. + #[test] + fn a_color_with_an_alpha_part_is_refused() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app.run("tool.options", json!({"tool": TOOL})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("color".into(), json!("#ff000080")); + d.fields.insert("width".into(), json!(8)); + let err = super::super::confirm(&mut app).unwrap_err(); + assert_eq!(err, "`#ff000080` is not a colour (#rrggbb or a preset name)"); + assert!(app.ui.dialog.is_some(), "the dialog stays open"); + let o = app.session.tool_options_of(TOOL); + assert_eq!((o["color"].as_str(), o["width"].as_f64()), (Some("#ff2a2a"), Some(4.0)), "nothing changes"); + app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!("#ff0000")); + super::super::confirm(&mut app).unwrap(); + assert_eq!(app.session.tool_options_of(TOOL)["color"], json!("#ff0000")); + } } diff --git a/crates/ui-egui/src/dialogs/tile_edge_color.rs b/crates/ui-egui/src/dialogs/tile_edge_color.rs index 3c7854346..62f3c5a4e 100644 --- a/crates/ui-egui/src/dialogs/tile_edge_color.rs +++ b/crates/ui-egui/src/dialogs/tile_edge_color.rs @@ -32,6 +32,23 @@ pub(super) fn resolve(color: &str) -> Option { } } +/// Whether `color` is an 8-digit hex color, `#rrggbbaa` (with or without `#`). +pub(super) fn has_alpha(color: &str) -> bool { + let color = color.trim(); + let digits = color.strip_prefix('#').unwrap_or(color); + digits.len() == 8 && digits.chars().all(|c| c.is_ascii_hexdigit()) +} + +/// `color` as [`resolve`] reads it, or an error naming the forms it takes. A hex color with an +/// alpha part (`#rrggbbaa` or `#rgba`) is an error. +pub(super) fn checked(color: &str) -> Result { + let refused = || format!("`{color}` is not a colour (#rrggbb or a preset name)"); + if has_alpha(color) { + return Err(refused()); + } + resolve(color).ok_or_else(refused) +} + fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { let color = resolve(&d.str("color")).unwrap_or(Color::BLACK); let [r, g, b, _] = color.to_rgba8(1.0); @@ -58,7 +75,7 @@ fn body(_: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { let color = d.str("color"); - let hex = resolve(&color).ok_or_else(|| format!("`{color}` is not a colour (#rrggbb or a preset name)"))?.to_hex(); + let hex = checked(&color)?.to_hex(); run_and_close(app, "prefs.set", json!({ "key": "patternTileEdgeColor", "value": hex })) } @@ -90,4 +107,36 @@ mod tests { app.run("ui.tileEdgeColor", json!({})).unwrap(); assert!(set(&mut app, "nope").is_err()); } + + /// A hex color with an alpha part is refused and leaves the preference as it was; `#rrggbb` + /// still sets it. + #[test] + fn a_color_with_an_alpha_part_is_refused() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("ui.tileEdgeColor", json!({})).unwrap(); + let before = app.session.prefs.pattern_tile_edge_color.clone(); + app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!("#ff000080")); + let err = super::super::confirm(&mut app).unwrap_err(); + assert_eq!(err, "`#ff000080` is not a colour (#rrggbb or a preset name)"); + assert_eq!(app.session.prefs.pattern_tile_edge_color, before); + assert!(app.ui.dialog.is_some(), "the dialog stays open"); + app.ui.dialog.as_mut().unwrap().fields.insert("color".into(), json!("#ff0000")); + super::super::confirm(&mut app).unwrap(); + assert_eq!(app.session.prefs.pattern_tile_edge_color, "#ff0000"); + } + + /// The swatches the dialogs draw with `resolve` show an 8-digit color by its RGB, while + /// `checked` refuses it; preset names and `#rrggbb` pass both. + #[test] + fn only_checked_refuses_a_color_with_an_alpha_part() { + let red = Color::rgb8(0xff, 0, 0).to_rgba8(1.0); + assert_eq!(resolve("#ff000080").map(|c| c.to_rgba8(1.0)), Some(red)); + assert_eq!(resolve("ff000080").map(|c| c.to_rgba8(1.0)), Some(red)); + assert!(checked("#ff000080").is_err() && checked(" ff000080 ").is_err()); + assert!(checked("#f008").is_err()); + assert_eq!(checked("#ff0000").map(|c| c.to_rgba8(1.0)), Ok(red)); + for (name, [r, g, b]) in LAYER_COLORS.iter() { + assert_eq!(checked(name).map(|c| c.to_rgba8(1.0)), Ok([*r, *g, *b, 255]), "{name}"); + } + } } diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 1356a2cef..3af5f1b9f 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -777,7 +777,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, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x, y and preview (on initially), previews one there and accepts it as one Undo step; Cancel rolls back the preview), lineSegment|arc|rectangularGrid|polarGrid → that tool's Tool Options, its click dialog without x and y (dialog kind = the tool id, `__toolOptions: true`; lineSegment: length (pt), angle (°), fillLine; arc: lengthX, lengthY (pt), reference (0–8), arcType: open|closed, baseAlong: x|y, slope (−100…100), fillArc; rectangularGrid: width, height (pt, the Default Size), reference (0–8), rows, columns, rowSkew, columnSkew (%), frame, fill; polarGrid: the same with concentric, radial, concentricSkew, radialSkew and compound for rows, columns, the skews and frame; OK runs tool.setOption {tool, values} and draws nothing: the next drag uses them (Fill Line, the arc's type, base, slope and fill, the grids' dividers, skews and switches) and a click's dialog opens at 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), typeOnPath|verticalTypeOnPath → Type on a Path Options for the selected type on a path (dialog `command` running type.pathOptions; error `nothing selected` without a selection, and the command's error when no type on a path is selected),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), livePreview (pencil, on: off draws a thin line while the stroke is drawn, and the path's fill and stroke on release), size (pt), angle (°, −180–180; 0) and roundness (%, 1–100; 100) of the eraser's tip; 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}), livePaintBucket → Live Paint Bucket Options (dialog `livePaintBucketOptions`, fields paintFills (on) and paintStrokes (off), highlight (on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the face highlight's width, 1–20 px; 4; an edge's highlight is half as wide); OK runs tool.setOption {tool: \"livePaintBucket\", values}), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values}), objectsOnPath → Objects on Path Tool Options (dialog `objectsOnPathOptions`, fields pivot (0–8, row by row from the top left; 4, the center) and rotate (°, −360–360; 0); Reset puts 4 and 0 back; Save runs tool.setOption {tool: \"objectsOnPath\", values}: the objects attached next use them, groups attached before keep theirs)", + "{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, showCenterMark, showCrossHairs; OK runs artboard.setProps, and with other artboards selected with the Artboard tool a new width or height goes to each of them too, in its `artboards` list, each resized about the reference point the form used for that size, or about its top-left corner when the size was set in the fields directly), flare → Flare Tool Options (dialog `flareOptions`, fields diameter and pathLength (pt), opacity, brightness, growth, fuzziness, longest, rayFuzziness, largest (%), rays, rings, direction (°), raysOn, ringsOn; OK runs tool.setOption {tool: \"flare\", values}: the next flare dragged out uses them, and a Flare click's dialog, which also has x, y and preview (on initially), previews one there and accepts it as one Undo step; Cancel rolls back the preview), lineSegment|arc|rectangularGrid|polarGrid → that tool's Tool Options, its click dialog without x and y (dialog kind = the tool id, `__toolOptions: true`; lineSegment: length (pt), angle (°), fillLine; arc: lengthX, lengthY (pt), reference (0–8), arcType: open|closed, baseAlong: x|y, slope (−100…100), fillArc; rectangularGrid: width, height (pt, the Default Size), reference (0–8), rows, columns, rowSkew, columnSkew (%), frame, fill; polarGrid: the same with concentric, radial, concentricSkew, radialSkew and compound for rows, columns, the skews and frame; OK runs tool.setOption {tool, values} and draws nothing: the next drag uses them (Fill Line, the arc's type, base, slope and fill, the grids' dividers, skews and switches) and a click's dialog opens at 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), typeOnPath|verticalTypeOnPath → Type on a Path Options for the selected type on a path (dialog `command` running type.pathOptions; error `nothing selected` without a selection, and the command's error when no type on a path is selected),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), livePreview (pencil, on: off draws a thin line while the stroke is drawn, and the path's fill and stroke on release), size (pt), angle (°, −180–180; 0) and roundness (%, 1–100; 100) of the eraser's tip; 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}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), livePaintBucket → Live Paint Bucket Options (dialog `livePaintBucketOptions`, fields paintFills (on) and paintStrokes (off), highlight (on), color (the highlight color, #rrggbb or a layer color's name; #ff2a2a), width (the face highlight's width, 1–20 px; 4; an edge's highlight is half as wide); OK runs tool.setOption {tool: \"livePaintBucket\", values}; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), shapeBuilder → Shape Builder Tool Options (dialog `shapeBuilderOptions`, fields gapDetection (off), gapLength (pt, 0–72; 3), openFilledAsClosed (on), clickStrokeSplitsPath (off), pickColorFrom: artwork|swatches (artwork), cursorSwatchPreview (off), selection: freeform|straightLine (freeform), highlightFill (on), highlightStroke (on), highlightColor (#rrggbb or a layer color's name; #ff4f4f); OK runs tool.setOption {tool: \"shapeBuilder\", values}), objectsOnPath → Objects on Path Tool Options (dialog `objectsOnPathOptions`, fields pivot (0–8, row by row from the top left; 4, the center) and rotate (°, −360–360; 0); Reset puts 4 and 0 back; Save runs tool.setOption {tool: \"objectsOnPath\", values}: the objects attached next use them, groups attached before keep theirs)", ), ( "ui.colorGuideOptions", @@ -883,7 +883,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "ui.tileEdgeColor", "Tile Edge Color…", "", - "{} open Tile Edge Color (dialog `tileEdgeColor`, field `color`: #rrggbb or a preset name such as \"Light Blue\"); OK sets the preference patternTileEdgeColor (prefs.set), the colour of the tile edge and swatch bounds in pattern editing mode", + "{} open Tile Edge Color (dialog `tileEdgeColor`, field `color`: #rrggbb or a preset name such as \"Light Blue\"); OK sets the preference patternTileEdgeColor (prefs.set), the colour of the tile edge and swatch bounds in pattern editing mode; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing", ), ( "ui.flattenTransparencyDialog", @@ -1505,7 +1505,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "ui.livePaintGapOptions", "Gap Options…", "", - "{} open Gap Options (Object › Live Paint › Gap Options…; dialog `livePaintGapOptions`: detect, size small|medium|large|custom, custom (pt), color (#rrggbb or a layer color's name), closeGaps, preview) on the selected Live Paint groups, filled in from the first; with preview on, the options apply live and the gaps found draw on the canvas in the preview color: OK runs livePaint.gapOptions as one undo step", + "{} open Gap Options (Object › Live Paint › Gap Options…; dialog `livePaintGapOptions`: detect, size small|medium|large|custom, custom (pt), color (#rrggbb or a layer color's name), closeGaps, preview) on the selected Live Paint groups, filled in from the first; with preview on, the options apply live and the gaps found draw on the canvas in the preview color: OK runs livePaint.gapOptions as one undo step; a color with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing, and with preview on a #rrggbbaa color is not previewed: the groups keep their own color and the gaps draw in black", ), ( "ui.perspectivePlane", diff --git a/docs/control-protocol.md b/docs/control-protocol.md index a6486d6f9..2aa1e3496 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -320,6 +320,7 @@ The User Defined libraries are `window.userBrushLibrary1`…`10` in Window → B Tile Edge Color: Object → Pattern → Tile Edge Color… (`ui.tileEdgeColor`) opens the `tileEdgeColor` dialog (field `color`: `#rrggbb` or a preset name such as "Light Blue"); `ui.dialog.confirm` sets the preference `patternTileEdgeColor` (`prefs.set`), the colour pattern editing mode draws the tile edge and the swatch bounds in. +A `color` with an alpha part (`#rrggbbaa` or `#rgba`) is an error, and the preference does not change. Window title bar: on Windows and Linux the window has no OS decorations and the app bar is the title bar. Its caption buttons (Minimize, Maximize/Restore, Close) sit at the bar's right end, 46 pt each; they are window chrome, diff --git a/docs/mcp.md b/docs/mcp.md index 83e8a3c24..6c181f0dc 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -1868,13 +1868,18 @@ 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 Gap Options dialog (`ui.livePaintGapOptions`) takes `color` as `#rrggbb` or a layer color's name; its OK gives + an error for a `color` with an alpha part (`#rrggbbaa` or `#rgba`) and changes nothing. With Preview on, a + `#rrggbbaa` color is not previewed: the groups keep their own color, and the previewed gaps draw in black. `view.livePaintGaps` (View › Show Live Paint Gaps, or Hide Live Paint Gaps while it is on) turns the drawing of those gaps on or off. While it is on and edges show, the canvas draws the gaps of each selected Live Paint group in that group's Gap Preview Color. The reply is the new state (`true` when on). 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 + pixels; 4). The dialog's OK gives an error for a `color` with an alpha part (`#rrggbbaa` or `#rgba`) and changes + nothing. + 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. A drag of 3 screen pixels or more draws a marquee, and the release runs `select.set {ids}` with the faces and edges it touches (a face or edge partly inside counts, and so does a face that holds the whole @@ -1887,7 +1892,8 @@ already walks Image Trace. Short agent entry points: 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 face highlight's width, 1 to 20 screen - pixels; 4; an edge's highlight is half as wide). With `paintFills` alone on, a click runs `livePaint.fill` and Shift-click runs `livePaint.strokeEdge`. + pixels; 4; an edge's highlight is half as wide). The dialog's OK gives an error for a `color` with an alpha part + (`#rrggbbaa` or `#rgba`) and changes nothing. 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 diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 68e17e748..200bc86d4 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1007,6 +1007,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur 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"}`). + As VectorCraft's own rule, OK in Live Paint Selection Tool Options, Live Paint Bucket Options, Gap Options and Tile Edge Color gives an error for a color with an alpha part (`#rrggbbaa` or `#rgba`) and changes nothing. A drag of 3 screen pixels or more with the Live Paint Selection tool draws a marquee and selects the faces and edges it touches, partly enclosed ones included, and a Shift-drag adds them to the selection or removes them from it ([Modify Live Paint groups](https://web.archive.org/web/20261006115247/https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/modify-live-paint-groups.html)). A click and a marquee change the selection on release, and Escape before the release leaves the selection as it was. Locked Live Paint groups, those on a locked layer or artboard, and in isolation mode those outside the isolated art are not highlighted or selected. A drag with the Live Paint Selection tool draws a marquee and selects the faces and edges it touches, partly enclosed ones included, and a Shift-drag adds them to the selection or removes them from it ([Modify Live Paint groups](https://web.archive.org/web/20261006115247/https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/modify-live-paint-groups.html)). A click and a marquee change the selection on release, and Escape before the release leaves the selection as it was. Locked Live Paint groups, those on a locked layer or artboard, and in isolation mode those outside the isolated art are not highlighted or selected. When the object the Blend tool picked first is deleted or undone away before the second click, the next click on an object picks that object first instead of doing nothing. Regression tests: `blend_tool_picks_again_when_its_first_pick_is_gone` in `meshblend.rs` and `the_blend_tool_picks_again_when_its_first_pick_is_deleted` in `tests/blendopts.rs`.