diff --git a/crates/engine/src/cmd/buildcmds.rs b/crates/engine/src/cmd/buildcmds.rs index 7c404571a5..07b178bacc 100644 --- a/crates/engine/src/cmd/buildcmds.rs +++ b/crates/engine/src/cmd/buildcmds.rs @@ -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); diff --git a/crates/engine/src/tests/build.rs b/crates/engine/src/tests/build.rs index e503dfe2fc..f45644d951 100644 --- a/crates/engine/src/tests/build.rs +++ b/crates/engine/src/tests/build.rs @@ -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