forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
6b4272521c
commit
3f9c29d6df
3 files changed
+79
-38
No files matched your search
+35
-28
@@ -265,40 +265,47 @@ impl HatchModel {
|
||||
let p1 = [lx + t1 * cos_a, ly + t1 * sin_a];
|
||||
segments.push([p0, p1]);
|
||||
} else {
|
||||
// Walk the dash sequence along this clipped span.
|
||||
// Negative entries are gaps, positive are dashes.
|
||||
// Skip ahead by `t0 mod period` so dash phase aligns
|
||||
// across consecutive line spans (matches the GPU
|
||||
// shader which evaluates dash on absolute t).
|
||||
// Walk the dash sequence along this clipped span with
|
||||
// absolute phase (so the pattern aligns across spans,
|
||||
// matching the GPU shader). Positive entries are
|
||||
// dashes, negative are gaps, and a zero-length entry is
|
||||
// a dot — rendered as a short mark so dot patterns
|
||||
// (e.g. DOTS) are visible instead of drawing nothing.
|
||||
let n = family.dashes.len();
|
||||
let dot_len = (period * 0.06).max(1e-3);
|
||||
let phase = t0.rem_euclid(period);
|
||||
let mut t = t0;
|
||||
let mut cursor = phase;
|
||||
// Start at the period boundary at or before t0; the
|
||||
// span clip below drops anything before t0.
|
||||
let mut seg_t = t0 - phase;
|
||||
let mut idx = 0usize;
|
||||
// Fast-forward `idx` and the in-element offset by
|
||||
// the phase amount.
|
||||
let mut consumed = 0.0_f32;
|
||||
loop {
|
||||
let max_iters = (((t1 - t0) / period).ceil() as usize + 2) * n + 8;
|
||||
let mut iters = 0usize;
|
||||
while seg_t < t1 && iters < max_iters {
|
||||
let d = family.dashes[idx];
|
||||
let d_world = d.abs() * scale;
|
||||
if consumed + d_world > cursor {
|
||||
let remaining = consumed + d_world - cursor;
|
||||
let t_end = (t + remaining).min(t1);
|
||||
if d > 0.0 && t_end > t {
|
||||
let p0 = [lx + t * cos_a, ly + t * sin_a];
|
||||
let p1 = [lx + t_end * cos_a, ly + t_end * sin_a];
|
||||
segments.push([p0, p1]);
|
||||
let dl = d.abs() * scale;
|
||||
if d > 0.0 {
|
||||
let a = seg_t.max(t0);
|
||||
let b = (seg_t + dl).min(t1);
|
||||
if b > a {
|
||||
segments.push([
|
||||
[lx + a * cos_a, ly + a * sin_a],
|
||||
[lx + b * cos_a, ly + b * sin_a],
|
||||
]);
|
||||
}
|
||||
t = t_end;
|
||||
if t >= t1 - 1e-6 {
|
||||
break;
|
||||
} else if d == 0.0 && seg_t >= t0 - 1e-6 && seg_t <= t1 + 1e-6 {
|
||||
// Short centered mark for the dot.
|
||||
let a = (seg_t - dot_len * 0.5).max(t0);
|
||||
let b = (seg_t + dot_len * 0.5).min(t1);
|
||||
if b > a {
|
||||
segments.push([
|
||||
[lx + a * cos_a, ly + a * sin_a],
|
||||
[lx + b * cos_a, ly + b * sin_a],
|
||||
]);
|
||||
}
|
||||
cursor = 0.0;
|
||||
consumed = 0.0;
|
||||
idx = (idx + 1) % family.dashes.len();
|
||||
} else {
|
||||
consumed += d_world;
|
||||
idx = (idx + 1) % family.dashes.len();
|
||||
}
|
||||
seg_t += dl;
|
||||
idx = (idx + 1) % n;
|
||||
iters += 1;
|
||||
if segments.len() >= MAX_SEGMENTS_TOTAL {
|
||||
return segments;
|
||||
}
|
||||
|
||||
+21
-5
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user