forked from 1b-agent/opencadstudio-upstream
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:
1 parent
893dcee6a7
commit
088dec4ab6
1 file changed
+92
-8
+92
-8
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user