Geom/Render: brackets and braces fill; arrowheads follow open paths, not brackets
- Left/right brackets and braces drew only their curve: a filled bracket showed no fill. PowerPoint fills them out to the straight side they open to (the fill path is the curve closed), outlining only the curve. - Arrowheads were drawn on any shape with nothing filled (brackets among them) but not on open freeforms that had a fill. PowerPoint puts them on lines, arcs and open freeforms whether filled or not, never on brackets. Wild synthetic set: preset galleries 9.44 -> 4.65, 9.55 -> 4.94, 9.60 -> 5.38; extreme-adjust galleries 10.7 -> 6.6. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
061e2c8ad5
commit
38793f13c7
|
|
@ -1124,7 +1124,10 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option<Geometry> {
|
|||
if name == "rightBracket" {
|
||||
path.apply_affine(kurbo::Affine::new([-1.0, 0.0, 0.0, 1.0, w, 0.0]));
|
||||
}
|
||||
Geometry { paths: vec![stroke_only(path)], text_rect: full, handles: vec![], sites: mid_sites(w, h) }
|
||||
// Filled (when the shape has a fill) out to the straight side, as PowerPoint does.
|
||||
let mut area = path.clone();
|
||||
area.close_path();
|
||||
Geometry { paths: vec![fill_only(area), stroke_only(path)], text_rect: full, handles: vec![], sites: mid_sites(w, h) }
|
||||
}
|
||||
"leftBrace" | "rightBrace" => {
|
||||
let r = (ss * a(0).clamp(0.0, 25000.0 * h / ss.max(1e-9)) / 1e5).min(h / 4.0);
|
||||
|
|
@ -1136,7 +1139,10 @@ pub fn build(name: &str, w: f64, h: f64, adj: &[f64]) -> Option<Geometry> {
|
|||
if name == "rightBrace" {
|
||||
path.apply_affine(kurbo::Affine::new([-1.0, 0.0, 0.0, 1.0, w, 0.0]));
|
||||
}
|
||||
Geometry { paths: vec![stroke_only(path)], text_rect: full, handles: vec![], sites: mid_sites(w, h) }
|
||||
// Filled (when the shape has a fill) out to the straight side, as PowerPoint does.
|
||||
let mut area = path.clone();
|
||||
area.close_path();
|
||||
Geometry { paths: vec![fill_only(area), stroke_only(path)], text_rect: full, handles: vec![], sites: mid_sites(w, h) }
|
||||
}
|
||||
"rightArrow" => arrow(Dir::Right, w, h, a(0), a(1)),
|
||||
"leftArrow" => arrow(Dir::Left, w, h, a(0), a(1)),
|
||||
|
|
@ -1968,4 +1974,19 @@ mod tests {
|
|||
assert!(build("line", 10.0, 10.0, &[]).unwrap().is_open());
|
||||
assert!(!build("rect", 10.0, 10.0, &[]).unwrap().is_open());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn brackets_and_braces_fill_out_to_their_straight_side() {
|
||||
// PowerPoint fills a bracket or brace (when it has a fill) between its curve and the
|
||||
// straight side it opens to; only the curve is outlined.
|
||||
for name in ["leftBracket", "rightBracket", "leftBrace", "rightBrace"] {
|
||||
let g = build(name, 40.0, 100.0, &[]).unwrap();
|
||||
let filled: Vec<_> = g.paths.iter().filter(|p| p.fill != FillMode::None).collect();
|
||||
assert_eq!(filled.len(), 1, "{name}");
|
||||
assert!(!filled[0].stroke, "{name}: the filled area is not outlined");
|
||||
let area = filled[0].path.area().abs();
|
||||
assert!(area > 40.0 * 100.0 * 0.4 && area < 40.0 * 100.0, "{name}: {area}");
|
||||
assert!(g.paths.iter().any(|p| p.stroke && p.fill == FillMode::None), "{name}: the curve is outlined");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -974,7 +974,12 @@ impl Renderer {
|
|||
if line.fill.as_ref().is_some_and(|f| !f.is_none()) {
|
||||
let lw = line.width.unwrap_or(0.75).max(0.0);
|
||||
let (base, rel) = self.line_space(m, depth);
|
||||
let ends = s.is_line() || geo.is_open();
|
||||
// Arrowheads go on lines, arcs and open freeforms (filled or not), never on closed
|
||||
// outlines or on presets like brackets.
|
||||
let ends = s.is_line()
|
||||
|| geo.is_open()
|
||||
|| (matches!(s.geom, Geom::Custom { .. })
|
||||
&& geo.paths.iter().find(|p| p.stroke).is_some_and(|p| !p.path.elements().iter().any(|e| matches!(e, PathEl::ClosePath))));
|
||||
let trims = if ends { head_trims(&line, lw) } else { (0.0, 0.0) };
|
||||
let has_heads = ends && [&line.head, &line.tail].iter().any(|e| e.as_ref().is_some_and(|e| e.kind != "none"));
|
||||
// A see-through line and its arrowheads are one shape in PowerPoint: drawn opaque into
|
||||
|
|
|
|||
|
|
@ -313,3 +313,21 @@ fn children_filled_with_the_group_fill_show_one_fill_across_the_group() {
|
|||
assert!(r[2] > 200 && r[0] < 60, "right end {r:?}");
|
||||
assert!(close(img.pixel(200, 150), WHITE, 5), "the gap between the children stays empty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_freeforms_take_arrowheads_even_when_filled() {
|
||||
// An open custom path with a fill and a tail arrow: PowerPoint draws the arrow.
|
||||
let end = deckcraft_model::style::LineEnd { kind: "triangle".into(), w: "med".into(), len: "med".into() };
|
||||
let sh = Shape {
|
||||
id: ShapeId(19),
|
||||
xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)),
|
||||
geom: Geom::Custom { paths: vec![CustomPath { w: 100.0, h: 100.0, d: "M 0 100 L 50 0 L 100 100".into(), ..Default::default() }] },
|
||||
fill: solid(Rgba::rgb(0, 200, 0)),
|
||||
line: Some(Line { fill: solid(RED), width: Some(4.0), tail: Some(end), ..Default::default() }),
|
||||
..Default::default()
|
||||
};
|
||||
let img = render(vec![sh]);
|
||||
// The line ends at (300, 200) heading down-right at 45°; the 12 pt head's base corner reaches
|
||||
// (295.7, 187.3), 3.5 pt off the line's axis: outside the 4 pt line and outside the fill.
|
||||
assert!(close(img.pixel(294, 189), RED, 60), "arrowhead drawn {:?}", img.pixel(294, 189));
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue