From 07a953441fd8d64b16658f8ef24d6dfbb563e669 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 20:22:10 +0100 Subject: [PATCH 1/3] Refactor: create `Stroke::round_center_to_pixel` --- crates/epaint/src/stroke.rs | 60 ++++++++++++++++++++++++++++++++ crates/epaint/src/tessellator.rs | 56 ++--------------------------- 2 files changed, 63 insertions(+), 53 deletions(-) diff --git a/crates/epaint/src/stroke.rs b/crates/epaint/src/stroke.rs index 5d82c1963db..50f4f678b96 100644 --- a/crates/epaint/src/stroke.rs +++ b/crates/epaint/src/stroke.rs @@ -2,6 +2,8 @@ use std::{fmt::Debug, sync::Arc}; +use emath::GuiRounding as _; + use super::{emath, Color32, ColorMode, Pos2, Rect}; /// Describes the width and color of a line. @@ -34,6 +36,46 @@ impl Stroke { pub fn is_empty(&self) -> bool { self.width <= 0.0 || self.color == Color32::TRANSPARENT } + + /// For vertical or horizontal lines: + /// round the stroke center to produce a sharp, pixel-aligned line. + pub fn round_center_to_pixel(&self, pixels_per_point: f32, coord: &mut f32) { + // If the stroke is an odd number of pixels wide, + // we want to round the center of it to the center of a pixel. + // + // If however it is an even number of pixels wide, + // we want to round the center to be between two pixels. + // + // We also want to treat strokes that are _almost_ odd as it it was odd, + // to make it symmetric. Same for strokes that are _almost_ even. + // + // For strokes less than a pixel wide we also round to the center, + // because it will rendered as a single row of pixels by the tessellator. + + let pixel_size = 1.0 / pixels_per_point; + + if self.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * self.width) { + *coord = coord.round_to_pixel_center(pixels_per_point); + } else { + *coord = coord.round_to_pixels(pixels_per_point); + } + } + + pub(crate) fn round_rect_to_pixel(&self, pixels_per_point: f32, rect: &mut Rect) { + // We put odd-width strokes in the center of pixels. + // To understand why, see `fn round_center_to_pixel`. + + let pixel_size = 1.0 / pixels_per_point; + + let width = self.width; + if width <= 0.0 { + *rect = rect.round_to_pixels(pixels_per_point); + } else if width <= pixel_size || is_nearest_integer_odd(pixels_per_point * width) { + *rect = rect.round_to_pixel_center(pixels_per_point); + } else { + *rect = rect.round_to_pixels(pixels_per_point); + } + } } impl From<(f32, Color)> for Stroke @@ -182,3 +224,21 @@ impl From for PathStroke { } } } + +/// Returns true if the nearest integer is odd. +fn is_nearest_integer_odd(x: f32) -> bool { + (x * 0.5 + 0.25).fract() > 0.5 +} + +#[test] +fn test_is_nearest_integer_odd() { + assert!(is_nearest_integer_odd(0.6)); + assert!(is_nearest_integer_odd(1.0)); + assert!(is_nearest_integer_odd(1.4)); + assert!(!is_nearest_integer_odd(1.6)); + assert!(!is_nearest_integer_odd(2.0)); + assert!(!is_nearest_integer_odd(2.4)); + assert!(is_nearest_integer_odd(2.6)); + assert!(is_nearest_integer_odd(3.0)); + assert!(is_nearest_integer_odd(3.4)); +} diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index bcb13a12c28..a7d74996035 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1656,7 +1656,7 @@ impl Tessellator { if a.x == b.x { // Vertical line let mut x = a.x; - round_line_segment(&mut x, &stroke, self.pixels_per_point); + stroke.round_coord_to_pixel(self.pixels_per_point, &mut x); a.x = x; b.x = x; @@ -1677,7 +1677,7 @@ impl Tessellator { if a.y == b.y { // Horizontal line let mut y = a.y; - round_line_segment(&mut y, &stroke, self.pixels_per_point); + stroke.round_coord_to_pixel(self.pixels_per_point, &mut y); a.y = y; b.y = y; @@ -1778,7 +1778,6 @@ impl Tessellator { let mut corner_radius = CornerRadiusF32::from(corner_radius); let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels); - let pixel_size = 1.0 / self.pixels_per_point; if stroke.width == 0.0 { stroke.color = Color32::TRANSPARENT; @@ -1849,17 +1848,7 @@ impl Tessellator { } StrokeKind::Middle => { // On this path we optimize for crisp and symmetric strokes. - // We put odd-width strokes in the center of pixels. - // To understand why, see `fn round_line_segment`. - if stroke.width <= 0.0 { - rect = rect.round_to_pixels(self.pixels_per_point); - } else if stroke.width <= pixel_size - || is_nearest_integer_odd(self.pixels_per_point * stroke.width) - { - rect = rect.round_to_pixel_center(self.pixels_per_point); - } else { - rect = rect.round_to_pixels(self.pixels_per_point); - } + stroke.round_rect_to_pixel(self.pixels_per_point, &mut rect); } StrokeKind::Outside => { // Put the inside of the stroke on a pixel boundary. @@ -2203,45 +2192,6 @@ impl Tessellator { } } -fn round_line_segment(coord: &mut f32, stroke: &Stroke, pixels_per_point: f32) { - // If the stroke is an odd number of pixels wide, - // we want to round the center of it to the center of a pixel. - // - // If however it is an even number of pixels wide, - // we want to round the center to be between two pixels. - // - // We also want to treat strokes that are _almost_ odd as it it was odd, - // to make it symmetric. Same for strokes that are _almost_ even. - // - // For strokes less than a pixel wide we also round to the center, - // because it will rendered as a single row of pixels by the tessellator. - - let pixel_size = 1.0 / pixels_per_point; - - if stroke.width <= pixel_size || is_nearest_integer_odd(pixels_per_point * stroke.width) { - *coord = coord.round_to_pixel_center(pixels_per_point); - } else { - *coord = coord.round_to_pixels(pixels_per_point); - } -} - -fn is_nearest_integer_odd(width: f32) -> bool { - (width * 0.5 + 0.25).fract() > 0.5 -} - -#[test] -fn test_is_nearest_integer_odd() { - assert!(is_nearest_integer_odd(0.6)); - assert!(is_nearest_integer_odd(1.0)); - assert!(is_nearest_integer_odd(1.4)); - assert!(!is_nearest_integer_odd(1.6)); - assert!(!is_nearest_integer_odd(2.0)); - assert!(!is_nearest_integer_odd(2.4)); - assert!(is_nearest_integer_odd(2.6)); - assert!(is_nearest_integer_odd(3.0)); - assert!(is_nearest_integer_odd(3.4)); -} - #[deprecated = "Use `Tessellator::new(…).tessellate_shapes(…)` instead"] pub fn tessellate_shapes( pixels_per_point: f32, From 3b9dc0ddc97d1736596bdb5f17307b0e1485decc Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 20:24:33 +0100 Subject: [PATCH 2/3] Pre-existing bug fix: round strikethrough and underline to pixel coord --- crates/epaint/src/tessellator.rs | 4 ++-- crates/epaint/src/text/text_layout.rs | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/epaint/src/tessellator.rs b/crates/epaint/src/tessellator.rs index a7d74996035..914b1cc3498 100644 --- a/crates/epaint/src/tessellator.rs +++ b/crates/epaint/src/tessellator.rs @@ -1656,7 +1656,7 @@ impl Tessellator { if a.x == b.x { // Vertical line let mut x = a.x; - stroke.round_coord_to_pixel(self.pixels_per_point, &mut x); + stroke.round_center_to_pixel(self.pixels_per_point, &mut x); a.x = x; b.x = x; @@ -1677,7 +1677,7 @@ impl Tessellator { if a.y == b.y { // Horizontal line let mut y = a.y; - stroke.round_coord_to_pixel(self.pixels_per_point, &mut y); + stroke.round_center_to_pixel(self.pixels_per_point, &mut y); a.y = y; b.y = y; diff --git a/crates/epaint/src/text/text_layout.rs b/crates/epaint/src/text/text_layout.rs index 638b7a705da..eb1adf5fb34 100644 --- a/crates/epaint/src/text/text_layout.rs +++ b/crates/epaint/src/text/text_layout.rs @@ -866,7 +866,8 @@ fn add_row_hline( let mut last_right_x = f32::NAN; for glyph in &row.glyphs { - let (stroke, y) = stroke_and_y(glyph); + let (stroke, mut y) = stroke_and_y(glyph); + stroke.round_center_to_pixel(point_scale.pixels_per_point, &mut y); if stroke == Stroke::NONE { end_line(line_start.take(), last_right_x); From 864e82843eb8deab41789abde9299fd22d60a902 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 28 Mar 2025 20:26:34 +0100 Subject: [PATCH 3/3] Update screenshot test --- crates/egui_demo_app/tests/snapshots/easymarkeditor.png | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png index f88cb379171..7666b658ea2 100644 --- a/crates/egui_demo_app/tests/snapshots/easymarkeditor.png +++ b/crates/egui_demo_app/tests/snapshots/easymarkeditor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fb4ac08fb40dd1413feee549ba977906160c82d0aba427d6d79d2e56080aa04e -size 178975 +oid sha256:3e6a383dca7e91d07df4bf501e2de13d046f04546a08d026efe3f82fc96b6e29 +size 178887