Shapes: a speech bubble's wedge mirrors with its tip, at 7..10 twelfths on the far side as presetShapeDefinitions give it

This commit is contained in:
Marco Arnulfo
2026-10-24 18:50:57 +02:00
parent 38d2ade2b7
commit 9830b57fbe
+46 -4
View File
@@ -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<Point> {
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<Point>| {
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<Point> = 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:?}");
}
}
}