Live Paint: a plain group named "Live Paint..." is not a Live Paint group

A group counted as a Live Paint group when its name started with "Live
Paint". A plain group named, for example, "Live Painting" was then read
as one. With it and another shape selected, the Shape Builder found no
regions in it, and a merge on the other shape deleted the group and its
paths. Make Live Paint took the group as a Live Paint group to merge
into, and its paths did not become sources.

A Live Paint group now also needs its sources group, the group named
"Live Paint Sources" that Make Live Paint puts in it. A plain group with
such a name is traversed like any other group.
This commit is contained in:
dotCooCoo
2026-10-26 09:09:44 +07:00
committed by bflatastic
parent 03ab7bb805
commit 3567e5be7d
3 changed files with 56 additions and 9 deletions
+4 -4
View File
@@ -1,10 +1,10 @@
//! Shape Builder (`shapeBuilder.*`), Live Paint (`livePaint.*`) and Image Trace (`imageTrace.*`).
//!
//! **Live Paint groups** need no special node kind: a Live Paint group is a plain group whose name
//! starts with "Live Paint". Its first child is a hidden group named "Live Paint Sources" holding
//! the original paths (the planar map's inputs, used by Release, Merge and re-computation); then
//! come the faces (paths named "Face", fill only) and the edges (open paths named "Edge", stroke
//! only). See `vectorcraft_tools::builder` for the shared helpers.
//! starts with "Live Paint" and that holds a group named "Live Paint Sources". That group is its
//! first child, hidden, and holds the original paths (the planar map's inputs, used by Release,
//! Merge and re-computation); then come the faces (paths named "Face", fill only) and the edges
//! (open paths named "Edge", stroke only). See `vectorcraft_tools::builder` for the shared helpers.
//!
//! **Image Trace** makes a group named "Image Trace" holding the source image (hidden, bottom)
//! and the traced filled paths above it, mapped with the image's transform. Expand drops the
+36
View File
@@ -2199,6 +2199,42 @@ fn shape_builder_keeps_the_edges_of_an_unfilled_live_paint_group() {
assert!((stroked_length(&s, &out) - 700.0).abs() < 1.0, "{}", stroked_length(&s, &out));
}
/// A plain group whose name starts with "Live Paint" is not a Live Paint group: the Shape Builder
/// takes its paths, and a merge elsewhere keeps them.
#[test]
fn shape_builder_takes_the_paths_of_a_plain_group_named_live_paint() {
let mut s = session();
let a = rect(&mut s, 0.0, 0.0, 100.0, 100.0);
set_fill(&mut s, a, Color::rgb(1.0, 0.0, 0.0));
s.execute("select.set", &json!({"ids": [a.0]})).unwrap();
let g = NodeId(s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap());
s.edit("t", |d, _| {
d.node_mut(g).unwrap().name = Some("Live Painting".into());
Ok(())
})
.unwrap();
let c = rect(&mut s, 160.0, 20.0, 100.0, 20.0);
set_fill(&mut s, c, Color::rgb(0.0, 1.0, 0.0));
s.execute("select.set", &json!({"ids": [g.0, c.0]})).unwrap();
let map = s.execute("shapeBuilder.regions", &json!({})).unwrap();
assert_eq!(map["regions"].as_array().unwrap().len(), 2, "{map}");
let r = s.execute("shapeBuilder.merge", &json!({"points": [[200, 30]]})).unwrap();
let out = ids(&r);
assert!(out.iter().any(|i| (area(&s, *i) - 10000.0).abs() < 1.0), "the group's rectangle is kept: {r}");
// A stroke of the group's rectangle splits as a plain path's does.
s.execute("edit.undo", &json!({})).unwrap();
s.execute("select.set", &json!({"ids": [g.0]})).unwrap();
let r = s.execute("shapeBuilder.merge", &json!({"points": [[-1, 50]], "splitStroke": true})).unwrap();
assert_eq!(node(&s, g).children().unwrap().len(), 2, "{r}");
// Make Live Paint takes the group as a source rather than as a Live Paint group to merge into.
s.execute("edit.undo", &json!({})).unwrap();
s.execute("select.set", &json!({"ids": [g.0]})).unwrap();
let r = s.execute("livePaint.make", &json!({})).unwrap();
assert_eq!(r["faces"], 1, "{r}");
let made = node(&s, NodeId(r["id"].as_u64().unwrap()));
assert!(vectorcraft_tools::builder::sources(&made).unwrap().children().unwrap().iter().any(|c| c.id == g));
}
// ---------- Image Trace ----------
/// A 100×100 image with a black disc (r = 30 px), placed at (50, 50) scaled ×2.
+16 -5
View File
@@ -23,7 +23,7 @@ use vectorcraft_pathops as po;
use crate::{Action, Cursor, Mods, Overlay, PointerEvent, PointerKind, Tool, ToolContext, ToolKey, json_ids};
/// Names starting with this mark a Live Paint group.
/// Names starting with this mark a Live Paint group, together with its sources group ([`SOURCES_NAME`]).
pub const LIVE_PAINT_PREFIX: &str = "Live Paint";
/// Default name of a new Live Paint group.
pub const LIVE_PAINT_NAME: &str = "Live Paint Group";
@@ -36,9 +36,12 @@ pub const EDGE_NAME: &str = "Edge";
pub const HATCH: [u8; 3] = [0x80, 0x80, 0x80];
pub const HIGHLIGHT_RED: [u8; 3] = [0xff, 0x2a, 0x2a];
/// Is `n` a Live Paint group?
/// Is `n` a Live Paint group: an unclipped group whose name starts with [`LIVE_PAINT_PREFIX`] and
/// that holds a group named [`SOURCES_NAME`]? A plain group with such a name is not one.
pub fn is_live_paint(n: &Node) -> bool {
matches!(n.kind, NodeKind::Group { clip: false, .. }) && n.name.as_deref().is_some_and(|s| s.starts_with(LIVE_PAINT_PREFIX))
matches!(n.kind, NodeKind::Group { clip: false, .. })
&& n.name.as_deref().is_some_and(|s| s.starts_with(LIVE_PAINT_PREFIX))
&& sources(n).is_some_and(|s| matches!(s.kind, NodeKind::Group { .. }))
}
/// Whether Live Paint can take `n` as it is: a Live Paint group, a path or compound path, or a
@@ -2099,6 +2102,14 @@ mod tests {
assert!(h.elements().len() > 10);
}
/// An empty hidden sources group for a Live Paint group made in a test.
fn empty_sources(d: &mut Document) -> Arc<Node> {
let mut g = Node::group(d.alloc_id(), vec![]);
g.name = Some(SOURCES_NAME.into());
g.visible = false;
Arc::new(g)
}
fn live_group_doc() -> (Document, NodeId, NodeId, NodeId) {
let mut d = Document::new(500.0, 500.0);
let l = d.layers[0].id;
@@ -2112,7 +2123,7 @@ mod tests {
let mut edge = Node::path(e, shapes::line(Point::new(50.0, 0.0), Point::new(50.0, 50.0)), Appearance::default());
edge.name = Some(EDGE_NAME.into());
let gid = d.alloc_id();
let mut g = Node::group(gid, vec![Arc::new(face1), Arc::new(face2), Arc::new(edge)]);
let mut g = Node::group(gid, vec![empty_sources(&mut d), Arc::new(face1), Arc::new(face2), Arc::new(edge)]);
g.name = Some(LIVE_PAINT_NAME.into());
d.insert(Some(l), 0, g).unwrap();
(d, gid, f2, e)
@@ -2125,7 +2136,7 @@ mod tests {
let mut face = Node::path(d.alloc_id(), shapes::rectangle(r), Appearance::default());
face.name = Some(FACE_NAME.into());
let (a, b, c, e) = (Point::new(r.x0, r.y0), Point::new(r.x1, r.y0), Point::new(r.x1, r.y1), Point::new(r.x0, r.y1));
let mut kids = vec![Arc::new(face)];
let mut kids = vec![empty_sources(d), Arc::new(face)];
let mut ids = [NodeId(0); 4];
for (k, (from, to)) in [(e, a), (a, b), (b, c), (c, e)].into_iter().enumerate() {
ids[k] = d.alloc_id();