Be smarter when rounding rectangles to the pixel grid (#5656)
This commit is contained in:
parent
4b9da5f650
commit
50294b5d9f
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:93f2bf32b0607b56a9eebefdc07b55d66988251eeb0ccf7799c2836281d5d5fb
|
||||
size 35573
|
||||
oid sha256:9fee66bab841602862c301a7df3add365ea93fde0ac9b71ca7f9b74a709a68d2
|
||||
size 35576
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ pub struct RectShape {
|
|||
pub stroke: Stroke,
|
||||
|
||||
/// Is the stroke on the inside, outside, or centered on the rectangle?
|
||||
///
|
||||
/// If you want to perfectly tile rectangles, use [`StrokeKind::Inside`].
|
||||
pub stroke_kind: StrokeKind,
|
||||
|
||||
/// Snap the rectangle to pixels?
|
||||
///
|
||||
/// Rounding produces sharper rectangles.
|
||||
/// It is the outside of the fill (=inside of the stroke)
|
||||
/// that will be rounded to the physical pixel grid.
|
||||
///
|
||||
/// If `None`, [`crate::TessellationOptions::round_rects_to_pixels`] will be used.
|
||||
pub round_to_pixels: Option<bool>,
|
||||
|
|
@ -117,8 +117,6 @@ impl RectShape {
|
|||
/// Snap the rectangle to pixels?
|
||||
///
|
||||
/// Rounding produces sharper rectangles.
|
||||
/// It is the outside of the fill (=inside of the stroke)
|
||||
/// that will be rounded to the physical pixel grid.
|
||||
///
|
||||
/// If `None`, [`crate::TessellationOptions::round_rects_to_pixels`] will be used.
|
||||
#[inline]
|
||||
|
|
|
|||
|
|
@ -8,14 +8,12 @@
|
|||
use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2};
|
||||
|
||||
use crate::{
|
||||
color, emath, stroke, texture_atlas::PreparedDisc, CircleShape, ClippedPrimitive, ClippedShape,
|
||||
Color32, CubicBezierShape, EllipseShape, Mesh, PathShape, Primitive, QuadraticBezierShape,
|
||||
RectShape, Rounding, Shape, Stroke, StrokeKind, TextShape, TextureId, Vertex, WHITE_UV,
|
||||
color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape,
|
||||
ClippedPrimitive, ClippedShape, Color32, CubicBezierShape, EllipseShape, Mesh, PathShape,
|
||||
Primitive, QuadraticBezierShape, RectShape, Rounding, Shape, Stroke, StrokeKind, TextShape,
|
||||
TextureId, Vertex, WHITE_UV,
|
||||
};
|
||||
|
||||
use self::color::ColorMode;
|
||||
use self::stroke::PathStroke;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
#[allow(clippy::approx_constant)]
|
||||
|
|
@ -920,13 +918,13 @@ fn fill_closed_path_with_uv(
|
|||
#[inline(always)]
|
||||
fn translate_stroke_point(p: &mut PathPoint, stroke: &PathStroke) {
|
||||
match stroke.kind {
|
||||
stroke::StrokeKind::Middle => { /* Nothing to do */ }
|
||||
stroke::StrokeKind::Outside => {
|
||||
p.pos += p.normal * stroke.width * 0.5;
|
||||
}
|
||||
stroke::StrokeKind::Inside => {
|
||||
StrokeKind::Inside => {
|
||||
p.pos -= p.normal * stroke.width * 0.5;
|
||||
}
|
||||
StrokeKind::Middle => { /* Nothing to do */ }
|
||||
StrokeKind::Outside => {
|
||||
p.pos += p.normal * stroke.width * 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -947,7 +945,7 @@ fn stroke_path(
|
|||
let idx = out.vertices.len() as u32;
|
||||
|
||||
// Translate the points along their normals if the stroke is outside or inside
|
||||
if stroke.kind != stroke::StrokeKind::Middle {
|
||||
if stroke.kind != StrokeKind::Middle {
|
||||
path.iter_mut()
|
||||
.for_each(|p| translate_stroke_point(p, stroke));
|
||||
}
|
||||
|
|
@ -1672,12 +1670,18 @@ impl Tessellator {
|
|||
/// * `rect`: the rectangle to tessellate.
|
||||
/// * `out`: triangles are appended to this.
|
||||
pub fn tessellate_rect(&mut self, rect_shape: &RectShape, out: &mut Mesh) {
|
||||
if self.options.coarse_tessellation_culling
|
||||
&& !rect_shape.visual_bounding_rect().intersects(self.clip_rect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let brush = rect_shape.brush.as_ref();
|
||||
let RectShape {
|
||||
mut rect,
|
||||
mut rounding,
|
||||
fill,
|
||||
stroke,
|
||||
mut stroke,
|
||||
stroke_kind,
|
||||
round_to_pixels,
|
||||
mut blur_width,
|
||||
|
|
@ -1686,9 +1690,56 @@ impl Tessellator {
|
|||
|
||||
let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels);
|
||||
|
||||
// Modify `rect` so that it represents the filled region, with the stroke on the outside:
|
||||
// Important: round to pixels BEFORE applying stroke_kind
|
||||
if round_to_pixels {
|
||||
// The rounding is aware of the stroke kind.
|
||||
// It is designed to be clever in trying to divine the intentions of the user.
|
||||
match stroke_kind {
|
||||
StrokeKind::Inside => {
|
||||
// The stroke is inside the rect, so the rect defines the _outside_ of the stroke.
|
||||
// We round the outside of the stroke on a pixel boundary.
|
||||
// This will make the outside of the stroke crisp.
|
||||
//
|
||||
// Will make each stroke asymmetric if not an even multiple of physical pixels,
|
||||
// but the left stroke will always be the mirror image of the right stroke,
|
||||
// and the top stroke will always be the mirror image of the bottom stroke.
|
||||
//
|
||||
// This is so that a user can tile rectangles with `StrokeKind::Inside`,
|
||||
// and get no pixel overlap between them.
|
||||
rect = rect.round_to_pixels(self.pixels_per_point);
|
||||
}
|
||||
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 <= self.feathering
|
||||
|| 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);
|
||||
}
|
||||
}
|
||||
StrokeKind::Outside => {
|
||||
// Put the inside of the stroke on a pixel boundary.
|
||||
// Makes the inside of the stroke and the filled rect crisp,
|
||||
// but the outside of the stroke may become feathered (blurry).
|
||||
//
|
||||
// Will make each stroke asymmetric if not an even multiple of physical pixels,
|
||||
// but the left stroke will always be the mirror image of the right stroke,
|
||||
// and the top stroke will always be the mirror image of the bottom stroke.
|
||||
rect = rect.round_to_pixels(self.pixels_per_point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Modify `rect` so that it represents the filled region, with the stroke on the outside.
|
||||
// Important: do this AFTER rounding to pixels
|
||||
match stroke_kind {
|
||||
StrokeKind::Inside => {
|
||||
// Shrink the stroke so it fits inside the rect:
|
||||
stroke.width = stroke.width.at_most(rect.size().min_elem() / 2.0);
|
||||
|
||||
rect = rect.shrink(stroke.width);
|
||||
}
|
||||
StrokeKind::Middle => {
|
||||
|
|
@ -1699,28 +1750,6 @@ impl Tessellator {
|
|||
}
|
||||
}
|
||||
|
||||
if self.options.coarse_tessellation_culling
|
||||
&& !rect.expand(stroke.width).intersects(self.clip_rect)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if round_to_pixels {
|
||||
// Since the stroke extends outside of the rectangle,
|
||||
// we can round the rectangle sides to the physical pixel edges,
|
||||
// and the filled rect will appear crisp, as will the inside of the stroke.
|
||||
let Stroke { width, .. } = stroke; // Make sure we remember to update this if we change `stroke` to `PathStroke`
|
||||
if width <= self.feathering && !stroke.is_empty() {
|
||||
// If the stroke is thin, make sure its center is in the center of the pixel:
|
||||
rect = rect
|
||||
.expand(width / 2.0)
|
||||
.round_to_pixel_center(self.pixels_per_point)
|
||||
.shrink(width / 2.0);
|
||||
} else {
|
||||
rect = rect.round_to_pixels(self.pixels_per_point);
|
||||
}
|
||||
}
|
||||
|
||||
// It is common to (sometimes accidentally) create an infinitely sized rectangle.
|
||||
// Make sure we can handle that:
|
||||
rect.min = rect.min.at_least(pos2(-1e7, -1e7));
|
||||
|
|
@ -1751,6 +1780,7 @@ impl Tessellator {
|
|||
|
||||
if rect.width() < 0.5 * self.feathering {
|
||||
// Very thin - approximate by a vertical line-segment:
|
||||
// There is room for improvement here, but it is not critical.
|
||||
let line = [rect.center_top(), rect.center_bottom()];
|
||||
if 0.0 < rect.width() && fill != Color32::TRANSPARENT {
|
||||
self.tessellate_line_segment(line, Stroke::new(rect.width(), fill), out);
|
||||
|
|
@ -1761,6 +1791,7 @@ impl Tessellator {
|
|||
}
|
||||
} else if rect.height() < 0.5 * self.feathering {
|
||||
// Very thin - approximate by a horizontal line-segment:
|
||||
// There is room for improvement here, but it is not critical.
|
||||
let line = [rect.left_center(), rect.right_center()];
|
||||
if 0.0 < rect.height() && fill != Color32::TRANSPARENT {
|
||||
self.tessellate_line_segment(line, Stroke::new(rect.height(), fill), out);
|
||||
|
|
@ -1776,22 +1807,25 @@ impl Tessellator {
|
|||
path.add_line_loop(&self.scratchpad_points);
|
||||
let path_stroke = PathStroke::from(stroke).outside();
|
||||
|
||||
if let Some(brush) = brush {
|
||||
let crate::Brush {
|
||||
fill_texture_id,
|
||||
uv,
|
||||
} = **brush;
|
||||
// Textured
|
||||
let uv_from_pos = |p: Pos2| {
|
||||
pos2(
|
||||
remap(p.x, rect.x_range(), uv.x_range()),
|
||||
remap(p.y, rect.y_range(), uv.y_range()),
|
||||
)
|
||||
};
|
||||
path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out);
|
||||
} else {
|
||||
// Untextured
|
||||
path.fill(self.feathering, fill, &path_stroke, out);
|
||||
if rect.is_positive() {
|
||||
// Fill
|
||||
if let Some(brush) = brush {
|
||||
// Textured
|
||||
let crate::Brush {
|
||||
fill_texture_id,
|
||||
uv,
|
||||
} = **brush;
|
||||
let uv_from_pos = |p: Pos2| {
|
||||
pos2(
|
||||
remap(p.x, rect.x_range(), uv.x_range()),
|
||||
remap(p.y, rect.y_range(), uv.y_range()),
|
||||
)
|
||||
};
|
||||
path.fill_with_uv(self.feathering, fill, fill_texture_id, uv_from_pos, out);
|
||||
} else {
|
||||
// Untextured
|
||||
path.fill(self.feathering, fill, &path_stroke, out);
|
||||
}
|
||||
}
|
||||
|
||||
path.stroke_closed(self.feathering, &path_stroke, out);
|
||||
|
|
|
|||
Loading…
Reference in New Issue