fix(pdf): plot wide polylines with square ends and mitred corners

A polyline band was stroked like a pen line, with round caps and round
joins, and each segment as its own path, so corners came out rounded
and overlapping. Bands now use butt caps and mitre joins in one path,
and a tapered band fills each segment's quad so a zero-width segment
stays a thin line.

Fixes #1721

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Opus 5.5 committed 2026-10-13 10:04:25 +03:00
1 parent 893dcee6a7
commit 088dec4ab6
1 file changed
+92 -8
+92 -8
View File
@@ -827,6 +827,13 @@ fn append_pdf_page(
}
[r, g, b] = plotted_color([r, g, b], a, screening, options);
// A wide polyline's band is geometry: square ends at its vertices
// and mitred corners, whatever pen the plot style gives lines.
let (cap, join) = if wire.world_width > 0.0 {
(Some(LineCapStyle::Butt), Some(LineJoinStyle::Miter))
} else {
(cap, join)
};
let cap = cap.unwrap_or(LineCapStyle::Round);
if last_cap != Some(cap) {
ops.push(Op::SetLineCapStyle { cap });
@@ -943,18 +950,95 @@ fn append_pdf_page(
let mut segment: Vec<LinePoint> = Vec::new();
let dot_radius = (wire.name == "viewport_hatch_pattern")
.then_some(Pt(SCREEN_DOT_MM * MM_TO_PT / (2.0 * scale.max(1e-6))));
// A tapered band varies along each segment, which one stroke width
// cannot draw: each segment fills its own quad, and a segment
// without width is a plain line at the pen weight.
if wire.world_width > 0.0 && !wire.taper_widths.is_empty() {
let page_h = paper_h as f64 / scale.max(1e-6) as f64;
let at = |i: usize| {
let p = wire.point_world(i, page_h);
[p.x + ox, p.y + oy]
};
let mm = |[x, y]: [f64; 2]| LinePoint {
p: Point::new(Mm(x as f32), Mm(y as f32)),
bezier: false,
};
let thin = if options.object_lineweights {
lw_override.unwrap_or_else(|| (wire.line_weight_px * LW_PX_TO_PT).max(0.1))
} else {
0.1
} / pen_divisor;
for i in 0..wire.points.len().saturating_sub(1) {
if !wire.points[i][0].is_finite() || !wire.points[i + 1][0].is_finite() {
continue;
}
let (a, b) = (at(i), at(i + 1));
let half = |k: usize| {
f64::from(wire.taper_widths.get(k).copied().unwrap_or(0.0)) * 0.5
};
let (wa, wb) = (half(i), half(i + 1));
let (dx, dy) = (b[0] - a[0], b[1] - a[1]);
let len = dx.hypot(dy);
if len <= 1e-12 {
continue;
}
if wa <= 0.0 && wb <= 0.0 {
if last_lw.is_none_or(|l| (l - thin).abs() > 0.01) {
ops.push(Op::SetOutlineThickness { pt: Pt(thin) });
last_lw = Some(thin);
}
flush_line(&mut ops, &[mm(a), mm(b)], None);
continue;
}
let n = [-dy / len, dx / len];
let side =
|p: [f64; 2], w: f64, s: f64| [p[0] + n[0] * w * s, p[1] + n[1] * w * s];
ops.push(Op::DrawPolygon {
polygon: Polygon {
rings: vec![PolygonRing {
points: vec![
mm(side(a, wa, 1.0)),
mm(side(b, wb, 1.0)),
mm(side(b, wb, -1.0)),
mm(side(a, wa, -1.0)),
],
}],
mode: PaintMode::Fill,
winding_order: WindingOrder::NonZero,
},
});
}
continue;
}
// A band's segments arrive pen-up separated; where the next one
// starts on the last one's end it stays in the same path, so the
// corner gets the mitre join instead of two overlapping ends.
let band = wire.world_width > 0.0;
let mut pen_up = false;
for (pi, &[x, y, _z]) in wire.points.iter().enumerate() {
if x.is_nan() || y.is_nan() {
flush_line(&mut ops, &segment, dot_radius);
segment.clear();
if band {
pen_up = true;
} else {
flush_line(&mut ops, &segment, dot_radius);
segment.clear();
}
} else {
let point = wire.point_world(pi, paper_h as f64 / scale.max(1e-6) as f64);
let wx = (point.x + ox) as f32;
let wy = (point.y + oy) as f32;
segment.push(LinePoint {
p: Point::new(Mm(wx), Mm(wy)),
bezier: false,
});
let p = Point::new(Mm((point.x + ox) as f32), Mm((point.y + oy) as f32));
if pen_up {
pen_up = false;
let joined = segment.last().is_some_and(|last| {
(last.p.x.0 - p.x.0).abs() <= 1e-4 && (last.p.y.0 - p.y.0).abs() <= 1e-4
});
if joined {
continue;
}
flush_line(&mut ops, &segment, dot_radius);
segment.clear();
}
segment.push(LinePoint { p, bezier: false });
}
}
flush_line(&mut ops, &segment, dot_radius);