fix(text): show selection highlight

Render tinted SDF glyphs in the final xray pass so base text and wipeouts cannot occlude them.\n\nRefs #479
This commit is contained in:
Hakan Seven 2026-07-27 14:13:35 +03:00
commit a4235a1243
2 changed files with 91 additions and 70 deletions

View file

@ -102,6 +102,8 @@ pub struct Pipeline {
/// SDF text-quad pipeline (Phase 2b): draws per-glyph quads sampling the /// SDF text-quad pipeline (Phase 2b): draws per-glyph quads sampling the
/// shared glyph atlas. Fed only when `OCS_TEXT_SDF` is set (else no verts). /// shared glyph atlas. Fed only when `OCS_TEXT_SDF` is set (else no verts).
text_pipeline: wgpu::RenderPipeline, text_pipeline: wgpu::RenderPipeline,
/// Depth-independent variant used by selection / rollover highlighting.
text_highlight_pipeline: wgpu::RenderPipeline,
mesh_pipeline: wgpu::RenderPipeline, mesh_pipeline: wgpu::RenderPipeline,
/// Depth-write-disabled variant of `mesh_pipeline` for non-opaque solids. /// Depth-write-disabled variant of `mesh_pipeline` for non-opaque solids.
mesh_transparent_pipeline: wgpu::RenderPipeline, mesh_transparent_pipeline: wgpu::RenderPipeline,
@ -1602,8 +1604,8 @@ impl Pipeline {
// ── Text (SDF glyph quads) ───────────────────────────────────────── // ── Text (SDF glyph quads) ─────────────────────────────────────────
let text_atlas_bgl = text_gpu::TextAtlasGpu::bind_group_layout(device); let text_atlas_bgl = text_gpu::TextAtlasGpu::bind_group_layout(device);
let text_pipeline = let (text_pipeline, text_highlight_pipeline) =
text_gpu::create_pipeline(device, &frame_bgl, &text_atlas_bgl, format, MSAA_SAMPLES); text_gpu::create_pipelines(device, &frame_bgl, &text_atlas_bgl, format, MSAA_SAMPLES);
let viewcube = ViewCubePipeline::new(device, queue, format); let viewcube = ViewCubePipeline::new(device, queue, format);
@ -1745,6 +1747,7 @@ impl Pipeline {
hatch_compat_pipeline, hatch_compat_pipeline,
image_pipeline, image_pipeline,
text_pipeline, text_pipeline,
text_highlight_pipeline,
text_atlas_bgl, text_atlas_bgl,
text_atlas_gpu: None, text_atlas_gpu: None,
text_vbuf: None, text_vbuf: None,
@ -3619,14 +3622,13 @@ impl Pipeline {
} }
// ── Pass 5c: SDF text quads (drawn over wires) ──────────────────── // ── Pass 5c: SDF text quads (drawn over wires) ────────────────────
// The highlight buffer (selected / hovered text, tinted) is drawn in // Selection / rollover text is drawn later with the selected-wire xray
// the same pass right after the base text so it recolours those glyphs. // overlay, after wipeouts, so normal text cannot hide its own tint.
if let Some(atlas) = &self.text_atlas_gpu { if let Some(atlas) = &self.text_atlas_gpu {
let have_base = self.text_vbuf.is_some() && self.text_vcount > 0; let have_base = self.text_vbuf.is_some() && self.text_vcount > 0;
let have_hl = self.text_highlight_vbuf.is_some() && self.text_highlight_vcount > 0;
let have_preview = let have_preview =
self.text_preview_vbuf.is_some() && self.text_preview_vcount > 0; self.text_preview_vbuf.is_some() && self.text_preview_vcount > 0;
if have_base || have_hl || have_preview { if have_base || have_preview {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("text.render_pass"), label: Some("text.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment { color_attachments: &[Some(wgpu::RenderPassColorAttachment {
@ -3660,12 +3662,6 @@ impl Pipeline {
pass.draw(0..self.text_vcount, 0..1); pass.draw(0..self.text_vcount, 0..1);
} }
} }
if let Some(hlbuf) = &self.text_highlight_vbuf {
if self.text_highlight_vcount > 0 {
pass.set_vertex_buffer(0, hlbuf.slice(..));
pass.draw(0..self.text_highlight_vcount, 0..1);
}
}
// Grip-drag / command-preview glyphs, drawn over the base text. // Grip-drag / command-preview glyphs, drawn over the base text.
if let Some(pbuf) = &self.text_preview_vbuf { if let Some(pbuf) = &self.text_preview_vbuf {
if self.text_preview_vcount > 0 { if self.text_preview_vcount > 0 {
@ -3714,12 +3710,14 @@ impl Pipeline {
} }
} }
// ── Pass 7: selected wire overlay pass ─────────────────────────── // ── Pass 7: selection overlay pass ───────────────────────────────
// Redraws selected wires with depth_compare=Always so they appear on // Redraws selected wires and text with depth_compare=Always so both
// top of all other geometry at full brightness. // appear on top of all other geometry at full brightness.
if !self.gpu_selected_wires.is_empty() { let have_text_highlight =
self.text_highlight_vbuf.is_some() && self.text_highlight_vcount > 0;
if !self.gpu_selected_wires.is_empty() || have_text_highlight {
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("wire_xray.render_pass"), label: Some("selection_xray.render_pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment { color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: msaa, view: msaa,
depth_slice: None, depth_slice: None,
@ -3741,19 +3739,31 @@ impl Pipeline {
occlusion_query_set: None, occlusion_query_set: None,
}); });
pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0);
pass.set_pipeline(&self.wire_xray_pipeline);
pass.set_bind_group(0, &self.uniform_bind_group, &[]); pass.set_bind_group(0, &self.uniform_bind_group, &[]);
pass.set_stencil_reference(stencil_ref); pass.set_stencil_reference(stencil_ref);
for wire in &self.gpu_selected_wires { if !self.gpu_selected_wires.is_empty() {
if wire.instance_count > 0 { pass.set_pipeline(&self.wire_xray_pipeline);
if let Some(bg) = &wire.const_bind_group { for wire in &self.gpu_selected_wires {
pass.set_bind_group(1, bg.as_ref(), &[]); if wire.instance_count > 0 {
if let Some(bg) = &wire.const_bind_group {
pass.set_bind_group(1, bg.as_ref(), &[]);
}
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..));
pass.draw(
0..6,
wire.first_instance..wire.first_instance + wire.instance_count,
);
} }
pass.set_vertex_buffer(0, wire.instance_buffer.slice(..)); }
pass.draw( }
0..6, if let (Some(atlas), Some(hlbuf)) =
wire.first_instance..wire.first_instance + wire.instance_count, (&self.text_atlas_gpu, &self.text_highlight_vbuf)
); {
if self.text_highlight_vcount > 0 {
pass.set_pipeline(&self.text_highlight_pipeline);
pass.set_bind_group(1, &atlas.bind_group, &[]);
pass.set_vertex_buffer(0, hlbuf.slice(..));
pass.draw(0..self.text_highlight_vcount, 0..1);
} }
} }
} }

View file

@ -250,16 +250,17 @@ impl TextAtlasGpu {
// ── Pipeline ───────────────────────────────────────────────────────────────── // ── Pipeline ─────────────────────────────────────────────────────────────────
/// Build the text render pipeline. Mirrors the image pipeline: alpha blending, /// Build the base and highlight text pipelines. The base mirrors the image
/// `LessEqual` depth with write, 4x MSAA. `frame_bgl` is group 0 (shared /// pipeline (`LessEqual` depth with write); the highlight variant always draws
/// uniforms), `atlas_bgl` is group 1 (the atlas). /// without changing depth, matching the selected-wire xray overlay.
pub fn create_pipeline( /// `frame_bgl` is group 0 (shared uniforms), `atlas_bgl` is group 1 (the atlas).
pub fn create_pipelines(
device: &wgpu::Device, device: &wgpu::Device,
frame_bgl: &wgpu::BindGroupLayout, frame_bgl: &wgpu::BindGroupLayout,
atlas_bgl: &wgpu::BindGroupLayout, atlas_bgl: &wgpu::BindGroupLayout,
color_format: wgpu::TextureFormat, color_format: wgpu::TextureFormat,
sample_count: u32, sample_count: u32,
) -> wgpu::RenderPipeline { ) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("text.wgsl"), label: Some("text.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/text.wgsl").into()), source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/text.wgsl").into()),
@ -269,44 +270,54 @@ pub fn create_pipeline(
bind_group_layouts: &[frame_bgl, atlas_bgl], bind_group_layouts: &[frame_bgl, atlas_bgl],
push_constant_ranges: &[], push_constant_ranges: &[],
}); });
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { let create = |label, depth_write_enabled, depth_compare| {
label: Some("text.pipeline"), device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
layout: Some(&layout), label: Some(label),
vertex: wgpu::VertexState { layout: Some(&layout),
module: &shader, vertex: wgpu::VertexState {
entry_point: Some("vs_main"), module: &shader,
buffers: &[TextVertex::layout()], entry_point: Some("vs_main"),
compilation_options: Default::default(), buffers: &[TextVertex::layout()],
}, compilation_options: Default::default(),
fragment: Some(wgpu::FragmentState { },
module: &shader, fragment: Some(wgpu::FragmentState {
entry_point: Some("fs_main"), module: &shader,
targets: &[Some(wgpu::ColorTargetState { entry_point: Some("fs_main"),
format: color_format, targets: &[Some(wgpu::ColorTargetState {
blend: Some(wgpu::BlendState::ALPHA_BLENDING), format: color_format,
write_mask: wgpu::ColorWrites::ALL, blend: Some(wgpu::BlendState::ALPHA_BLENDING),
})], write_mask: wgpu::ColorWrites::ALL,
compilation_options: Default::default(), })],
}), compilation_options: Default::default(),
primitive: wgpu::PrimitiveState { }),
topology: wgpu::PrimitiveTopology::TriangleList, primitive: wgpu::PrimitiveState {
..Default::default() topology: wgpu::PrimitiveTopology::TriangleList,
}, ..Default::default()
depth_stencil: Some(wgpu::DepthStencilState { },
format: wgpu::TextureFormat::Depth24PlusStencil8, depth_stencil: Some(wgpu::DepthStencilState {
depth_write_enabled: true, format: wgpu::TextureFormat::Depth24PlusStencil8,
depth_compare: wgpu::CompareFunction::LessEqual, depth_write_enabled,
stencil: wgpu::StencilState::default(), depth_compare,
bias: wgpu::DepthBiasState::default(), stencil: wgpu::StencilState::default(),
}), bias: wgpu::DepthBiasState::default(),
multisample: wgpu::MultisampleState { }),
count: sample_count, multisample: wgpu::MultisampleState {
mask: !0, count: sample_count,
alpha_to_coverage_enabled: false, mask: !0,
}, alpha_to_coverage_enabled: false,
multiview: None, },
cache: None, multiview: None,
}) cache: None,
})
};
(
create("text.pipeline", true, wgpu::CompareFunction::LessEqual),
create(
"text.highlight.pipeline",
false,
wgpu::CompareFunction::Always,
),
)
} }
/// Upload a finished vertex list; `None` if empty. /// Upload a finished vertex list; `None` if empty.