1
0
mirror of https://github.com/emilk/egui.git synced 2026-08-31 13:50:04 -04:00

Improve tessellation quality (#5669)

## Defining what `Rounding` is
This PR defines what `Rounding` means: it is the corner radius of
underlying `RectShape` rectangle. If you use `StrokeKind::Inside`, this
means the rounding is of the outer part of the stroke. Conversely, if
you use `StrokeKind::Outside`, the stroke is outside the rounded
rectangle, so the stroke has an inner radius or `rounding`, and an outer
radius that is larger by `stroke.width`.

This definitions is the same as Figma uses.

## Improving general shape rendering
The rendering of filled shapes (rectangles, circles, paths, bezier) has
been rewritten. Instead of first painting the fill with the stroke on
top, we now paint them as one single mesh with shared vertices at the
border. This has several benefits:

* Less work (faster and with fewer vertices produced)
* No overdraw (nicer rendering of translucent shapes)
* Correct blending of stroke and fill

The logic for rendering thin strokes has also been improved, so that the
width of a stroke of `StrokeKind::Outside` never affects the filled area
(this used to be wrong for thin strokes).

## Improving of rectangle rendering
Rectangles also has specific improvements in how thin rectangles are
painted.
The handling of "Blur width" is also a lot better, and now works for
rectangles with strokes.
There also used to be bugs with specific combinations of corner radius
and stroke width, that are now fixed.

##  But why?
With the new `egui::Scene` we end up with a lot of zoomed out shapes,
with sub-pixel strokes. These need to look good! One thing led to
another, and then I became obsessive 😅

## Tessellation Test
In order to investigate the rendering, I created a Tessellation Test in
the `egui_demo_lib`.

[Try it
here](https://egui-pr-preview.github.io/pr/5669-emilkimprove-tessellator)

![Screenshot 2025-02-04 at 08 45
50](https://github.com/user-attachments/assets/20b47a30-de6a-4ff5-885b-2e2fd6d88321)


![image](https://github.com/user-attachments/assets/e17c50eb-5ae7-48d4-bb0d-4f2165075897)
This commit is contained in:
Emil Ernerfeldt
2025-02-04 11:30:12 +01:00
committed by GitHub
parent 9e1117019a
commit 3c07e01d08
63 changed files with 947 additions and 345 deletions

View File

@@ -98,6 +98,13 @@ impl Mesh {
self.indices.is_empty() && self.vertices.is_empty()
}
/// Iterate over the triangles of this mesh, returning vertex indices.
pub fn triangles(&self) -> impl Iterator<Item = [u32; 3]> + '_ {
self.indices
.chunks_exact(3)
.map(|chunk| [chunk[0], chunk[1], chunk[2]])
}
/// Calculate a bounding rectangle.
pub fn calc_bounds(&self) -> Rect {
let mut bounds = Rect::NOTHING;

View File

@@ -1,5 +1,9 @@
/// How rounded the corners of things should be.
///
/// This specific the _corner radius_ of the underlying geometric shape (e.g. rectangle).
/// If there is a stroke, then the stroke will have an inner and outer corner radius
/// which will depends on its width and [`crate::StrokeKind`].
///
/// The rounding uses `u8` to save space,
/// so the amount of rounding is limited to integers in the range `[0, 255]`.
///
@@ -100,10 +104,23 @@ impl std::ops::Add for Rounding {
#[inline]
fn add(self, rhs: Self) -> Self {
Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
nw: self.nw.saturating_add(rhs.nw),
ne: self.ne.saturating_add(rhs.ne),
sw: self.sw.saturating_add(rhs.sw),
se: self.se.saturating_add(rhs.se),
}
}
}
impl std::ops::Add<u8> for Rounding {
type Output = Self;
#[inline]
fn add(self, rhs: u8) -> Self {
Self {
nw: self.nw.saturating_add(rhs),
ne: self.ne.saturating_add(rhs),
sw: self.sw.saturating_add(rhs),
se: self.se.saturating_add(rhs),
}
}
}
@@ -112,10 +129,10 @@ impl std::ops::AddAssign for Rounding {
#[inline]
fn add_assign(&mut self, rhs: Self) {
*self = Self {
nw: self.nw + rhs.nw,
ne: self.ne + rhs.ne,
sw: self.sw + rhs.sw,
se: self.se + rhs.se,
nw: self.nw.saturating_add(rhs.nw),
ne: self.ne.saturating_add(rhs.ne),
sw: self.sw.saturating_add(rhs.sw),
se: self.se.saturating_add(rhs.se),
};
}
}
@@ -145,6 +162,19 @@ impl std::ops::Sub for Rounding {
}
}
impl std::ops::Sub<u8> for Rounding {
type Output = Self;
#[inline]
fn sub(self, rhs: u8) -> Self {
Self {
nw: self.nw.saturating_sub(rhs),
ne: self.ne.saturating_sub(rhs),
sw: self.sw.saturating_sub(rhs),
se: self.se.saturating_sub(rhs),
}
}
}
impl std::ops::SubAssign for Rounding {
#[inline]
fn sub_assign(&mut self, rhs: Self) {

View File

@@ -8,7 +8,16 @@ use crate::*;
pub struct RectShape {
pub rect: Rect,
/// How rounded the corners are. Use `Rounding::ZERO` for no rounding.
/// How rounded the corners of the rectangle are.
///
/// Use `Rounding::ZERO` for for sharp corners.
///
/// This is the corner radii of the rectangle.
/// If there is a stroke, then the stroke will have an inner and outer corner radius,
/// and those will depend on [`StrokeKind`] and the stroke width.
///
/// For [`StrokeKind::Inside`], the outside of the stroke coincides with the rectangle,
/// so the rounding will in this case specify the outer corner radius.
pub rounding: Rounding,
/// How to fill the rectangle.

View File

@@ -451,8 +451,9 @@ impl Shape {
}
Self::Rect(rect_shape) => {
rect_shape.rect = transform * rect_shape.rect;
rect_shape.stroke.width *= transform.scaling;
rect_shape.rounding *= transform.scaling;
rect_shape.stroke.width *= transform.scaling;
rect_shape.blur_width *= transform.scaling;
}
Self::Text(text_shape) => {
text_shape.pos = transform * text_shape.pos;
@@ -472,17 +473,17 @@ impl Shape {
Self::Mesh(mesh) => {
Arc::make_mut(mesh).transform(transform);
}
Self::QuadraticBezier(bezier_shape) => {
bezier_shape.points[0] = transform * bezier_shape.points[0];
bezier_shape.points[1] = transform * bezier_shape.points[1];
bezier_shape.points[2] = transform * bezier_shape.points[2];
bezier_shape.stroke.width *= transform.scaling;
}
Self::CubicBezier(cubic_curve) => {
for p in &mut cubic_curve.points {
Self::QuadraticBezier(bezier) => {
for p in &mut bezier.points {
*p = transform * *p;
}
cubic_curve.stroke.width *= transform.scaling;
bezier.stroke.width *= transform.scaling;
}
Self::CubicBezier(bezier) => {
for p in &mut bezier.points {
*p = transform * *p;
}
bezier.stroke.width *= transform.scaling;
}
Self::Callback(shape) => {
shape.rect = transform * shape.rect;
@@ -502,7 +503,7 @@ fn points_from_line(
shapes: &mut Vec<Shape>,
) {
let mut position_on_segment = 0.0;
path.windows(2).for_each(|window| {
for window in path.windows(2) {
let (start, end) = (window[0], window[1]);
let vector = end - start;
let segment_length = vector.length();
@@ -512,7 +513,7 @@ fn points_from_line(
position_on_segment += spacing;
}
position_on_segment -= segment_length;
});
}
}
/// Creates dashes from a line.
@@ -529,7 +530,7 @@ fn dashes_from_line(
let mut drawing_dash = false;
let mut step = 0;
let steps = dash_lengths.len();
path.windows(2).for_each(|window| {
for window in path.windows(2) {
let (start, end) = (window[0], window[1]);
let vector = end - start;
let segment_length = vector.length();
@@ -560,5 +561,5 @@ fn dashes_from_line(
}
position_on_segment -= segment_length;
});
}
}

View File

@@ -119,7 +119,13 @@ impl PathStroke {
}
}
#[inline]
pub fn with_kind(self, kind: StrokeKind) -> Self {
Self { kind, ..self }
}
/// Set the stroke to be painted right on the edge of the shape, half inside and half outside.
#[inline]
pub fn middle(self) -> Self {
Self {
kind: StrokeKind::Middle,
@@ -128,6 +134,7 @@ impl PathStroke {
}
/// Set the stroke to be painted entirely outside of the shape
#[inline]
pub fn outside(self) -> Self {
Self {
kind: StrokeKind::Outside,
@@ -136,6 +143,7 @@ impl PathStroke {
}
/// Set the stroke to be painted entirely inside of the shape
#[inline]
pub fn inside(self) -> Self {
Self {
kind: StrokeKind::Inside,

View File

@@ -10,7 +10,7 @@ use emath::{pos2, remap, vec2, GuiRounding as _, NumExt, Pos2, Rect, Rot2, Vec2}
use crate::{
color::ColorMode, emath, stroke::PathStroke, texture_atlas::PreparedDisc, CircleShape,
ClippedPrimitive, ClippedShape, Color32, CubicBezierShape, EllipseShape, Mesh, PathShape,
Primitive, QuadraticBezierShape, RectShape, Rounding, Shape, Stroke, StrokeKind, TextShape,
Primitive, QuadraticBezierShape, RectShape, Roundingf, Shape, Stroke, StrokeKind, TextShape,
TextureId, Vertex, WHITE_UV,
};
@@ -475,6 +475,20 @@ impl Path {
}
}
/// The path is taken to be closed (i.e. returning to the start again).
///
/// Calling this may reverse the vertices in the path if they are wrong winding order.
/// The preferred winding order is clockwise.
pub fn fill_and_stroke(
&mut self,
feathering: f32,
fill: Color32,
stroke: &PathStroke,
out: &mut Mesh,
) {
stroke_and_fill_path(feathering, &mut self.0, PathType::Closed, stroke, fill, out);
}
/// Open-ended.
pub fn stroke_open(&mut self, feathering: f32, stroke: &PathStroke, out: &mut Mesh) {
stroke_path(feathering, &mut self.0, PathType::Open, stroke, out);
@@ -498,12 +512,9 @@ impl Path {
/// The path is taken to be closed (i.e. returning to the start again).
///
/// Calling this may reverse the vertices in the path if they are wrong winding order.
///
/// The preferred winding order is clockwise.
///
/// The stroke colors is used for color-correct feathering.
pub fn fill(&mut self, feathering: f32, color: Color32, stroke: &PathStroke, out: &mut Mesh) {
fill_closed_path(feathering, &mut self.0, color, stroke, out);
pub fn fill(&mut self, feathering: f32, color: Color32, out: &mut Mesh) {
fill_closed_path(feathering, &mut self.0, color, out);
}
/// Like [`Self::fill`] but with texturing.
@@ -523,11 +534,11 @@ impl Path {
pub mod path {
//! Helpers for constructing paths
use crate::Rounding;
use crate::Roundingf;
use emath::{pos2, Pos2, Rect};
/// overwrites existing points
pub fn rounded_rectangle(path: &mut Vec<Pos2>, rect: Rect, rounding: Rounding) {
pub fn rounded_rectangle(path: &mut Vec<Pos2>, rect: Rect, rounding: Roundingf) {
path.clear();
let min = rect.min;
@@ -535,7 +546,7 @@ pub mod path {
let r = clamp_rounding(rounding, rect);
if r == Rounding::ZERO {
if r == Roundingf::ZERO {
path.reserve(4);
path.push(pos2(min.x, min.y)); // left top
path.push(pos2(max.x, min.y)); // right top
@@ -546,8 +557,6 @@ pub mod path {
// Duplicated vertices can happen when one side is all rounding, with no straight edge between.
let eps = f32::EPSILON * rect.size().max_elem();
let r = crate::Roundingf::from(r);
add_circle_quadrant(path, pos2(max.x - r.se, max.y - r.se), r.se, 0.0); // south east
if rect.width() <= r.se + r.sw + eps {
@@ -624,11 +633,11 @@ pub mod path {
}
// Ensures the radius of each corner is within a valid range
fn clamp_rounding(rounding: Rounding, rect: Rect) -> Rounding {
fn clamp_rounding(rounding: Roundingf, rect: Rect) -> Roundingf {
let half_width = rect.width() * 0.5;
let half_height = rect.height() * 0.5;
let max_cr = half_width.min(half_height);
rounding.at_most(max_cr.floor() as _).at_least(0)
rounding.at_most(max_cr).at_least(0.0)
}
}
@@ -753,36 +762,17 @@ fn cw_signed_area(path: &[PathPoint]) -> f64 {
/// Calling this may reverse the vertices in the path if they are wrong winding order.
///
/// The preferred winding order is clockwise.
///
/// A stroke is required so that the fill's feathering can fade to the right color. You can pass `&PathStroke::NONE` if
/// this path won't be stroked.
fn fill_closed_path(
feathering: f32,
path: &mut [PathPoint],
color: Color32,
stroke: &PathStroke,
out: &mut Mesh,
) {
if color == Color32::TRANSPARENT {
fn fill_closed_path(feathering: f32, path: &mut [PathPoint], fill_color: Color32, out: &mut Mesh) {
if fill_color == Color32::TRANSPARENT {
return;
}
// TODO(juancampa): This bounding box is computed twice per shape: once here and another when tessellating the
// stroke, consider hoisting that logic to the tessellator/scratchpad.
let bbox = if matches!(stroke.color, ColorMode::UV(_)) {
Rect::from_points(&path.iter().map(|p| p.pos).collect::<Vec<Pos2>>()).expand(feathering)
} else {
Rect::NAN
};
let stroke_color = &stroke.color;
let get_stroke_color: Box<dyn Fn(Pos2) -> Color32> = match stroke_color {
ColorMode::Solid(col) => Box::new(|_pos: Pos2| *col),
ColorMode::UV(fun) => Box::new(|pos: Pos2| fun(bbox, pos)),
};
let n = path.len() as u32;
if feathering > 0.0 {
if n < 3 {
return;
}
if 0.0 < feathering {
if cw_signed_area(path) < 0.0 {
// Wrong winding order - fix:
path.reverse();
@@ -809,10 +799,9 @@ fn fill_closed_path(
let pos_inner = p1.pos - dm;
let pos_outer = p1.pos + dm;
let color_outer = get_stroke_color(pos_outer);
out.colored_vertex(pos_inner, color);
out.colored_vertex(pos_outer, color_outer);
out.colored_vertex(pos_inner, fill_color);
out.colored_vertex(pos_outer, Color32::TRANSPARENT);
out.add_triangle(idx_inner + i1 * 2, idx_inner + i0 * 2, idx_outer + 2 * i0);
out.add_triangle(idx_outer + i0 * 2, idx_outer + i1 * 2, idx_inner + 2 * i1);
i0 = i1;
@@ -823,7 +812,7 @@ fn fill_closed_path(
out.vertices.extend(path.iter().map(|p| Vertex {
pos: p.pos,
uv: WHITE_UV,
color,
color: fill_color,
}));
for i in 2..n {
out.add_triangle(idx, idx + i - 1, idx + i);
@@ -856,7 +845,7 @@ fn fill_closed_path_with_uv(
}
let n = path.len() as u32;
if feathering > 0.0 {
if 0.0 < feathering {
if cw_signed_area(path) < 0.0 {
// Wrong winding order - fix:
path.reverse();
@@ -914,20 +903,6 @@ fn fill_closed_path_with_uv(
}
}
/// Translate a point along their normals according to the stroke kind.
#[inline(always)]
fn translate_stroke_point(p: &mut PathPoint, stroke: &PathStroke) {
match stroke.kind {
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;
}
}
}
/// Tessellate the given path as a stroke with thickness.
fn stroke_path(
feathering: f32,
@@ -935,55 +910,122 @@ fn stroke_path(
path_type: PathType,
stroke: &PathStroke,
out: &mut Mesh,
) {
let fill = Color32::TRANSPARENT;
stroke_and_fill_path(feathering, path, path_type, stroke, fill, out);
}
/// Tessellate the given path as a stroke with thickness, with optional fill color.
///
/// Calling this may reverse the vertices in the path if they are wrong winding order.
///
/// The preferred winding order is clockwise.
fn stroke_and_fill_path(
feathering: f32,
path: &mut [PathPoint],
path_type: PathType,
stroke: &PathStroke,
color_fill: Color32,
out: &mut Mesh,
) {
let n = path.len() as u32;
if stroke.is_empty() || n < 2 {
if n < 2 {
return;
}
if stroke.width == 0.0 {
// Skip the stroke, just fill.
return fill_closed_path(feathering, path, color_fill, out);
}
if color_fill != Color32::TRANSPARENT && cw_signed_area(path) < 0.0 {
// Wrong winding order - fix:
path.reverse();
for point in &mut *path {
point.normal = -point.normal;
}
}
if stroke.color == ColorMode::TRANSPARENT {
// Skip the stroke, just fill. But subtract the width from the path:
match stroke.kind {
StrokeKind::Inside => {
for point in &mut *path {
point.pos -= stroke.width * point.normal;
}
}
StrokeKind::Middle => {
for point in &mut *path {
point.pos -= 0.5 * stroke.width * point.normal;
}
}
StrokeKind::Outside => {}
}
// Skip the stroke, just fill.
return fill_closed_path(feathering, path, color_fill, out);
}
let idx = out.vertices.len() as u32;
// Translate the points along their normals if the stroke is outside or inside
if stroke.kind != StrokeKind::Middle {
path.iter_mut()
.for_each(|p| translate_stroke_point(p, stroke));
// Move the points so that the stroke is on middle of the path.
match stroke.kind {
StrokeKind::Inside => {
for point in &mut *path {
point.pos -= 0.5 * stroke.width * point.normal;
}
}
StrokeKind::Middle => {
// correct
}
StrokeKind::Outside => {
for point in &mut *path {
point.pos += 0.5 * stroke.width * point.normal;
}
}
}
// Expand the bounding box to include the thickness of the path
let bbox = if matches!(stroke.color, ColorMode::UV(_)) {
let uv_bbox = if matches!(stroke.color, ColorMode::UV(_)) {
Rect::from_points(&path.iter().map(|p| p.pos).collect::<Vec<Pos2>>())
.expand((stroke.width / 2.0) + feathering)
} else {
Rect::NAN
};
let get_color = |col: &ColorMode, pos: Pos2| match col {
ColorMode::Solid(col) => *col,
ColorMode::UV(fun) => fun(bbox, pos),
ColorMode::UV(fun) => fun(uv_bbox, pos),
};
if feathering > 0.0 {
let color_inner = &stroke.color;
if 0.0 < feathering {
let color_outer = Color32::TRANSPARENT;
let color_middle = &stroke.color;
let thin_line = stroke.width <= feathering;
if thin_line {
/*
We paint the line using three edges: outer, inner, outer.
. o i o outer, inner, outer
. |---| feathering (pixel width)
*/
// Fade out as it gets thinner:
if let ColorMode::Solid(col) = color_inner {
let color_inner = mul_color(*col, stroke.width / feathering);
if color_inner == Color32::TRANSPARENT {
return;
// If the stroke is painted smaller than the pixel width (=feathering width),
// then we risk severe aliasing.
// Instead, we paint the stroke as a triangular ridge, two feather-widths wide,
// and lessen the opacity of the middle part instead of making it thinner.
if color_fill != Color32::TRANSPARENT && stroke.width < feathering {
// If this is filled shape, then we need to also compensate so that the
// filled area remains the same as it would have been without the
// artificially wide line.
for point in &mut *path {
point.pos += 0.5 * (feathering - stroke.width) * point.normal;
}
}
let opacity = stroke.width / feathering;
/*
We paint the line using three edges: outer, middle, fill.
. o m i outer, middle, fill
. |---| feathering (pixel width)
*/
out.reserve_triangles(4 * n as usize);
out.reserve_vertices(3 * n as usize);
@@ -994,11 +1036,8 @@ fn stroke_path(
let p = p1.pos;
let n = p1.normal;
out.colored_vertex(p + n * feathering, color_outer);
out.colored_vertex(
p,
mul_color(get_color(color_inner, p), stroke.width / feathering),
);
out.colored_vertex(p - n * feathering, color_outer);
out.colored_vertex(p, mul_color(get_color(color_middle, p), opacity));
out.colored_vertex(p - n * feathering, color_fill);
if connect_with_previous {
out.add_triangle(idx + 3 * i0 + 0, idx + 3 * i0 + 1, idx + 3 * i1 + 0);
@@ -1007,15 +1046,24 @@ fn stroke_path(
out.add_triangle(idx + 3 * i0 + 1, idx + 3 * i0 + 2, idx + 3 * i1 + 1);
out.add_triangle(idx + 3 * i0 + 2, idx + 3 * i1 + 1, idx + 3 * i1 + 2);
}
i0 = i1;
}
if color_fill != Color32::TRANSPARENT {
out.reserve_triangles(n as usize - 2);
let idx_fill = idx + 2;
for i in 2..n {
out.add_triangle(idx_fill + 3 * (i - 1), idx_fill, idx_fill + 3 * i);
}
}
} else {
// thick anti-aliased line
/*
We paint the line using four edges: outer, inner, inner, outer
We paint the line using four edges: outer, middle, middle, fill
. o i p i o outer, inner, point, inner, outer
. o m p m f outer, middle, point, middle, fill
. |---| feathering (pixel width)
. |--------------| width
. |---------| outer_rad
@@ -1038,13 +1086,13 @@ fn stroke_path(
out.colored_vertex(p + n * outer_rad, color_outer);
out.colored_vertex(
p + n * inner_rad,
get_color(color_inner, p + n * inner_rad),
get_color(color_middle, p + n * inner_rad),
);
out.colored_vertex(
p - n * inner_rad,
get_color(color_inner, p - n * inner_rad),
get_color(color_middle, p - n * inner_rad),
);
out.colored_vertex(p - n * outer_rad, color_outer);
out.colored_vertex(p - n * outer_rad, color_fill);
out.add_triangle(idx + 4 * i0 + 0, idx + 4 * i0 + 1, idx + 4 * i1 + 0);
out.add_triangle(idx + 4 * i0 + 1, idx + 4 * i1 + 0, idx + 4 * i1 + 1);
@@ -1057,6 +1105,14 @@ fn stroke_path(
i0 = i1;
}
if color_fill != Color32::TRANSPARENT {
out.reserve_triangles(n as usize - 2);
let idx_fill = idx + 3;
for i in 2..n {
out.add_triangle(idx_fill + 4 * (i - 1), idx_fill, idx_fill + 4 * i);
}
}
}
PathType::Open => {
// Anti-alias the ends by extruding the outer edge and adding
@@ -1084,11 +1140,11 @@ fn stroke_path(
out.colored_vertex(p + n * outer_rad + back_extrude, color_outer);
out.colored_vertex(
p + n * inner_rad,
get_color(color_inner, p + n * inner_rad),
get_color(color_middle, p + n * inner_rad),
);
out.colored_vertex(
p - n * inner_rad,
get_color(color_inner, p - n * inner_rad),
get_color(color_middle, p - n * inner_rad),
);
out.colored_vertex(p - n * outer_rad + back_extrude, color_outer);
@@ -1104,11 +1160,11 @@ fn stroke_path(
out.colored_vertex(p + n * outer_rad, color_outer);
out.colored_vertex(
p + n * inner_rad,
get_color(color_inner, p + n * inner_rad),
get_color(color_middle, p + n * inner_rad),
);
out.colored_vertex(
p - n * inner_rad,
get_color(color_inner, p - n * inner_rad),
get_color(color_middle, p - n * inner_rad),
);
out.colored_vertex(p - n * outer_rad, color_outer);
@@ -1133,11 +1189,11 @@ fn stroke_path(
out.colored_vertex(p + n * outer_rad + back_extrude, color_outer);
out.colored_vertex(
p + n * inner_rad,
get_color(color_inner, p + n * inner_rad),
get_color(color_middle, p + n * inner_rad),
);
out.colored_vertex(
p - n * inner_rad,
get_color(color_inner, p - n * inner_rad),
get_color(color_middle, p - n * inner_rad),
);
out.colored_vertex(p - n * outer_rad + back_extrude, color_outer);
@@ -1183,32 +1239,21 @@ fn stroke_path(
let thin_line = stroke.width <= feathering;
if thin_line {
// Fade out thin lines rather than making them thinner
let opacity = stroke.width / feathering;
let radius = feathering / 2.0;
if let ColorMode::Solid(color) = stroke.color {
let color = mul_color(color, stroke.width / feathering);
if color == Color32::TRANSPARENT {
return;
}
}
for p in path {
for p in path.iter_mut() {
out.colored_vertex(
p.pos + radius * p.normal,
mul_color(
get_color(&stroke.color, p.pos + radius * p.normal),
stroke.width / feathering,
),
mul_color(get_color(&stroke.color, p.pos + radius * p.normal), opacity),
);
out.colored_vertex(
p.pos - radius * p.normal,
mul_color(
get_color(&stroke.color, p.pos - radius * p.normal),
stroke.width / feathering,
),
mul_color(get_color(&stroke.color, p.pos - radius * p.normal), opacity),
);
}
} else {
let radius = stroke.width / 2.0;
for p in path {
for p in path.iter_mut() {
out.colored_vertex(
p.pos + radius * p.normal,
get_color(&stroke.color, p.pos + radius * p.normal),
@@ -1219,6 +1264,18 @@ fn stroke_path(
);
}
}
if color_fill != Color32::TRANSPARENT {
// We Need to create new vertices, because the ones we used for the stroke
// has the wrong color.
// Shrink to ignore the stroke…
for point in &mut *path {
point.pos -= 0.5 * stroke.width * point.normal;
}
// …then fill:
fill_closed_path(feathering, path, color_fill, out);
}
}
}
@@ -1467,9 +1524,7 @@ impl Tessellator {
self.scratchpad_path.clear();
self.scratchpad_path.add_circle(center, radius);
self.scratchpad_path
.fill(self.feathering, fill, &path_stroke, out);
self.scratchpad_path
.stroke_closed(self.feathering, &path_stroke, out);
.fill_and_stroke(self.feathering, fill, &path_stroke, out);
}
/// Tessellate a single [`EllipseShape`] into a [`Mesh`].
@@ -1536,9 +1591,7 @@ impl Tessellator {
self.scratchpad_path.clear();
self.scratchpad_path.add_line_loop(&points);
self.scratchpad_path
.fill(self.feathering, fill, &path_stroke, out);
self.scratchpad_path
.stroke_closed(self.feathering, &path_stroke, out);
.fill_and_stroke(self.feathering, fill, &path_stroke, out);
}
/// Tessellate a single [`Mesh`] into a [`Mesh`].
@@ -1642,27 +1695,24 @@ impl Tessellator {
} = path_shape;
self.scratchpad_path.clear();
if *closed {
self.scratchpad_path.add_line_loop(points);
} else {
self.scratchpad_path.add_open_points(points);
}
if *fill != Color32::TRANSPARENT {
debug_assert!(
closed,
self.scratchpad_path
.fill_and_stroke(self.feathering, *fill, stroke, out);
} else {
debug_assert_eq!(
*fill,
Color32::TRANSPARENT,
"You asked to fill a path that is not closed. That makes no sense."
);
self.scratchpad_path.add_open_points(points);
self.scratchpad_path
.fill(self.feathering, *fill, stroke, out);
.stroke(self.feathering, PathType::Open, stroke, out);
}
let typ = if *closed {
PathType::Closed
} else {
PathType::Open
};
self.scratchpad_path
.stroke(self.feathering, typ, stroke, out);
}
/// Tessellate a single [`Rect`] into a [`Mesh`].
@@ -1679,18 +1729,69 @@ impl Tessellator {
let brush = rect_shape.brush.as_ref();
let RectShape {
mut rect,
mut rounding,
fill,
rounding,
mut fill,
mut stroke,
stroke_kind,
mut stroke_kind,
round_to_pixels,
mut blur_width,
brush: _, // brush is extracted on its own, because it is not Copy
} = *rect_shape;
let mut rounding = Roundingf::from(rounding);
let round_to_pixels = round_to_pixels.unwrap_or(self.options.round_rects_to_pixels);
let pixel_size = 1.0 / self.pixels_per_point;
// Important: round to pixels BEFORE applying stroke_kind
if stroke.width == 0.0 {
stroke.color = Color32::TRANSPARENT;
}
// 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));
rect.max = rect.max.at_most(pos2(1e7, 1e7));
if !stroke.is_empty() {
// Check if the stroke covers the whole rectangle
let rect_with_stroke = match stroke_kind {
StrokeKind::Inside => rect,
StrokeKind::Middle => rect.expand(stroke.width / 2.0),
StrokeKind::Outside => rect.expand(stroke.width),
};
if rect_with_stroke.size().min_elem() <= 2.0 * stroke.width + 0.5 * self.feathering {
// The stroke covers the fill.
// Change this to be a fill-only shape, using the stroke color as the new fill color.
rect = rect_with_stroke;
// We blend so that if the stroke is semi-transparent,
// the fill still shines through.
fill = stroke.color;
stroke = Stroke::NONE;
}
}
if stroke.is_empty() {
// Approximate thin rectangles with line segments.
// This is important so that thin rectangles look good.
if rect.width() <= 2.0 * self.feathering {
return self.tessellate_line_segment(
[rect.center_top(), rect.center_bottom()],
(rect.width(), fill),
out,
);
}
if rect.height() <= 2.0 * self.feathering {
return self.tessellate_line_segment(
[rect.left_center(), rect.right_center()],
(rect.height(), fill),
out,
);
}
}
// Important: round to pixels BEFORE modifying/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.
@@ -1712,7 +1813,9 @@ impl Tessellator {
// 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
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);
@@ -1733,28 +1836,6 @@ impl Tessellator {
}
}
// 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 => {
rect = rect.shrink(stroke.width / 2.0);
}
StrokeKind::Outside => {
// Already good
}
}
// 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));
rect.max = rect.max.at_most(pos2(1e7, 1e7));
let old_feathering = self.feathering;
if self.feathering < blur_width {
@@ -1762,73 +1843,102 @@ impl Tessellator {
// Feathering is usually used to make the edges of a shape softer for anti-aliasing.
// The tessellator can't handle blurring/feathering larger than the smallest side of the rect.
// Thats because the tessellator approximate very thin rectangles as line segments,
// and these line segments don't have rounded corners.
// When the feathering is small (the size of a pixel), this is usually fine,
// but here we have a huge feathering to simulate blur,
// so we need to avoid this optimization in the tessellator,
// which is also why we add this rather big epsilon:
let eps = 0.1;
let eps = 0.1; // avoid numerical problems
blur_width = blur_width
.at_most(rect.size().min_elem() - eps)
.at_most(rect.size().min_elem() - eps - 2.0 * stroke.width)
.at_least(0.0);
rounding += Rounding::from(0.5 * blur_width);
rounding += 0.5 * blur_width;
self.feathering = self.feathering.max(blur_width);
}
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);
}
if !stroke.is_empty() {
self.tessellate_line_segment(line, stroke, out); // back…
self.tessellate_line_segment(line, stroke, out); // …and forth
}
} 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);
}
if !stroke.is_empty() {
self.tessellate_line_segment(line, stroke, out); // back…
self.tessellate_line_segment(line, stroke, out); // …and forth
}
} else {
let path = &mut self.scratchpad_path;
path.clear();
path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding);
path.add_line_loop(&self.scratchpad_points);
let path_stroke = PathStroke::from(stroke).outside();
{
// Modify `rect` so that it represents the OUTER border
// We do this because `path::rounded_rectangle` uses the
// corner radius to pick the fidelity/resolution of the corner.
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);
let original_rounding = rounding;
match stroke_kind {
StrokeKind::Inside => {}
StrokeKind::Middle => {
rect = rect.expand(stroke.width / 2.0);
rounding += stroke.width / 2.0;
}
StrokeKind::Outside => {
rect = rect.expand(stroke.width);
rounding += stroke.width;
}
}
path.stroke_closed(self.feathering, &path_stroke, out);
stroke_kind = StrokeKind::Inside;
// A small rounding is incompatible with a wide stroke,
// because the small bend will be extruded inwards and cross itself.
// There are two ways to solve this (wile maintaining constant stroke width):
// either we increase the rounding, or we set it to zero.
// We choose the former: if the user asks for _any_ rounding, they should get it.
let min_inside_rounding = 0.1; // Large enough to avoid numerical issues
let min_outside_rounding = stroke.width + min_inside_rounding;
let extra_rounding_tweak = 0.4; // Otherwise is doesn't _feels_ enough.
if 0.0 < original_rounding.nw {
rounding.nw += extra_rounding_tweak;
rounding.nw = rounding.nw.at_least(min_outside_rounding);
}
if 0.0 < original_rounding.ne {
rounding.ne += extra_rounding_tweak;
rounding.ne = rounding.ne.at_least(min_outside_rounding);
}
if 0.0 < original_rounding.sw {
rounding.sw += extra_rounding_tweak;
rounding.sw = rounding.sw.at_least(min_outside_rounding);
}
if 0.0 < original_rounding.se {
rounding.se += extra_rounding_tweak;
rounding.se = rounding.se.at_least(min_outside_rounding);
}
}
let path = &mut self.scratchpad_path;
path.clear();
path::rounded_rectangle(&mut self.scratchpad_points, rect, rounding);
path.add_line_loop(&self.scratchpad_points);
let path_stroke = PathStroke::from(stroke).with_kind(stroke_kind);
if let Some(brush) = brush {
// Textured fill
let fill_rect = match stroke_kind {
StrokeKind::Inside => rect.shrink(stroke.width),
StrokeKind::Middle => rect.shrink(stroke.width / 2.0),
StrokeKind::Outside => rect,
};
if fill_rect.is_positive() {
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);
}
if !stroke.is_empty() {
path.stroke_closed(self.feathering, &path_stroke, out);
}
} else {
// Stroke and maybe fill
path.fill_and_stroke(self.feathering, fill, &path_stroke, out);
}
self.feathering = old_feathering; // restore
@@ -2029,24 +2139,21 @@ impl Tessellator {
self.scratchpad_path.clear();
if closed {
self.scratchpad_path.add_line_loop(points);
} else {
self.scratchpad_path.add_open_points(points);
}
if fill != Color32::TRANSPARENT {
debug_assert!(
closed,
"You asked to fill a path that is not closed. That makes no sense."
);
self.scratchpad_path
.fill(self.feathering, fill, stroke, out);
}
let typ = if closed {
PathType::Closed
.fill_and_stroke(self.feathering, fill, stroke, out);
} else {
PathType::Open
};
self.scratchpad_path
.stroke(self.feathering, typ, stroke, out);
debug_assert_eq!(
fill,
Color32::TRANSPARENT,
"You asked to fill a bezier path that is not closed. That makes no sense."
);
self.scratchpad_path.add_open_points(points);
self.scratchpad_path
.stroke(self.feathering, PathType::Open, stroke, out);
}
}
}