Render: table borders draw their dashes, compound bands and diagonals

Explicit cell borders were stroked as plain solid lines (no dash, compound
or cap), and diagonal borders (lnTlToBr, lnBlToTr) were read but never
drawn. Both now go through the shape line stroker; diagonals are drawn
under the cell edges.

Wild synthetic set: border styles slide 5.67 -> 2.26, thick merged-cell
borders 1.20 -> 0.69.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-13 07:04:09 -04:00
parent a609af24cb
commit 22070a3610
3 changed files with 40 additions and 6 deletions

View File

@ -1376,7 +1376,7 @@ fn blur(sigma: f64) -> Filter {
/// Stroke `path` with `line`. A gradient line is laid out over `extent` (the untrimmed outline the
/// line belongs to; `path` itself when `None`).
#[allow(clippy::too_many_arguments)]
fn stroke(
pub(crate) fn stroke(
ctx: &mut RenderContext,
rctx: &Ctx,
line: &Line,

View File

@ -348,6 +348,17 @@ pub fn draw(
},
}
}
// Diagonal borders, under the edges: drawn like any line (dashes, compound, caps).
for (r, c, rect) in &rects {
let Some(cell) = t.cell(*r, *c) else { continue };
for (line, a, b) in [(&cell.diag_down, (rect.x0, rect.y0), (rect.x1, rect.y1)), (&cell.diag_up, (rect.x0, rect.y1), (rect.x1, rect.y0))] {
let Some(l) = line.as_ref().filter(|l| l.fill.as_ref().is_some_and(|f| !f.is_none())) else { continue };
let mut p = BezPath::new();
p.move_to(a);
p.line_to(b);
crate::stroke(ctx, rctx, l, None, &p, None, m, l.width.unwrap_or(1.0).max(0.0), 1.0);
}
}
// Borders: explicit cell borders, else the style's grid.
for (r, c, rect) in &rects {
let Some(cell) = t.cell(*r, *c) else { continue };
@ -361,11 +372,16 @@ pub fn draw(
for (i, e) in edges.iter().enumerate() {
let line = cell.borders.get(i).and_then(|b| b.as_ref());
let (col, wd) = match line {
Some(l) => match &l.fill {
Some(Fill::Solid { color: cc }) => (rctx.color(cc, None), l.width.unwrap_or(1.0)),
Some(Fill::None) => continue,
_ => continue,
},
// An explicit border is drawn like any line: dashed, compound, gradient.
Some(l) => {
if l.fill.as_ref().is_some_and(|f| !f.is_none()) {
let mut p = BezPath::new();
p.move_to((e.0, e.1));
p.line_to((e.2, e.3));
crate::stroke(ctx, rctx, l, None, &p, None, m, l.width.unwrap_or(1.0).max(0.25), 1.0);
}
continue;
}
None => match lk.sides.get(i).copied().flatten().or(lk.border) {
Some(b) if b.0.a > 0 && b.1 > 0.0 => b,
_ => continue,

View File

@ -580,3 +580,21 @@ fn a_bevelled_shape_is_lit_from_the_upper_left() {
let img = render(vec![bevelled(r#"<a:sp3d><a:bevelT w="152400" h="76200"/></a:sp3d>"#)]);
assert!(close(img.pixel(165, 104), base, 2) && close(img.pixel(165, 165), base, 2));
}
#[test]
fn table_borders_take_their_dashes_and_diagonals_are_drawn() {
let mut t = deckcraft_model::Table::new(1, 1, 200.0, 100.0);
t.style = "none".into();
let mut dashed = Line::solid(ColorRef::rgb(RED), 4.0);
dashed.dash = Some(deckcraft_model::style::Dash::Preset { name: "dash".into() });
if let Some(c) = t.rows.first_mut().and_then(|r| r.cells.first_mut()) {
c.borders[2] = Some(dashed);
c.diag_down = Some(Line::solid(ColorRef::rgb(Rgba::rgb(0, 0, 255)), 4.0));
}
let sh = Shape { id: ShapeId(27), xfrm: Some(Xfrm::new(100.0, 100.0, 200.0, 100.0)), kind: ShapeKind::Table(t), ..Default::default() };
let img = render(vec![sh]);
// Dashes of 4 widths (16 pt) on, 3 (12 pt) off along the top edge, from its start.
assert!(close(img.pixel(108, 100), RED, 40) && close(img.pixel(122, 100), WHITE, 30) && close(img.pixel(135, 100), RED, 40));
// The diagonal from top-left to bottom-right passes through the middle.
assert!(close(img.pixel(200, 150), Rgba::rgb(0, 0, 255), 40), "{:?}", img.pixel(200, 150));
}