fix(hatch): render dot patterns (zero-length dashes)

A zero-length dash entry is a dot (e.g. the DOTS pattern), but every
render path treated only positive entries as marks and skipped it, so
dot patterns drew nothing.

- GPU shaders: light exactly the one pixel containing each dot's lattice
  centre. The dot grid rotates with the pattern angle while the lit pixel
  stays a steady single pixel, so dots no longer flicker at oblique
  angles or zoom (an earlier coverage/round test still aliased).
- PDF export (pattern_segments): walk the dash sequence with absolute
  phase and emit a short mark for each dot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-06-09 23:15:45 +03:00
commit 3f9c29d6df
3 changed files with 79 additions and 38 deletions

View file

@ -165,13 +165,21 @@ fn check_family(
let perp = -px * sin_a + pz * cos_a;
let k = round(perp / perp_step);
let d = abs(perp - k * perp_step);
let dperp = perp - k * perp_step;
let d = abs(dperp);
let half_px = length(vec2<f32>(dpdx(perp), dpdy(perp))) * 0.5;
if d > half_px { return false; }
// World units per screen pixel on each axis — used to light exactly the
// one pixel that contains a dot's centre (pixel-snapped, so a dot stays a
// steady single pixel at any pattern angle instead of flickering).
let wpx = length(vec2<f32>(dpdx(xz.x), dpdy(xz.x)));
let wpy = length(vec2<f32>(dpdx(xz.y), dpdy(xz.y)));
// A fragment within ~1px of a line may be a dot; further out is empty fill.
if d > half_px * 2.0 { return false; }
// Solid line family — no dash check needed.
if fam.n_dashes == 0u { return true; }
if fam.n_dashes == 0u { return d <= half_px; }
// Dash pattern test: position along line k.
let along_step = fam.along_step * scale;
@ -185,10 +193,18 @@ fn check_family(
let idx = fam.dash_off + j;
let sv = f.dash_values[idx / 4u][idx % 4u] * scale; // scale dash lengths
if sv > 0.0 {
if t_mod >= pos && t_mod < pos + sv { return true; }
if d <= half_px && t_mod >= pos && t_mod < pos + sv { return true; }
pos = pos + sv;
} else {
} else if sv < 0.0 {
pos = pos - sv;
} else {
// Dot: signed distance to its lattice centre, rotated back to world
// and snapped to the pixel grid — the grid rotates with the
// pattern but the lit pixel stays a steady single pixel.
let dtv = (t - pos) - round((t - pos) / period) * period;
let owx = -dtv * cos_a + dperp * sin_a;
let owy = -dtv * sin_a - dperp * cos_a;
if abs(owx / wpx) <= 0.5 && abs(owy / wpy) <= 0.5 { return true; }
}
}
return false;

View file

@ -190,11 +190,20 @@ fn check_family(
let perp = -px * sin_a + pz * cos_a;
let k = round(perp / perp_step);
let d = abs(perp - k * perp_step);
let dperp = perp - k * perp_step;
let d = abs(dperp);
let half_px = length(vec2<f32>(dpdx(perp), dpdy(perp))) * 0.5;
if d > half_px { return false; }
if fam.n_dashes == 0u { return true; }
// World units per screen pixel on each axis — used to light exactly the
// one pixel that contains a dot's centre (pixel-snapped, so the dot stays
// a steady single pixel at any pattern angle instead of flickering).
let wpx = length(vec2<f32>(dpdx(xz.x), dpdy(xz.x)));
let wpy = length(vec2<f32>(dpdx(xz.y), dpdy(xz.y)));
// A fragment within ~1px of a line may be a dot; everything further out is
// empty fill. (A dot's pixel sits on a line, so its perp offset is < 1px.)
if d > half_px * 2.0 { return false; }
if fam.n_dashes == 0u { return d <= half_px; }
let along_step = fam.along_step * scale;
let period = fam.period * scale;
@ -206,10 +215,19 @@ fn check_family(
for (var j = 0u; j < fam.n_dashes; j++) {
let sv = dashes[fam.dash_offset + j] * scale;
if sv > 0.0 {
if t_mod >= pos && t_mod < pos + sv { return true; }
if d <= half_px && t_mod >= pos && t_mod < pos + sv { return true; }
pos = pos + sv;
} else {
} else if sv < 0.0 {
pos = pos - sv;
} else {
// Dot: signed distance to its lattice centre (along the line and
// across lines), rotated back to world, then snapped to the pixel
// grid. The dot grid rotates with the pattern; the lit pixel does
// not, so it never thins/flickers.
let dtv = (t - pos) - round((t - pos) / period) * period;
let owx = -dtv * cos_a + dperp * sin_a;
let owy = -dtv * sin_a - dperp * cos_a;
if abs(owx / wpx) <= 0.5 && abs(owy / wpy) <= 0.5 { return true; }
}
}
return false;