diff --git a/Cargo.lock b/Cargo.lock index 24affbbd..886adcc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1157,9 +1157,8 @@ checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] name = "ecolor" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30429559efada7bd06098ced68e90f1f745acb9a25b51caf158741493c5b94bb" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" dependencies = [ "bytemuck", "emath", @@ -1168,9 +1167,8 @@ dependencies = [ [[package]] name = "eframe" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a9f6000a8c01ca5b79bb914777c85902a5173d9f6c44278463aa0039bee4ca4" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" dependencies = [ "ahash", "bytemuck", @@ -1206,9 +1204,8 @@ dependencies = [ [[package]] name = "egui" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e70d4e8e8896ba5becb0bc1c8c06d203cb44ff16a93d7a619a5e565500085d" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" dependencies = [ "accesskit", "ahash", @@ -1228,9 +1225,8 @@ dependencies = [ [[package]] name = "egui-wgpu" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a262a88482609aeda48457fa6f76f5b754b2965e88797c22ac8ab2709f397906" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" dependencies = [ "ahash", "bytemuck", @@ -1248,9 +1244,8 @@ dependencies = [ [[package]] name = "egui-winit" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc5ff243c9d41a665caeeb1e2b18258904524eb9f7c6840e00e9055017f0ade" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" dependencies = [ "accesskit_winit", "arboard", @@ -1278,16 +1273,15 @@ dependencies = [ "ahash", "egui", "enum-map", - "itertools", + "itertools 0.15.0", "log", "profiling", ] [[package]] name = "egui_glow" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc088c86612837efb5a79a0cef947edc59db0bf52a2230785836d27005866b58" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" dependencies = [ "bytemuck", "egui", @@ -1299,9 +1293,8 @@ dependencies = [ [[package]] name = "egui_kittest" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0faf48edfa198e2343de2083bd10628a41197c1a17855cd12cec4c0f140dd3b" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" dependencies = [ "dify", "eframe", @@ -1343,9 +1336,8 @@ checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" [[package]] name = "emath" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b4d19daeb47a1e42e9e67fdc8dd003364b4d3e6c6bc4df865a2b5ad2c920e0" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" dependencies = [ "bytemuck", "serde", @@ -1434,9 +1426,8 @@ dependencies = [ [[package]] name = "epaint" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84a7cb8f831a5a43423e4838101ac4a3264910f2f326670c24f4f4b7ee43d9c8" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" dependencies = [ "ahash", "bytemuck", @@ -1460,9 +1451,8 @@ dependencies = [ [[package]] name = "epaint_default_fonts" -version = "0.36.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16e23bfeaa81d590306ae25da24269a0686d240c623013badbda44d3f5fab46" +version = "0.36.1" +source = "git+https://github.com/emilk/egui?branch=emilk%2Fband-shape#45df33a21619dcc3449deec7fb315dbe21bf27c3" [[package]] name = "equivalent" @@ -1751,9 +1741,9 @@ dependencies = [ [[package]] name = "glifo" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed4a1bb24121291d27230c1b1b44e07d6a9b28cefdb32fe1581dfb84e14f940a" +checksum = "282a26c1e23de04bdab3e34a21b6f877a479b96737792516b9b8b8f69b6661be" dependencies = [ "bytemuck", "foldhash", @@ -4161,9 +4151,9 @@ dependencies = [ [[package]] name = "vello_common" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2e9aed918117e8152c9eddfd8362d73c465c23f26c44786aa331707b8a64fa2" +checksum = "bbb2141a2bca6e6d598e471fd4d1d7eed8e020aad6a28187edda07f091a325dd" dependencies = [ "bytemuck", "fearless_simd", @@ -4176,9 +4166,9 @@ dependencies = [ [[package]] name = "vello_cpu" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac7349e1f55f6b801c7c277958df4ea53e7f20f21e8014910ad888b2ecda93ea" +checksum = "ace506ca414548966fcf3c283be94eb1cad8254de488791ee698bf438b0890a9" dependencies = [ "bytemuck", "glifo", diff --git a/Cargo.toml b/Cargo.toml index 660a9426..1b845954 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,10 +71,10 @@ web-sys = "0.3.83" [patch.crates-io] # When we want to use the bleeding edge version of egui and eframe: -# egui = { git = "https://github.com/emilk/egui", branch = "main" } -# egui_kittest = { git = "https://github.com/emilk/egui", branch = "main" } -# eframe = { git = "https://github.com/emilk/egui", branch = "main" } -# emath = { git = "https://github.com/emilk/egui", branch = "main" } +egui = { git = "https://github.com/emilk/egui", branch = "emilk/band-shape" } +egui_kittest = { git = "https://github.com/emilk/egui", branch = "emilk/band-shape" } +eframe = { git = "https://github.com/emilk/egui", branch = "emilk/band-shape" } +emath = { git = "https://github.com/emilk/egui", branch = "emilk/band-shape" } # If you fork https://github.com/emilk/egui you can test with: # egui = { path = "../egui/crates/egui" } diff --git a/egui_plot/src/items/series.rs b/egui_plot/src/items/series.rs index f77ce8ba..af89304e 100644 --- a/egui_plot/src/items/series.rs +++ b/egui_plot/src/items/series.rs @@ -1,14 +1,16 @@ use std::ops::RangeInclusive; use std::sync::Arc; -use egui::Color32; -use egui::Id; use egui::Mesh; use egui::Rgba; use egui::Shape; use egui::Stroke; use egui::Ui; +use egui::epaint::BandPoint; +use egui::epaint::BandShape; use egui::epaint::PathStroke; +use egui::{Color32, Rangef}; +use egui::{Id, epaint::ColorMode}; use emath::Float as _; use emath::NumExt as _; use emath::Pos2; @@ -37,6 +39,7 @@ pub struct Line<'a> { pub(crate) gradient_color: Option Color32 + Send + Sync>>, pub(crate) gradient_fill: bool, pub(crate) style: LineStyle, + pub(crate) allow_band: bool, } impl<'a> Line<'a> { @@ -51,6 +54,7 @@ impl<'a> Line<'a> { gradient_color: None, gradient_fill: false, style: LineStyle::Solid, + allow_band: true, } } @@ -115,6 +119,13 @@ impl<'a> Line<'a> { self } + /// Allow aggregating high-resolution line points into band shapes. + #[inline] + pub fn allow_band(mut self, allow_band: bool) -> Self { + self.allow_band = allow_band; + self + } + /// Name of this plot item. /// /// This name will show up in the plot legend, if legends are turned on. @@ -159,7 +170,7 @@ impl<'a> Line<'a> { } impl PlotItem for Line<'_> { - fn shapes(&self, _ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { + fn shapes(&self, ui: &Ui, transform: &PlotTransform, shapes: &mut Vec) { let Self { base, series, @@ -184,12 +195,8 @@ impl PlotItem for Line<'_> { (*stroke).into() }; - let values_tf: Vec<_> = series - .points() - .iter() - .map(|v| transform.position_from_point(v)) - .collect(); - let n_values = values_tf.len(); + let line_runs = visible_line_runs(series.points(), transform, ui.clip_rect()); + let n_values: usize = line_runs.iter().map(Vec::len).sum(); // Fill the area between the line and a reference line, if required. if n_values < 2 { @@ -216,30 +223,59 @@ impl PlotItem for Line<'_> { let mut mesh = Mesh::default(); let expected_intersections = 20; - mesh.reserve_triangles((n_values - 1) * 2); + mesh.reserve_triangles(n_values.saturating_sub(1) * 2); mesh.reserve_vertices(n_values * 2 + expected_intersections); - for [prev, next] in values_tf.array_windows::<2>() { - let fill_color = fill_color_for_point(*prev); - let i = mesh.vertices.len() as u32; - mesh.colored_vertex(*prev, fill_color); - mesh.colored_vertex(pos2(prev.x, y), fill_color); - if let Some(x) = y_intersection(prev, next, y) { - let point = pos2(x, y); - mesh.colored_vertex(point, fill_color_for_point(point)); - mesh.add_triangle(i, i + 1, i + 2); - mesh.add_triangle(i + 2, i + 3, i + 4); - } else { - mesh.add_triangle(i, i + 1, i + 2); - mesh.add_triangle(i + 1, i + 2, i + 3); + for values_tf in &line_runs { + for [prev, next] in values_tf.array_windows::<2>() { + let fill_color = fill_color_for_point(*prev); + let i = mesh.vertices.len() as u32; + mesh.colored_vertex(*prev, fill_color); + mesh.colored_vertex(pos2(prev.x, y), fill_color); + if let Some(x) = y_intersection(prev, next, y) { + let point = pos2(x, y); + mesh.colored_vertex(point, fill_color_for_point(point)); + mesh.add_triangle(i, i + 1, i + 2); + mesh.add_triangle(i + 2, i + 3, i + 4); + } else { + mesh.add_triangle(i, i + 1, i + 2); + mesh.add_triangle(i + 1, i + 2, i + 3); + } + } + if let Some(&last) = values_tf.last() { + let fill_color = fill_color_for_point(last); + mesh.colored_vertex(last, fill_color); + mesh.colored_vertex(pos2(last.x, y), fill_color); } } - let last = values_tf[n_values - 1]; - let fill_color = fill_color_for_point(last); - mesh.colored_vertex(last, fill_color); - mesh.colored_vertex(pos2(last.x, y), fill_color); - shapes.push(Shape::Mesh(std::sync::Arc::new(mesh))); + if !mesh.vertices.is_empty() { + shapes.push(Shape::Mesh(std::sync::Arc::new(mesh))); + } + } + + for values_tf in line_runs { + if self.allow_band + && self.gradient_color.is_none() + && *style == LineStyle::Solid + && let ColorMode::Solid(stroke_color) = final_stroke.color + && values_tf.array_windows().all(|[previous, next]| previous.x <= next.x) + && let mut band_points = band_points(&values_tf) + && 1 < band_points.len() + && band_points.len() < values_tf.len() + { + for point in &mut band_points { + let center = point.y.center(); + let mut width = point.y.span(); + width += final_stroke.width; + if base.highlight { + width *= 2.0; + } + point.y = Rangef::new(center - 0.5 * width, center + 0.5 * width); + } + shapes.push(BandShape::filled(band_points, stroke_color).into()); + } else { + style.style_line(values_tf, final_stroke.clone(), base.highlight, shapes); + } } - style.style_line(values_tf, final_stroke, base.highlight, shapes); } fn find_closest(&self, point: Pos2, transform: &PlotTransform) -> Option { @@ -301,3 +337,115 @@ impl PlotItem for Line<'_> { self.series.bounds() } } + +fn band_points(values_tf: &[Pos2]) -> Vec { + let mut band_points: Vec = Vec::with_capacity(values_tf.len()); + let bucket_width = 1.0; + + for &point in values_tf { + if let Some(band_point) = band_points.last_mut() + && band_point.is_finite() + && point.is_finite() + && band_point.x <= point.x + && point.x - band_point.x <= bucket_width + { + band_point.y.min = band_point.y.min.min(point.y); + band_point.y.max = band_point.y.max.max(point.y); + } else { + band_points.push(BandPoint::new(point.x, point.y..=point.y)); + } + } + + for bp in band_points.iter_mut().skip(1) { + bp.x += 0.5 * bucket_width; + } + + if let (Some(last_bp), Some(last_pos)) = (band_points.last_mut(), values_tf.last()) { + last_bp.x = last_pos.x; + } + + band_points +} + +fn visible_line_runs(points: &[PlotPoint], transform: &PlotTransform, clip_rect: Rect) -> Vec> { + let clip_min_x = transform.value_from_position(clip_rect.min).x; + let clip_max_x = transform.value_from_position(clip_rect.max).x; + let clip_x_range = clip_min_x.min(clip_max_x)..=clip_min_x.max(clip_max_x); + let mut line_runs = Vec::new(); + let mut line_run = Vec::new(); + + for [previous, next] in points.array_windows::<2>() { + if segment_intersects_x_range(*previous, *next, &clip_x_range) { + let previous_position = transform.position_from_point(previous); + let next_position = transform.position_from_point(next); + if !clip_rect.intersects(Rect::from_two_pos(previous_position, next_position)) { + if !line_run.is_empty() { + line_runs.push(std::mem::take(&mut line_run)); + } + continue; + } + if line_run.last().copied() != Some(previous_position) { + line_run.push(previous_position); + } + line_run.push(next_position); + } else if !line_run.is_empty() { + line_runs.push(std::mem::take(&mut line_run)); + } + } + + if points.len() == 1 { + let point = transform.position_from_point(&points[0]); + if clip_rect.contains(point) { + line_runs.push(vec![point]); + } + } else if !line_run.is_empty() { + line_runs.push(line_run); + } + + line_runs +} + +fn segment_intersects_x_range(previous: PlotPoint, next: PlotPoint, x_range: &RangeInclusive) -> bool { + previous.x.min(next.x) <= *x_range.end() && *x_range.start() <= previous.x.max(next.x) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn culls_segments_outside_the_clip_x_range() { + let x_range = 0.0..=1.0; + + assert!(!segment_intersects_x_range( + PlotPoint::new(-2.0, 0.0), + PlotPoint::new(-1.0, 0.0), + &x_range + )); + assert!(segment_intersects_x_range( + PlotPoint::new(-1.0, 0.0), + PlotPoint::new(0.0, 0.0), + &x_range + )); + } + + #[test] + fn bands_points_more_than_a_pixel_from_the_bucket_start() { + let band_points = band_points(&[pos2(0.0, 2.0), pos2(0.4, -1.0), pos2(1.01, 3.0)]); + + assert_eq!( + band_points, + vec![BandPoint::new(0.0, -1.0..=2.0), BandPoint::new(1.01, 3.0..=3.0)] + ); + } + + #[test] + fn bands_keep_points_more_than_a_pixel_apart() { + let band_points = band_points(&[pos2(0.0, 2.0), pos2(1.01, -1.0)]); + + assert_eq!( + band_points, + vec![BandPoint::new(0.0, 2.0..=2.0), BandPoint::new(1.01, -1.0..=-1.0)] + ); + } +} diff --git a/examples/custom_axes/screenshot_thumb.png b/examples/custom_axes/screenshot_thumb.png index de1c4b97..c2d5c0a8 100644 --- a/examples/custom_axes/screenshot_thumb.png +++ b/examples/custom_axes/screenshot_thumb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d286987c63569937a12ad6767026adc48d6a489d7c13ed1bcc2ba55bc0df2eff -size 9213 +oid sha256:31597cbe0dca753327ffc2ec46758fd861274b7f37df8e8a41dbbc0ee02a8355 +size 9208 diff --git a/examples/custom_plot_manipulation/screenshot.png b/examples/custom_plot_manipulation/screenshot.png index 958f129f..015abfc9 100644 --- a/examples/custom_plot_manipulation/screenshot.png +++ b/examples/custom_plot_manipulation/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:da5e2e20c09109f071f552a304cc6e3ed86e1e22c47831c9a90e2bc91b930393 -size 46900 +oid sha256:35688e85c7f7f40b15ffbd0420ab7e7ee17360c463b361342291e124f95b11aa +size 47391 diff --git a/examples/custom_plot_manipulation/screenshot_thumb.png b/examples/custom_plot_manipulation/screenshot_thumb.png index 3cd96570..611f1327 100644 --- a/examples/custom_plot_manipulation/screenshot_thumb.png +++ b/examples/custom_plot_manipulation/screenshot_thumb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:83f7e94701511a1993855cf3dd7c8156519019533db05844dec124eb7f651256 -size 7710 +oid sha256:c11b887b5b4de468b816050666ad4b09be2deeee6fedba6a5aedd60350e72350 +size 7824 diff --git a/examples/items/screenshot_thumb.png b/examples/items/screenshot_thumb.png index 17ce29bb..ef75f042 100644 --- a/examples/items/screenshot_thumb.png +++ b/examples/items/screenshot_thumb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4f7454b524014cfd3a30ccb5151682a95cee8eca8600446ba66c3afe84b80e1 -size 25605 +oid sha256:8e978a7fb6db2248432ea80343c2bf3e82d5958aad198387cda607a12edf0a66 +size 25603 diff --git a/examples/performance_lines/screenshot.png b/examples/performance_lines/screenshot.png index 5029a516..345f2e3c 100644 --- a/examples/performance_lines/screenshot.png +++ b/examples/performance_lines/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6427f5d3253fef27b3ba90ed6add740239bccee06b91d2d44dbb371923e9c27a -size 283036 +oid sha256:6f5e7b068c27b457a25896fca5ffd5b549068b82119309bab715a09187b2447d +size 305098 diff --git a/examples/performance_lines/screenshot_thumb.png b/examples/performance_lines/screenshot_thumb.png index 61110792..3fc3d68f 100644 --- a/examples/performance_lines/screenshot_thumb.png +++ b/examples/performance_lines/screenshot_thumb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:44edf7c108de20f58f612cc57c0d4c0df9540ccae7f41f039c4e21334503d8eb -size 27107 +oid sha256:e0eac7b70cf9e55f77a0cba369da9b967e07609876a3877be1d5032874e0a686 +size 34077 diff --git a/examples/performance_lines/src/app.rs b/examples/performance_lines/src/app.rs index 4e6095fb..0c680324 100644 --- a/examples/performance_lines/src/app.rs +++ b/examples/performance_lines/src/app.rs @@ -11,6 +11,7 @@ const DEFAULT_VALUES_PER_WALK: usize = 20_000; pub struct PerformanceLinesDemo { values_per_walk: usize, walks: Vec>, + allow_band: bool, } impl Default for PerformanceLinesDemo { @@ -18,6 +19,7 @@ impl Default for PerformanceLinesDemo { Self { values_per_walk: DEFAULT_VALUES_PER_WALK, walks: make_walks(DEFAULT_VALUES_PER_WALK), + allow_band: true, } } } @@ -33,7 +35,7 @@ impl PerformanceLinesDemo { Plot::new("performance_lines_demo") .show(ui, |plot_ui| { for (walk_index, walk) in self.walks.iter().enumerate() { - plot_ui.line(Line::new(format!("Walk {walk_index}"), walk.as_slice())); + plot_ui.line(Line::new(format!("Walk {walk_index}"), walk.as_slice()).allow_band(self.allow_band)); } }) .response @@ -57,6 +59,7 @@ impl PerformanceLinesDemo { } ui.label(format!("FPS: {fps}")); + ui.checkbox(&mut self.allow_band, "Use band shapes"); }); ui.label(format!("{NUM_WALKS} random walks are shown.")); diff --git a/examples/plot_span/screenshot.png b/examples/plot_span/screenshot.png index 987d0a38..53f61f91 100644 --- a/examples/plot_span/screenshot.png +++ b/examples/plot_span/screenshot.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f406b5a41ff211c40a7c85f17aa96283130e7573c3695c887a25db806550152b -size 46251 +oid sha256:b754002974de3a0a384a857690e4c412948d39014155314ff3f84a29596ace8c +size 46694 diff --git a/examples/plot_span/screenshot_thumb.png b/examples/plot_span/screenshot_thumb.png index 90522b7a..ad8ae939 100644 --- a/examples/plot_span/screenshot_thumb.png +++ b/examples/plot_span/screenshot_thumb.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dbe52412adad0d7da557bc7be792034d4ee98fcc59642251130bad9410b7188d -size 7353 +oid sha256:acef248d619c41f5c235df2896f92374f0c589787e7f6421dcbce763a3c0d03e +size 7469