Render: presets PowerPoint mitres by default get sharp corners; miter limit 8

An outline naming no join was always rounded. PowerPoint rounds most
presets but mitres some by default: classified per preset by comparing
its export (8 pt outline, no join) with round- and miter-joined renders:
the plus, right/up/down block arrows (not the left one), chevrons, stars
from five points up, the equation shapes, curved ribbons and the action
buttons. Miter joins also take PowerPoint's default limit of 8 widths
(kurbo's default was 4), so sharp star points no longer bevel off.

Wild synthetic set: joins slide 0.43 -> 0.21, outline-on-pattern slide
3.95 -> 3.76, preset galleries slightly better.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-13 07:18:36 -04:00
parent 2e68e54eec
commit 9238a915a2
3 changed files with 69 additions and 1 deletions

View File

@ -339,6 +339,45 @@ pub fn info(name: &str) -> Option<&'static PresetInfo> {
CATALOG.iter().find(|p| p.name == name)
}
/// Whether PowerPoint joins this preset's outline with sharp (miter) corners when the line names
/// no join (it rounds them otherwise). Observed per preset on its export: the plus, the right,
/// up and down block arrows (not the left one), chevrons, stars from five points up, the
/// equation shapes, curved ribbons and the action buttons are mitred.
pub fn mitred_by_default(name: &str) -> bool {
matches!(
name,
"plus"
| "rightArrow"
| "upArrow"
| "downArrow"
| "upDownArrow"
| "leftUpArrow"
| "bentUpArrow"
| "stripedRightArrow"
| "notchedRightArrow"
| "chevron"
| "leftArrowCallout"
| "leftRightArrowCallout"
| "upDownArrowCallout"
| "quadArrowCallout"
| "mathPlus"
| "mathMinus"
| "mathMultiply"
| "mathEqual"
| "mathNotEqual"
| "star5"
| "star6"
| "star8"
| "star10"
| "star12"
| "star16"
| "star24"
| "star32"
| "ellipseRibbon"
| "ellipseRibbon2"
) || name.starts_with("actionButton")
}
pub fn is_line_like(name: &str) -> bool {
matches!(name, "line" | "straightConnector1" | "bentConnector2" | "bentConnector3" | "curvedConnector3" | "arc")
|| name.starts_with("bentConnector")

View File

@ -998,6 +998,13 @@ impl Renderer {
ctx.pop_layer();
}
}
// Some presets' outlines default to sharp corners (PowerPoint's own per-shape default).
let line = match (&s.geom, line.join) {
(Geom::Preset { name, .. }, None) if preset::mitred_by_default(name) => {
Line { join: Some(deckcraft_model::style::LineJoin::Miter), ..line }
}
_ => line,
};
// A 3-D bevel: the face and a band inside the outline lit as surfaces.
if let Some(bv) = effects.as_ref().and_then(|e| e.raw3d.as_deref()).and_then(bevel::from_raw) {
let solid = match &fill {
@ -1404,7 +1411,8 @@ pub(crate) fn stroke(
// Hairlines stay visible at any zoom.
let min_w = 0.5 / scale_of(m).max(1e-6);
let sw = lw.max(min_w);
let mut sk = kurbo::Stroke::new(sw).with_caps(cap).with_join(join);
// PowerPoint's default miter limit is 8 widths (`a:miter lim="800000"`).
let mut sk = kurbo::Stroke::new(sw).with_caps(cap).with_join(join).with_miter_limit(8.0);
// Square caps on a dashed line square off only the line's own two ends (where a dash is
// drawn): each dash itself ends flat, as PowerPoint draws them.
let mut square_ends = None;

View File

@ -662,3 +662,24 @@ fn a_gradient_not_rotating_with_its_shape_keeps_to_the_slide() {
}
}
}
#[test]
fn stars_and_right_arrows_have_sharp_corners_by_default() {
let shape = |name: &str, join: Option<deckcraft_model::style::LineJoin>| Shape {
id: ShapeId(29),
xfrm: Some(Xfrm::new(100.0, 100.0, 100.0, 100.0)),
geom: Geom::preset(name),
fill: solid(WHITE),
line: Some(Line { join, ..Line::solid(ColorRef::rgb(RED), 10.0) }),
..Default::default()
};
// A star5's top point mitred reaches about 16 pt above the box; rounded only 5.
let img = render(vec![shape("star5", None)]);
assert!(close(img.pixel(150, 90), RED, 40), "mitred point {:?}", img.pixel(150, 90));
let img = render(vec![shape("star5", Some(deckcraft_model::style::LineJoin::Round))]);
assert!(close(img.pixel(150, 90), WHITE, 10), "an explicit round join stays round");
// A rectangle keeps PowerPoint's round default: its corner is cut round.
let img = render(vec![shape("rect", None)]);
assert!(close(img.pixel(95, 95), WHITE, 30), "{:?}", img.pixel(95, 95));
assert!(deckcraft_geom::preset::mitred_by_default("rightArrow") && !deckcraft_geom::preset::mitred_by_default("leftArrow"));
}