From a83a0054ea5836981190d3e7a3a63d39890be104 Mon Sep 17 00:00:00 2001 From: Sven Lafebre Date: Fri, 6 Nov 2026 20:44:09 +0100 Subject: [PATCH] Part of #5964: stroke gradients cover an arrowhead to the end of its reach With ribs only at the path's corners (an Along gradient), the ribs that reach across an arrowhead stopped at the last corner inside its reach, so the slice narrowed from the head's tip towards the next corner and left the corners of a Triangle head's base outside every slice. Where each arrowhead's reach ends now gets a rib of its own. --- crates/effects/src/stroke/gradient.rs | 17 ++++++++++++++--- crates/effects/src/stroke/tests_gradient.rs | 21 +++++++++++++++++++++ docs/target-app-parity.md | 3 ++- 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/crates/effects/src/stroke/gradient.rs b/crates/effects/src/stroke/gradient.rs index d274bdc2a..677eea308 100644 --- a/crates/effects/src/stroke/gradient.rs +++ b/crates/effects/src/stroke/gradient.rs @@ -174,10 +174,21 @@ fn strips(bp: &BezPath, rule: FillRule, st: &StrokeLayer, tol: f64, margin: f64, } let steps = if max_dt < 1.0 { (1.0 / max_dt).ceil() as usize } else { 1 }; let profile = st.profile.iter().flat_map(|p| p.points.iter().map(|q| q.0)); - let at: Vec = - marks.iter().copied().chain(profile).chain((1..steps).map(|i| i as f64 / steps as f64)).map(|t| t * total).collect(); + let heads = if closed { (0.0, 0.0) } else { heads }; + // Where an arrowhead's reach ends gets a rib of its own, so the ribs that reach + // across a head run all the way there instead of narrowing towards the next corner + // from the head's tip, which leaves the corners of its base outside (#5964). + let head_ends = [heads.0, total - heads.1].into_iter().filter(|&s| s > 0.0 && s < total); + let at: Vec = marks + .iter() + .copied() + .chain(profile) + .chain((1..steps).map(|i| i as f64 / steps as f64)) + .map(|t| t * total) + .chain(head_ends) + .collect(); let (pts, _) = split_at(&pts, &corner, closed, &at); - strip(&lay, &pts, closed, total, side, if closed { (0.0, 0.0) } else { heads }) + strip(&lay, &pts, closed, total, side, heads) } }; out.push(ribs); diff --git a/crates/effects/src/stroke/tests_gradient.rs b/crates/effects/src/stroke/tests_gradient.rs index 489182406..bf63b46b8 100644 --- a/crates/effects/src/stroke/tests_gradient.rs +++ b/crates/effects/src/stroke/tests_gradient.rs @@ -216,3 +216,24 @@ fn meshes_colour_the_stroke_as_the_slices_do() { } } } + +/// #5964: with ribs only at the path's corners (an Along gradient), an arrowhead whose tip sits on +/// the end of a long straight leg is covered all the way to where it can reach: the ribs that +/// reach across it run to the end of its reach, rather than narrowing from its tip towards the +/// next corner and leaving the corners of its base outside. +#[test] +fn slices_cover_an_arrowhead_on_a_long_straight_leg() { + let zig = poly(&[(0.0, 0.0), (60.0, 40.0), (0.0, 80.0), (70.0, 90.0), (75.0, 20.0)], false); + for profile in [None, WidthProfile::preset("lens"), WidthProfile::preset("taperStart")] { + for align in [ArrowAlign::Tip, ArrowAlign::Extend] { + for mode in [StrokeGradientMode::Along, StrokeGradientMode::Across] { + let mut st = stroke(12.0, mode); + st.start_arrow = Some(Arrowhead::Triangle); + st.end_arrow = Some(Arrowhead::Triangle); + st.arrow_align = align; + st.profile = profile.clone(); + assert_covered(&zig, &st); + } + } + } +} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index f128c1e9d..7cc9e14dd 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Width profiles: smooth widths between width points, #5714; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Stroke gradients: an arrowhead aligned to the tip is painted to the corners of its base, #5964; Drop Shadow and glow dialogs: spinner arrows, #5367, #5368; Stroke Weight Up and Down shortcuts, #5797; Hindi catalog and contributor loanword preferences; full wording review in progress; Properties in Edit Artboards: a Transform section with the reference point above the Artboards section, #5219; the Control bar's Stroke: and Opacity: links scrub their fields, #4088; an image zoomed in on is separated, colorized or proofed only where it shows (#4155); a document's first frame renders on the render worker, so opening a heavy file no longer freezes the window, #4155; Control bar Paragraph section with type selected, #5372; translating unchanged type reuses its glyph and character-stroke outlines; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -31,6 +31,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-06 | minor | Stroke gradients: an arrowhead whose tip sits on the end of a long straight leg is painted by the gradient to the corners of its base. The slices that reach across a head now run to the end of its reach instead of narrowing towards the next corner (#5964). Test: `stroke::tests_gradient::slices_cover_an_arrowhead_on_a_long_straight_leg`. Scores and estimates unchanged. | | 2026-11-06 | minor | Width profiles: the width between two width points follows a smooth curve instead of a straight line, so a stroke no longer kinks at each width point (#5714). Each side is a monotone cubic through its points: flat at a widest or narrowest point, never past either point of a span; stacked points still step. Straight runs are sampled between width points to the drawing tolerance. Scores and estimates unchanged. | | 2026-11-05 | minor | A nearby hit on a plain long stroke or outline avoids assembling its whole path. A hit near the middle of a 200,000-anchor path took about 11–13 ms before and 1.0–1.4 ms after in paired runs on a busy Windows machine; a miss inside the path's box fell from 18.7 ms to 1.6 ms, while a distant miss was unchanged. Other stroke styles and fill hits keep their existing checks. Scores and estimates unchanged; idle performance budgets remain unverified. | | 2026-11-05 | minor | Translating a type object without changing its text or formatting reuses glyph geometry and expanded character-stroke outlines between frames. The text appears at its new position with unchanged pixels. Scaling the type, editing its content or formatting, or changing installed fonts rebuilds the geometry; scores and estimates unchanged. |