From fcb97b12da7685734d84449518139142cf6e0cab Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 25 Jun 2026 17:37:24 +0300 Subject: [PATCH] fix(linetype): render dotted and dash-dot patterns (#149) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dot in an LTYPE pattern is a zero-length element. The pattern resolver encoded it as a tiny `0.01 * scale` world-length dash, which had two bad effects: it was sub-pixel at normal zoom so the dash LOD saw the pattern's smallest feature drop below one pixel and collapsed the whole line to solid, and at larger LTSCALE it left only invisible sub-pixel dots between big gaps. Either way dotted and dash-dot linetypes were never visible; only plain dashed (no zero elements) worked. Keep dots as literal 0.0 so they no longer drag `min_elem` down, and render each as a fixed ~1.5 px mark in the wire shader (pixel-snapped via world_per_pixel, mirroring the hatch shader's dot handling). The shader tells a real dot from trailing array padding by element count — the index of the last non-zero element plus one — so no producer needs a sentinel. Co-Authored-By: Claude Opus 4.8 --- src/scene/view/render.rs | 21 +++++++++++++-------- src/shaders/wire.wgsl | 32 ++++++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index afd725e3f..ba67795dc 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -889,17 +889,22 @@ pub(crate) fn resolve_pattern( return solid; } + // Keep dots (element length exactly 0) as 0.0 so the shader can render + // them as a fixed ~1 px mark; trailing array slots stay 0.0 padding and + // the shader tells the two apart by position (a 0.0 before the last + // non-zero element is a dot, trailing 0.0s are padding). The old code + // encoded dots as `0.01 * scale` — a tiny world-length dash that went + // sub-pixel at normal zoom and dragged the pattern's `min_elem` below one + // pixel, so the dash LOD collapsed dotted / dash-dot lines to solid (or, + // at larger LTSCALE, left only invisible sub-pixel dots between big + // gaps). (#149) let mut pat = [0.0f32; 8]; let mut pat_len = 0.0f32; for (i, el) in lt.elements.iter().take(8).enumerate() { - let raw = el.length as f32 * scale; - let encoded = if raw == 0.0 { - 0.01 * scale.max(0.01) - } else { - raw - }; - pat[i] = encoded; - pat_len += encoded.abs(); + // positive = dash, negative = gap, exactly 0 = dot. + let v = el.length as f32 * scale; + pat[i] = v; + pat_len += v.abs(); } if pat_len < 1e-6 { return solid; diff --git a/src/shaders/wire.wgsl b/src/shaders/wire.wgsl index a2bb2cd4a..9e95e54e3 100644 --- a/src/shaders/wire.wgsl +++ b/src/shaders/wire.wgsl @@ -11,7 +11,9 @@ // • distance = cumulative arc-length, linearly interpolated from // (distance_a, distance_b) by `which_end`. // • pattern_length > 0 enables the dash test; 0 = solid (no discard). -// • pat0/pat1 encode up to 8 elements: positive=dash, negative=gap. +// • pat0/pat1 encode up to 8 elements: positive=dash, negative=gap, +// exactly 0=dot (rendered as a fixed ~1 px mark). Trailing 0.0 slots are +// padding; the real element count is (index of last non-zero) + 1. struct Uniforms { viewport_size: vec2, @@ -154,17 +156,35 @@ struct VertexOut { return out; } -// Returns true if arc-length `dist` falls inside a dash segment. +// Returns true if arc-length `dist` falls inside a dash or on a dot. fn in_dash(dist: f32, pat_len: f32, p0: vec4, p1: vec4) -> bool { let d = ((dist % pat_len) + pat_len) % pat_len; var pos = 0.0f; + // A dot is a zero-length element: render it as a fixed ~1.5 px mark + // (half-width ~0.75 px in world units) so it stays visible at any zoom + // instead of vanishing with its zero world-length. Mirrors the hatch + // shader's pixel-snapped dot. (#149) + let dot_half = u.world_per_pixel * 0.75; let elems = array(p0.x, p0.y, p0.z, p0.w, p1.x, p1.y, p1.z, p1.w); + // Real element count = (index of last non-zero) + 1. Trailing 0.0 slots + // are padding; a 0.0 within this range is a real dot. + var count = 0u; for (var i = 0u; i < 8u; i++) { + if elems[i] != 0.0 { count = i + 1u; } + } + for (var i = 0u; i < count; i++) { let elem = elems[i]; - if elem == 0.0 { break; } - let len = abs(elem); - if d < pos + len { return elem > 0.0; } - pos += len; + if elem == 0.0 { + // Dot centred at `pos` (zero length); light a small mark, wrapped + // around the pattern so a dot at 0 also covers the seam at pat_len. + let dd = abs(d - pos); + if min(dd, pat_len - dd) <= dot_half { return true; } + } else if elem > 0.0 { + if d >= pos && d < pos + elem { return true; } // inside a dash + pos += elem; + } else { + pos += -elem; // skip a gap + } } return false; }