From 3567e5be7d991db253a94e0b0950d49fb9a282e5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 24 Oct 2026 13:17:32 -0700 Subject: [PATCH] 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. --- crates/engine/src/cmd/buildcmds.rs | 8 +++---- crates/engine/src/tests_build.rs | 36 ++++++++++++++++++++++++++++++ crates/tools/src/builder.rs | 21 ++++++++++++----- 3 files changed, 56 insertions(+), 9 deletions(-) diff --git a/crates/engine/src/cmd/buildcmds.rs b/crates/engine/src/cmd/buildcmds.rs index 3f7da1629..8c8c07225 100644 --- a/crates/engine/src/cmd/buildcmds.rs +++ b/crates/engine/src/cmd/buildcmds.rs @@ -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 diff --git a/crates/engine/src/tests_build.rs b/crates/engine/src/tests_build.rs index 1ec8da624..c7763d422 100644 --- a/crates/engine/src/tests_build.rs +++ b/crates/engine/src/tests_build.rs @@ -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. diff --git a/crates/tools/src/builder.rs b/crates/tools/src/builder.rs index 0ef8c7654..143ade594 100644 --- a/crates/tools/src/builder.rs +++ b/crates/tools/src/builder.rs @@ -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 { + 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();