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 ThomasandClaude Opus 5.5 committed 2026-10-13 08:16:41 -04:00
1 parent 9d4541b8b4
commit cef6e1a8c8
3 files changed
+40 -6

No files matched your search

+1 -1
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,
+21 -5
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,
+18
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));
}