diff --git a/crates/geom/src/preset.rs b/crates/geom/src/preset.rs index 1edde1c8..ca75f58a 100644 --- a/crates/geom/src/preset.rs +++ b/crates/geom/src/preset.rs @@ -1860,10 +1860,10 @@ fn wedge_rect(w: f64, h: f64, a0: f64, a1: f64, r: f64) -> Geometry { let r = r.min(w / 2.0).min(h / 2.0); // Walk clockwise from the top-left; insert the wedge on the edge facing the tip. let horiz = dx.abs() > dy.abs(); - let (bx0, bx1) = (w * 5.0 / 12.0, w * 7.0 / 12.0); - let (by0, by1) = (h * 5.0 / 12.0, h * 7.0 / 12.0); - let (bx0, bx1) = if tip.x < w / 2.0 { (w * 2.0 / 12.0, w * 5.0 / 12.0) } else { (bx0 + w / 12.0 * 2.0 - w / 12.0 * 2.0, bx1 + w * 2.0 / 12.0) }; - let (by0, by1) = if tip.y < h / 2.0 { (h * 2.0 / 12.0, h * 5.0 / 12.0) } else { (by0, by1 + h * 2.0 / 12.0) }; + // The wedge's base: 2..5 twelfths of the edge on the tip's side of the centre when it is + // before it, 7..10 twelfths when after (presetShapeDefinitions' xg1/xg2, yg1/yg2). + let (bx0, bx1) = if tip.x < w / 2.0 { (w * 2.0 / 12.0, w * 5.0 / 12.0) } else { (w * 7.0 / 12.0, w * 10.0 / 12.0) }; + let (by0, by1) = if tip.y < h / 2.0 { (h * 2.0 / 12.0, h * 5.0 / 12.0) } else { (h * 7.0 / 12.0, h * 10.0 / 12.0) }; b.m(r, 0.0); if !inside && !horiz && dy < 0.0 { b.l(bx0, 0.0).l(tip.x, tip.y).l(bx1, 0.0); @@ -2590,4 +2590,46 @@ mod tests { assert!(g.paths.iter().any(|p| p.stroke && p.fill == FillMode::None), "{name}: the curve is outlined"); } } + + /// The end points of a path's move/line elements, in order. + fn vertices(p: &BezPath) -> Vec { + p.elements() + .iter() + .filter_map(|e| match e { + kurbo::PathEl::MoveTo(q) | kurbo::PathEl::LineTo(q) => Some(*q), + _ => None, + }) + .collect() + } + + #[test] + fn wedge_callout_mirrors_with_its_tip() { + // A tip below and right of the centre draws the mirror image of the same tip on the left; + // the same for a tip right of the centre, above or below it. presetShapeDefinitions puts + // the wedge's base at 2..5 twelfths of the edge on the near side of the centre and at + // 7..10 twelfths on the far side. + let (w, h) = (240.0, 120.0); + // Sorted and deduplicated: the outline closes on the corner it starts from, which the + // mirror moves to the opposite corner. + let sorted = |mut v: Vec| { + v.sort_by(|a, b| a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y))); + v.dedup_by(|a, b| (*a - *b).hypot() < 1e-9); + v + }; + for (right, left, flip_x) in [([10000.0, 80000.0], [-10000.0, 80000.0], true), ([80000.0, 10000.0], [80000.0, -10000.0], false)] { + let a = vertices(&build("wedgeRectCallout", w, h, &right).unwrap().paths[0].path); + let b = vertices(&build("wedgeRectCallout", w, h, &left).unwrap().paths[0].path); + let b: Vec = b.iter().map(|p| if flip_x { Point::new(w - p.x, p.y) } else { Point::new(p.x, h - p.y) }).collect(); + let (a, b) = (sorted(a), sorted(b)); + assert_eq!(a.len(), b.len(), "{right:?}: {a:?} vs mirrored {left:?}: {b:?}"); + for (p, q) in a.iter().zip(&b) { + assert!((*p - *q).hypot() < 1e-6, "{right:?}: {a:?}\n is not the mirror of {left:?}: {b:?}"); + } + } + // The bottom wedge of a tip right of the centre leaves the edge at 7/12 and 10/12 of the width. + let a = vertices(&build("wedgeRectCallout", w, h, &[10000.0, 80000.0]).unwrap().paths[0].path); + for x in [w * 7.0 / 12.0, w * 10.0 / 12.0] { + assert!(a.iter().any(|p| (p.x - x).abs() < 1e-6 && (p.y - h).abs() < 1e-6), "no wedge corner at ({x}, {h}): {a:?}"); + } + } }