Live Paint: a loop cut from a self-crossing closed path keeps no brush

A closed path that crosses itself is cut into closed loops. The loop
holding the path's first anchor counted as the whole path, so it kept
the path's width profile and brush. A closed edge now counts as the
whole path only when it is as long as the path.
This commit is contained in:
dotCooCoo
2026-11-14 12:49:39 -08:00
parent 6a5e6fa212
commit 4d4acd97c3
2 changed files with 20 additions and 2 deletions
+4 -2
View File
@@ -576,8 +576,9 @@ impl Track {
/// ends at its end (an open subpath), and whether it runs the whole subpath. The middle of the
/// edge's first segment places it, and its ends are taken where they lie that far along the source,
/// so a path that passes a point twice is told apart. An edge that runs against the direction of its
/// source is reversed first, and a closed edge is started at the anchor where its source starts
/// (one with no anchor there does not count as running the whole subpath). `None` when the edge is
/// source is reversed first, and a closed edge as long as its closed source is started at the anchor
/// where its source starts (one with no anchor there, or a shorter loop cut from a source that
/// crosses itself, does not count as running the whole subpath). `None` when the edge is
/// not a single piece found on the source.
fn edge_run(tracks: &[Track], edge: &mut PathData) -> Option<(f64, bool, bool, bool)> {
use kurbo::{ParamCurve, ParamCurveArclen};
@@ -630,6 +631,7 @@ fn edge_run(tracks: &[Track], edge: &mut PathData) -> Option<(f64, bool, bool, b
// Start it at the anchor where the source starts, so settings that follow the path begin there.
let start = track.segs.first().map(|s| s.start())?;
let moved = edge_closed
&& (length - track.len()).abs() <= ON_SOURCE.max(track.len() * 1e-4)
&& edge.subpaths.first_mut().is_some_and(|sp| match sp.anchors.iter().position(|a| a.p.distance(start) <= ON_SOURCE) {
Some(k) => {
sp.anchors.rotate_left(k);
+16
View File
@@ -2830,6 +2830,22 @@ fn live_paint_make_starts_a_closed_edge_where_its_path_starts() {
assert_eq!(e.appearance.items, rows, "clockwise {clockwise}, start {start}");
}
}
// A closed path that crosses itself is cut into loops, and the loop that holds the path's
// first anchor is still only part of the path: it keeps no brush.
let mut s = session();
let anchors = [(0.0, 0.0), (100.0, 100.0), (100.0, 0.0), (0.0, 100.0)].map(|(x, y)| json!({"x": x, "y": y}));
let z = NodeId(s.execute("path.create", &json!({"anchors": anchors, "closed": true})).unwrap()["id"].as_u64().unwrap());
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.brush = Some("Charcoal".into());
});
set_rows(&mut s, z, vec![row]);
s.execute("select.set", &json!({"ids": [z.0]})).unwrap();
let g = NodeId(s.execute("livePaint.make", &json!({})).unwrap()["id"].as_u64().unwrap());
let gn = node(&s, g);
let edges = vectorcraft_tools::builder::edges(&gn);
assert!(edges.len() > 1, "the crossing cuts the path, got {} edge", edges.len());
assert!(edges.iter().all(|e| e.appearance.stroke().is_some_and(|st| st.brush.is_none())), "no loop is the whole path");
}
/// When Make cuts a stroke whose only setting that depends on direction is a gradient along or