mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 02:46:51 -06:00
This commit is contained in:
@@ -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 < {
|
||||
// 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 ¢res {
|
||||
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 };
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user