From 7d73882d3a1729d236360aa9caf9a50abc006379 Mon Sep 17 00:00:00 2001 From: Julius van der Vaart Date: Mon, 9 Nov 2026 20:47:16 +0100 Subject: [PATCH 1/4] Stroke: Line Arrow arrowhead (#5421) Add Line Arrow, a 41st generated arrowhead: two arms as thick as the line (at 100 % scale) meeting at right angles in a sharp tip, cut square at their ends. It is saved by the new name `LineArrow`, appended so existing names stay stable, and listed third in the arrowhead menus after Arrow (open). Its arms spread four weights either side of the line, twice as wide as the other heads, so Arrowhead now states its own reach and half width. Visual bounds and clicks on the canvas (head_reach, head_contains) use them instead of one fixed 4.6 x 2 box; every other head keeps the sizes it had. Tests: the outline test checks each head against its own reach and half width; a new test checks the line arrow's tip, corners, open middle and arm thickness; outset_covers_arrowheads checks that the wider head grows the outset; the saved-name list includes LineArrow. --- crates/doc/src/appearance.rs | 1 - crates/doc/src/appearance/layers.rs | 9 ++---- crates/doc/src/appearance/stroke_style.rs | 27 +++++++++++++++- crates/doc/src/appearance/tests.rs | 13 ++++++-- crates/doc/src/reach.rs | 12 +++---- crates/effects/src/stroke/arrow.rs | 18 ++++++++--- crates/effects/src/stroke/tests_arrows.rs | 39 ++++++++++++++++++++--- crates/engine/src/cmd/stroke.rs | 2 +- docs/mcp.md | 7 ++-- docs/target-app-parity.md | 5 +-- 10 files changed, 101 insertions(+), 32 deletions(-) diff --git a/crates/doc/src/appearance.rs b/crates/doc/src/appearance.rs index 3ac145fd22..2e19ad44a3 100644 --- a/crates/doc/src/appearance.rs +++ b/crates/doc/src/appearance.rs @@ -8,7 +8,6 @@ mod layers; mod profile; mod stroke_style; -pub(crate) use layers::HEAD_REACH; pub use layers::{Effect, FillLayer, StrokeGradientMode, StrokeLayer, stroke_paint_bounds}; pub use profile::{AngleProfile, PressureProfile, ProfilePreset, SavedProfile, WidthProfile}; pub use stroke_style::{ArrowAlign, Arrowhead, Dash, LineCap, LineJoin, StrokeAlign}; diff --git a/crates/doc/src/appearance/layers.rs b/crates/doc/src/appearance/layers.rs index 880f3490da..923275999b 100644 --- a/crates/doc/src/appearance/layers.rs +++ b/crates/doc/src/appearance/layers.rs @@ -139,10 +139,6 @@ impl StrokeGradientMode { } } -/// How far any arrowhead reaches from its tip, in units of its weight (the open arrow's arms are -/// cut square a little behind its 4 × 4 box). -pub(crate) const HEAD_REACH: f64 = 4.6; - fn ten() -> f64 { 10.0 } @@ -188,7 +184,8 @@ impl StrokeLayer { } } /// Weight of the start (`end == false`) or end arrowhead: stroke weight × its scale, at least - /// a quarter point. A head of weight `hw` fits a box about `4·hw` long and wide. + /// a quarter point. A head of weight `hw` fits a box about `4·hw` long and wide (see + /// [`Arrowhead::reach`] and [`Arrowhead::half_width`]). pub fn arrow_weight(&self, end: bool) -> f64 { let pct = if end { self.arrow_scale.1 } else { self.arrow_scale.0 }; (self.width * pct / 100.0).max(0.25) @@ -197,7 +194,7 @@ impl StrokeLayer { /// without one): the head's diagonal, or with [`ArrowAlign::Extend`] its length, plus the cap. pub fn head_reach(&self, end: bool) -> f64 { let head = if end { self.end_arrow } else { self.start_arrow }; - head.map_or(0.0, |_| HEAD_REACH * self.arrow_weight(end) + self.width / 2.0) + head.map_or(0.0, |k| k.reach() * self.arrow_weight(end) + self.width / 2.0) } /// How far the arrowheads can reach from the path's end points (0 without heads). pub fn arrow_reach(&self) -> f64 { diff --git a/crates/doc/src/appearance/stroke_style.rs b/crates/doc/src/appearance/stroke_style.rs index acd4d81890..1bc88d2d07 100644 --- a/crates/doc/src/appearance/stroke_style.rs +++ b/crates/doc/src/appearance/stroke_style.rs @@ -49,6 +49,10 @@ impl Dash { } } +/// How far the arrowheads drawn in the 4 × 4 box reach from their tip, in units of their weight +/// (the open arrow's arms are cut square a little behind the box). +const HEAD_REACH: f64 = 4.6; + /// An arrowhead shape. Saved by variant name: new shapes are appended, never renamed or reordered. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum Arrowhead { @@ -94,13 +98,17 @@ pub enum Arrowhead { Kite, TriangleBar, Oval, + /// Two arms of the line's own weight meeting at right angles in a sharp tip, cut square at + /// their ends. Twice as wide as the other heads. + LineArrow, } impl Arrowhead { /// Every arrowhead, in menu order (arrows, then outlined shapes, then bars and marks). - pub const ALL: [Arrowhead; 40] = [ + pub const ALL: [Arrowhead; 41] = [ Arrowhead::Arrow, Arrowhead::ArrowOpen, + Arrowhead::LineArrow, Arrowhead::Barbed, Arrowhead::Concave, Arrowhead::DoubleArrow, @@ -141,6 +149,22 @@ impl Arrowhead { Arrowhead::Fork, ]; + /// How far any part of the head can be from its tip, in units of its weight. + pub fn reach(self) -> f64 { + match self { + // Its farthest corners are 4 + ½√2 back and 4 − ½√2 out. + Arrowhead::LineArrow => 5.75, + _ => HEAD_REACH, + } + } + /// How far the head spreads either side of its line, in units of its weight. + pub fn half_width(self) -> f64 { + match self { + Arrowhead::LineArrow => 4.0, + _ => 2.0, + } + } + /// The name the arrowhead menus show. pub fn label(self) -> &'static str { match self { @@ -184,6 +208,7 @@ impl Arrowhead { Arrowhead::Kite => "Kite", Arrowhead::TriangleBar => "Triangle to Bar", Arrowhead::Oval => "Oval", + Arrowhead::LineArrow => "Line Arrow", } } } diff --git a/crates/doc/src/appearance/tests.rs b/crates/doc/src/appearance/tests.rs index c297b34759..0a1b3e1a5d 100644 --- a/crates/doc/src/appearance/tests.rs +++ b/crates/doc/src/appearance/tests.rs @@ -219,7 +219,7 @@ fn profile_presets_round_trip_their_ids() { #[test] fn arrowhead_names_are_stable_and_every_one_is_listed_once() { // Saved files store these names: they may only ever be appended to. - const SAVED: [&str; 40] = [ + const SAVED: [&str; 41] = [ "Triangle", "TriangleOpen", "Circle", @@ -260,6 +260,7 @@ fn arrowhead_names_are_stable_and_every_one_is_listed_once() { "Kite", "TriangleBar", "Oval", + "LineArrow", ]; for name in SAVED { let a: Arrowhead = serde_json::from_value(serde_json::json!(name)).unwrap(); @@ -293,9 +294,15 @@ fn outset_covers_arrowheads() { st.arrow_scale = (100.0, 200.0); assert_eq!(st.arrow_weight(false), 4.0); assert_eq!(st.arrow_weight(true), 8.0); - assert_eq!((st.head_reach(false), st.head_reach(true)), (0.0, HEAD_REACH * 8.0 + 2.0)); + let reach = Arrowhead::Triangle.reach(); + assert_eq!((st.head_reach(false), st.head_reach(true)), (0.0, reach * 8.0 + 2.0)); // A 32 pt head whose tip sits up to its length (plus the cap) past the end point. - assert_eq!(a.outset(), HEAD_REACH * 8.0 + 2.0); + assert_eq!(a.outset(), reach * 8.0 + 2.0); + // The line arrow's arms spread twice as wide, so it reaches further from its tip. + a.stroke_mut().unwrap().end_arrow = Some(Arrowhead::LineArrow); + assert!(Arrowhead::LineArrow.reach() > reach); + assert_eq!(a.outset(), Arrowhead::LineArrow.reach() * 8.0 + 2.0); + a.stroke_mut().unwrap().end_arrow = Some(Arrowhead::Triangle); a.stroke_mut().unwrap().width = 0.01; assert_eq!(a.stroke().unwrap().arrow_weight(true), 0.25, "heads keep a minimum size"); } diff --git a/crates/doc/src/reach.rs b/crates/doc/src/reach.rs index 0c4ef497f3..58ebaafc6d 100644 --- a/crates/doc/src/reach.rs +++ b/crates/doc/src/reach.rs @@ -7,7 +7,6 @@ use vectorcraft_geom::hit::fill_contains; use vectorcraft_geom::kurbo::{ParamCurveArclen, ParamCurveNearest}; use vectorcraft_geom::{BezPath, FillRule, ParamCurve, PathSeg, Point, Rect, Shape, Vec2}; -use crate::appearance::HEAD_REACH; use crate::{Appearance, ArrowAlign, Dash, LineCap, LineJoin, StrokeAlign, StrokeLayer, WidthProfile}; /// Arc-length accuracy for placing a point along a profile (document points). @@ -283,18 +282,19 @@ impl StrokeLayer { } /// Is `p` within `tol` of where the start (`end == false`) or end arrowhead at `at` (the path - /// leaving along `out`) can paint? Heads are at most about 4.6 weights long and 4 wide. With + /// leaving along `out`) can paint? A head reaches [`crate::Arrowhead::reach`] weights from its tip and + /// [`crate::Arrowhead::half_width`] either side of its line (4.6 and 2 for most). With /// [`ArrowAlign::Extend`] a head points along `out` with its tip up to its length (or the cap) /// past the end; with [`ArrowAlign::Tip`] its tip is on the end and it lies back along the /// chord the path bends through under it, so anywhere within its length of the end. fn head_contains(&self, at: Point, out: Vec2, end: bool, p: Point, tol: f64) -> bool { - let hw = self.arrow_weight(end); - let v = p - at; + let Some(kind) = (if end { self.end_arrow } else { self.start_arrow }) else { return false }; + let (hw, v) = (self.arrow_weight(end), p - at); match self.arrow_align { - ArrowAlign::Tip => v.hypot() <= HEAD_REACH * hw + tol, + ArrowAlign::Tip => v.hypot() <= kind.reach() * hw + tol, ArrowAlign::Extend => { let along = v.dot(out); - v.cross(out).abs() <= 2.0 * hw + tol && along <= 4.0 * hw + self.width / 2.0 + tol && along >= -HEAD_REACH * hw - tol + v.cross(out).abs() <= kind.half_width() * hw + tol && along <= 4.0 * hw + self.width / 2.0 + tol && along >= -kind.reach() * hw - tol } } } diff --git a/crates/effects/src/stroke/arrow.rs b/crates/effects/src/stroke/arrow.rs index 1a5c900954..ac64cb30d2 100644 --- a/crates/effects/src/stroke/arrow.rs +++ b/crates/effects/src/stroke/arrow.rs @@ -1,5 +1,5 @@ //! Arrowheads. A head of weight `hw` (stroke weight × scale) fits a box `4·hw` long and `4·hw` -//! wide. Hollow kinds are rings (`hw/2` walls) or, for the open arrow, a chevron with `hw` arms. +//! wide; the line arrow's arms reach twice as far out ([`Arrowhead::half_width`]). Hollow kinds are rings (`hw/2` walls) or, for the open arrow, a chevron with `hw` arms. //! The stroke ends under the head at the head's *inset* from the tip: halfway into solid heads, //! in the back wall of hollow ones (with the cap kept out of the hole) and at the chevron's inner //! corner. With [`ArrowAlign::Tip`] the tip sits on the end point and the stroke is shortened by @@ -8,7 +8,7 @@ use std::borrow::Cow; -use std::f64::consts::{FRAC_PI_2, PI}; +use std::f64::consts::{FRAC_1_SQRT_2, FRAC_PI_2, PI}; use kurbo::{Affine, BezPath, ParamCurve, ParamCurveArclen, PathSeg, Point, Shape, Vec2}; use vectorcraft_doc::{ArrowAlign, Arrowhead, StrokeLayer}; @@ -261,7 +261,7 @@ fn local_stop(kind: Arrowhead) -> Stop { Arrowhead::Fork => Stop::At(2.7), Arrowhead::HalfArrowLeft | Arrowhead::HalfArrowRight | Arrowhead::Feather => Stop::At(1.3), Arrowhead::Swallowtail => Stop::At(1.4), - Arrowhead::Chevron => Stop::At(1.0), + Arrowhead::Chevron | Arrowhead::LineArrow => Stop::At(1.0), Arrowhead::Slash => Stop::At(1.2), Arrowhead::DoubleSlash => Stop::At(1.05), Arrowhead::DoubleArrow => Stop::At(1.7), @@ -273,8 +273,9 @@ fn local_stop(kind: Arrowhead) -> Stop { } /// The outline of a head added after the first ten, in local units: x from the tip (0) back -/// along the line (to at most 4), y across it (−2 to 2, positive to the right of the direction -/// the head points). Every shape is generated here. +/// along the line (to at most 4; the line arrow's square-cut arms 4.71), y across it (−2 to 2, +/// the line arrow −4 to 4; positive to the right of the direction the head points). Every shape +/// is generated here. fn local(kind: Arrowhead) -> BezPath { let mut p = BezPath::new(); let poly = |p: &mut BezPath, pts: &[(f64, f64)]| polygon(p, &pts.iter().map(|&q| Point::from(q)).collect::>()); @@ -363,6 +364,13 @@ fn local(kind: Arrowhead) -> BezPath { poly(&mut p, &[(0.0, 2.0), (0.6, 2.0), (0.6, 0.25), (4.0, 1.8), (4.0, -1.8), (0.6, -0.25), (0.6, -2.0), (0.0, -2.0)]) } Arrowhead::Oval => p.extend(kurbo::Ellipse::new((2.0, 0.0), (2.0, 1.2), 0.0).path_elements(LOCAL_TOL)), + Arrowhead::LineArrow => { + // Arms one unit thick (the line's weight at 100 %) at right angles to each other, from + // a sharp tip to their outer corners at (4, ±4), cut square across: a unit across an + // arm at 45° is ½√2 along each axis, and the arms' inner edges meet √2 behind the tip. + let t = FRAC_1_SQRT_2; + poly(&mut p, &[(0.0, 0.0), (4.0, 4.0), (4.0 + t, 4.0 - t), (2.0 * t, 0.0), (4.0 + t, t - 4.0), (4.0, -4.0)]); + } // The first ten are drawn by `shape`. Arrowhead::Triangle | Arrowhead::TriangleOpen diff --git a/crates/effects/src/stroke/tests_arrows.rs b/crates/effects/src/stroke/tests_arrows.rs index 39d6d9d57d..4328048991 100644 --- a/crates/effects/src/stroke/tests_arrows.rs +++ b/crates/effects/src/stroke/tests_arrows.rs @@ -1,6 +1,6 @@ -//! Every arrowhead's outline: drawn, inside its box (4 × 4 weights; the open arrow's square-cut -//! arms reach a little further back), without self-intersections, and covering the end of the -//! stroke under it. +//! Every arrowhead's outline: drawn, inside its reach and half width (4 × 4 weights for most; the +//! open arrow's square-cut arms reach a little further back, the line arrow's twice as far out), +//! without self-intersections, and covering the end of the stroke under it. use kurbo::{BezPath, PathEl, Point, Shape}; use vectorcraft_color::{Color, Paint}; @@ -53,7 +53,13 @@ fn every_outline_is_drawn_inside_its_box_without_self_intersections() { let h = head(kind); assert!(h.outline.area().abs() > 4.0, "{kind:?} is drawn"); let b = h.outline.bounding_box(); - assert!(b.x0 >= 91.0 - 1e-6 && b.x1 <= 100.0 + 1e-6 && b.y0 >= -4.0 - 1e-6 && b.y1 <= 4.0 + 1e-6, "{kind:?} leaves its box: {b:?}"); + let (reach, side) = (2.0 * kind.reach(), 2.0 * kind.half_width()); + assert!(b.x1 <= 100.0 + 1e-6 && b.y0 >= -side - 1e-6 && b.y1 <= side + 1e-6, "{kind:?} leaves its box: {b:?}"); + if kind.half_width() == 2.0 { + assert!(b.x0 >= 91.0 - 1e-6, "{kind:?} is longer than the 4 × 4 box: {b:?}"); + } + let far = contours(&h.outline).iter().flatten().map(|p| p.distance(h.tip)).fold(0.0, f64::max); + assert!(far <= reach + 1e-6, "{kind:?} reaches {far} from its tip, past {reach}"); let edges: Vec<(Point, Point)> = contours(&h.outline).iter().flat_map(|c| (0..c.len()).map(|i| (c[i], c[(i + 1) % c.len()]))).collect(); assert!(edges.len() >= 3, "{kind:?}"); for (i, a) in edges.iter().enumerate() { @@ -74,6 +80,31 @@ fn the_stroke_ends_inside_every_head() { } } +#[test] +fn the_line_arrow_is_a_right_angle_of_two_arms_as_thick_as_the_line() { + // Weight 2 at 100 %: a local unit (x back from the tip, y across) is 2 pt. + let h = head(Arrowhead::LineArrow); + let at = |x: f64, y: f64| Point::new(100.0 - 2.0 * x, 2.0 * y); + // A sharp tip on the end point; outer corners 4 units back and out, square-cut ends. + let pts: Vec = contours(&h.outline).concat(); + for (corner, name) in [(at(0.0, 0.0), "tip"), (at(4.0, 4.0), "right corner"), (at(4.0, -4.0), "left corner")] { + assert!(pts.iter().any(|p| p.distance(corner) < 1e-9), "{name} {corner:?} in {pts:?}"); + } + // Open between the arms, solid at the tip. + assert_eq!(h.outline.winding(at(2.5, 0.0)), 0); + assert_ne!(h.outline.winding(at(0.7, 0.0)), 0); + // Halfway along each arm, 45° off the line, it is one unit (the line's weight) across. + let s = std::f64::consts::FRAC_1_SQRT_2; + for side in [1.0, -1.0] { + let (mx, my) = (2.0 + 0.5 * s, side * (2.0 - 0.5 * s)); + let (nx, ny) = (-side * s, s); + for (d, inside) in [(0.45, true), (-0.45, true), (0.55, false), (-0.55, false)] { + let w = h.outline.winding(at(mx + nx * d, my + ny * d)); + assert_eq!(w != 0, inside, "arm {side} at {d} across"); + } + } +} + #[test] fn half_arrows_barb_on_their_own_side() { // Pointing along +x on a y-down page, left is −y. diff --git a/crates/engine/src/cmd/stroke.rs b/crates/engine/src/cmd/stroke.rs index 1fb3373c7a..e97659bfc9 100644 --- a/crates/engine/src/cmd/stroke.rs +++ b/crates/engine/src/cmd/stroke.rs @@ -20,7 +20,7 @@ pub fn specs() -> Vec { "Stroke Options", ["Window", "Stroke"], None, - "{weight?: pt, cap?: butt|round|square, join?: miter|round|bevel, miterLimit?, align?: center|inside|outside, dash?: [d,g,…]|null (a 0 dash with a round or projecting cap draws dots or squares; a new pattern keeps the current offset and alignment), dashOffset? (exact dashes only), alignDashes?: bool (true: dashes fitted to corners and path ends, every run between them holding whole periods with a dash centred on each corner and end; false, the default for a new pattern: exact lengths), startArrow?, endArrow?: Arrow|ArrowOpen|Barbed|Concave|DoubleArrow|HalfArrowLeft|HalfArrowRight|Chevron|Feather|Swallowtail|Triangle|TriangleOpen|TriangleReverse|TriangleBar|Kite|Leaf|Drop|Circle|CircleOpen|HalfCircle|Oval|Target|Square|SquareOpen|Tag|TagOpen|Diamond|DiamondOpen|Hexagon|HexagonOpen|Star|Cross|Plus|Bar|DoubleBar|DotOnBar|Slash|DoubleSlash|Bracket|Fork|null (HalfArrowLeft/Right: one barb, left or right of the direction the head points), arrowAlign?: \"extend\" (tip past the end point, default)|\"tip\" (tip on the end point; the stroke is shortened), profile?: \"uniform\"|\"lens\"|\"taperStart\"|\"taperEnd\"|\"pinch\"|\"teardrop\"|\"wave\"|a saved profile's name (stroke.widthProfile.list), widthPoints?, gradientMode? (as document.inspect reports them in strokeOptions, so those read back unchanged; ignored here: a custom profile is set with the Width tool, gradientMode with paint.editGradient), item?: stroke item index|null (omitted: the Appearance panel's active item if it is a stroke, else the top stroke, created when missing), ids?|id?, target?: \"object\"|\"contents\", aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} Without a stroke item, type takes weight, cap, join, miterLimit, align (around its glyph outlines; the type stays live) and the dash options as its characters' stroke (every run; text.setRangeStyle `strokeOptions` styles a range), and images and symbol instances (also in groups) are left alone. With nothing selected (and no ids) they set up the next object drawn (appearance.newArt; weight always does) → {objects: how many objects took the stroke options}", + "{weight?: pt, cap?: butt|round|square, join?: miter|round|bevel, miterLimit?, align?: center|inside|outside, dash?: [d,g,…]|null (a 0 dash with a round or projecting cap draws dots or squares; a new pattern keeps the current offset and alignment), dashOffset? (exact dashes only), alignDashes?: bool (true: dashes fitted to corners and path ends, every run between them holding whole periods with a dash centred on each corner and end; false, the default for a new pattern: exact lengths), startArrow?, endArrow?: Arrow|ArrowOpen|LineArrow|Barbed|Concave|DoubleArrow|HalfArrowLeft|HalfArrowRight|Chevron|Feather|Swallowtail|Triangle|TriangleOpen|TriangleReverse|TriangleBar|Kite|Leaf|Drop|Circle|CircleOpen|HalfCircle|Oval|Target|Square|SquareOpen|Tag|TagOpen|Diamond|DiamondOpen|Hexagon|HexagonOpen|Star|Cross|Plus|Bar|DoubleBar|DotOnBar|Slash|DoubleSlash|Bracket|Fork|null (HalfArrowLeft/Right: one barb, left or right of the direction the head points), arrowAlign?: \"extend\" (tip past the end point, default)|\"tip\" (tip on the end point; the stroke is shortened), profile?: \"uniform\"|\"lens\"|\"taperStart\"|\"taperEnd\"|\"pinch\"|\"teardrop\"|\"wave\"|a saved profile's name (stroke.widthProfile.list), widthPoints?, gradientMode? (as document.inspect reports them in strokeOptions, so those read back unchanged; ignored here: a custom profile is set with the Width tool, gradientMode with paint.editGradient), item?: stroke item index|null (omitted: the Appearance panel's active item if it is a stroke, else the top stroke, created when missing), ids?|id?, target?: \"object\"|\"contents\", aligned?: bool (index and item are rows of the first target's stack: each other target edits its equal one, as the Appearance panel does for a mixed selection)} Without a stroke item, type takes weight, cap, join, miterLimit, align (around its glyph outlines; the type stays live) and the dash options as its characters' stroke (every run; text.setRangeStyle `strokeOptions` styles a range), and images and symbol instances (also in groups) are left alone. With nothing selected (and no ids) they set up the next object drawn (appearance.newArt; weight always does) → {objects: how many objects took the stroke options}", has_doc, stroke_set ), diff --git a/docs/mcp.md b/docs/mcp.md index 8f605eee0f..574a0e8118 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3177,11 +3177,12 @@ and `stroke.widthProfile.reset` removes every saved one. `stroke.set {profile}` ## Arrowheads -`stroke.set {startArrow, endArrow}` takes any of 40 generated heads by name (the params doc lists them; `null` for -none): arrows (`Arrow`, `Barbed`, `Concave`, `DoubleArrow`, `HalfArrowLeft`/`Right`, `Chevron`, `Feather`, +`stroke.set {startArrow, endArrow}` takes any of 41 generated heads by name (the params doc lists them; `null` for +none): arrows (`Arrow`, `LineArrow`, `Barbed`, `Concave`, `DoubleArrow`, `HalfArrowLeft`/`Right`, `Chevron`, `Feather`, `Swallowtail`…), filled and open shapes (`Triangle`, `Circle`, `Oval`, `Target`, `Tag`, `Diamond`, `Hexagon`, `Star`, the `…Open` rings…) and marks (`Bar`, `DoubleBar`, `DotOnBar`, `Slash`, `DoubleSlash`, `Bracket`, `Fork`, `Cross`, -`Plus`). Each head fits a box about four times its weight (stroke weight × `arrowScale`) long and wide. +`Plus`). Each head fits a box about four times its weight (stroke weight × `arrowScale`) long and wide; `LineArrow`, two arms +of the line's weight at right angles, spreads twice as wide. ## Stroke bounds and clicks diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2effc11421..e5e7cb97dc 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-09 · **Change:** minor (Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; 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; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-09 · **Change:** minor (Line Arrow arrowhead (#5421); scores and estimates unchanged; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; G5: UI Frames built-in symbol library with default 9-slice; bundled content ~32%; previously minor: G5: dynamic symbol part on-canvas opacity/blend/gradient/pattern; strokes remaining no longer lists Overlap/auto corners; symbols ~88%; previously minor: interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; previously minor: harakat paint fallback clarified; previously minor: current Guides feature-area rows consolidated using existing ruler evidence; scores and estimates unchanged; previously minor: Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; previously minor: G5 docs: Art brush Overlap and Pattern brush auto corners removed from strokes remaining; symbols remaining lists dynamic on-canvas beyond solid and guides outside the art; previously minor: custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; `.ai` and EPS import: older Art brush objects read; numeric fields step on wheel hover without taking keyboard focus, and a panel wheel turn cannot seed a canvas zoom glide, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Tools stays beside the canvas with left-side panels; new gradients follow document color mode; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; the canvas keeps selection outlines and anchors between frames; Indian-language hyphenation review and license allowlist, #4947; 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; Units › East Asian Type acts, with Q and Ha units; scores unchanged) ## Feature submission evidence @@ -41,6 +41,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms as thick as the line at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | | 2026-11-07 | minor | File formats and Type core (#6147): `.ai`/EPS type keeps its paragraphs' tab stops (positions and alignments; leaders aren't read; at most 1,024 a paragraph and 2^20 a story), and right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops; centred and justified lines are aligned as a whole as before; evidence added. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6137): `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them, and a line a soft return ends is justified; evidence added. Limits: frames grow only for area type of one frame, horizontal, by its lines' deepest descent plus 0.05 pt; type in a font that isn't installed, and hyphenation in a language that isn't imported, still lay out differently. Scores and estimates unchanged. | | 2026-11-08 | minor | Style panels: a double-click on a user character style's row opens its Character Style Options (`ui.charStyleOptions`), and on a paragraph style's row its Paragraph Style Options (rename); the Character Style Options dialog keeps a fixed width and scrolls its sections in a shorter window, so OK and Cancel stay on screen. Tests `panels::text_styles::tests::double_clicking_a_character_style_row_opens_its_options` and `dialogs::char_style_options::tests::the_sections_scroll_in_a_window_shorter_than_the_general_list`. Scores and estimates unchanged. | @@ -1231,7 +1232,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Cmd/Ctrl-click on the Gradient panel's Gradient Fill box or on the Tools panel's Gradient button resets the gradient of the active Fill or Stroke to the linear White, Black gradient, as one undo step ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). With a gradient stop or a freeform gradient point selected, Alt-clicking a color swatch in the Swatches panel recolors it instead of the inactive Fill or Stroke. - **Freeform gradient colors in their own model:** Object ▸ Expand and Object ▸ Create Gradient Mesh (`object.mesh.create`) mix a freeform gradient's points in the color model they share: points of C50 M20 Y0 K10 give mesh points of C50 M20 Y0 K10, and RGB points keep their values in a working RGB space other than sRGB. The canvas, the gradient chips and the SVG, PDF, EPS and EMF writers mix the points in that model too. PDF and EPS write a gradient whose points are all CMYK, or all Gray in a CMYK file, as a CMYK image, which PDF's Output settings convert as they convert any CMYK image. Points in different models mix in display RGB (on the canvas in a CMYK document that shows transparency, as the inks of each point), and a CMYK document stores the result of Expand as CMYK through the color settings. Points colored along CMYK stops (`paint.editGradient {stops}`, or a CMYK gradient switched to freeform) are CMYK, and so are a point `paint.freeform.splitLine` adds between CMYK points and a point `paint.freeform.addPoint` adds without `color` to CMYK points. Public reference: [Gradients](https://web.archive.org/web/20250714042311/https://helpx.adobe.com/illustrator/using/gradients.html) and [Meshes](https://web.archive.org/web/20250812201019/https://helpx.adobe.com/illustrator/using/meshes.html), which do not state how colors in different models mix; the rule is VectorCraft's own. - - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty more arrowheads, and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. + - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the line's own weight at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. - **Brushes (part of #852):** double-clicking a brush in the Brushes panel (or the panel menu's Brush Options…) opens Calligraphic Brush Options (Name; Angle, Roundness and Size, each Fixed, Random, Pressure, Tilt, Bearing or Rotation with its Variation; live previews of the nib and of a light–heavy–light stroke) or Bristle Brush Options (Shape and the bristle sliders); OK edits the brush and every stroke painted with it. The Paintbrush records the pen pressure along each stroke (kept with the path, saved, and joined when a stroke continues a path); a Calligraphic value on Pressure goes from value − variation at the lightest pressure to value + variation at the heaviest, on the canvas and in SVG, PDF and the other exports. Brushes saved before keep their look. Scatter Brush Options (Size, Spacing, Scatter and Rotation, each Fixed, Random between two values, Pressure from the first at the lightest pressure to the second at the heaviest, Tilt, Bearing or Rotation; Rotation relative to Page or Path), Art Brush Options (Width, Scale Proportionately, Stretch to Fit Stroke Length or Stretch Between Guides with the guides' start and end, the four Directions, Flip Along and Across, Overlap) and Pattern Brush Options (Scale, Spacing, the five tiles shown, Flip Along and Across, Stretch to fit, Add space to fit or Approximate path) each have Colorization (None, Tints, Tints and Shades, Hue Shift with its Key Color picked from the art's colours) and a live stroke preview; values from files and commands are clamped to their ranges. Art Brush Options › Overlap places art on each run between sharp corners so folds do not overlap (`ArtBrush.overlap`; `art_brush_overlap_splits_at_corners`). Still to come: Stylus Wheel, pen pressure on macOS where the windowing layer reports none, and tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web. Under Tints and Tints and Shades, art in black ink alone goes by its ink amount, and a process or gray stroke color's tints and shades stay in that color's model; these are VectorCraft's own rules. Type in a scatter brush's art takes the brush's colorization as its paths do: Tints, Tints and Shades and Hue Shift recolor the characters' solid fills and strokes in each copy the brush draws, and Brush Options' Key Color list, whose first color is Hue Shift's default key, counts the characters' fills and their strokes wider than 0. The Brushes page says that a scatter, art or pattern brush takes its colors from the current stroke color and the brush's colorization method () and does not mention type there; recoloring the characters' paints like the art's fills and strokes is VectorCraft's own rule. The Symbol Stainer mixes the characters' strokes of type toward the stain as it mixes their fills. Evidence: `tests::scatter_colorization_recolors_type`, `tests::art_colors_list_the_most_used_first`, `tests::stain_mixes_type_strokes_too` (vectorcraft-brush). From 0153a5d33e9f0ebe20e0565206f309450663ea2d Mon Sep 17 00:00:00 2001 From: Julius van der Vaart Date: Mon, 9 Nov 2026 21:17:41 +0100 Subject: [PATCH 2/4] Stroke: document where the stroke stops under a local arrowhead (#5421) --- crates/effects/src/stroke/arrow.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/effects/src/stroke/arrow.rs b/crates/effects/src/stroke/arrow.rs index ac64cb30d2..603f46e289 100644 --- a/crates/effects/src/stroke/arrow.rs +++ b/crates/effects/src/stroke/arrow.rs @@ -252,6 +252,7 @@ const HEX: f64 = 2.0 * 1.732_050_807_568_877_2; /// Flattening tolerance of the curved local heads, in units. const LOCAL_TOL: f64 = 1e-3; +/// Where the stroke ends under a head drawn in local units ([`local`]), in those units. fn local_stop(kind: Arrowhead) -> Stop { match kind { Arrowhead::Target | Arrowhead::TagOpen | Arrowhead::DiamondOpen => Stop::Wall(4.0), From c8d7f2cce061205e06ea315b12cff994e03384be Mon Sep 17 00:00:00 2001 From: Julius van der Vaart Date: Tue, 10 Nov 2026 06:42:19 +0100 Subject: [PATCH 3/4] Stroke: Line Arrow's arms take the head's weight, at least 0.25 pt (#5421) --- crates/doc/src/appearance/stroke_style.rs | 4 ++-- docs/mcp.md | 5 +++-- docs/target-app-parity.md | 4 ++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/doc/src/appearance/stroke_style.rs b/crates/doc/src/appearance/stroke_style.rs index 1bc88d2d07..b100a8a7b9 100644 --- a/crates/doc/src/appearance/stroke_style.rs +++ b/crates/doc/src/appearance/stroke_style.rs @@ -98,8 +98,8 @@ pub enum Arrowhead { Kite, TriangleBar, Oval, - /// Two arms of the line's own weight meeting at right angles in a sharp tip, cut square at - /// their ends. Twice as wide as the other heads. + /// Two arms of the head's weight (the line's at 100 % scale) meeting at right angles in a + /// sharp tip, cut square at their ends. Twice as wide as the other heads. LineArrow, } diff --git a/docs/mcp.md b/docs/mcp.md index df33d3a268..e7c9b6025c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3240,8 +3240,9 @@ and `stroke.widthProfile.reset` removes every saved one. `stroke.set {profile}` none): arrows (`Arrow`, `LineArrow`, `Barbed`, `Concave`, `DoubleArrow`, `HalfArrowLeft`/`Right`, `Chevron`, `Feather`, `Swallowtail`…), filled and open shapes (`Triangle`, `Circle`, `Oval`, `Target`, `Tag`, `Diamond`, `Hexagon`, `Star`, the `…Open` rings…) and marks (`Bar`, `DoubleBar`, `DotOnBar`, `Slash`, `DoubleSlash`, `Bracket`, `Fork`, `Cross`, -`Plus`). Each head fits a box about four times its weight (stroke weight × `arrowScale`) long and wide; `LineArrow`, two arms -of the line's weight at right angles, spreads twice as wide. +`Plus`). Each head fits a box about four times its weight long and wide. A head's weight is the stroke weight scaled by +`arrowScale`, at least 0.25 pt. `LineArrow` has two arms of the head's weight (the line's at 100 %) at right angles and +spreads twice as wide. ## Stroke bounds and clicks diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 170402aeae..6385088779 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -42,7 +42,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| | 2026-11-10 | minor | Type advanced: right-click suggestions, Ignore All and Add to Dictionary on a word Auto Spell Check underlined, #4936; evidence added; the dialogs remain; scores and hours unchanged. | -| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms as thick as the line at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | +| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms of the head's weight (the line's at 100 % arrowhead scale, at least 0.25 pt) at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | | 2026-11-09 | minor | Canvas: a 3D effect (Extrude & Bevel, Inflate, Rotate, Revolve) changed on type or a group draws again where its art was and is, measured by that art; the canvas measures through its renderer and draws the part with the art it made to measure it. Exports and Object › Rasterize take the solid of 3D type. Fit to Artwork Bounds and per-artboard SVG export measure every artboard through one renderer, building each solid once. A 3D effect on a fill or stroke that builds nothing (Revolve after Roughen) no longer overflows the stack on the canvas, in Object › Rasterize and in raster and EPS exports: the fill or stroke draws as without it. Tests `tests::changed::a_3d_effect_changed_on_type_or_a_group_changes_where_its_solid_was`, `fx::tests::a_renderer_measures_the_art_on_each_artboard_building_each_solid_once`, `canvas::tests::an_edit_to_a_3d_effect_redraws_with_the_art_measured`, `fx::tests::a_3d_effect_that_builds_nothing_draws_the_item_as_without_it`. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6147): `.ai`/EPS type keeps its paragraphs' tab stops (positions and alignments; leaders aren't read; at most 1,024 a paragraph and 2^20 a story), and right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops; centred and justified lines are aligned as a whole as before; evidence added. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6137): `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them, and a line a soft return ends is justified; evidence added. Limits: frames grow only for area type of one frame, horizontal, by its lines' deepest descent plus 0.05 pt; type in a font that isn't installed, and hyphenation in a language that isn't imported, still lay out differently. Scores and estimates unchanged. | @@ -1248,7 +1248,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Cmd/Ctrl-click on the Gradient panel's Gradient Fill box or on the Tools panel's Gradient button resets the gradient of the active Fill or Stroke to the linear White, Black gradient, as one undo step ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). With a gradient stop or a freeform gradient point selected, Alt-clicking a color swatch in the Swatches panel recolors it instead of the inactive Fill or Stroke. - **Freeform gradient colors in their own model:** Object ▸ Expand and Object ▸ Create Gradient Mesh (`object.mesh.create`) mix a freeform gradient's points in the color model they share: points of C50 M20 Y0 K10 give mesh points of C50 M20 Y0 K10, and RGB points keep their values in a working RGB space other than sRGB. The canvas, the gradient chips and the SVG, PDF, EPS and EMF writers mix the points in that model too. PDF and EPS write a gradient whose points are all CMYK, or all Gray in a CMYK file, as a CMYK image, which PDF's Output settings convert as they convert any CMYK image. Points in different models mix in display RGB (on the canvas in a CMYK document that shows transparency, as the inks of each point), and a CMYK document stores the result of Expand as CMYK through the color settings. Points colored along CMYK stops (`paint.editGradient {stops}`, or a CMYK gradient switched to freeform) are CMYK, and so are a point `paint.freeform.splitLine` adds between CMYK points and a point `paint.freeform.addPoint` adds without `color` to CMYK points. Public reference: [Gradients](https://web.archive.org/web/20250714042311/https://helpx.adobe.com/illustrator/using/gradients.html) and [Meshes](https://web.archive.org/web/20250812201019/https://helpx.adobe.com/illustrator/using/meshes.html), which do not state how colors in different models mix; the rule is VectorCraft's own. - - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the line's own weight at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. + - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the head's weight (the line's at 100 % scale, at least 0.25 pt) at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. - **Brushes (part of #852):** double-clicking a brush in the Brushes panel (or the panel menu's Brush Options…) opens Calligraphic Brush Options (Name; Angle, Roundness and Size, each Fixed, Random, Pressure, Tilt, Bearing or Rotation with its Variation; live previews of the nib and of a light–heavy–light stroke) or Bristle Brush Options (Shape and the bristle sliders); OK edits the brush and every stroke painted with it. The Paintbrush records the pen pressure along each stroke (kept with the path, saved, and joined when a stroke continues a path); a Calligraphic value on Pressure goes from value − variation at the lightest pressure to value + variation at the heaviest, on the canvas and in SVG, PDF and the other exports. Brushes saved before keep their look. Scatter Brush Options (Size, Spacing, Scatter and Rotation, each Fixed, Random between two values, Pressure from the first at the lightest pressure to the second at the heaviest, Tilt, Bearing or Rotation; Rotation relative to Page or Path), Art Brush Options (Width, Scale Proportionately, Stretch to Fit Stroke Length or Stretch Between Guides with the guides' start and end, the four Directions, Flip Along and Across, Overlap) and Pattern Brush Options (Scale, Spacing, the five tiles shown, Flip Along and Across, Stretch to fit, Add space to fit or Approximate path) each have Colorization (None, Tints, Tints and Shades, Hue Shift with its Key Color picked from the art's colours) and a live stroke preview; values from files and commands are clamped to their ranges. Art Brush Options › Overlap places art on each run between sharp corners so folds do not overlap (`ArtBrush.overlap`; `art_brush_overlap_splits_at_corners`). Still to come: Stylus Wheel, pen pressure on macOS where the windowing layer reports none, and tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web. Under Tints and Tints and Shades, art in black ink alone goes by its ink amount, and a process or gray stroke color's tints and shades stay in that color's model; these are VectorCraft's own rules. Type in a scatter brush's art takes the brush's colorization as its paths do: Tints, Tints and Shades and Hue Shift recolor the characters' solid fills and strokes in each copy the brush draws, and Brush Options' Key Color list, whose first color is Hue Shift's default key, counts the characters' fills and their strokes wider than 0. The Brushes page says that a scatter, art or pattern brush takes its colors from the current stroke color and the brush's colorization method () and does not mention type there; recoloring the characters' paints like the art's fills and strokes is VectorCraft's own rule. The Symbol Stainer mixes the characters' strokes of type toward the stain as it mixes their fills. Evidence: `tests::scatter_colorization_recolors_type`, `tests::art_colors_list_the_most_used_first`, `tests::stain_mixes_type_strokes_too` (vectorcraft-brush). From 89482ba940f6b7d7e0a96efbfcfe6a9e0a94b922 Mon Sep 17 00:00:00 2001 From: Julius van der Vaart Date: Tue, 10 Nov 2026 06:48:16 +0100 Subject: [PATCH 4/4] Stroke: Line Arrow's arms match the line at 100 % only from 0.25 pt (#5421) --- crates/doc/src/appearance/stroke_style.rs | 5 +++-- crates/effects/src/stroke/arrow.rs | 7 ++++--- docs/mcp.md | 4 ++-- docs/target-app-parity.md | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/crates/doc/src/appearance/stroke_style.rs b/crates/doc/src/appearance/stroke_style.rs index b100a8a7b9..46a0c30c33 100644 --- a/crates/doc/src/appearance/stroke_style.rs +++ b/crates/doc/src/appearance/stroke_style.rs @@ -98,8 +98,9 @@ pub enum Arrowhead { Kite, TriangleBar, Oval, - /// Two arms of the head's weight (the line's at 100 % scale) meeting at right angles in a - /// sharp tip, cut square at their ends. Twice as wide as the other heads. + /// Two arms of the head's weight (the line's weight at 100 % scale, for lines of 0.25 pt or + /// more) meeting at right angles in a sharp tip, cut square at their ends. Twice as wide as + /// the other heads. LineArrow, } diff --git a/crates/effects/src/stroke/arrow.rs b/crates/effects/src/stroke/arrow.rs index 603f46e289..c96dcb477c 100644 --- a/crates/effects/src/stroke/arrow.rs +++ b/crates/effects/src/stroke/arrow.rs @@ -366,9 +366,10 @@ fn local(kind: Arrowhead) -> BezPath { } Arrowhead::Oval => p.extend(kurbo::Ellipse::new((2.0, 0.0), (2.0, 1.2), 0.0).path_elements(LOCAL_TOL)), Arrowhead::LineArrow => { - // Arms one unit thick (the line's weight at 100 %) at right angles to each other, from - // a sharp tip to their outer corners at (4, ±4), cut square across: a unit across an - // arm at 45° is ½√2 along each axis, and the arms' inner edges meet √2 behind the tip. + // Arms one unit thick (the head's weight: the line's at 100 % for lines of 0.25 pt or + // more) at right angles to each other, from a sharp tip to their outer corners at + // (4, ±4), cut square across: a unit across an arm at 45° is ½√2 along each axis, and + // the arms' inner edges meet √2 behind the tip. let t = FRAC_1_SQRT_2; poly(&mut p, &[(0.0, 0.0), (4.0, 4.0), (4.0 + t, 4.0 - t), (2.0 * t, 0.0), (4.0 + t, t - 4.0), (4.0, -4.0)]); } diff --git a/docs/mcp.md b/docs/mcp.md index e7c9b6025c..77638be630 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3241,8 +3241,8 @@ none): arrows (`Arrow`, `LineArrow`, `Barbed`, `Concave`, `DoubleArrow`, `HalfAr `Swallowtail`…), filled and open shapes (`Triangle`, `Circle`, `Oval`, `Target`, `Tag`, `Diamond`, `Hexagon`, `Star`, the `…Open` rings…) and marks (`Bar`, `DoubleBar`, `DotOnBar`, `Slash`, `DoubleSlash`, `Bracket`, `Fork`, `Cross`, `Plus`). Each head fits a box about four times its weight long and wide. A head's weight is the stroke weight scaled by -`arrowScale`, at least 0.25 pt. `LineArrow` has two arms of the head's weight (the line's at 100 %) at right angles and -spreads twice as wide. +`arrowScale`, at least 0.25 pt. `LineArrow` has two arms of the head's weight (the line's weight at 100 % for lines of +0.25 pt or more) at right angles and spreads twice as wide. ## Stroke bounds and clicks diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6385088779..d7d2e6fc5e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -42,7 +42,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| | 2026-11-10 | minor | Type advanced: right-click suggestions, Ignore All and Add to Dictionary on a word Auto Spell Check underlined, #4936; evidence added; the dialogs remain; scores and hours unchanged. | -| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms of the head's weight (the line's at 100 % arrowhead scale, at least 0.25 pt) at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | +| 2026-11-09 | minor | Stroke panel: Line Arrow, a 41st generated arrowhead: two arms of the head's weight (stroke weight × arrowhead scale, at least 0.25 pt: the line's weight at 100 % for lines of 0.25 pt or more) at right angles, a sharp tip and square-cut ends, twice as wide as the other heads; bounds and clicks follow each head's own reach (`stroke.set` `LineArrow`, #5421); scores and estimates unchanged | | 2026-11-09 | minor | Canvas: a 3D effect (Extrude & Bevel, Inflate, Rotate, Revolve) changed on type or a group draws again where its art was and is, measured by that art; the canvas measures through its renderer and draws the part with the art it made to measure it. Exports and Object › Rasterize take the solid of 3D type. Fit to Artwork Bounds and per-artboard SVG export measure every artboard through one renderer, building each solid once. A 3D effect on a fill or stroke that builds nothing (Revolve after Roughen) no longer overflows the stack on the canvas, in Object › Rasterize and in raster and EPS exports: the fill or stroke draws as without it. Tests `tests::changed::a_3d_effect_changed_on_type_or_a_group_changes_where_its_solid_was`, `fx::tests::a_renderer_measures_the_art_on_each_artboard_building_each_solid_once`, `canvas::tests::an_edit_to_a_3d_effect_redraws_with_the_art_measured`, `fx::tests::a_3d_effect_that_builds_nothing_draws_the_item_as_without_it`. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6147): `.ai`/EPS type keeps its paragraphs' tab stops (positions and alignments; leaders aren't read; at most 1,024 a paragraph and 2^20 a story), and right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops; centred and justified lines are aligned as a whole as before; evidence added. Scores and estimates unchanged. | | 2026-11-07 | minor | File formats and Type core (#6137): `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them, and a line a soft return ends is justified; evidence added. Limits: frames grow only for area type of one frame, horizontal, by its lines' deepest descent plus 0.05 pt; type in a font that isn't installed, and hyphenation in a language that isn't imported, still lay out differently. Scores and estimates unchanged. | @@ -1248,7 +1248,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Cmd/Ctrl-click on the Gradient panel's Gradient Fill box or on the Tools panel's Gradient button resets the gradient of the active Fill or Stroke to the linear White, Black gradient, as one undo step ([Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html)). With a gradient stop or a freeform gradient point selected, Alt-clicking a color swatch in the Swatches panel recolors it instead of the inactive Fill or Stroke. - **Freeform gradient colors in their own model:** Object ▸ Expand and Object ▸ Create Gradient Mesh (`object.mesh.create`) mix a freeform gradient's points in the color model they share: points of C50 M20 Y0 K10 give mesh points of C50 M20 Y0 K10, and RGB points keep their values in a working RGB space other than sRGB. The canvas, the gradient chips and the SVG, PDF, EPS and EMF writers mix the points in that model too. PDF and EPS write a gradient whose points are all CMYK, or all Gray in a CMYK file, as a CMYK image, which PDF's Output settings convert as they convert any CMYK image. Points in different models mix in display RGB (on the canvas in a CMYK document that shows transparency, as the inks of each point), and a CMYK document stores the result of Expand as CMYK through the color settings. Points colored along CMYK stops (`paint.editGradient {stops}`, or a CMYK gradient switched to freeform) are CMYK, and so are a point `paint.freeform.splitLine` adds between CMYK points and a point `paint.freeform.addPoint` adds without `color` to CMYK points. Public reference: [Gradients](https://web.archive.org/web/20250714042311/https://helpx.adobe.com/illustrator/using/gradients.html) and [Meshes](https://web.archive.org/web/20250812201019/https://helpx.adobe.com/illustrator/using/meshes.html), which do not state how colors in different models mix; the rule is VectorCraft's own. - - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the head's weight (the line's at 100 % scale, at least 0.25 pt) at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. + - **Strokes:** one stroke-geometry module shared by the canvas, SVG, PDF and Outline Stroke; dotted lines (zero-length dashes draw dots or squares); arrowheads with hollow outlines, tip-on-end or extend-past-end alignment and one opacity for line and head. SVG and PDF export arrowheads, width profiles, brushes and aligned strokes exactly as the canvas draws them; Outline Stroke and the live Outline Stroke effect outline every stroke item as drawn (dashes, heads, profiles, alignment); dashes can be fitted to corners and path ends; width profiles carry through dashes, and their corners take the join. The Stroke panel edits type's character strokes, follows the stroke units (its weight presets are round values in that unit: `0.25 mm`, `0.0625 in`, `1 px`, #426; fields show four decimals in mm, cm and inches, three in points), shows mixed values blank and opens from the Control bar. A width profile library, thirty-one more arrowheads (among them Line Arrow: two arms of the head's weight (stroke weight × arrowhead scale, at least 0.25 pt: the line's weight at 100 % for lines of 0.25 pt or more) at right angles, cut square, #5421), and Width Point Edit with multi-select and discontinuous points. Visual bounds and clicks cover the whole painted stroke. Scale Strokes & Effects, Scale Corners and Use Preview Bounds work everywhere, and new art inherits stroke options, a graphic style or the whole last appearance. - **Brushes (part of #852):** double-clicking a brush in the Brushes panel (or the panel menu's Brush Options…) opens Calligraphic Brush Options (Name; Angle, Roundness and Size, each Fixed, Random, Pressure, Tilt, Bearing or Rotation with its Variation; live previews of the nib and of a light–heavy–light stroke) or Bristle Brush Options (Shape and the bristle sliders); OK edits the brush and every stroke painted with it. The Paintbrush records the pen pressure along each stroke (kept with the path, saved, and joined when a stroke continues a path); a Calligraphic value on Pressure goes from value − variation at the lightest pressure to value + variation at the heaviest, on the canvas and in SVG, PDF and the other exports. Brushes saved before keep their look. Scatter Brush Options (Size, Spacing, Scatter and Rotation, each Fixed, Random between two values, Pressure from the first at the lightest pressure to the second at the heaviest, Tilt, Bearing or Rotation; Rotation relative to Page or Path), Art Brush Options (Width, Scale Proportionately, Stretch to Fit Stroke Length or Stretch Between Guides with the guides' start and end, the four Directions, Flip Along and Across, Overlap) and Pattern Brush Options (Scale, Spacing, the five tiles shown, Flip Along and Across, Stretch to fit, Add space to fit or Approximate path) each have Colorization (None, Tints, Tints and Shades, Hue Shift with its Key Color picked from the art's colours) and a live stroke preview; values from files and commands are clamped to their ranges. Art Brush Options › Overlap places art on each run between sharp corners so folds do not overlap (`ArtBrush.overlap`; `art_brush_overlap_splits_at_corners`). Still to come: Stylus Wheel, pen pressure on macOS where the windowing layer reports none, and tilt, bearing and rotation for Art, Pattern and Bristle brushes and on native Wayland and the web. Under Tints and Tints and Shades, art in black ink alone goes by its ink amount, and a process or gray stroke color's tints and shades stay in that color's model; these are VectorCraft's own rules. Type in a scatter brush's art takes the brush's colorization as its paths do: Tints, Tints and Shades and Hue Shift recolor the characters' solid fills and strokes in each copy the brush draws, and Brush Options' Key Color list, whose first color is Hue Shift's default key, counts the characters' fills and their strokes wider than 0. The Brushes page says that a scatter, art or pattern brush takes its colors from the current stroke color and the brush's colorization method () and does not mention type there; recoloring the characters' paints like the art's fills and strokes is VectorCraft's own rule. The Symbol Stainer mixes the characters' strokes of type toward the stain as it mixes their fills. Evidence: `tests::scatter_colorization_recolors_type`, `tests::art_colors_list_the_most_used_first`, `tests::stain_mixes_type_strokes_too` (vectorcraft-brush).