From 6a5e6fa2125dda4af3cf166f865f83bbd52454d8 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 14 Nov 2026 12:16:14 -0800 Subject: [PATCH] Live Paint: Gap Options fits an edge's dashes and arrowheads to each piece it cuts When Gap Options cut an edge in two, each piece took the old edge's stroke rows unchanged: both arrowheads, the dash phase of the edge's start, and a brush or width profile spread over the whole edge. Each piece now takes those rows fitted to its part of the old edge, as Make fits a source path's rows to the edges cut from it. --- crates/engine/src/cmd/buildcmds.rs | 19 +++++++++++-- crates/engine/src/tests/build.rs | 44 ++++++++++++++++++++++++++++++ docs/mcp.md | 3 +- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/cmd/buildcmds.rs b/crates/engine/src/cmd/buildcmds.rs index a0decb33d7..7c404571a5 100644 --- a/crates/engine/src/cmd/buildcmds.rs +++ b/crates/engine/src/cmd/buildcmds.rs @@ -749,8 +749,9 @@ fn build_children(d: &mut Document, sources: Vec>, max_gap: Option = old_path.subpaths.iter().map(Track::new).collect(); + let run = n.path_data_mut().and_then(|path| edge_run(&tracks, path)); + let mut a = (*n.appearance).clone(); + for r in &mut a.items { + fit_to_edge(r, run); + } + n.appearance = a.into(); + } *c = Arc::new(n); } } diff --git a/crates/engine/src/tests/build.rs b/crates/engine/src/tests/build.rs index 960cab107e..e503dfe2fc 100644 --- a/crates/engine/src/tests/build.rs +++ b/crates/engine/src/tests/build.rs @@ -2753,6 +2753,50 @@ fn live_paint_make_fits_dashes_and_arrowheads_to_each_edge() { assert_eq!(edges, vec![rows]); } +/// When Gap Options cuts an edge in two, each piece keeps the edge's dashes going on from where it +/// starts along the edge, an arrowhead only at the end of the edge it ends at, and no brush. +#[test] +fn live_paint_gap_options_fits_an_edge_look_to_each_piece() { + use vectorcraft_doc::{Arrowhead, Dash}; + let mut s = session(); + let l = line(&mut s, (0.0, 50.0), (100.0, 50.0)); + let stub = line(&mut s, (50.0, 0.0), (50.0, 48.0)); + let row = stroke_row(Paint::solid(Color::BLACK), 2.0, |st| { + st.dash = Some(Dash { pattern: vec![10.0, 5.0], offset: 2.0, align_corners: false }); + st.start_arrow = Some(Arrowhead::Triangle); + st.end_arrow = Some(Arrowhead::Circle); + st.brush = Some("Charcoal".into()); + }); + let rows = set_rows(&mut s, l, vec![row]); + s.execute("select.set", &json!({"ids": [l.0, stub.0]})).unwrap(); + let g = NodeId(s.execute("livePaint.make", &json!({})).unwrap()["id"].as_u64().unwrap()); + let dashed = |s: &Session| { + let mut out: Vec<(f64, f64, vectorcraft_doc::StrokeLayer)> = vectorcraft_tools::builder::edges(&node(s, g)) + .iter() + .filter_map(|e| { + let st = e.appearance.stroke().filter(|st| st.dash.is_some())?.clone(); + let sp = e.path_data()?.subpaths.first()?; + Some(((sp.anchors.first()?.p.x * 1e3).round() / 1e3, (sp.anchors.last()?.p.x * 1e3).round() / 1e3, st)) + }) + .collect(); + out.sort_by(|p, q| p.0.total_cmp(&q.0)); + out + }; + let before = dashed(&s); + assert_eq!(before.len(), 1, "nothing cuts the line yet"); + assert_eq!(vec![AppearanceItem::Stroke(before[0].2.clone())], rows); + let r = s.execute("livePaint.gapOptions", &json!({"id": g.0, "closeGaps": true})).unwrap(); + assert_eq!(r["closed"], json!(1)); + let pieces = dashed(&s); + let ends: Vec<(f64, f64)> = pieces.iter().map(|p| (p.0, p.1)).collect(); + assert_eq!(ends, vec![(0.0, 50.0), (50.0, 100.0)], "the line is cut where the gap closes"); + let offsets: Vec = pieces.iter().map(|p| (p.2.dash.as_ref().unwrap().offset * 1e3).round() / 1e3).collect(); + assert_eq!(offsets, vec![2.0, 52.0], "the dashes go on along the line"); + let arrows: Vec<_> = pieces.iter().map(|p| (p.2.start_arrow.clone(), p.2.end_arrow.clone())).collect(); + assert_eq!(arrows, vec![(Some(Arrowhead::Triangle), None), (None, Some(Arrowhead::Circle))]); + assert!(pieces.iter().all(|p| p.2.brush.is_none()), "a brush spreads over the whole line"); +} + /// The edge Make builds from a closed path that nothing cuts starts at the anchor the path starts /// at and runs the way the path does, whichever anchor that is, so its dashes and brush lie where /// they do on the path, and it keeps the path's stroke row as it is. diff --git a/docs/mcp.md b/docs/mcp.md index 86724f5b7f..823c8bc577 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3614,7 +3614,8 @@ already walks Image Trace. Short agent entry points: the path (Align Dashes to Corners is turned off on it), the arrowheads stay only at the path's own ends, and a width profile or brush stays only on an edge that runs the whole path. A gradient applied along a stroke starts again on each cut edge, and an inside or outside stroke is drawn centered on an edge that is an open piece of a closed path. - A face or edge given no such row has one unpainted row. Rows that do not draw stay with the stored sources, which + When `livePaint.gapOptions` cuts an edge into pieces, each piece takes the edge's rows fitted to its part of the + edge in the same way. A face or edge given no such row has one unpainted row. Rows that do not draw stay with the stored sources, which `livePaint.release` returns unchanged. The source's object effects and the strokes' pen pressure are not carried. Expand keeps those face fills. `livePaint.expand` keeps each face and edge that draws, with all its fill and stroke rows: it is shown, its opacity