diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 52a230f05..8b2a8e0d3 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -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 = 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);