diff --git a/crates/ui-egui/src/dialogs/shape_builder_options.rs b/crates/ui-egui/src/dialogs/shape_builder_options.rs index 2262c9429..9393e0707 100644 --- a/crates/ui-egui/src/dialogs/shape_builder_options.rs +++ b/crates/ui-egui/src/dialogs/shape_builder_options.rs @@ -13,7 +13,7 @@ use serde_json::{Map, Value, json}; use vectorcraft_color::Color; use vectorcraft_doc::LAYER_COLORS; -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}; @@ -118,8 +118,11 @@ fn body(_app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool { false } -/// OK: set the Shape Builder's options. +/// OK: set the Shape Builder's options, or an error for a `highlightColor` that is not a color. fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result { + if d.fields.contains_key("highlightColor") { + checked(&d.str("highlightColor"))?; + } 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": "shapeBuilder", "values": values })) } @@ -251,6 +254,25 @@ mod tests { assert_eq!(bounds, vec![[100.0, 100.0, 300.0, 100.0], [100.0, 100.0, 300.0, 200.0]]); } + /// 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_highlight_color_with_an_alpha_part_is_refused() { + let mut app = app(); + app.run("tool.options", json!({"tool": "shapeBuilder"})).unwrap(); + let d = app.ui.dialog.as_mut().unwrap(); + d.fields.insert("highlightColor".into(), json!("#ff000080")); + d.fields.insert("highlightFill".into(), json!(false)); + 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("shapeBuilder"); + assert_eq!((o["highlightColor"].as_str(), o["highlightFill"].as_bool()), (Some("#ff4f4f"), Some(true)), "nothing changes"); + app.ui.dialog.as_mut().unwrap().fields.insert("highlightColor".into(), json!("#00ff00")); + super::super::confirm(&mut app).unwrap(); + assert_eq!(app.session.tool_options_of("shapeBuilder")["highlightColor"], json!("#00ff00")); + } + #[test] fn a_straight_line_drag_merges_only_the_regions_the_line_crosses() { // A wide rectangle and a tall one over the middle of its top edge: three regions, the wide diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 14322a3f3..a3bda3523 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}; 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)", + "{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}; a highlightColor with an alpha part (#rrggbbaa or #rgba) is an error and OK changes nothing), 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", diff --git a/docs/mcp.md b/docs/mcp.md index 7076ce5ed..5bb5a0c29 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5919,7 +5919,8 @@ pointer show the current fill and the swatches before and after it), `selection` `straightLine`: a drag touches what the line from its first point to the pointer crosses), `highlightFill` (on by default; when it is off, merge mode shades no regions), `gapDetection` (off) with `gapLength` (pt, 0 to 72; 3), `clickStrokeSplitsPath` (In Merge Mode, Clicking Stroke Splits the Path; off), `highlightStroke` (Highlight Stroke When Editable; on) and `highlightColor` -(`#rrggbb` or a layer color's name; `#ff4f4f`, the layer color Red). With `clickStrokeSplitsPath` on, in merge mode, +(`#rrggbb` or a layer color's name; `#ff4f4f`, the layer color Red). The dialog's OK gives an error for a +`highlightColor` with an alpha part (`#rrggbbaa` or `#rgba`) and changes nothing. With `clickStrokeSplitsPath` on, in merge mode, the pointer shows scissors within 4 screen pixels of a stroke, `highlightStroke` draws the run of stroke under it in `highlightColor`, and a click splits that run off its path: the run goes from the anchor or crossing point before the pointer to the one after it, the path keeps the rest of it, and a copy of the path with the run, in the path's diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index fa2bc1f7d..7f8770872 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -814,6 +814,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur With two open paths selected, a Pencil stroke from an end of one that ends within Edit selected paths' distance of an end of the other connects them into one path with the first path's appearance, in one undo step, as [Connect two paths with the Pencil tool](https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/connect-two-paths-with-the-pencil-tool.html) describes (`path.freehand` `connect`). Double-clicking the Shape Builder tool opens Shape Builder Tool Options ([Shape Builder tool options](https://web.archive.org/web/20260105172244/https://helpx.adobe.com/illustrator/using/building-new-shapes-using-shape.html)): with Consider Open Filled Paths as Closed off, a filled open path cuts regions as an unfilled one does; Pick Color From Color Swatches fills merged shapes with the current fill, the Left and Right arrow keys step it through the document's color swatches, and Cursor Swatch Preview shows the current fill and the swatches before and after it above the pointer; Selection Straight Line merges what the line from a drag's first point to its last crosses ([Setting Illustrator's Shape Builder Options](https://web.archive.org/web/20260615204453/https://creativepro.com/setting-illustrators-shape-builder-options/)); with Highlight Fill off, merge mode shades no regions (`shapeBuilder.merge` `openFilledAsClosed` and `pickColorFrom`). With In Merge Mode, Clicking Stroke Splits the Path on in the same dialog, a click on a stroke in merge mode splits its path into the run of stroke under the pointer and the rest of the path, as the [Shape Builder tool options](https://web.archive.org/web/20260105172244/https://helpx.adobe.com/illustrator/using/building-new-shapes-using-shape.html) page describes; the run goes from the anchor or crossing point before the pointer to the one after it, and the pointer shows scissors over it. Highlight Stroke When Editable draws that run in the color chosen in its Color list (Red by default), and with Pick Color From Color Swatches the run takes the current fill as its stroke (`shapeBuilder.merge` `splitStroke`). + As VectorCraft's own rule, OK in Shape Builder Tool Options gives an error for a highlight Color with an alpha part (`#rrggbbaa` or `#rgba`) and changes nothing. Regression test: `dialogs::shape_builder_options::tests::a_highlight_color_with_an_alpha_part_is_refused`. With Pick Color From Artwork, a merged shape takes the appearance, opacity and blending mode of the object where the drag starts, else of the object where it ends, else of the front-most selected object, as the [Shape Builder combine page](https://web.archive.org/web/20251006211643/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/combine-shapes-with-shape-builder-tool.html) describes. A click in an area that no selected object covers, such as a hole other shapes enclose, takes the front-most selected object's look (`shapeBuilder.merge` `points`: the first point is where the drag starts, and the last point is where it ends). A Shift-drag with the Shape Builder tool draws a rectangular marquee for merging many paths at once, as [Combine shapes with the Shape Builder tool](https://web.archive.org/web/20251006211643/https://helpx.adobe.com/illustrator/desktop/draw-shapes-and-paths/draw-shapes/combine-shapes-with-shape-builder-tool.html) describes: the regions it reaches are shaded while it is drawn and merge into one shape on release, or with Alt they are deleted. The marquee reaches each region whose outline crosses it or has an anchor inside it, and the region that holds the whole marquee; this rule is VectorCraft's own. For Pick Color From Artwork, the marquee's starting corner is where the drag starts and the opposite corner is where it ends (`shapeBuilder.merge` `rect`). Shape Builder pieces on dense hatches (lines at exact 0.25 or 0.5 pt steps across another line) come back one per stretch between crossings, with no speck where the sweep cut a line's two passes a rounding error apart: the planar map contracts the stub between such cuts into one vertex, bounded by each cluster's extent so a run of tiny level segments keeps its length (`crates/pathops/src/planar.rs` tests `a_dense_hatch_at_exact_binary_steps_cuts_each_line_once_per_crossing`, `a_run_of_tiny_level_segments_is_not_contracted_as_stubs`, `a_line_crossed_where_its_two_passes_part_is_cut_once`).