From fd801fac1b98727c054f82c20a4cb11247415b97 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 25 Aug 2026 22:44:51 +0300 Subject: [PATCH] fix(hatch): render weighted pattern strokes Show styled boundaries only while selected.\n\nRefs #821 --- src/scene/pipeline/hatch_gpu/storage.rs | 4 +--- src/scene/pipeline/hatch_gpu/texture.rs | 7 +++++- src/scene/preview.rs | 13 ++++++----- src/scene/view/render.rs | 31 +++++++++++++++++++++---- src/shaders/hatch.wgsl | 12 ++++++---- src/shaders/hatch_texture.wgsl | 11 +++++---- 6 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/scene/pipeline/hatch_gpu/storage.rs b/src/scene/pipeline/hatch_gpu/storage.rs index 43a8edd1c..950a98929 100644 --- a/src/scene/pipeline/hatch_gpu/storage.rs +++ b/src/scene/pipeline/hatch_gpu/storage.rs @@ -295,9 +295,7 @@ impl StorageHatchBatch { // along-line phase. let perp_step = fam.dy; let along_step = fam.dx; - // Screen-space derivative drives 1-px line width - // in the shader; this stored field is unused. - let line_width = 0.0_f32; + let line_width = h.line_weight_px.max(1.0); let period: f32 = fam.dashes.iter().map(|d| d.abs()).sum(); families.push(LineFamilyGpu { cos_a: fam.angle_deg.to_radians().cos(), diff --git a/src/scene/pipeline/hatch_gpu/texture.rs b/src/scene/pipeline/hatch_gpu/texture.rs index f0d58b25c..c777b3bb1 100644 --- a/src/scene/pipeline/hatch_gpu/texture.rs +++ b/src/scene/pipeline/hatch_gpu/texture.rs @@ -286,7 +286,12 @@ impl TextureHatch { texels.push([angle_r.cos(), angle_r.sin(), fam.x0, fam.y0]); texels.push([fam.dx, fam.dy, fam.dy, fam.dx]); // Counts as exact f32 (small integers → no denormal/bitcast risk). - texels.push([0.0, period, n_dashes as f32, dash_rel as f32]); + texels.push([ + model.line_weight_px.max(1.0), + period, + n_dashes as f32, + dash_rel as f32, + ]); n_families += 1; } } diff --git a/src/scene/preview.rs b/src/scene/preview.rs index 5c838dc96..10abfa5ad 100644 --- a/src/scene/preview.rs +++ b/src/scene/preview.rs @@ -351,10 +351,8 @@ impl Scene { (plain, texts) } - /// Boundary outline wire for a hatch, reconstructed from its cached - /// `HatchModel` (offsets from `world_origin`). Used only for edit previews — - /// the normal render shows the fill, not this outline. - fn hatch_outline_wire(&self, handle: Handle) -> Option { + /// Boundary wire for edit previews and selected hatches. + pub(super) fn hatch_outline_wire(&self, handle: Handle) -> Option { let m = self.hatches.get(&handle)?; Self::hatch_model_outline_wire(handle, m) } @@ -375,12 +373,15 @@ impl Scene { if pts.len() < 2 { return None; } - Some(WireModel::solid_f64( + let mut wire = WireModel::solid_f64( handle.value().to_string(), pts, m.color, false, - )) + ); + wire.aci = m.aci; + wire.line_weight_px = m.line_weight_px; + Some(wire) } /// Fill-only geometry has no resident wire for the normal rollover xray. diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 29a9f25cc..2e5fd7032 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -3076,15 +3076,38 @@ impl Scene { let Some(entity) = self.document.get_entity(handle) else { continue; }; - if !crate::scene::annotative::is_annotative(&self.document, entity) - || !self.resident_entity_visible( + if !self.resident_entity_visible( entity, target_block, Some(&frozen), annotation_scale_handle, true, - ) - { + ) { + continue; + } + + if matches!(entity, EntityType::Hatch(_)) { + if selected { + if let Some(mut wire) = self.hatch_outline_wire(handle) { + let (_, pattern_length, pattern, line_weight_px, aci) = + render_style_for_viewport( + &self.document, + entity, + content_viewport.then_some(inst.handle), + ); + wire.color = WireModel::SELECTED; + wire.selected = true; + wire.pattern_length = pattern_length; + wire.pattern = pattern; + wire.line_weight_px = line_weight_px; + wire.aci = aci; + wires.push(wire); + } + } + continue; + } + + if !crate::scene::annotative::is_annotative(&self.document, entity) { continue; } diff --git a/src/shaders/hatch.wgsl b/src/shaders/hatch.wgsl index f288faee5..33c6696b7 100644 --- a/src/shaders/hatch.wgsl +++ b/src/shaders/hatch.wgsl @@ -151,7 +151,6 @@ fn check_family( let pz = xz.y - oz; let perp_step = fam.perp_step * scale; - let line_w = abs(fam.line_width * scale); let perp = -px * sin_a + pz * cos_a; let k = round(perp / perp_step); @@ -163,6 +162,8 @@ fn check_family( -ddx_xz.x * sin_a + ddx_xz.y * cos_a, -ddy_xz.x * sin_a + ddy_xz.y * cos_a, )) * 0.5; + let width_px = select(1.0, max(fam.line_width, 1.0), u.lwdisplay_enable > 0.5); + let half_line = half_px * width_px; // 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 @@ -172,8 +173,8 @@ fn check_family( // 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; } + if d > max(half_line, half_px * 2.0) { return false; } + if fam.n_dashes == 0u { return d <= half_line; } let along_step = fam.along_step * scale; let period = fam.period * scale; @@ -185,7 +186,7 @@ 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 d <= half_px && t_mod >= pos && t_mod < pos + sv { return true; } + if d <= half_line && t_mod >= pos && t_mod < pos + sv { return true; } pos = pos + sv; } else if sv < 0.0 { pos = pos - sv; @@ -197,7 +198,8 @@ fn check_family( 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; } + let dot_half = width_px * 0.5; + if abs(owx / wpx) <= dot_half && abs(owy / wpy) <= dot_half { return true; } } } return false; diff --git a/src/shaders/hatch_texture.wgsl b/src/shaders/hatch_texture.wgsl index c67df0b8d..de40284bc 100644 --- a/src/shaders/hatch_texture.wgsl +++ b/src/shaders/hatch_texture.wgsl @@ -146,13 +146,15 @@ fn check_family( -ddx_xz.x * sin_a + ddx_xz.y * cos_a, -ddy_xz.x * sin_a + ddy_xz.y * cos_a, )) * 0.5; + let width_px = select(1.0, max(fam.line_width, 1.0), u.lwdisplay_enable > 0.5); + let half_line = half_px * width_px; let wpx = length(vec2(ddx_xz.x, ddy_xz.x)); let wpy = length(vec2(ddx_xz.y, ddy_xz.y)); - if d > half_px * 2.0 { return false; } + if d > max(half_line, half_px * 2.0) { return false; } - if fam.n_dashes == 0u { return d <= half_px; } + if fam.n_dashes == 0u { return d <= half_line; } let along_step = fam.along_step * scale; let period = fam.period * scale; @@ -165,7 +167,7 @@ fn check_family( let idx = fam.dash_off + j; let sv = texel(h.dash_off + idx / 4u)[idx % 4u] * scale; if sv > 0.0 { - if d <= half_px && t_mod >= pos && t_mod < pos + sv { return true; } + if d <= half_line && t_mod >= pos && t_mod < pos + sv { return true; } pos = pos + sv; } else if sv < 0.0 { pos = pos - sv; @@ -173,7 +175,8 @@ fn check_family( 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; } + let dot_half = width_px * 0.5; + if abs(owx / wpx) <= dot_half && abs(owy / wpy) <= dot_half { return true; } } } return false;