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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 947 additions and 345 deletions

View File

@ -72,6 +72,8 @@ impl Color32 {
pub const BLUE: Self = Self::from_rgb(0, 0, 255);
pub const LIGHT_BLUE: Self = Self::from_rgb(0xAD, 0xD8, 0xE6);
pub const PURPLE: Self = Self::from_rgb(0x80, 0, 0x80);
pub const GOLD: Self = Self::from_rgb(255, 215, 0);
pub const DEBUG_COLOR: Self = Self::from_rgba_premultiplied(0, 200, 0, 128);
@ -233,6 +235,23 @@ impl Color32 {
])
}
/// Multiply with 127 to make color half as opaque, perceptually.
///
/// Fast multiplication in gamma-space.
///
/// This is perceptually even, and faster that [`Self::linear_multiply`].
#[inline]
pub fn gamma_multiply_u8(self, factor: u8) -> Self {
let Self([r, g, b, a]) = self;
let factor = factor as u32;
Self([
((r as u32 * factor + 127) / 255) as u8,
((g as u32 * factor + 127) / 255) as u8,
((b as u32 * factor + 127) / 255) as u8,
((a as u32 * factor + 127) / 255) as u8,
])
}
/// Multiply with 0.5 to make color half as opaque in linear space.
///
/// This is using linear space, which is not perceptually even.
@ -271,6 +290,11 @@ impl Color32 {
fast_round(lerp((self[3] as f32)..=(other[3] as f32), t)),
)
}
/// Blend two colors, so that `self` is behind the argument.
pub fn blend(self, on_top: Self) -> Self {
self.gamma_multiply_u8(255 - on_top.a()) + on_top
}
}
impl std::ops::Mul for Color32 {
@ -287,3 +311,17 @@ impl std::ops::Mul for Color32 {
])
}
}
impl std::ops::Add for Color32 {
type Output = Self;
#[inline]
fn add(self, other: Self) -> Self {
Self([
self[0].saturating_add(other[0]),
self[1].saturating_add(other[1]),
self[2].saturating_add(other[2]),
self[3].saturating_add(other[3]),
])
}
}

View File

@ -115,7 +115,10 @@ pub struct Frame {
#[doc(alias = "border")]
pub stroke: Stroke,
/// The rounding of the corners of [`Self::stroke`] and [`Self::fill`].
/// The rounding of the _outer_ corner of the [`Self::stroke`]
/// (or, if there is no stroke, the outer corner of [`Self::fill`]).
///
/// In other words, this is the corner radius of the _widget rect_.
pub rounding: Rounding,
/// Margin outside the painted frame.
@ -269,7 +272,10 @@ impl Frame {
self
}
/// The rounding of the corners of [`Self::stroke`] and [`Self::fill`].
/// The rounding of the _outer_ corner of the [`Self::stroke`]
/// (or, if there is no stroke, the outer corner of [`Self::fill`]).
///
/// In other words, this is the corner radius of the _widget rect_.
#[inline]
pub fn rounding(mut self, rounding: impl Into<Rounding>) -> Self {
self.rounding = rounding.into();
@ -423,15 +429,14 @@ impl Frame {
shadow,
} = *self;
let fill_rect = self.fill_rect(content_rect);
let widget_rect = self.widget_rect(content_rect);
let frame_shape = Shape::Rect(epaint::RectShape::new(
fill_rect,
widget_rect,
rounding,
fill,
stroke,
epaint::StrokeKind::Outside,
epaint::StrokeKind::Inside,
));
if shadow == Default::default() {

View File

@ -611,7 +611,8 @@ impl Window<'_> {
title_bar.inner_rect.round_to_pixels(ctx.pixels_per_point());
if on_top && area_content_ui.visuals().window_highlight_topmost {
let mut round = window_frame.rounding;
let mut round =
window_frame.rounding - window_frame.stroke.width.round() as u8;
if !is_collapsed {
round.se = 0;

View File

@ -160,7 +160,7 @@ impl Widget for &mut epaint::TessellationOptions {
.on_hover_text("Apply feathering to smooth out the edges of shapes. Turn off for small performance gain.");
if *feathering {
ui.add(crate::DragValue::new(feathering_size_in_pixels).range(0.0..=10.0).speed(0.1).suffix(" px"));
ui.add(crate::DragValue::new(feathering_size_in_pixels).range(0.0..=10.0).speed(0.025).suffix(" px"));
}
});

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2292f0f80bfd3c80055a72eb983549ac2875d36acb333732bd0a67e51b24ae4f
size 102983
oid sha256:57274cec5ee7e5522073249b931ea65ead22752aea1de40666543e765c1b6b85
size 102929

View File

@ -100,6 +100,7 @@ impl Default for DemoGroups {
Box::<super::tests::InputTest>::default(),
Box::<super::tests::LayoutTest>::default(),
Box::<super::tests::ManualLayoutTest>::default(),
Box::<super::tests::TessellationTest>::default(),
Box::<super::tests::WindowResizeTest>::default(),
]),
}

View File

@ -124,9 +124,9 @@ impl View for MiscDemoWindow {
)
.changed()
{
self.checklist
.iter_mut()
.for_each(|checked| *checked = all_checked);
for check in &mut self.checklist {
*check = all_checked;
}
}
for (i, checked) in self.checklist.iter_mut().enumerate() {
ui.checkbox(checked, format!("Item {}", i + 1));

View File

@ -6,6 +6,7 @@ mod input_event_history;
mod input_test;
mod layout_test;
mod manual_layout_test;
mod tessellation_test;
mod window_resize_test;
pub use clipboard_test::ClipboardTest;
@ -16,4 +17,5 @@ pub use input_event_history::InputEventHistory;
pub use input_test::InputTest;
pub use layout_test::LayoutTest;
pub use manual_layout_test::ManualLayoutTest;
pub use tessellation_test::TessellationTest;
pub use window_resize_test::WindowResizeTest;

View File

@ -0,0 +1,364 @@
use egui::{
emath::{GuiRounding, TSTransform},
epaint::{self, RectShape},
vec2, Color32, Pos2, Rect, Sense, StrokeKind, Vec2,
};
#[derive(Clone, Debug, PartialEq)]
pub struct TessellationTest {
shape: RectShape,
magnification_pixel_size: f32,
tessellation_options: epaint::TessellationOptions,
paint_edges: bool,
}
impl Default for TessellationTest {
fn default() -> Self {
let shape = Self::interesting_shapes()[0].1.clone();
Self {
shape,
magnification_pixel_size: 12.0,
tessellation_options: Default::default(),
paint_edges: false,
}
}
}
impl TessellationTest {
fn interesting_shapes() -> Vec<(&'static str, RectShape)> {
fn sized(size: impl Into<Vec2>) -> Rect {
Rect::from_center_size(Pos2::ZERO, size.into())
}
let baby_blue = Color32::from_rgb(0, 181, 255);
let mut shapes = vec![
(
"Normal",
RectShape::new(
sized([20.0, 16.0]),
2.0,
baby_blue,
(1.0, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Minimal rounding",
RectShape::new(
sized([20.0, 16.0]),
1.0,
baby_blue,
(1.0, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Thin filled",
RectShape::filled(sized([20.0, 0.5]), 2.0, baby_blue),
),
(
"Thin stroked",
RectShape::new(
sized([20.0, 0.5]),
2.0,
baby_blue,
(0.5, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Blurred",
RectShape::filled(sized([20.0, 16.0]), 2.0, baby_blue).with_blur_width(50.0),
),
(
"Thick stroke, minimal rounding",
RectShape::new(
sized([20.0, 16.0]),
1.0,
baby_blue,
(3.0, Color32::WHITE),
StrokeKind::Inside,
),
),
(
"Blurred stroke",
RectShape::new(
sized([20.0, 16.0]),
0.0,
baby_blue,
(5.0, Color32::WHITE),
StrokeKind::Inside,
)
.with_blur_width(5.0),
),
];
for (_name, shape) in &mut shapes {
shape.round_to_pixels = Some(true);
}
shapes
}
}
impl crate::Demo for TessellationTest {
fn name(&self) -> &'static str {
"Tessellation Test"
}
fn show(&mut self, ctx: &egui::Context, open: &mut bool) {
egui::Window::new(self.name())
.resizable(false)
.open(open)
.show(ctx, |ui| {
use crate::View as _;
self.ui(ui);
});
}
}
impl crate::View for TessellationTest {
fn ui(&mut self, ui: &mut egui::Ui) {
ui.add(crate::egui_github_link_file!());
egui::reset_button(ui, self, "Reset");
ui.horizontal(|ui| {
ui.group(|ui| {
ui.vertical(|ui| {
rect_shape_ui(ui, &mut self.shape);
});
});
ui.group(|ui| {
ui.vertical(|ui| {
ui.heading("Real size");
egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
let (resp, painter) =
ui.allocate_painter(Vec2::splat(128.0), Sense::hover());
let canvas = resp.rect;
let pixels_per_point = ui.pixels_per_point();
let pixel_size = 1.0 / pixels_per_point;
let mut shape = self.shape.clone();
shape.rect = Rect::from_center_size(canvas.center(), shape.rect.size())
.round_to_pixel_center(pixels_per_point)
.translate(Vec2::new(pixel_size / 3.0, pixel_size / 5.0)); // Intentionally offset to test the effect of rounding
painter.add(shape);
});
});
});
});
ui.group(|ui| {
ui.heading("Zoomed in");
let magnification_pixel_size = &mut self.magnification_pixel_size;
let tessellation_options = &mut self.tessellation_options;
egui::Grid::new("TessellationOptions")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Magnification");
ui.add(
egui::DragValue::new(magnification_pixel_size)
.speed(0.5)
.range(0.0..=64.0),
);
ui.end_row();
ui.label("Feathering width");
ui.horizontal(|ui| {
ui.checkbox(&mut tessellation_options.feathering, "");
ui.add_enabled(
tessellation_options.feathering,
egui::DragValue::new(
&mut tessellation_options.feathering_size_in_pixels,
)
.speed(0.1)
.range(0.0..=4.0)
.suffix(" px"),
);
});
ui.end_row();
ui.label("Paint edges");
ui.checkbox(&mut self.paint_edges, "");
ui.end_row();
});
let magnification_pixel_size = *magnification_pixel_size;
egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
let (resp, painter) = ui.allocate_painter(
magnification_pixel_size * (self.shape.rect.size() + Vec2::splat(8.0)),
Sense::hover(),
);
let canvas = resp.rect;
let mut shape = self.shape.clone();
shape.rect = shape.rect.translate(Vec2::new(1.0 / 3.0, 1.0 / 5.0)); // Intentionally offset to test the effect of rounding
let mut mesh = epaint::Mesh::default();
let mut tessellator = epaint::Tessellator::new(
1.0,
*tessellation_options,
ui.fonts(|f| f.font_image_size()),
vec![],
);
tessellator.tessellate_rect(&shape, &mut mesh);
// Scale and position the mesh:
mesh.transform(
TSTransform::from_translation(canvas.center().to_vec2())
* TSTransform::from_scaling(magnification_pixel_size),
);
let mesh = std::sync::Arc::new(mesh);
painter.add(epaint::Shape::mesh(mesh.clone()));
if self.paint_edges {
let stroke = epaint::Stroke::new(0.5, Color32::MAGENTA);
for triangle in mesh.triangles() {
let a = mesh.vertices[triangle[0] as usize];
let b = mesh.vertices[triangle[1] as usize];
let c = mesh.vertices[triangle[2] as usize];
painter.line_segment([a.pos, b.pos], stroke);
painter.line_segment([b.pos, c.pos], stroke);
painter.line_segment([c.pos, a.pos], stroke);
}
}
// Draw pixel centers:
let pixel_radius = 0.75;
let pixel_color = Color32::GRAY;
for yi in 0.. {
let y = (yi as f32 + 0.5) * magnification_pixel_size;
if y > canvas.height() / 2.0 {
break;
}
for xi in 0.. {
let x = (xi as f32 + 0.5) * magnification_pixel_size;
if x > canvas.width() / 2.0 {
break;
}
for offset in [vec2(x, y), vec2(x, -y), vec2(-x, y), vec2(-x, -y)] {
painter.circle_filled(
canvas.center() + offset,
pixel_radius,
pixel_color,
);
}
}
}
});
});
}
}
fn rect_shape_ui(ui: &mut egui::Ui, shape: &mut RectShape) {
egui::ComboBox::from_id_salt("prefabs")
.selected_text("Prefabs")
.show_ui(ui, |ui| {
for (name, prefab) in TessellationTest::interesting_shapes() {
ui.selectable_value(shape, prefab, name);
}
});
ui.add_space(4.0);
let RectShape {
rect,
rounding,
fill,
stroke,
stroke_kind,
blur_width,
round_to_pixels,
brush: _,
} = shape;
let round_to_pixels = round_to_pixels.get_or_insert(true);
egui::Grid::new("RectShape")
.num_columns(2)
.spacing([12.0, 8.0])
.striped(true)
.show(ui, |ui| {
ui.label("Size");
ui.horizontal(|ui| {
let mut size = rect.size();
ui.add(
egui::DragValue::new(&mut size.x)
.speed(0.2)
.range(0.0..=64.0),
);
ui.add(
egui::DragValue::new(&mut size.y)
.speed(0.2)
.range(0.0..=64.0),
);
*rect = Rect::from_center_size(Pos2::ZERO, size);
});
ui.end_row();
ui.label("Rounding");
ui.add(rounding);
ui.end_row();
ui.label("Fill");
ui.color_edit_button_srgba(fill);
ui.end_row();
ui.label("Stroke");
ui.add(stroke);
ui.end_row();
ui.label("Stroke kind");
ui.horizontal(|ui| {
ui.selectable_value(stroke_kind, StrokeKind::Inside, "Inside");
ui.selectable_value(stroke_kind, StrokeKind::Middle, "Middle");
ui.selectable_value(stroke_kind, StrokeKind::Outside, "Outside");
});
ui.end_row();
ui.label("Blur width");
ui.add(
egui::DragValue::new(blur_width)
.speed(0.5)
.range(0.0..=20.0),
);
ui.end_row();
ui.label("Round to pixels");
ui.checkbox(round_to_pixels, "");
ui.end_row();
});
}
#[cfg(test)]
mod tests {
use crate::View as _;
use super::*;
#[test]
fn snapshot_tessellation_test() {
for (name, shape) in TessellationTest::interesting_shapes() {
let mut test = TessellationTest {
shape,
..Default::default()
};
let mut harness = egui_kittest::Harness::new_ui(|ui| {
test.ui(ui);
});
harness.fit_contents();
harness.run();
harness.snapshot(&format!("tessellation_test/{name}"));
}
}
}

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4cfd5191dc7046a782ef2350dc8e0547d2702182badcb15b6b928ce077b76c1
size 32154
oid sha256:23f19871720b67659a7b56cee8a78edc941c4bac86f55efc6fa549f10c4712fb
size 31754

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4631f841b9e23833505af39eb1c45908013d3b1e1278d477bcedf6a460c71802
size 27163
oid sha256:e4d8fee9fd8e69ecd60ebd0dd41c29b61cc13e7013b1d20ad93d40fc4ed1cc03
size 27091

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96e750ebfcc6ec2c130674407388c30cce844977cde19adfebf351fd08698a4f
size 81726
oid sha256:b291e7efd895ab095590285b841903f05dc7d4dadbab7d9001b04a92953c1694
size 81677

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b03613e597631da3b922ef3d696c4ce74cec41f86a2779fc5b084a316fc9e8e8
size 11764
oid sha256:5d75230689807ae7fb692bac5c9b33ee04c02b9e54963e2d6ada05860157daf6
size 11705

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:76d77e2df39af248d004a023b238bb61ed598ab2bea3e0c6f2885f9537ec1103
size 25988
oid sha256:f512159108c7681834ae2a52c5e1d4eb4dbe678a509f3dab3384e35d6c8dbee2
size 25865

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:003d905893b80ffc132a783155971ad3054229e9d6c046e2760c912559a66b3d
size 20869
oid sha256:f2a4f5e67ff6615877794304e8be5a5db647d48e20e4248259179cddefbf4088
size 20806

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:be0f96c700b7662aab5098f8412dae3676116eeed65e70f6b295dd3375b329d0
size 10968
oid sha256:0bc474a37d6d3a7a08dd41963bf9009c05315de91bb515cc2b19443b79480bff
size 10723

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:99c94a09c0f6984909481189f9a1415ea90bd7c45e42b4b3763ee36f3d831a65
size 133231
oid sha256:0bff769b3f9e46c5e38170885b2c8301294cbcc1c8ee22c17672205edd509924
size 133170

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e4fef5fa8661f207bae2c58381e729cdaf77aecc8b3f178caf262dc310e3a490
size 24206
oid sha256:f90f94a842a1d0f1386c3cdd28e60e6ad6efe968f510a11bde418b5bc70a81d2
size 23897

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7b38828e423195628dcea09b0cbdd66aa4c077334ab7082efd358c7a3297009d
size 17827
oid sha256:fdbdf1159f0ab4579bf9ceefbd8e40ac7af468368ab11ffec8578214b57d867c
size 17758

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c53403996451da14f7991ea61bd20b96dbc67eb67dd2041975dd6ce5015a6980
size 22485
oid sha256:ce647fd9626f126e7f19b4494bb98a2086071f9c45f7dd6ba4708632e7433a7b
size 22418

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:63673b070951246b98ca07613fa81496dbfdd10627bac3c9c4356ebff1a36b20
size 64319
oid sha256:a843e6772809a1c904968fac19b49973675e726a3b7727ca1b898ad3b9072b0c
size 64257

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:374b4095a3c7b827b80d6ab01b945458ae0128a2974c2af8aaf6b7e9fef6b459
size 32554
oid sha256:b437f27c46ddf82e4268d9bb86d33f43c382d1ab3ed45297bc136600cdc9960c
size 32493

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5e756c90069803bb4d2821fff723877bffffd44b26613f5b06c8a042d6901ca4
size 36578
oid sha256:2cd0bda8b4d3d7f833273097c8bb52cfd8ea63a405d6798b9aeb7002b143ac74
size 36459

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6efc59cb9908533baa1a7346b359e9e21c5faf0e373dac6fa7db5476e644233d
size 17678
oid sha256:a57c3bf373a283b79188080e2ddf7407c2974655fc5ad59222442e14faae055c
size 17508

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:eb631bd2608aec6c83f8815b9db0b28627bf82692fd2b1cb793119083b4f8ad1
size 264496
oid sha256:161b5853f528206f2531587753369f2f6f3c52203af668eba0d81a41c2a915e0
size 264432

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9fee66bab841602862c301a7df3add365ea93fde0ac9b71ca7f9b74a709a68d2
size 35576
oid sha256:118413a5ec56c6589914c5cb59bda2884a4d6acec3b34bbc367bc893c3de8127
size 35409

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e0870e9e1c9dc718690124a4d490f1061414f15fa40571e571d9319c3b65e74e
size 23709
oid sha256:361791f30739f841c46433f5d16d281ba9d0c52027b4cf156c3ac293aa795f46
size 23592

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:532dbcbec94bb9c9fb8cc0665646790a459088e98077118b5fbb2112898e1a43
size 183854
oid sha256:81338d7b0412989590bc0808419d93788e972e7ab6f70f481f86bd23263a5395
size 183821

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3fd584643b68084ec4b65369e08d229e2d389551bbefa59c84ad4b58621593f7
size 117754
oid sha256:41c51350c09360738ca284b05c944a11164e4a614beb95ce4cc962222af33d87
size 117764

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7e2b854d99c9b17d15ef92188cdac521d7c0635aef9ba457cd3801884a784009
size 26159
oid sha256:794475478cfe2fc954731742165b1f0419a0e64616e72b3766c1e031dbba7ca1
size 26092

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f0b7dc029de8e669d76f3be5e0800e973c354edcf7cefa239faed07c2cd5e0d5
size 70452
oid sha256:7073aa30f3e7dfd116d9a4f02f6c5a075ce068d2e6f43eaede3c4dd6c56f925e
size 70439

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64ba40810a6480e511e8d198b0dfb39e8b220eb2f5592877e27b17ee8d47b9c3
size 66387
oid sha256:4bb3cd05f253c0e109b83a0556b975af7a96d57678e57de3b9fa130cc8a8de1c
size 66318

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0a8151f5bd01b2cb5183c532d11f57bbb7e8cc1e77a3c4b890537d157922c961
size 21261
oid sha256:5b550769d5ec5d834f89fba56375d10e5f9b3ba703599d90a5b6607aff1c2b06
size 21194

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:96ce36fcc334793801ab724c711f592faf086a9c98c812684e6b76010e9d1974
size 59714
oid sha256:45f343b55be98976de32bd3c5438212a46b787123ea3096cf8fc6bec99493184
size 59699

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8155d93b78692ced67ddee4011526ef627838ede78351871df5feef8aa512857
size 13141
oid sha256:8d25f774320ca844fa4dfba5215ed66f067d0f30c6d7c8ad7ec29d97ee7732e0
size 13073

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf6da022ab97f9d4b862cc8e91bdfd7f9785a3ab0540aa1c2bd2646bd30a3450
size 35115
oid sha256:540a05c5b1de7e362bcb48a04611323fbc65c8787b8ae852ada8aa145803753d
size 34968

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:035b35ed053cabd5022d9a97f7d4d5a79d194c9f14594e7602ecd9ef94c24ae5
size 48053
oid sha256:2276b8221389da4f644cad43ce446cd7d28f41820cea36fa3ea860808710053d
size 47878

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:80a2968e211c83639b60e84b805f1327fb37b87144cada672a403c7e92ace8a8
size 48066
oid sha256:054d606681e08bf61762e5c7d7596c4f483e66ac889795e4be4956103328e0bb
size 47862

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:86df5dc4b4ddd6f44226242b6d9b5e9f2aacd45193ae9f784fb5084a7a509e0b
size 43987
oid sha256:f7fb29285e53b619f333757052d05f86d973680ac1ce6d1b25d28b7824bbce51
size 43725

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1138bbc3b7e73cccd555f0fd58c27a5bda4d84484fdc1bd5223fc9802d0c5328
size 44089
oid sha256:be50a396838d4fe29bcfd0807377ad0cda538fea8569acf709da0b13505bf09b
size 43871

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3562bb87b155b2c406a72a651ffb1991305aa1e15273ce9f32cedc5766318537
size 554922
oid sha256:9c6ce16a8a8c34d882485da6bbe08039fb55f90636da8136f68b1bb9baf0effb
size 557610

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ba6c0bd127ab02375f2ae5fbc3eeef33a1bdf074cbb180de2733c700b81df3e5
size 771069
oid sha256:a49c052578a46adb41bc02c6d7fdc264ed0ab8ae636cc8a11ac729fe1e48091b
size 791802

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d85ab6d04059009fd2c3ad8001332b27e710c46c9300f2f1f409b882c49211dc
size 918967
oid sha256:989c55a83b8bc7cce4f459d8b835962377927a98f2bce085e92cba8438438ecd
size 943736

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a64f1bdec565357fe4dee3acb46b12eeb0492b522fb3bb9697d39dadce2e8c21
size 1039455
oid sha256:01ac61dc5bcecf6bf0d13c8399460b1afae652efe6fecc1d0e4b2f27d9f1c5a4
size 1046906

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ca008dca03372bb334564e55fa2d1d25a36751a43df6001a1c1cf3e4db9bcd4
size 1130930
oid sha256:3348923ecbad34e385e3ed52ab9b7c88b5d4fc07de00620302d5b191d90a453f
size 1140236

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:7f695127e7fe6cb3b752a0acd71db85d51d2de68e45051a7afe91f4d960acf27
size 1311641
oid sha256:179c17b0405c6e87bd3cafaa7272e3e6d6eefd462b4406cca2a7abfe8af6f2bd
size 1317569

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:aa7d25b097911f6b18308bab56d302e3dae9f8f9916f563d5703632a26eda260
size 72501

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c39868f184364555ae90fbfc035aa668f61189be7aeee6bec4e45a8de438ad8e
size 87661

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dd029fdc49e6d4078337472c39b9d58bf69073c1b7750c6dd1b7ccd450d52395
size 119869

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:e698ba12efd129099877248f9630ba983d683e1b495b2523ed3569989341e905
size 51735

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:18b81b5cd88372b65b1ecc62e9a5e894960279310b05a1bd5c8df5bffa244ad0
size 54922

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bf5df173431d330e4b6045a72227c2bb7613ec98c63f013ea899a3a57cd6617a
size 55522

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:987c162842a08271e833c41a55573d9f30cf045bf7ca3cb03e81d0cc13d5a16e
size 36763

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8c5c3055cd190823a4204aa6f23362a88bc5ab5ed5453d9be1b6077dded6cd54
size 36809

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a291f3a5724aefc59ba7881f48752ccc826ca5e480741c221d195061f562ccc9
size 158220
oid sha256:946bf96ae558ee7373b50bf11959e82b1f4d91866ec61b04b0336ae170b6f7b2
size 158553

View File

@ -195,7 +195,12 @@ pub fn try_image_snapshot_options(
output_path,
} = options;
std::fs::create_dir_all(output_path).ok();
let parent_path = if let Some(parent) = PathBuf::from(name).parent() {
output_path.join(parent)
} else {
output_path.clone()
};
std::fs::create_dir_all(parent_path).ok();
// The one that is checked in to git
let snapshot_path = output_path.join(format!("{name}.png"));

View File

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:64fd46da67cab2afae0ea8997a88fb43fd207e24cc3943086d978a8de717320f
size 7542
oid sha256:f65efbf60e190d83d187ec51f3f7811eb55135ef4feb9586e931e8498bc05d64
size 7430

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);
}
}
}