diff --git a/crates/doc/src/arrow.rs b/crates/doc/src/arrow.rs index e3f3387fe..432cd4b93 100644 --- a/crates/doc/src/arrow.rs +++ b/crates/doc/src/arrow.rs @@ -75,7 +75,12 @@ fn end_of(seg: &PathSeg, at_start: bool) -> (Point, Vec2) { let mut d = match seg { PathSeg::Line(l) => l.p1 - l.p0, PathSeg::Quad(q) => q.deriv().eval(t).to_vec2(), - PathSeg::Cubic(c) => c.deriv().eval(t).to_vec2(), + PathSeg::Cubic(c) => { + // A handle sitting on its anchor has no direction: the tangent then points at the next + // control point of the segment that differs from the end point. + let pts = if at_start { [c.p1 - c.p0, c.p2 - c.p0, c.p3 - c.p0] } else { [c.p3 - c.p2, c.p3 - c.p1, c.p3 - c.p0] }; + pts.into_iter().find(|v| v.hypot() >= 1e-9).unwrap_or(Vec2::ZERO) + } }; if d.hypot() < 1e-9 { d = seg.end() - seg.start(); diff --git a/crates/doc/tests/autocraft_regression_2514.rs b/crates/doc/tests/autocraft_regression_2514.rs new file mode 100644 index 000000000..addd40ef2 --- /dev/null +++ b/crates/doc/tests/autocraft_regression_2514.rs @@ -0,0 +1,28 @@ +use designcraft_doc::arrow::apply; +use designcraft_doc::geom::BezPath; +use designcraft_doc::geom::kurbo::Shape; +use designcraft_doc::{Arrowhead, Stroke}; + +#[test] +fn start_head_follows_tangent_when_start_handle_is_missing() { + let mut bp = BezPath::new(); + bp.move_to((0.0, 0.0)); + bp.curve_to((0.0, 0.0), (100.0, 0.0), (100.0, 100.0)); + let st = Stroke { weight: 1.0, start: Arrowhead::Bar, ..Stroke::default() }; + let (_, heads) = apply(&bp, &st, false).unwrap(); + // The curve leaves heading along +x, so the bar at the start stands upright. + let b = heads[0].path.bounding_box(); + assert!(b.height() > b.width() * 3.0, "{b:?}"); +} + +#[test] +fn end_head_follows_tangent_when_end_handle_is_missing() { + let mut bp = BezPath::new(); + bp.move_to((0.0, 0.0)); + bp.curve_to((100.0, 0.0), (100.0, 100.0), (100.0, 100.0)); + let st = Stroke { weight: 1.0, end: Arrowhead::Bar, ..Stroke::default() }; + let (_, heads) = apply(&bp, &st, false).unwrap(); + // The curve arrives heading along +y, so the bar at the end lies across, horizontally. + let b = heads[0].path.bounding_box(); + assert!(b.width() > b.height() * 3.0, "{b:?}"); +}