From 5fe686609d7b762c80dd66aff27b483c46eb9f6c Mon Sep 17 00:00:00 2001 From: junzhuo Date: Sun, 22 Mar 2026 13:24:56 +0800 Subject: [PATCH 1/2] feat: add configurable line width --- examples/controls_toggles.rs | 2 +- examples/custom_ticks.rs | 2 +- examples/fills.rs | 6 +- examples/high_precision.rs | 10 +- examples/interactive_spline.rs | 9 +- examples/line_only.rs | 12 +- examples/reference_lines.rs | 12 +- examples/three_plots.rs | 12 +- src/lib.rs | 2 +- src/picking.rs | 4 +- src/plot_renderer.rs | 337 +++++++++++++++++++++++---------- src/plot_state.rs | 8 +- src/plot_widget.rs | 10 +- src/reference_lines.rs | 54 ++++-- src/series.rs | 152 ++++++++++++--- src/shaders/line.wgsl | 141 +++++++++++--- 16 files changed, 560 insertions(+), 213 deletions(-) diff --git a/examples/controls_toggles.rs b/examples/controls_toggles.rs index 0fa88a3..14e621c 100644 --- a/examples/controls_toggles.rs +++ b/examples/controls_toggles.rs @@ -45,7 +45,7 @@ impl App { [x, (x * 0.8).sin()] }) .collect(), - LineStyle::Solid, + LineStyle::solid(), ) .with_label("sin(x)") .with_color(Color::from_rgb(0.3, 0.7, 1.0)); diff --git a/examples/custom_ticks.rs b/examples/custom_ticks.rs index 9e25766..e261d70 100644 --- a/examples/custom_ticks.rs +++ b/examples/custom_ticks.rs @@ -53,7 +53,7 @@ fn new() -> PlotWidget { }) .collect(); - let series = Series::new(positions, MarkerStyle::circle(5.0), LineStyle::Solid) + let series = Series::new(positions, MarkerStyle::circle(5.0), LineStyle::solid()) .with_label("Temperature") .with_point_colors(colors) .with_color(Color::from_rgb(1.0, 0.5, 0.2)); diff --git a/examples/fills.rs b/examples/fills.rs index 4db6f9f..a96d5b0 100644 --- a/examples/fills.rs +++ b/examples/fills.rs @@ -38,17 +38,17 @@ fn new() -> PlotWidget { .collect(); // Create a bunch of series and lines. - let upper_series = Series::line_only(upper_positions, LineStyle::Solid) + let upper_series = Series::line_only(upper_positions, LineStyle::solid()) .with_marker_style(MarkerStyle::circle(3.5)) .with_color(Color::from_rgb(0.15, 0.55, 0.95)) .with_label("upper series"); - let lower_series = Series::line_only(lower_positions, LineStyle::Dashed { length: 8.0 }) + let lower_series = Series::line_only(lower_positions, LineStyle::dashed(8.0)) .with_marker_style(MarkerStyle::ring(3.0)) .with_color(Color::from_rgb(0.95, 0.45, 0.15)) .with_label("lower series (sparse)"); let baseline = HLine::new(0.0) .with_label("y = 0") - .with_style(LineStyle::Dotted { spacing: 4.0 }) + .with_style(LineStyle::dotted(4.0)) .with_color(Color::from_rgb(0.7, 0.7, 0.7)); let hband_low = HLine::new(1.9) .with_label("h-band low") diff --git a/examples/high_precision.rs b/examples/high_precision.rs index 398d375..259356d 100644 --- a/examples/high_precision.rs +++ b/examples/high_precision.rs @@ -43,13 +43,9 @@ fn new() -> PlotWidget { }) .with_cursor_provider(|x, y| format!("Cursor:\nt = {:.9} s\nvalue = {:.6}", x, y)) .add_series( - Series::new( - positions, - MarkerStyle::square(4.0), - LineStyle::Dashed { length: 10.0 }, - ) - .with_label("both_markers_and_lines") - .with_color(Color::from_rgb(0.3, 0.9, 0.3)), + Series::new(positions, MarkerStyle::square(4.0), LineStyle::dashed(10.0)) + .with_label("both_markers_and_lines") + .with_color(Color::from_rgb(0.3, 0.9, 0.3)), ) .with_cursor_overlay(true) .with_crosshairs(true) diff --git a/examples/interactive_spline.rs b/examples/interactive_spline.rs index edee263..5cd1d5b 100644 --- a/examples/interactive_spline.rs +++ b/examples/interactive_spline.rs @@ -42,16 +42,15 @@ impl App { [4.0, 0.2], ]; - let control_poly = - Series::line_only(control_points.clone(), LineStyle::Dashed { length: 8.0 }) - .with_label("control polygon") - .with_color(Color::from_rgb(0.5, 0.5, 0.5)); + let control_poly = Series::line_only(control_points.clone(), LineStyle::dashed(8.0)) + .with_label("control polygon") + .with_color(Color::from_rgb(0.5, 0.5, 0.5)); let control_series = Series::markers_only(control_points.clone(), MarkerStyle::circle(7.0)) .with_label("control points") .with_color(Color::from_rgb(1.0, 0.5, 0.2)); - let spline = Series::line_only(sample_catmull_rom(&control_points, 28), LineStyle::Solid) + let spline = Series::line_only(sample_catmull_rom(&control_points, 28), LineStyle::solid()) .with_label("catmull-rom spline") .with_color(Color::from_rgb(0.2, 0.8, 1.0)); diff --git a/examples/line_only.rs b/examples/line_only.rs index 1cb8689..c0b7909 100644 --- a/examples/line_only.rs +++ b/examples/line_only.rs @@ -58,7 +58,7 @@ fn new() -> PlotWidget { }) .collect(); - let s1 = Series::line_only(positions, LineStyle::Solid) + let s1 = Series::line_only(positions, LineStyle::solid().with_pixel_width(4.0)) .with_label("sine_line_only") .with_color(Color::from_rgb(0.3, 0.3, 0.9)); @@ -80,13 +80,9 @@ fn new() -> PlotWidget { [x, y] }) .collect(); - let s3 = Series::new( - positions, - MarkerStyle::square(4.0), - LineStyle::Dashed { length: 10.0 }, - ) - .with_label("both_markers_and_lines") - .with_color(Color::from_rgb(0.3, 0.9, 0.3)); + let s3 = Series::new(positions, MarkerStyle::square(4.0), LineStyle::dashed(10.0)) + .with_label("both_markers_and_lines") + .with_color(Color::from_rgb(0.3, 0.9, 0.3)); PlotWidgetBuilder::new() .with_hover_highlight_provider(|context, point| { diff --git a/examples/reference_lines.rs b/examples/reference_lines.rs index 950629c..cbd3ba7 100644 --- a/examples/reference_lines.rs +++ b/examples/reference_lines.rs @@ -47,7 +47,7 @@ fn new() -> PlotWidget { .with_label("tan_1") .with_color(Color::from_rgb(0.3, 0.6, 0.9)); - let tan2 = Series::line_only(tan_seg2, LineStyle::Solid) + let tan2 = Series::line_only(tan_seg2, LineStyle::solid()) .with_marker_style(MarkerStyle::star(10.0)) .with_color(Color::from_rgb(0.7, 0.2, 0.1)) .with_label("tan_2"); @@ -59,7 +59,7 @@ fn new() -> PlotWidget { tanh_positions.push([x, (k * (x - 1.5 * PI)).tanh()]); x += 0.01; } - let tanh_s = Series::line_only(tanh_positions, LineStyle::Dashed { length: 8.0 }) + let tanh_s = Series::line_only(tanh_positions, LineStyle::dashed(8.0)) .with_label("y = tanh(1.2·(x - 1.5π))") .with_color(Color::from_rgb(0.2, 0.8, 0.5)); @@ -68,26 +68,26 @@ fn new() -> PlotWidget { .with_label("π") .with_color(Color::from_rgb(0.9, 0.3, 0.3)) .with_width(2.0) - .with_style(LineStyle::Solid); + .with_style(LineStyle::solid()); let vline2 = VLine::new(TAU) .with_label("2π") .with_color(Color::from_rgb(0.9, 0.5, 0.3)) .with_width(2.0) - .with_style(LineStyle::Dashed { length: 1.0 }); + .with_style(LineStyle::dashed(1.0)); // Add horizontal reference lines at y = ±1 (asymptotes of tanh) let hline1 = HLine::new(1.0) .with_label("y=1.0") .with_color(Color::from_rgb(0.3, 0.9, 0.5)) .with_width(2.5) - .with_style(LineStyle::Dotted { spacing: 5.0 }); + .with_style(LineStyle::dotted(5.0)); let hline2 = HLine::new(-1.0) .with_label("y=-1.0") .with_color(Color::from_rgb(0.3, 0.9, 0.5)) .with_width(2.5) - .with_style(LineStyle::Dotted { spacing: 5.0 }); + .with_style(LineStyle::dotted(5.0)); PlotWidgetBuilder::new() .with_x_label("x") diff --git a/examples/three_plots.rs b/examples/three_plots.rs index 0903522..351859a 100644 --- a/examples/three_plots.rs +++ b/examples/three_plots.rs @@ -156,7 +156,7 @@ impl App { [x, y] }) .collect(); - let s1 = Series::line_only(positions, LineStyle::Solid).with_label("sine_line_only"); + let s1 = Series::line_only(positions, LineStyle::solid()).with_label("sine_line_only"); let s1_id = s1.id; let w1 = PlotWidgetBuilder::new() .disable_scroll_to_pan() @@ -200,13 +200,9 @@ impl App { [x, y] }) .collect(); - let s3 = Series::new( - positions, - MarkerStyle::square(4.0), - LineStyle::Dashed { length: 10.0 }, - ) - .with_label("both_markers_and_lines") - .with_color([0.3, 0.9, 0.3]); + let s3 = Series::new(positions, MarkerStyle::square(4.0), LineStyle::dashed(10.0)) + .with_label("both_markers_and_lines") + .with_color([0.3, 0.9, 0.3]); let s3_id = s3.id; let w3 = PlotWidgetBuilder::new() .disable_scroll_to_pan() diff --git a/src/lib.rs b/src/lib.rs index cf5d19a..6b812fa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ pub use plot_widget::{HighlightPoint, PlotWidget}; pub use plot_widget_builder::PlotWidgetBuilder; pub use point::{MarkerType, Point}; pub use reference_lines::{HLine, VLine}; -pub use series::{LineStyle, MarkerSize, MarkerStyle, Series, ShapeId}; +pub use series::{LineStyle, LineType, MarkerStyle, Series, ShapeId, Size}; pub use ticks::{ Tick, TickFormatter, TickProducer, default_formatter, default_tick_producer, log_formatter, log_tick_producer, diff --git a/src/picking.rs b/src/picking.rs index 158e36f..fbf091b 100644 --- a/src/picking.rs +++ b/src/picking.rs @@ -8,7 +8,7 @@ use glam::{DVec2, Vec2}; use iced::Rectangle; use iced::wgpu::*; -use crate::{MarkerSize, Point, PointId, camera::Camera, plot_state::SeriesSpan}; +use crate::{Point, PointId, Size, camera::Camera, plot_state::SeriesSpan}; /// Threshold for number of points above which GPU picking is used instead of CPU picking. pub(crate) const CPU_PICK_THRESHOLD: usize = 5000; @@ -217,7 +217,7 @@ fn cpu_pick_hit( let dx = screen_x - cursor_x; let dy = screen_y - cursor_y; let d2 = dx * dx + dy * dy; - let marker_px = MarkerSize::marker_size_px(pt.size, pt.size_mode, camera, bounds) as f64; + let marker_px = Size::size_px(pt.size, pt.size_mode, camera, bounds) as f64; let radius = hover_radius_px as f64 + marker_px * 0.5; if d2 <= radius * radius { if let Some((_, best_d2)) = best { diff --git a/src/plot_renderer.rs b/src/plot_renderer.rs index 8fc4f36..faaa0df 100644 --- a/src/plot_renderer.rs +++ b/src/plot_renderer.rs @@ -2,7 +2,7 @@ use crate::LineStyle; use crate::axis_scale::data_point_to_plot; use crate::picking::PickingPass; -use crate::{camera::CameraUniform, grid::Grid, plot_state::PlotState}; +use crate::{LineType, Size, camera::CameraUniform, grid::Grid, plot_state::PlotState}; use iced::widget::shader::Viewport; use iced::{Rectangle, wgpu::*}; @@ -25,7 +25,7 @@ struct VertexBuffer { vertex_count: u32, } -/// Helper struct for managing line buffers with segments +/// Helper struct for managing line vertex buffers with strip segments struct LineBuffer { buffer: Buffer, segments: Vec, @@ -139,19 +139,29 @@ impl VertexWriter { fn write_line_vertex( &mut self, pos: [f32; 2], + prev: [f32; 2], + next: [f32; 2], color: &iced::Color, style: u32, distance: f32, param: f32, + width: f32, + width_mode: u32, + side: f32, ) { self.write_position(pos); + self.write_position(prev); + self.write_position(next); self.write_color(color); self.write_u32(style); self.write_f32(distance); self.write_f32(param); + self.write_f32(width); + self.write_u32(width_mode); + self.write_f32(side); } - fn len(&self) -> usize { + fn byte_len(&self) -> usize { self.data.len() } @@ -261,6 +271,13 @@ impl PlotRenderer { ] } + fn world_per_pixel(&self, camera: &crate::camera::Camera) -> [f32; 2] { + [ + ((2.0 * camera.half_extents.x) / self.bounds_w.max(1) as f64) as f32, + ((2.0 * camera.half_extents.y) / self.bounds_h.max(1) as f64) as f32, + ] + } + fn ensure_pipelines_and_update_grid( &mut self, device: &Device, @@ -274,7 +291,10 @@ impl PlotRenderer { if !state.fills.is_empty() { self.ensure_fill_pipeline(device); } - if !state.series.is_empty() && state.series.iter().any(|s| s.line_style.is_some()) { + if state.series.iter().any(|s| s.line_style.is_some()) + || !state.vlines.is_empty() + || !state.hlines.is_empty() + { self.ensure_line_pipeline(device); } self.ensure_overlay_pipeline(device); @@ -485,7 +505,8 @@ impl PlotRenderer { entry_point: Some("vs_main"), compilation_options: PipelineCompilationOptions::default(), buffers: &[VertexBufferLayout { - array_stride: 36, // vec2 position (8) + vec4 color (16) + u32 line_style (4) + f32 distance (4) + f32 style_param (4) + // vec2 position (8) + vec2 prev_position (8) + vec2 next_position (8) + vec4 color (16) + u32 line_style (4) + f32 distance_along_line (4) + f32 style_param (4) + f32 width (4) + u32 width_mode (4) + f32 side (4) + array_stride: 64, step_mode: VertexStepMode::Vertex, attributes: &[ VertexAttribute { @@ -496,23 +517,48 @@ impl PlotRenderer { VertexAttribute { offset: 8, shader_location: 1, - format: VertexFormat::Float32x4, // color + format: VertexFormat::Float32x2, // prev_position }, VertexAttribute { - offset: 24, + offset: 16, shader_location: 2, - format: VertexFormat::Uint32, // line_style + format: VertexFormat::Float32x2, // next_position }, VertexAttribute { - offset: 28, + offset: 24, shader_location: 3, - format: VertexFormat::Float32, // distance_along_line + format: VertexFormat::Float32x4, // color }, VertexAttribute { - offset: 32, + offset: 40, shader_location: 4, + format: VertexFormat::Uint32, // line_style + }, + VertexAttribute { + offset: 44, + shader_location: 5, + format: VertexFormat::Float32, // distance_along_line + }, + VertexAttribute { + offset: 48, + shader_location: 6, format: VertexFormat::Float32, // style_param }, + VertexAttribute { + offset: 52, + shader_location: 7, + format: VertexFormat::Float32, // width + }, + VertexAttribute { + offset: 56, + shader_location: 8, + format: VertexFormat::Uint32, // width_mode + }, + VertexAttribute { + offset: 60, + shader_location: 9, + format: VertexFormat::Float32, // side + }, ], }], }, @@ -527,7 +573,7 @@ impl PlotRenderer { })], }), primitive: PrimitiveState { - topology: PrimitiveTopology::LineStrip, + topology: PrimitiveTopology::TriangleStrip, strip_index_format: None, front_face: FrontFace::Ccw, cull_mode: None, @@ -846,19 +892,21 @@ impl PlotRenderer { let mut writer = VertexWriter::new(); let mut segs: Vec = Vec::new(); - for s in state.series.iter() { - if s.line_style.is_none() || s.len < 2 { + let Some(line_style) = s.line_style else { + continue; + }; + if s.len < 2 { continue; } - let (line_style_u32, style_param) = line_style_params(s.line_style.unwrap()); - + let (line_style_u32, style_param) = line_style_params(line_style); let points_slice = &state.points[s.start..s.start + s.len]; - let mut segment_first = 0u32; - let mut segment_count = 0u32; + let mut poly_positions: Vec<[f32; 2]> = Vec::new(); + let mut poly_distances: Vec = Vec::new(); + let mut poly_colors: Vec = Vec::new(); let mut cumulative_distance = 0.0f32; - for (i, p) in points_slice.iter().enumerate() { + for (i, point) in points_slice.iter().enumerate() { let break_segment = i > 0 && s.point_indices .get(i) @@ -866,45 +914,53 @@ impl PlotRenderer { .is_some_and(|(curr, prev)| *curr != *prev + 1); if break_segment { - if segment_count >= 2 { - segs.push(LineSegment { - first_vertex: segment_first, - vertex_count: segment_count, - }); - } - segment_first = (writer.len() / 36) as u32; - segment_count = 0; + write_polyline_strip( + &mut writer, + &mut segs, + &poly_positions, + &poly_distances, + &poly_colors, + line_style.width, + line_style_u32, + style_param, + ); + poly_positions.clear(); + poly_distances.clear(); + poly_colors.clear(); cumulative_distance = 0.0; - } else if i > 0 { - let prev = &points_slice[i - 1]; - let dx = p.position[0] - prev.position[0]; - let dy = p.position[1] - prev.position[1]; - cumulative_distance += (dx * dx + dy * dy).sqrt() as f32; } - if segment_count == 0 { - segment_first = (writer.len() / 36) as u32; + let render_pos = self.world_to_render_pos(point.position, &state.camera); + let color = *state.point_colors.get(s.start + i).unwrap_or(&s.color); + + if let Some(last_pos) = poly_positions.last() { + let dx = render_pos[0] - last_pos[0]; + let dy = render_pos[1] - last_pos[1]; + let segment_length = (dx * dx + dy * dy).sqrt(); + if segment_length <= f32::EPSILON { + if let Some(last_color) = poly_colors.last_mut() { + *last_color = color; + } + continue; + } + cumulative_distance += segment_length; } - let render_pos = self.world_to_render_pos(p.position, &state.camera); - let color_idx = s.start + i; - let color = state.point_colors.get(color_idx).unwrap_or(&s.color); - writer.write_line_vertex( - render_pos, - color, - line_style_u32, - cumulative_distance, - style_param, - ); - segment_count += 1; + poly_positions.push(render_pos); + poly_distances.push(cumulative_distance); + poly_colors.push(color); } - if segment_count >= 2 { - segs.push(LineSegment { - first_vertex: segment_first, - vertex_count: segment_count, - }); - } + write_polyline_strip( + &mut writer, + &mut segs, + &poly_positions, + &poly_distances, + &poly_colors, + line_style.width, + line_style_u32, + style_param, + ); } if writer.is_empty() { @@ -936,6 +992,7 @@ impl PlotRenderer { let mut writer = VertexWriter::new(); let mut segs: Vec = Vec::new(); + let world_per_px = self.world_per_pixel(&state.camera); // Get visible viewport bounds in world coordinates let cam = &state.camera; @@ -949,31 +1006,30 @@ impl PlotRenderer { let Some(vx_plot) = state.x_axis_scale.data_to_plot(vline.x) else { continue; }; - // Check if vline is within viewport - if vx_plot < left || vx_plot > right { + // Check if the stroked vline still overlaps the viewport. + let half_width = reference_line_half_extent(vline.line_style.width, true, world_per_px); + if vx_plot + (half_width as f64) < left || vx_plot - (half_width as f64) > right { continue; } - let first = (writer.len() / 36) as u32; let (line_style_u32, style_param) = line_style_params(vline.line_style); - - // Create two vertices: bottom and top of viewport - for (idx, y) in [bottom, top].iter().enumerate() { - let render_pos = self.world_to_render_pos([vx_plot, *y], &state.camera); - let distance = if idx == 0 { 0.0 } else { (top - bottom) as f32 }; - writer.write_line_vertex( - render_pos, - &vline.color, - line_style_u32, - distance, - style_param, - ); - } - - segs.push(LineSegment { - first_vertex: first, - vertex_count: 2, - }); + // Create two endpoints spanning the visible vertical extent. + let positions = [ + self.world_to_render_pos([vx_plot, bottom], &state.camera), + self.world_to_render_pos([vx_plot, top], &state.camera), + ]; + let distances = [0.0, (top - bottom) as f32]; + let colors = [vline.color, vline.color]; + write_polyline_strip( + &mut writer, + &mut segs, + &positions, + &distances, + &colors, + vline.line_style.width, + line_style_u32, + style_param, + ); } // Add horizontal lines @@ -981,31 +1037,31 @@ impl PlotRenderer { let Some(hy_plot) = state.y_axis_scale.data_to_plot(hline.y) else { continue; }; - // Check if hline is within viewport - if hy_plot < bottom || hy_plot > top { + // Check if the stroked hline still overlaps the viewport. + let half_width = + reference_line_half_extent(hline.line_style.width, false, world_per_px); + if hy_plot + (half_width as f64) < bottom || hy_plot - (half_width as f64) > top { continue; } - let first = (writer.len() / 36) as u32; let (line_style_u32, style_param) = line_style_params(hline.line_style); - - // Create two vertices: left and right of viewport - for (idx, x) in [left, right].iter().enumerate() { - let render_pos = self.world_to_render_pos([*x, hy_plot], &state.camera); - let distance = if idx == 0 { 0.0 } else { (right - left) as f32 }; - writer.write_line_vertex( - render_pos, - &hline.color, - line_style_u32, - distance, - style_param, - ); - } - - segs.push(LineSegment { - first_vertex: first, - vertex_count: 2, - }); + // Create two endpoints spanning the visible horizontal extent. + let positions = [ + self.world_to_render_pos([left, hy_plot], &state.camera), + self.world_to_render_pos([right, hy_plot], &state.camera), + ]; + let distances = [0.0, (right - left) as f32]; + let colors = [hline.color, hline.color]; + write_polyline_strip( + &mut writer, + &mut segs, + &positions, + &distances, + &colors, + hline.line_style.width, + line_style_u32, + style_param, + ); } if writer.is_empty() { @@ -1095,7 +1151,7 @@ impl PlotRenderer { // but we need to center the mask_box at the marker's center. // Adjust position if marker is world-space (same as shader does) let mut world_pos = [highlight_point.x, highlight_point.y]; - if let crate::MarkerSize::World(size) = marker_style.size { + if let crate::Size::World(size) = marker_style.size { let half_size = size * 0.5; world_pos[0] += half_size; world_pos[1] += half_size; @@ -1419,9 +1475,94 @@ impl PlotRenderer { // Helper to extract line style parameters fn line_style_params(style: LineStyle) -> (u32, f32) { - match style { - LineStyle::Solid => (0u32, 0.0f32), - LineStyle::Dotted { spacing } => (1u32, spacing), - LineStyle::Dashed { length } => (2u32, length), + match style.line_type { + LineType::Solid => (0u32, 0.0f32), + LineType::Dotted { spacing } => (1u32, spacing), + LineType::Dashed { length } => (2u32, length), + } +} + +fn write_polyline_strip( + writer: &mut VertexWriter, + segs: &mut Vec, + positions: &[[f32; 2]], + distances: &[f32], + colors: &[iced::Color], + width: Size, + line_style_u32: u32, + style_param: f32, +) { + if positions.len() < 2 || positions.len() != distances.len() || positions.len() != colors.len() + { + return; + } + + let (width, width_mode) = line_width_params(width); + let first_vertex = (writer.byte_len() / 64) as u32; + for i in 0..positions.len() { + let prev = if i == 0 { + positions[i] + } else { + positions[i - 1] + }; + let next = if i + 1 == positions.len() { + positions[i] + } else { + positions[i + 1] + }; + + writer.write_line_vertex( + positions[i], + prev, + next, + &colors[i], + line_style_u32, + distances[i], + style_param, + width, + width_mode, + 1.0, + ); + writer.write_line_vertex( + positions[i], + prev, + next, + &colors[i], + line_style_u32, + distances[i], + style_param, + width, + width_mode, + -1.0, + ); + } + + segs.push(LineSegment { + first_vertex, + vertex_count: (positions.len() * 2) as u32, + }); +} + +fn reference_line_half_extent(width: Size, vertical: bool, world_per_px: [f32; 2]) -> f32 { + match width { + Size::Pixels(size) => { + let axis_scale = if vertical { + world_per_px[0] + } else { + world_per_px[1] + }; + size.max(0.5) * axis_scale * 0.5 + } + Size::World(size) => size.max(f64::EPSILON) as f32 * 0.5, + } +} + +fn line_width_params(width: Size) -> (f32, u32) { + match width { + Size::Pixels(size) => (size.max(0.5), crate::point::MARKER_SIZE_PIXELS), + Size::World(size) => ( + size.max(f64::EPSILON) as f32, + crate::point::MARKER_SIZE_WORLD, + ), } } diff --git a/src/plot_state.rs b/src/plot_state.rs index ae04dbf..596d62a 100644 --- a/src/plot_state.rs +++ b/src/plot_state.rs @@ -8,8 +8,8 @@ use iced::{ }; use crate::{ - AxisLink, AxisScale, DragEvent, HLine, HoverPickEvent, LineStyle, MarkerSize, PlotWidget, - Point, ShapeId, VLine, + AxisLink, AxisScale, DragEvent, HLine, HoverPickEvent, LineStyle, PlotWidget, Point, ShapeId, + Size, VLine, axis_scale::{data_point_to_plot, plot_point_to_data}, camera::Camera, picking::PickingState, @@ -218,8 +218,8 @@ impl PlotState { // If this series has a world-space marker, the data_max should be adjusted to account for the marker size. if let Some(size) = series.marker_style.as_ref().and_then(|m| match m.size { - MarkerSize::World(size) => Some(size), - MarkerSize::Pixels(_) => None, + Size::World(size) => Some(size), + Size::Pixels(_) => None, }) && let Some(data_max) = &mut data_max { if widget.x_axis_scale == AxisScale::Linear { diff --git a/src/plot_widget.rs b/src/plot_widget.rs index 30d2f05..dbdb214 100644 --- a/src/plot_widget.rs +++ b/src/plot_widget.rs @@ -23,8 +23,8 @@ use iced::{ use indexmap::IndexMap; use crate::{ - AxisScale, DragEvent, Fill, HLine, HoverPickEvent, MarkerSize, MarkerStyle, PlotUiMessage, - PointId, Series, TooltipContext, VLine, axes_labels, + AxisScale, DragEvent, Fill, HLine, HoverPickEvent, MarkerStyle, PlotUiMessage, PointId, Series, + Size, TooltipContext, VLine, axes_labels, axis_link::AxisLink, axis_scale::{data_point_to_plot, plot_point_to_data}, camera::Camera, @@ -314,7 +314,7 @@ impl PlotWidget { /// so we anchor the tooltip at the marker's center (x + size/2, y + size/2). fn tooltip_anchor_world(point: &HighlightPoint) -> [f64; 2] { if let Some(marker_style) = point.marker_style - && let MarkerSize::World(size) = marker_style.size + && let Size::World(size) = marker_style.size { let half = size * 0.5; [point.x + half, point.y + half] @@ -1558,10 +1558,10 @@ impl HighlightPoint { pub fn resize_marker(&mut self, factor: f64) { if let Some(marker_style) = &mut self.marker_style { match &mut marker_style.size { - MarkerSize::Pixels(size) => { + Size::Pixels(size) => { *size *= factor as f32; } - MarkerSize::World(size) => { + Size::World(size) => { *size *= factor; } } diff --git a/src/reference_lines.rs b/src/reference_lines.rs index 9b25755..a39f8fe 100644 --- a/src/reference_lines.rs +++ b/src/reference_lines.rs @@ -1,4 +1,4 @@ -use crate::{Color, LineStyle, series::ShapeId}; +use crate::{Color, LineStyle, LineType, Size, series::ShapeId}; /// A vertical line at a fixed x-coordinate. #[derive(Debug, Clone)] @@ -11,9 +11,7 @@ pub struct VLine { pub label: Option, /// Color of the line. pub color: Color, - /// Line width in pixels. - pub width: f32, - /// Line style (solid, dashed, dotted). + /// Line styling options, including width and pattern (solid, dashed, dotted). pub line_style: LineStyle, } @@ -25,8 +23,7 @@ impl VLine { x, label: None, color: Color::from_rgb(0.5, 0.5, 0.5), - width: 1.0, - line_style: LineStyle::Solid, + line_style: LineStyle::default(), } } @@ -47,13 +44,30 @@ impl VLine { /// Set the line width in pixels. pub fn with_width(mut self, width: f32) -> Self { - self.width = width.max(0.5); + self.line_style.width = Size::Pixels(width.max(0.5)); + self + } + + /// Set the line width in world units. + pub fn with_width_world(mut self, width: f64) -> Self { + self.line_style.width = Size::World(width.max(f64::EPSILON)); self } /// Set the line style. pub fn with_style(mut self, style: LineStyle) -> Self { + let old_width = self.line_style.width; + let preserve_width = style.width == LineStyle::default().width; self.line_style = style; + if preserve_width { + self.line_style.width = old_width; + } + self + } + + /// Set only the line type while preserving the current width. + pub fn with_line_type(mut self, line_type: LineType) -> Self { + self.line_style.line_type = line_type; self } } @@ -69,9 +83,7 @@ pub struct HLine { pub label: Option, /// Color of the line. pub color: Color, - /// Line width in pixels. - pub width: f32, - /// Line style (solid, dashed, dotted). + /// Line styling options, including width and pattern (solid, dashed, dotted). pub line_style: LineStyle, } @@ -83,8 +95,7 @@ impl HLine { y, label: None, color: Color::from_rgb(0.5, 0.5, 0.5), - width: 1.0, - line_style: LineStyle::Solid, + line_style: LineStyle::default(), } } @@ -105,13 +116,30 @@ impl HLine { /// Set the line width in pixels. pub fn with_width(mut self, width: f32) -> Self { - self.width = width.max(0.5); + self.line_style.width = Size::Pixels(width.max(0.5)); + self + } + + /// Set the line width in world units. + pub fn with_width_world(mut self, width: f64) -> Self { + self.line_style.width = Size::World(width.max(f64::EPSILON)); self } /// Set the line style. pub fn with_style(mut self, style: LineStyle) -> Self { + let old_width = self.line_style.width; + let preserve_width = style.width == LineStyle::default().width; self.line_style = style; + if preserve_width { + self.line_style.width = old_width; + } + self + } + + /// Set only the line type while preserving the current width. + pub fn with_line_type(mut self, line_type: LineType) -> Self { + self.line_style.line_type = line_type; self } } diff --git a/src/series.rs b/src/series.rs index 40f9a3b..7803921 100644 --- a/src/series.rs +++ b/src/series.rs @@ -8,7 +8,7 @@ use crate::{Color, camera::Camera, point::MarkerType}; /// /// Determines how points in a series are connected. #[derive(Debug, Clone, Copy, PartialEq)] -pub enum LineStyle { +pub enum LineType { /// Solid continuous line. Solid, /// Dotted line with configurable spacing. @@ -17,41 +17,110 @@ pub enum LineStyle { Dashed { length: f32 }, } +impl Default for LineType { + fn default() -> Self { + Self::Solid + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +/// Line styling options for series lines. +/// +/// Defines how individual lines are rendered. +pub struct LineStyle { + /// Width of the line in pixels or world units. + pub width: Size, + /// Shape of the line. + pub line_type: LineType, +} + +impl Default for LineStyle { + fn default() -> Self { + Self { + width: Size::Pixels(1.0), + line_type: LineType::Solid, + } + } +} + +impl LineStyle { + pub fn new(width: Size, line_type: LineType) -> Self { + Self { width, line_type } + } + + pub fn solid() -> Self { + Self::default() + } + + pub fn dotted(spacing: f32) -> Self { + Self { + line_type: LineType::Dotted { spacing }, + ..Self::default() + } + } + + pub fn dashed(length: f32) -> Self { + Self { + line_type: LineType::Dashed { length }, + ..Self::default() + } + } + + pub fn with_width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + pub fn with_pixel_width(mut self, width: f32) -> Self { + self.width = Size::Pixels(width); + self + } + + pub fn with_world_width(mut self, width: f64) -> Self { + self.width = Size::World(width); + self + } + + pub fn with_line_type(mut self, line_type: LineType) -> Self { + self.line_type = line_type; + self + } +} + #[derive(Debug, Clone, Copy, PartialEq)] -/// Marker size modes. -pub enum MarkerSize { - /// Marker size in logical pixels. The marker will be centered on the data position. +/// Shared size modes for markers and line widths. +pub enum Size { + /// Size in logical pixels. + /// + /// Markers are centered on the data position. Lines use this as a screen-space width. /// /// This is usually the right default. Pixels(f32), - /// Marker size in world units. The marker will be painted at the data position such that - /// the lower-left corner of the marker is at the data position. + /// Size in world units. + /// + /// Markers are painted such that the lower-left corner is at the data position. + /// Lines use this as a width measured directly in plot units. /// /// This is useful for implementing heatmaps and similar applications where markers /// need to paint an area of the plot. World(f64), } -impl From for MarkerSize { +impl From for Size { fn from(size: f32) -> Self { Self::Pixels(size) } } -impl MarkerSize { +impl Size { pub(crate) fn to_raw(self) -> (f32, u32) { match self { Self::Pixels(size) => (size, 0), Self::World(size) => (size as f32, 1), } } - pub(crate) fn marker_size_px( - size: f32, - size_mode: u32, - camera: &Camera, - bounds: &Rectangle, - ) -> f32 { + pub(crate) fn size_px(size: f32, size_mode: u32, camera: &Camera, bounds: &Rectangle) -> f32 { if size_mode != crate::point::MARKER_SIZE_WORLD { return size; } @@ -67,7 +136,7 @@ impl MarkerSize { } pub(crate) fn to_px(self, camera: &Camera, bounds: &Rectangle) -> f32 { let (size, size_mode) = self.to_raw(); - Self::marker_size_px(size, size_mode, camera, bounds) + Self::size_px(size, size_mode, camera, bounds) } } @@ -77,7 +146,7 @@ impl MarkerSize { /// Defines how individual data points are rendered. pub struct MarkerStyle { /// Size of the marker in pixels or world units. - pub size: MarkerSize, + pub size: Size, /// Shape of the marker. pub marker_type: MarkerType, } @@ -85,7 +154,7 @@ pub struct MarkerStyle { impl Default for MarkerStyle { fn default() -> Self { Self { - size: MarkerSize::Pixels(5.0), + size: Size::Pixels(5.0), marker_type: MarkerType::FilledCircle, } } @@ -94,49 +163,49 @@ impl Default for MarkerStyle { impl MarkerStyle { pub fn new(size: f32, marker_type: MarkerType) -> Self { Self { - size: MarkerSize::Pixels(size), + size: Size::Pixels(size), marker_type, } } pub fn new_world(size: f64, marker_type: MarkerType) -> Self { Self { - size: MarkerSize::World(size), + size: Size::World(size), marker_type, } } pub fn circle(size: f32) -> Self { Self { - size: MarkerSize::Pixels(size), + size: Size::Pixels(size), marker_type: MarkerType::FilledCircle, } } pub fn ring(size: f32) -> Self { Self { - size: MarkerSize::Pixels(size), + size: Size::Pixels(size), marker_type: MarkerType::EmptyCircle, } } pub fn square(size: f32) -> Self { Self { - size: MarkerSize::Pixels(size), + size: Size::Pixels(size), marker_type: MarkerType::Square, } } pub fn star(size: f32) -> Self { Self { - size: MarkerSize::Pixels(size), + size: Size::Pixels(size), marker_type: MarkerType::Star, } } pub fn triangle(size: f32) -> Self { Self { - size: MarkerSize::Pixels(size), + size: Size::Pixels(size), marker_type: MarkerType::Triangle, } } @@ -169,7 +238,11 @@ pub enum SeriesError { /// or [Fill](crate::Fill) by `id` field: /// ```rust /// use iced_plot::{Series, VLine, HLine, MarkerStyle, LineStyle}; -/// let series = Series::new(vec![[0.0, 0.0], [1.0, 1.0]], MarkerStyle::circle(5.0), LineStyle::Solid); +/// let series = Series::new( +/// vec![[0.0, 0.0], [1.0, 1.0]], +/// MarkerStyle::circle(5.0), +/// LineStyle::solid(), +/// ); /// let id1 = series.id; /// /// let vline = VLine::new(0.0); @@ -323,19 +396,42 @@ impl Series { self } + /// Set or change the line width for the series. + pub fn line_width(mut self, width: impl Into) -> Self { + let width = width.into(); + self.line_style = Some(self.line_style.unwrap_or_default().with_width(width)); + self + } + + /// Set or change the line width for the series in world units. + pub fn line_width_world(mut self, width: f64) -> Self { + self.line_style = Some(self.line_style.unwrap_or_default().with_world_width(width)); + self + } + + /// Set or change only the line type while preserving width if it already exists. + pub fn line_type(mut self, line_type: LineType) -> Self { + self.line_style = Some( + self.line_style + .unwrap_or_default() + .with_line_type(line_type), + ); + self + } + /// Set solid line style. pub fn line_solid(self) -> Self { - self.line_style(LineStyle::Solid) + self.line_type(LineType::Solid) } /// Set dotted line style with given spacing. pub fn line_dotted(self, spacing: f32) -> Self { - self.line_style(LineStyle::Dotted { spacing }) + self.line_type(LineType::Dotted { spacing }) } /// Set dashed line style with given dash length. pub fn line_dashed(self, length: f32) -> Self { - self.line_style(LineStyle::Dashed { length }) + self.line_type(LineType::Dashed { length }) } pub(super) fn validate(&self) -> Result<(), SeriesError> { diff --git a/src/shaders/line.wgsl b/src/shaders/line.wgsl index 07116b8..704cc83 100644 --- a/src/shaders/line.wgsl +++ b/src/shaders/line.wgsl @@ -1,17 +1,22 @@ struct CameraUniform { view_proj: mat4x4, pixel_to_clip: vec4, // (2/width, 2/height, _, _) - for screen-space sizing - pixel_to_world: vec4, // (world_per_pixel_x, world_per_pixel_y, _, _) - for world-space patterns + pixel_to_world: vec4, // (world_per_pixel_x, world_per_pixel_y, _, _) - for world-space sizing and patterns }; @group(0) @binding(0) var camera: CameraUniform; struct VsIn { @location(0) position: vec2, - @location(1) color: vec4, - @location(2) line_style: u32, // 0=solid, 1=dotted, 2=dashed - @location(3) distance_along_line: f32, // cumulative distance along the line - @location(4) style_param: f32, // spacing for dotted, length for dashed + @location(1) prev_position: vec2, + @location(2) next_position: vec2, + @location(3) color: vec4, + @location(4) line_style: u32, // 0=solid, 1=dotted, 2=dashed + @location(5) distance_along_line: f32, // cumulative distance along the line + @location(6) style_param: f32, // spacing for dotted, length for dashed + @location(7) width: f32, + @location(8) width_mode: u32, + @location(9) side: f32, }; struct VsOut { @@ -22,10 +27,100 @@ struct VsOut { @location(3) style_param: f32, }; +fn safe_normalize(v: vec2) -> vec2 { + let len = length(v); + if len <= 1e-6 { + return vec2(0.0, 0.0); + } + return v / len; +} + +fn perp(v: vec2) -> vec2 { + return vec2(-v.y, v.x); +} + +fn join_offset( + current: vec2, + previous: vec2, + next: vec2, + side: f32, + half_width: f32, +) -> vec2 { + let prev_delta = current - previous; + let next_delta = next - current; + let has_prev = length(prev_delta) > 1e-6; + let has_next = length(next_delta) > 1e-6; + + if has_prev && has_next { + let dir_prev = safe_normalize(prev_delta); + let dir_next = safe_normalize(next_delta); + let normal_prev = perp(dir_prev) * side; + let normal_next = perp(dir_next) * side; + let miter_sum = normal_prev + normal_next; + + if length(miter_sum) > 1e-4 { + let miter = normalize(miter_sum); + let denom = dot(miter, normal_next); + if abs(denom) > 1e-3 { + let miter_length = min(half_width / abs(denom), half_width * 4.0); + return miter * miter_length; + } + } + + return normal_next * half_width; + } + + if has_next { + return perp(safe_normalize(next_delta)) * side * half_width; + } + + if has_prev { + return perp(safe_normalize(prev_delta)) * side * half_width; + } + + return vec2(0.0, 0.0); +} + @vertex fn vs_main(in: VsIn) -> VsOut { var out: VsOut; - out.clip = camera.view_proj * vec4(in.position, 0.0, 1.0); + let current_clip = camera.view_proj * vec4(in.position, 0.0, 1.0); + + if in.width_mode == 0u { + let pixel_to_world = vec2( + max(camera.pixel_to_world.x, 1e-6), + max(camera.pixel_to_world.y, 1e-6), + ); + let current_px = in.position / pixel_to_world; + let previous_px = in.prev_position / pixel_to_world; + let next_px = in.next_position / pixel_to_world; + let offset_px = join_offset( + current_px, + previous_px, + next_px, + in.side, + max(in.width, 0.5) * 0.5, + ); + let offset_ndc = vec2( + offset_px.x * camera.pixel_to_clip.x, + offset_px.y * camera.pixel_to_clip.y, + ); + out.clip = vec4( + current_clip.xy + offset_ndc * current_clip.w, + current_clip.z, + current_clip.w, + ); + } else { + let offset_world = join_offset( + in.position, + in.prev_position, + in.next_position, + in.side, + max(in.width, 1e-6) * 0.5, + ); + out.clip = camera.view_proj * vec4(in.position + offset_world, 0.0, 1.0); + } + out.color = in.color; out.line_style = in.line_style; out.distance_along_line = in.distance_along_line; @@ -36,36 +131,36 @@ fn vs_main(in: VsIn) -> VsOut { @fragment fn fs_main(in: VsOut) -> @location(0) vec4 { var alpha = 1.0; - - // Convert style parameter from logical pixels to world coordinates - // camera.pixel_to_world.x contains the world size of one pixel - let pixel_to_world = camera.pixel_to_world.x; - - if in.line_style == 1u { // Dotted - // Convert logical pixels to world units - let spacing_world = in.style_param * camera.pixel_to_world.x; + + // Convert style parameter from logical pixels to world coordinates. + // camera.pixel_to_world.x contains the world size of one pixel. + let pixel_to_world = max(camera.pixel_to_world.x, 1e-6); + + if in.line_style == 1u { + // Convert logical pixels to world units. + let spacing_world = in.style_param * pixel_to_world; let pattern_length = spacing_world * 2.0; let t = fract(in.distance_along_line / pattern_length); - // Create dots: visible for first half of pattern, invisible for second half + // Create dots: visible for first half of pattern, invisible for second half. if t > 0.5 { alpha = 0.0; } - } else if in.line_style == 2u { // Dashed - // Convert logical pixels to world units - let dash_length_world = in.style_param * camera.pixel_to_world.x; - let gap_length_world = dash_length_world * 0.5; // Gap is half the dash length + } else if in.line_style == 2u { + // Convert logical pixels to world units. + let dash_length_world = in.style_param * pixel_to_world; + let gap_length_world = dash_length_world * 0.5; let pattern_length = dash_length_world + gap_length_world; let t = fract(in.distance_along_line / pattern_length); - // Create dashes: visible for first part of pattern (dash), invisible for gap + // Create dashes: visible for first part of pattern, invisible for the gap. if t > (dash_length_world / pattern_length) { alpha = 0.0; } } // else: solid line (line_style == 0u), alpha remains 1.0 - + if alpha < 0.1 { discard; } - - return vec4(in.color.rgb, alpha); + + return vec4(in.color.rgb, in.color.a * alpha); } From 56277847b60e7a525df3cdab8e4e6cd0f9354bf7 Mon Sep 17 00:00:00 2001 From: junzhuo Date: Wed, 25 Mar 2026 17:09:15 +0800 Subject: [PATCH 2/2] shader-side anti-aliasing --- src/plot_renderer.rs | 175 ++++++++++++++++++++++++++++------------- src/shaders/line.wgsl | 176 +++++++++++++++++------------------------- 2 files changed, 193 insertions(+), 158 deletions(-) diff --git a/src/plot_renderer.rs b/src/plot_renderer.rs index faaa0df..b1843bf 100644 --- a/src/plot_renderer.rs +++ b/src/plot_renderer.rs @@ -25,7 +25,7 @@ struct VertexBuffer { vertex_count: u32, } -/// Helper struct for managing line vertex buffers with strip segments +/// Helper struct for managing line vertex buffers struct LineBuffer { buffer: Buffer, segments: Vec, @@ -138,26 +138,28 @@ impl VertexWriter { fn write_line_vertex( &mut self, - pos: [f32; 2], - prev: [f32; 2], - next: [f32; 2], + start: [f32; 2], + end: [f32; 2], color: &iced::Color, style: u32, - distance: f32, + distance_start: f32, + segment_length_world: f32, param: f32, width: f32, width_mode: u32, + along: f32, side: f32, ) { - self.write_position(pos); - self.write_position(prev); - self.write_position(next); + self.write_position(start); + self.write_position(end); self.write_color(color); self.write_u32(style); - self.write_f32(distance); + self.write_f32(distance_start); + self.write_f32(segment_length_world); self.write_f32(param); self.write_f32(width); self.write_u32(width_mode); + self.write_f32(along); self.write_f32(side); } @@ -505,58 +507,66 @@ impl PlotRenderer { entry_point: Some("vs_main"), compilation_options: PipelineCompilationOptions::default(), buffers: &[VertexBufferLayout { - // vec2 position (8) + vec2 prev_position (8) + vec2 next_position (8) + vec4 color (16) + u32 line_style (4) + f32 distance_along_line (4) + f32 style_param (4) + f32 width (4) + u32 width_mode (4) + f32 side (4) + // vec2 segment_start (8) + vec2 segment_end (8) + vec4 color (16) + // + u32 line_style (4) + f32 distance_start (4) + f32 segment_length_world (4) + // + f32 style_param (4) + f32 width (4) + u32 width_mode (4) + // + f32 along (4) + f32 side (4) array_stride: 64, step_mode: VertexStepMode::Vertex, attributes: &[ VertexAttribute { offset: 0, shader_location: 0, - format: VertexFormat::Float32x2, // position + format: VertexFormat::Float32x2, // segment_start }, VertexAttribute { offset: 8, shader_location: 1, - format: VertexFormat::Float32x2, // prev_position + format: VertexFormat::Float32x2, // segment_end }, VertexAttribute { offset: 16, shader_location: 2, - format: VertexFormat::Float32x2, // next_position + format: VertexFormat::Float32x4, // color }, VertexAttribute { - offset: 24, + offset: 32, shader_location: 3, - format: VertexFormat::Float32x4, // color + format: VertexFormat::Uint32, // line_style }, VertexAttribute { - offset: 40, + offset: 36, shader_location: 4, - format: VertexFormat::Uint32, // line_style + format: VertexFormat::Float32, // distance_start }, VertexAttribute { - offset: 44, + offset: 40, shader_location: 5, - format: VertexFormat::Float32, // distance_along_line + format: VertexFormat::Float32, // segment_length_world }, VertexAttribute { - offset: 48, + offset: 44, shader_location: 6, format: VertexFormat::Float32, // style_param }, VertexAttribute { - offset: 52, + offset: 48, shader_location: 7, format: VertexFormat::Float32, // width }, VertexAttribute { - offset: 56, + offset: 52, shader_location: 8, format: VertexFormat::Uint32, // width_mode }, VertexAttribute { - offset: 60, + offset: 56, shader_location: 9, + format: VertexFormat::Float32, // along + }, + VertexAttribute { + offset: 60, + shader_location: 10, format: VertexFormat::Float32, // side }, ], @@ -573,7 +583,7 @@ impl PlotRenderer { })], }), primitive: PrimitiveState { - topology: PrimitiveTopology::TriangleStrip, + topology: PrimitiveTopology::TriangleList, strip_index_format: None, front_face: FrontFace::Ccw, cull_mode: None, @@ -914,7 +924,7 @@ impl PlotRenderer { .is_some_and(|(curr, prev)| *curr != *prev + 1); if break_segment { - write_polyline_strip( + write_polyline_triangles( &mut writer, &mut segs, &poly_positions, @@ -951,7 +961,7 @@ impl PlotRenderer { poly_colors.push(color); } - write_polyline_strip( + write_polyline_triangles( &mut writer, &mut segs, &poly_positions, @@ -1020,7 +1030,7 @@ impl PlotRenderer { ]; let distances = [0.0, (top - bottom) as f32]; let colors = [vline.color, vline.color]; - write_polyline_strip( + write_polyline_triangles( &mut writer, &mut segs, &positions, @@ -1052,7 +1062,7 @@ impl PlotRenderer { ]; let distances = [0.0, (right - left) as f32]; let colors = [hline.color, hline.color]; - write_polyline_strip( + write_polyline_triangles( &mut writer, &mut segs, &positions, @@ -1482,7 +1492,7 @@ fn line_style_params(style: LineStyle) -> (u32, f32) { } } -fn write_polyline_strip( +fn write_polyline_triangles( writer: &mut VertexWriter, segs: &mut Vec, positions: &[[f32; 2]], @@ -1499,48 +1509,105 @@ fn write_polyline_strip( let (width, width_mode) = line_width_params(width); let first_vertex = (writer.byte_len() / 64) as u32; - for i in 0..positions.len() { - let prev = if i == 0 { - positions[i] - } else { - positions[i - 1] - }; - let next = if i + 1 == positions.len() { - positions[i] - } else { - positions[i + 1] - }; + for index in 0..positions.len() - 1 { + let start = positions[index]; + let end = positions[index + 1]; + let segment_length_world = distances[index + 1] - distances[index]; + if segment_length_world <= f32::EPSILON { + continue; + } + + let start_color = &colors[index]; + let end_color = &colors[index + 1]; + let distance_start = distances[index]; writer.write_line_vertex( - positions[i], - prev, - next, - &colors[i], + start, + end, + start_color, line_style_u32, - distances[i], + distance_start, + segment_length_world, style_param, width, width_mode, + 0.0, 1.0, ); writer.write_line_vertex( - positions[i], - prev, - next, - &colors[i], + start, + end, + start_color, line_style_u32, - distances[i], + distance_start, + segment_length_world, style_param, width, width_mode, + 0.0, + -1.0, + ); + writer.write_line_vertex( + start, + end, + end_color, + line_style_u32, + distance_start, + segment_length_world, + style_param, + width, + width_mode, + 1.0, + 1.0, + ); + writer.write_line_vertex( + start, + end, + start_color, + line_style_u32, + distance_start, + segment_length_world, + style_param, + width, + width_mode, + 0.0, + -1.0, + ); + writer.write_line_vertex( + start, + end, + end_color, + line_style_u32, + distance_start, + segment_length_world, + style_param, + width, + width_mode, + 1.0, + 1.0, + ); + writer.write_line_vertex( + start, + end, + end_color, + line_style_u32, + distance_start, + segment_length_world, + style_param, + width, + width_mode, + 1.0, -1.0, ); } - segs.push(LineSegment { - first_vertex, - vertex_count: (positions.len() * 2) as u32, - }); + let vertex_count = (writer.byte_len() / 64) as u32 - first_vertex; + if vertex_count > 0 { + segs.push(LineSegment { + first_vertex, + vertex_count, + }); + } } fn reference_line_half_extent(width: Size, vertical: bool, world_per_px: [f32; 2]) -> f32 { diff --git a/src/shaders/line.wgsl b/src/shaders/line.wgsl index 704cc83..53c53cf 100644 --- a/src/shaders/line.wgsl +++ b/src/shaders/line.wgsl @@ -7,158 +7,126 @@ struct CameraUniform { var camera: CameraUniform; struct VsIn { - @location(0) position: vec2, - @location(1) prev_position: vec2, - @location(2) next_position: vec2, - @location(3) color: vec4, - @location(4) line_style: u32, // 0=solid, 1=dotted, 2=dashed - @location(5) distance_along_line: f32, // cumulative distance along the line + @location(0) segment_start: vec2, + @location(1) segment_end: vec2, + @location(2) color: vec4, + @location(3) line_style: u32, // 0=solid, 1=dotted, 2=dashed + @location(4) distance_start: f32, // cumulative distance at the segment start + @location(5) segment_length_world: f32, @location(6) style_param: f32, // spacing for dotted, length for dashed @location(7) width: f32, @location(8) width_mode: u32, - @location(9) side: f32, + @location(9) along: f32, // 0=start edge, 1=end edge + @location(10) side: f32, // -1 or +1 }; struct VsOut { @builtin(position) clip: vec4, @location(0) color: vec4, @interpolate(flat) @location(1) line_style: u32, - @location(2) distance_along_line: f32, - @location(3) style_param: f32, + @location(2) distance_start: f32, + @location(3) segment_length_world: f32, + @location(4) style_param: f32, + @location(5) local_x_px: f32, + @location(6) local_y_px: f32, + @location(7) half_width_px: f32, + @location(8) segment_length_px: f32, }; -fn safe_normalize(v: vec2) -> vec2 { - let len = length(v); - if len <= 1e-6 { - return vec2(0.0, 0.0); - } - return v / len; -} +const LINE_AA_RADIUS_PX: f32 = 1.0; +const PIXEL_TO_WORLD_MIN: f32 = 1e-12; fn perp(v: vec2) -> vec2 { return vec2(-v.y, v.x); } -fn join_offset( - current: vec2, - previous: vec2, - next: vec2, - side: f32, - half_width: f32, -) -> vec2 { - let prev_delta = current - previous; - let next_delta = next - current; - let has_prev = length(prev_delta) > 1e-6; - let has_next = length(next_delta) > 1e-6; - - if has_prev && has_next { - let dir_prev = safe_normalize(prev_delta); - let dir_next = safe_normalize(next_delta); - let normal_prev = perp(dir_prev) * side; - let normal_next = perp(dir_next) * side; - let miter_sum = normal_prev + normal_next; - - if length(miter_sum) > 1e-4 { - let miter = normalize(miter_sum); - let denom = dot(miter, normal_next); - if abs(denom) > 1e-3 { - let miter_length = min(half_width / abs(denom), half_width * 4.0); - return miter * miter_length; - } - } - - return normal_next * half_width; - } - - if has_next { - return perp(safe_normalize(next_delta)) * side * half_width; - } - - if has_prev { - return perp(safe_normalize(prev_delta)) * side * half_width; - } - - return vec2(0.0, 0.0); -} - @vertex fn vs_main(in: VsIn) -> VsOut { var out: VsOut; - let current_clip = camera.view_proj * vec4(in.position, 0.0, 1.0); + let pixel_to_world = vec2( + max(camera.pixel_to_world.x, PIXEL_TO_WORLD_MIN), + max(camera.pixel_to_world.y, PIXEL_TO_WORLD_MIN), + ); + let start_px = in.segment_start / pixel_to_world; + let end_px = in.segment_end / pixel_to_world; + let delta_px = end_px - start_px; + let segment_length_px = max(length(delta_px), 1e-6); + let tangent_px = delta_px / segment_length_px; + let normal_px = perp(tangent_px); + + var half_width_px: f32; if in.width_mode == 0u { - let pixel_to_world = vec2( - max(camera.pixel_to_world.x, 1e-6), - max(camera.pixel_to_world.y, 1e-6), - ); - let current_px = in.position / pixel_to_world; - let previous_px = in.prev_position / pixel_to_world; - let next_px = in.next_position / pixel_to_world; - let offset_px = join_offset( - current_px, - previous_px, - next_px, - in.side, - max(in.width, 0.5) * 0.5, - ); - let offset_ndc = vec2( - offset_px.x * camera.pixel_to_clip.x, - offset_px.y * camera.pixel_to_clip.y, - ); - out.clip = vec4( - current_clip.xy + offset_ndc * current_clip.w, - current_clip.z, - current_clip.w, - ); + half_width_px = max(in.width, 0.5) * 0.5; } else { - let offset_world = join_offset( - in.position, - in.prev_position, - in.next_position, - in.side, - max(in.width, 1e-6) * 0.5, - ); - out.clip = camera.view_proj * vec4(in.position + offset_world, 0.0, 1.0); + half_width_px = max(in.width / max(pixel_to_world.x, pixel_to_world.y), 0.5) * 0.5; } - + let outer_half_width_px = half_width_px + LINE_AA_RADIUS_PX; + + let local_x_px = select( + -outer_half_width_px, + segment_length_px + outer_half_width_px, + in.along > 0.5, + ); + let local_y_px = in.side * outer_half_width_px; + + let offset_px = tangent_px * local_x_px + normal_px * local_y_px; + let start_clip = camera.view_proj * vec4(in.segment_start, 0.0, 1.0); + let offset_ndc = vec2( + offset_px.x * camera.pixel_to_clip.x, + offset_px.y * camera.pixel_to_clip.y, + ); + + out.clip = vec4( + start_clip.xy + offset_ndc * start_clip.w, + start_clip.z, + start_clip.w, + ); out.color = in.color; out.line_style = in.line_style; - out.distance_along_line = in.distance_along_line; + out.distance_start = in.distance_start; + out.segment_length_world = in.segment_length_world; out.style_param = in.style_param; + out.local_x_px = local_x_px; + out.local_y_px = local_y_px; + out.half_width_px = half_width_px; + out.segment_length_px = segment_length_px; return out; } @fragment fn fs_main(in: VsOut) -> @location(0) vec4 { - var alpha = 1.0; + let clamped_x_px = clamp(in.local_x_px, 0.0, in.segment_length_px); + let cap_dx_px = in.local_x_px - clamped_x_px; + let distance_to_stroke_px = length(vec2(cap_dx_px, in.local_y_px)); + let edge_alpha = clamp(in.half_width_px + 0.5 - distance_to_stroke_px, 0.0, 1.0); + + let pixel_to_world = max(camera.pixel_to_world.x, PIXEL_TO_WORLD_MIN); + let distance_along_line = in.distance_start + + clamped_x_px * (in.segment_length_world / max(in.segment_length_px, 1e-6)); - // Convert style parameter from logical pixels to world coordinates. - // camera.pixel_to_world.x contains the world size of one pixel. - let pixel_to_world = max(camera.pixel_to_world.x, 1e-6); + var alpha = 1.0; if in.line_style == 1u { - // Convert logical pixels to world units. let spacing_world = in.style_param * pixel_to_world; let pattern_length = spacing_world * 2.0; - let t = fract(in.distance_along_line / pattern_length); - // Create dots: visible for first half of pattern, invisible for second half. + let t = fract(distance_along_line / pattern_length); if t > 0.5 { alpha = 0.0; } } else if in.line_style == 2u { - // Convert logical pixels to world units. let dash_length_world = in.style_param * pixel_to_world; let gap_length_world = dash_length_world * 0.5; let pattern_length = dash_length_world + gap_length_world; - let t = fract(in.distance_along_line / pattern_length); - // Create dashes: visible for first part of pattern, invisible for the gap. + let t = fract(distance_along_line / pattern_length); if t > (dash_length_world / pattern_length) { alpha = 0.0; } } - // else: solid line (line_style == 0u), alpha remains 1.0 - if alpha < 0.1 { + alpha *= edge_alpha; + + if alpha < 1e-3 { discard; }