mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 18:51:35 +00:00
Live Paint: the Live Paint Bucket highlights an edge thinner than a face
The bucket outlines the face under the pointer at its Width setting and an edge at half of it (4 px and 2 px by default), as the help page says faces are highlighted with a thick line and edges with a thin line. The Live Paint Selection tool keeps one width for faces and edges.
This commit is contained in:
@@ -36,7 +36,10 @@ use cache::RegionCache;
|
||||
#[cfg(test)]
|
||||
use live_paint::LpHover;
|
||||
use live_paint::lp_color;
|
||||
pub use live_paint::{LP_BUCKET_WIDTH, LP_BUCKET_WIDTH_RANGE, LP_SELECT_WIDTH, LP_SELECT_WIDTH_RANGE, LivePaintBucketTool, LivePaintSelectionTool};
|
||||
pub use live_paint::{
|
||||
LP_BUCKET_EDGE_FRACTION, LP_BUCKET_WIDTH, LP_BUCKET_WIDTH_RANGE, LP_SELECT_WIDTH, LP_SELECT_WIDTH_RANGE, LivePaintBucketTool,
|
||||
LivePaintSelectionTool,
|
||||
};
|
||||
pub use lookup::{
|
||||
EDGE_NAME, FACE_NAME, Face, LIVE_PAINT_NAME, LIVE_PAINT_PREFIX, SOURCES_NAME, builder_members, drawn_outlines, edge_near, edges, face_at, faces,
|
||||
is_live_paint, leaf_shapes, live_paint_at, live_paint_takes, node_outline, outline_snap, path_contains, shape_builder_shapes, shapes_for,
|
||||
|
||||
@@ -13,8 +13,10 @@ use super::{
|
||||
pub(super) enum LpHover {
|
||||
#[default]
|
||||
None,
|
||||
/// A face (or edge) of an existing Live Paint group.
|
||||
/// A face of an existing Live Paint group.
|
||||
Part(BezPath),
|
||||
/// An edge of an existing Live Paint group.
|
||||
Edge(BezPath),
|
||||
/// A region of the (not yet live) selection.
|
||||
Region(usize),
|
||||
}
|
||||
@@ -66,7 +68,8 @@ fn lp_selectable(cx: &ToolContext, id: NodeId) -> bool {
|
||||
/// region is highlighted.
|
||||
fn lp_hover(cx: &ToolContext, cache: Option<&mut RegionCache>, p: Point, edge: bool, takes: &dyn Fn(NodeId) -> bool) -> LpHover {
|
||||
if let Some((_, part)) = lp_part_at(cx.doc, p, cx.tol(4.0), edge, takes) {
|
||||
return part.and_then(|n| n.path_data()).map(|pd| LpHover::Part(pd.to_bezpath())).unwrap_or_default();
|
||||
let path = part.and_then(|n| n.path_data()).map(|pd| pd.to_bezpath());
|
||||
return path.map(|bp| if edge { LpHover::Edge(bp) } else { LpHover::Part(bp) }).unwrap_or_default();
|
||||
}
|
||||
let sel_is_live = cx.selection.objects.iter().any(|id| cx.doc.node(*id).is_some_and(is_live_paint));
|
||||
if !sel_is_live && let Some(i) = cache.and_then(|c| c.ready(cx, true)).and_then(|m| m.region_at(p)) {
|
||||
@@ -75,11 +78,13 @@ fn lp_hover(cx: &ToolContext, cache: Option<&mut RegionCache>, p: Point, edge: b
|
||||
LpHover::None
|
||||
}
|
||||
|
||||
/// The highlight of `h`, `width` screen pixels wide in `color`.
|
||||
fn lp_overlays(h: &LpHover, cache: &RegionCache, color: [u8; 3], width: f32) -> Vec<Overlay> {
|
||||
/// The highlight of `h` in `color`: a face or region `width` screen pixels wide, an edge
|
||||
/// `edge_width` screen pixels wide.
|
||||
fn lp_overlays(h: &LpHover, cache: &RegionCache, color: [u8; 3], width: f32, edge_width: f32) -> Vec<Overlay> {
|
||||
match h {
|
||||
LpHover::None => vec![],
|
||||
LpHover::Part(bp) => vec![Overlay::Path { path: bp.clone(), color, width, dashed: false }],
|
||||
LpHover::Edge(bp) => vec![Overlay::Path { path: bp.clone(), color, width: edge_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()
|
||||
}
|
||||
@@ -92,6 +97,10 @@ pub const LP_BUCKET_WIDTH: f64 = 4.0;
|
||||
/// The least and greatest highlight width (screen pixels) the Live Paint Bucket takes.
|
||||
pub const LP_BUCKET_WIDTH_RANGE: (f64, f64) = (1.0, 20.0);
|
||||
|
||||
/// The width of the Live Paint Bucket's edge highlight, as a fraction of its Width setting: a
|
||||
/// face is outlined at the Width and an edge at this fraction of it.
|
||||
pub const LP_BUCKET_EDGE_FRACTION: f64 = 0.5;
|
||||
|
||||
/// What a Live Paint Bucket click paints.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum LpPaint {
|
||||
@@ -110,7 +119,8 @@ enum LpPaint {
|
||||
/// Clicking selected ordinary paths without Alt makes them a Live Paint group first. Its Tool
|
||||
/// Options (double-click the tool) are `paintFills` (on) and `paintStrokes` (off), `highlight`
|
||||
/// (on), the highlight `color` (`#rrggbb`, red by default) and its `width` ([`LP_BUCKET_WIDTH`]
|
||||
/// pixels). With Paint Strokes alone on, a click paints the edge nearest the pointer and
|
||||
/// pixels): a face is outlined at the width and an edge at [`LP_BUCKET_EDGE_FRACTION`] of it.
|
||||
/// With Paint Strokes alone on, a click paints the edge nearest the pointer and
|
||||
/// Shift-click fills the face. With both on, a click paints the edge nearest the pointer when one
|
||||
/// is within 4 pixels, else it fills the face, and Shift-click fills the face. With both off, a
|
||||
/// click paints nothing. With Highlight off, the face or edge under the pointer is not
|
||||
@@ -167,7 +177,7 @@ impl LivePaintBucketTool {
|
||||
// A click paints no region of the selection, so none is highlighted.
|
||||
LpPaint::Strokes => lp_hover(cx, None, p, true, &takes),
|
||||
LpPaint::Both => match lp_part_at(cx.doc, p, cx.tol(4.0), true, &takes) {
|
||||
Some((_, Some(e))) => e.path_data().map(|pd| LpHover::Part(pd.to_bezpath())).unwrap_or_default(),
|
||||
Some((_, Some(e))) => e.path_data().map(|pd| LpHover::Edge(pd.to_bezpath())).unwrap_or_default(),
|
||||
_ => lp_hover(cx, Some(&mut self.cache), p, false, &takes),
|
||||
},
|
||||
}
|
||||
@@ -235,7 +245,7 @@ impl Tool for LivePaintBucketTool {
|
||||
}
|
||||
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
lp_overlays(&self.hover, &self.cache, self.color, self.width as f32)
|
||||
lp_overlays(&self.hover, &self.cache, self.color, self.width as f32, (self.width * LP_BUCKET_EDGE_FRACTION) as f32)
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, m: Mods) -> Cursor {
|
||||
@@ -416,7 +426,7 @@ impl Tool for LivePaintSelectionTool {
|
||||
PointerKind::Move => {
|
||||
// Only the parts of Live Paint groups highlight, so no map of the selection is built.
|
||||
self.hover = match edge.map(|e| lp_hover(cx, None, p, e, &|id| lp_selectable(cx, id))) {
|
||||
Some(h @ LpHover::Part(_)) => h,
|
||||
Some(h @ (LpHover::Part(_) | LpHover::Edge(_))) => h,
|
||||
_ => LpHover::None,
|
||||
};
|
||||
vec![]
|
||||
@@ -479,7 +489,7 @@ impl Tool for LivePaintSelectionTool {
|
||||
if let Some(r) = self.drag.and_then(|d| d.marquee(cx)) {
|
||||
return vec![Overlay::Marquee(r)];
|
||||
}
|
||||
lp_overlays(&self.hover, &self.cache, self.color, self.width as f32)
|
||||
lp_overlays(&self.hover, &self.cache, self.color, self.width as f32, self.width as f32)
|
||||
}
|
||||
|
||||
fn cursor(&self, _cx: &ToolContext, _p: Point, _m: Mods) -> Cursor {
|
||||
|
||||
@@ -865,6 +865,37 @@ fn bucket_options_set_the_highlight() {
|
||||
assert!(highlight(&mut t).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_highlights_an_edge_thinner_than_a_face() {
|
||||
let (d, ..) = live_group_doc();
|
||||
let s = Selection::default();
|
||||
let p = paint();
|
||||
let cx = cx(&d, &s, &p);
|
||||
let mut t = LivePaintBucketTool::default();
|
||||
let width = |t: &mut LivePaintBucketTool, x: f64, shift: bool| {
|
||||
t.pointer(&cx, &ev(PointerKind::Move, x, 25.0).with_mods(Mods { shift, ..Default::default() }));
|
||||
match t.overlays(&cx).as_slice() {
|
||||
[Overlay::Path { width, .. }] => *width,
|
||||
ov => panic!("{ov:?}"),
|
||||
}
|
||||
};
|
||||
// The face at (75, 25) and, with Shift (Paint Strokes), the edge at x = 50.
|
||||
assert_eq!((width(&mut t, 75.0, false), width(&mut t, 50.0, true)), (4.0, 2.0));
|
||||
t.set_option("width", &json!(9));
|
||||
assert_eq!((width(&mut t, 75.0, false), width(&mut t, 50.0, true)), (9.0, 4.5));
|
||||
// With both on, the edge a click paints is drawn thin too.
|
||||
t.set_option("paintStrokes", &json!(true));
|
||||
assert_eq!((width(&mut t, 75.0, false), width(&mut t, 50.0, false)), (9.0, 4.5));
|
||||
// The Live Paint Selection tool outlines a face and an edge (Alt) at its one Width.
|
||||
let mut sel = LivePaintSelectionTool::default();
|
||||
for alt in [false, true] {
|
||||
let x = if alt { 50.0 } else { 75.0 };
|
||||
sel.pointer(&cx, &ev(PointerKind::Move, x, 25.0).with_mods(Mods { alt, ..Default::default() }));
|
||||
let ov = sel.overlays(&cx);
|
||||
assert!(matches!(ov.as_slice(), [Overlay::Path { width: 4.0, .. }]), "{ov:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bucket_alt_samples_face_edge_and_selected_region_without_painting() {
|
||||
let (mut d, _gid, face, edge) = live_group_doc();
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//!
|
||||
//! Fields: `paintFills` and `paintStrokes` (the faces and the edges of Live Paint groups),
|
||||
//! `highlight` (whether the face or edge under the pointer is outlined), `color` (the highlight
|
||||
//! color: `#rrggbb`, or a layer color's name such as "Red") and `width` (the highlight's width in
|
||||
//! screen pixels).
|
||||
//! color: `#rrggbb`, or a layer color's name such as "Red") and `width` (the width of a face's
|
||||
//! highlight in screen pixels; an edge's highlight is half as wide).
|
||||
|
||||
use serde_json::{Map, Value, json};
|
||||
use vectorcraft_color::Color;
|
||||
@@ -132,9 +132,9 @@ mod tests {
|
||||
d.fields.insert("width".into(), json!(8));
|
||||
super::super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none());
|
||||
// The tool takes them: by the edge, the edge highlights in the new color and width.
|
||||
// The tool takes them: by the edge, the edge highlights in the new color at half the new width.
|
||||
let ov = hover(&mut app, 151.0);
|
||||
assert!(ov.iter().any(|o| matches!(o, Overlay::Path { color: [0x4f, 0xff, 0x4f], width: 8.0, .. })), "{ov:?}");
|
||||
assert!(ov.iter().any(|o| matches!(o, Overlay::Path { color: [0x4f, 0xff, 0x4f], width: 4.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);
|
||||
|
||||
File diff suppressed because one or more lines are too long
+2
-2
@@ -1728,8 +1728,8 @@ already walks Image Trace. Short agent entry points:
|
||||
a locked artboard, or in isolation mode outside the isolated art.
|
||||
The Live Paint Bucket's options (`tool.setOption {tool: "livePaintBucket", values}`; double-clicking the tool opens
|
||||
them, dialog `livePaintBucketOptions`) are `paintFills` (on), `paintStrokes` (off), `highlight` (on), `color` (the
|
||||
highlight color, `#rrggbb` or a layer color's name; `#ff2a2a`) and `width` (the highlight's width, 1 to 20 screen
|
||||
pixels; 4). With `paintFills` alone on, a click runs `livePaint.fill` and Shift-click runs `livePaint.strokeEdge`.
|
||||
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`.
|
||||
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
|
||||
|
||||
@@ -936,7 +936,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
- Create Strokes uses exact squared Euclidean distances and Guo–Hall thinning in bounded per-blob crops. Maximal-disc pruning, split-crossing merges and degree-two joins simplify its skeleton graph. The whole-blob width rule and ink fitting remain; 46 code-drawn inputs measure ends and junctions in [`image-trace-quality.md`](image-trace-quality.md#centre-lines). Junction degrees measure 44/45 correct in each input variant; remaining spurs are recorded. This is a measured skeleton improvement; the Image Trace score, missing per-feature selection and width controls, and estimates remain unchanged.
|
||||
Object › Live Paint › Merge adds the other selected paths and Live Paint groups to the bottom selected Live Paint group ([Live Paint groups](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/live-paint-groups.html)), which keeps its id, the paint of its faces and edges, and its name, lock, visibility, transparency, appearance, opacity mask and graphic style link. Object › Live Paint › Make with a Live Paint group selected, and a Live Paint Bucket click on a selected path beside a selected Live Paint group, add the paths to that group the same way. Make, Merge and the Live Paint Bucket take paths, compound paths and groups of them; with type, an image or another object among the selection they report that it has to be converted to paths first and change nothing, as type and raster images can't go into a Live Paint group directly ([Create Live Paint groups](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/create-live-paint-groups.html)); before, such an object went among the group's hidden sources and vanished from the canvas. Evidence: `tests_build::live_paint_refuses_type_and_images_and_changes_nothing`.
|
||||
With the Live Paint Bucket selected, Alt-click on vector art samples its fill, stroke and stroke weight into the paint defaults ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)). On a placed image it samples the image pixel as a solid fill with no stroke. Alt+Shift-click samples only the color under the pointer, including an intermediate gradient color or image pixels, into the active fill or stroke ([Default keyboard shortcuts](https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). Neither click paints or changes the selection or selected art, and the next Bucket click paints with the sampled paint. Evidence: `tests_build::live_paint_bucket_alt_sample_paints_next_face_without_changing_selection`.
|
||||
Double-clicking the Live Paint Bucket opens Live Paint Bucket Options ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)): Paint Fills (on) and Paint Strokes (off) set whether a click paints faces, edges or both, Highlight turns the outline of the face or edge under the pointer on or off, and Color and Width set it (red and 4 px by default). With both on, a click paints the edge within 4 px of the pointer, else the face, and Shift switches to Paint Fills only; with one on, Shift switches to the other. The options last across tool switches and are saved with the preferences (`tool.setOption {tool: "livePaintBucket"}`). Evidence: `builder::tests::{bucket_options_choose_what_a_click_paints, bucket_options_set_the_highlight}`, `dialogs::live_paint_bucket::tests::double_clicking_the_tool_opens_its_options_and_ok_sets_them`.
|
||||
Double-clicking the Live Paint Bucket opens Live Paint Bucket Options ([Paint with the Live Paint Bucket tool](https://helpx.adobe.com/illustrator/desktop/paint-and-fill/learn-painting-basics/paint-with-the-live-paint-bucket-tool.html)): Paint Fills (on) and Paint Strokes (off) set whether a click paints faces, edges or both, Highlight turns the outline of the face or edge under the pointer on or off, and Color and Width set it (red and 4 px by default). Faces are outlined at the Width and edges at half of it, as the page says faces are highlighted with a thick line and edges with a thin line. With both on, a click paints the edge within 4 px of the pointer, else the face, and Shift switches to Paint Fills only; with one on, Shift switches to the other. The options last across tool switches and are saved with the preferences (`tool.setOption {tool: "livePaintBucket"}`). Evidence: `builder::tests::{bucket_options_choose_what_a_click_paints, bucket_options_set_the_highlight, bucket_highlights_an_edge_thinner_than_a_face}`, `dialogs::live_paint_bucket::tests::double_clicking_the_tool_opens_its_options_and_ok_sets_them`.
|
||||
Live Paint 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"}`).
|
||||
|
||||
Reference in New Issue
Block a user