A wide polyline thinner than a pixel keeps every side on screen (fixes #1434) (#1453)

This commit is contained in:
Marco Arnulfo
2026-10-19 02:32:27 -04:00
committed by GitHub
parent abce3a718b
commit 2a3b0c2626
3 changed files with 116 additions and 8 deletions
+32 -2
View File
@@ -343,6 +343,18 @@ impl Builder<'_> {
}
self.note_pen(color);
let (color, lw) = self.styled(color, lw);
self.push_polyline(ctx, color, lw, pts);
}
/// A one-pixel line in `color` as the plot style prints it, without the style's lineweight
/// or pen: the centre line drawn on screen over a thin wide polyline.
fn hairline(&mut self, ctx: &Ctx, color: Ink, pts: &[Vec2]) {
if pts.len() < 2 {
return;
}
let (color, _) = self.styled(color, 0.0);
self.push_polyline(ctx, color, 0.0, pts);
}
fn push_polyline(&mut self, ctx: &Ctx, color: Ink, lw: f32, pts: &[Vec2]) {
let start = self.list.verts.len() as u32;
for p in pts {
let q = ctx.xf.apply(*p);
@@ -1124,12 +1136,24 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) {
EntityKind::MLine(m) => mline::draw(b, ctx, e, m, rgb, lw, tol),
EntityKind::LwPolyline(p) if p.const_width > 0.0 || p.vertices.iter().any(|v| v.start_width > 0.0 || v.end_width > 0.0) => {
if b.opts.fill && ctx.d.header.i64("FILLMODE", 1) != 0 {
// The GPU canvas has no MSAA: a band narrower than a pixel lights only where it
// covers pixel centres, so whole sides vanish at some zooms (#1434). On screen the
// centre line is drawn too, as a hairline (wide bands are drawn without lineweight),
// so a thin band still shows as a one-pixel line. Always, not below a threshold:
// the list serves a whole band of zooms. Plots and exports draw the band alone, and
// a transparent band gets none (its alpha would add up along the centre).
let hairline = !b.plotting && b.opts.min_dash > 0.0 && b.transparency == 0;
match &lt {
// Dashed: each dash a filled piece of the band; dots a line across it.
Some(lt) => {
let min = b.opts.min_dash / ctx.xf.scale_factor().max(1e-12);
let (tris, ticks) = wide::dashed(p, tol, lt, ltscale, min);
let (tris, ticks, centres) = wide::dashed(p, tol, lt, ltscale, min);
b.tris(ctx, rgb, &tris);
if hairline {
for c in &centres {
b.hairline(ctx, rgb, c);
}
}
for t in &ticks {
match t.as_slice() {
[q] => b.point(ctx, rgb, *q),
@@ -1137,7 +1161,13 @@ fn entity_parts(b: &mut Builder, ctx: &Ctx, e: &Entity) {
}
}
}
None => b.tris(ctx, rgb, &fill::wide_polyline(p, tol)),
None => {
b.tris(ctx, rgb, &fill::wide_polyline(p, tol));
if hairline {
let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed };
b.hairline(ctx, rgb, &pl.tessellate(tol));
}
}
}
} else {
let pl = Polyline { vertices: p.vertices.clone(), closed: p.closed };
+75
View File
@@ -924,3 +924,78 @@ fn cancelled_build_stops_early() {
assert!(!crate::hatch::pattern_lines_until(&drawing, &h, &loops, 0.0, None, None).is_empty());
assert!(crate::hatch::pattern_lines_until(&drawing, &h, &loops, 0.0, None, set.as_deref()).is_empty());
}
/// #1434: a closed polyline with a global width narrower than a pixel loses whole sides at
/// some zooms, while the same polyline with width 0 always shows every side.
#[test]
fn thin_wide_polyline_keeps_every_side_at_every_zoom() {
let square = |const_width: f64| {
let mut d = Drawing::new_imperial();
let vertices = [(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)].map(|(x, y)| PolyVertex::new(Vec2::new(x, y))).to_vec();
let p = cadcraft_doc::LwPolyline { vertices, closed: true, const_width, elevation: 0.0, plinegen: false };
d.add(&Space::Model, Common::default(), EntityKind::LwPolyline(p)).unwrap();
d
};
// Side midpoints and the direction across each side.
let sides = [("bottom", Vec2::new(5.0, 0.0)), ("right", Vec2::new(10.0, 5.0)), ("top", Vec2::new(5.0, 10.0)), ("left", Vec2::new(0.0, 5.0))];
let mut missing = Vec::new();
for const_width in [0.0, 0.01, 0.05] {
let d = square(const_width);
for step in 0..40 {
let scale = 20.0 + f64::from(step) * 0.37;
let px = 1.0 / scale;
let l = build(&d, &Space::Model, &screen(px, None));
let v = View { center: Vec2::new(5.17, 4.62), scale, width: 400, height: 400 };
// No anti-aliasing: pixel-centre coverage, as the GPU canvas rasterises (no MSAA).
let pm = render(&l, &v, &RasterOptions { background: Rgb(0, 0, 0), antialias: false, ..Default::default() }).unwrap();
for (name, mid) in sides {
let (sx, sy) = v.to_screen(mid);
let lit = (-2i32..=2)
.flat_map(|dy| (-2i32..=2).map(move |dx| (dx, dy)))
.any(|(dx, dy)| pm.pixel((sx as i32 + dx) as u32, (sy as i32 + dy) as u32).is_some_and(|c| c.red() > 0));
if !lit {
missing.push(format!("width {const_width} scale {scale:.2} ({:.2} px wide): {name} side missing", const_width * scale));
}
}
}
}
assert!(missing.is_empty(), "{} misses:\n{}", missing.len(), missing.join("\n"));
}
/// #1434: on screen a wide polyline also draws its centre line (a hairline in its colour, solid and
/// dashed alike); plots and exports (`min_dash` 0) draw the band alone.
#[test]
fn wide_polyline_centre_line_is_drawn_on_screen_only() {
for lt in ["Continuous", "DASHED"] {
let drawing = |transparency: u8| {
let mut d = Drawing::new_imperial();
d.linetypes.extend(cadcraft_doc::library::standard_linetypes().into_iter().filter(|l| l.name == "DASHED"));
let vertices = [(0.0, 0.0), (3.0, 0.0), (3.0, 3.0)].map(|(x, y)| PolyVertex::new(Vec2::new(x, y))).to_vec();
let p = cadcraft_doc::LwPolyline { vertices, closed: false, const_width: 0.2, elevation: 0.0, plinegen: false };
let common = Common {
color: Color::Index(1),
linetype: lt.into(),
lineweight: cadcraft_doc::Lineweight::Mm100(50),
transparency: cadcraft_doc::Transparency::Percent(transparency),
..Default::default()
};
d.add(&Space::Model, common, EntityKind::LwPolyline(p)).unwrap();
d
};
let d = drawing(0);
let opts = Options { lineweights: true, transparency: true, ..screen(0.01, None) };
let lines = |l: &DisplayList| l.prims.iter().filter(|p| p.kind == Kind::Polyline).count();
let on_screen = build(&d, &Space::Model, &opts);
assert!(on_screen.prims.iter().any(|p| p.kind == Kind::Tris), "{lt}: the band");
let hair: Vec<&DPrim> = on_screen.prims.iter().filter(|p| p.kind == Kind::Polyline).collect();
assert!(!hair.is_empty(), "{lt}: the centre line on screen");
assert!(hair.iter().all(|p| p.color == Rgb(255, 0, 0) && p.lw == 0.0), "{lt}: hairline in the polyline's colour");
assert!(on_screen.points(hair[0]).iter().all(|q| q.y.abs() < 1e-9 || (q.x - 3.0).abs() < 1e-9), "{lt}: on the centre line");
assert_eq!(lines(&build(&d, &Space::Model, &Options { min_dash: 0.0, ..opts.clone() })), 0, "{lt}: exports draw the band alone");
assert_eq!(lines(&build_plot(&d, &Space::Model, &opts)), 0, "{lt}: plots draw the band alone");
// Its alpha would add up along the centre: a transparent band gets no hairline.
let see_through = build(&drawing(50), &Space::Model, &opts);
assert!(see_through.prims.iter().any(|p| p.kind == Kind::Tris && p.transparency == 50), "{lt}: the transparent band");
assert_eq!(lines(&see_through), 0, "{lt}: a transparent band gets no hairline");
}
}
+9 -6
View File
@@ -66,10 +66,11 @@ fn edges(x: &Sample) -> (Vec2, Vec2) {
(x.p + nrm, x.p - nrm)
}
/// Triangles for the dashes of a wide polyline drawn with `lt` at `scale`, plus a line across the
/// band for each dot. With PLINEGEN the pattern runs along the whole polyline; otherwise it
/// restarts at every vertex. A pattern shorter than `min_pattern` fills the whole band.
pub fn dashed(p: &LwPolyline, tol: f64, lt: &Linetype, scale: f64, min_pattern: f64) -> (Vec<Vec2>, Vec<Vec<Vec2>>) {
/// Triangles for the dashes of a wide polyline drawn with `lt` at `scale`, a line across the band
/// for each dot, and the centre line of each dash. With PLINEGEN the pattern runs along the whole
/// polyline; otherwise it restarts at every vertex. A pattern shorter than `min_pattern` fills
/// the whole band.
pub fn dashed(p: &LwPolyline, tol: f64, lt: &Linetype, scale: f64, min_pattern: f64) -> (Vec<Vec2>, Vec<Vec<Vec2>>, Vec<Vec<Vec2>>) {
let mut runs = segments(p, tol);
if p.plinegen && runs.len() > 1 {
let mut whole: Vec<Sample> = Vec::new();
@@ -81,6 +82,7 @@ pub fn dashed(p: &LwPolyline, tol: f64, lt: &Linetype, scale: f64, min_pattern:
}
let mut tris = Vec::new();
let mut ticks = Vec::new();
let mut centres = Vec::new();
for run in &runs {
let len = run.last().map(|x| x.s).unwrap_or(0.0);
let dashes = linetype::ranges(len, lt, scale, min_pattern).unwrap_or_else(|| vec![(0.0, len)]);
@@ -102,10 +104,11 @@ pub fn dashed(p: &LwPolyline, tol: f64, lt: &Linetype, scale: f64, min_pattern:
tris.extend_from_slice(&[a0, a1, b1, a0, b1, b0]);
}
}
centres.push(pts.iter().map(|x| x.p).collect());
if tris.len() > 6_000_000 {
return (tris, ticks);
return (tris, ticks, centres);
}
}
}
(tris, ticks)
(tris, ticks, centres)
}